-
Notifications
You must be signed in to change notification settings - Fork 239
/
metrics.go
381 lines (344 loc) · 10.6 KB
/
metrics.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package metrics implements Service Weaver metrics.
package metrics
import (
"encoding/binary"
"fmt"
"math"
"slices"
"sort"
"sync"
"sync/atomic"
"github.com/ServiceWeaver/weaver/runtime/protos"
"github.com/google/uuid"
"golang.org/x/exp/maps"
)
var (
// metricNames stores the name of every metric (labeled or not).
metricNamesMu sync.RWMutex
metricNames = map[string]bool{}
// metrics stores every metric.
metricsMu sync.RWMutex
metrics = []*Metric{}
)
// Metric is a thread-safe readable and writeable metric. It is the underlying
// implementation of the user-facing metrics like Counter and Gauge.
//
// Every metric has a unique name assigned by the user. For example, the user
// may create a histogram called "http_request_duration". Every metric also has
// a fixed, possibly empty, set of labels. For example, the user may assign an
// "endpoint" label to their "http_request_duration" to differentiate the
// latency of different HTTP endpoints. A metric name and set of label values
// uniquely identify a metric. For example, the following two metrics are
// different:
//
// http_request_duration{endpoint="/"}
// http_request_duration{endpoint="/foo"}
type Metric struct {
typ protos.MetricType // the type of the metric
name string // the globally unique metric name
help string // a short description of the metric
labelsThunk func() map[string]string // the (deferred) metric labels
// Users may call Get on the critical path of their application, so we want
// a call of `Get(labels)` to be as fast as possible. Converting `labels`
// into a map[string]string requires reflection and can be slow. Computing
// the metric's id is similarly slow. We avoid doing either of these in the
// call to Get and instead initialize them only when needed (i.e. before
// exporting).
once sync.Once // used to initialize id and labels
id uint64 // globally unique metric id
labels map[string]string // materialized labels from calling labelsThunk
fvalue atomicFloat64 // value for Counter and Gauge, sum for Histogram
ivalue atomic.Uint64 // integer increments for Counter (separated for speed)
// For histograms only:
putCount atomic.Uint64 // incremented on every Put, for change detection
bounds []float64 // histogram bounds
counts []atomic.Uint64 // histogram counts
}
// A MetricSnapshot is a snapshot of a metric.
type MetricSnapshot struct {
Id uint64
Type protos.MetricType
Name string
Labels map[string]string
Help string
Value float64
Bounds []float64
Counts []uint64
}
// MetricDef returns a MetricDef derived from the metric.
func (m *MetricSnapshot) MetricDef() *protos.MetricDef {
return &protos.MetricDef{
Id: m.Id,
Name: m.Name,
Typ: m.Type,
Help: m.Help,
Labels: m.Labels,
Bounds: m.Bounds,
}
}
// MetricValue returns a MetricValue derived from the metric.
func (m *MetricSnapshot) MetricValue() *protos.MetricValue {
return &protos.MetricValue{
Id: m.Id,
Value: m.Value,
Counts: m.Counts,
}
}
// ToProto converts a MetricSnapshot to its proto equivalent.
func (m *MetricSnapshot) ToProto() *protos.MetricSnapshot {
return &protos.MetricSnapshot{
Id: m.Id,
Name: m.Name,
Typ: m.Type,
Help: m.Help,
Labels: m.Labels,
Bounds: m.Bounds,
Value: m.Value,
Counts: m.Counts,
}
}
// UnProto converts a protos.MetricSnapshot into a metrics.MetricSnapshot.
func UnProto(m *protos.MetricSnapshot) *MetricSnapshot {
return &MetricSnapshot{
Id: m.Id,
Type: m.Typ,
Name: m.Name,
Labels: m.Labels,
Help: m.Help,
Value: m.Value,
Bounds: m.Bounds,
Counts: m.Counts,
}
}
// Clone returns a deep copy of m.
func (m *MetricSnapshot) Clone() *MetricSnapshot {
c := *m
c.Labels = maps.Clone(m.Labels)
c.Bounds = slices.Clone(m.Bounds)
c.Counts = slices.Clone(m.Counts)
return &c
}
// config configures the creation of a metric.
type config struct {
Type protos.MetricType
Name string
Labels func() map[string]string
Bounds []float64
Help string
}
// Register registers and returns a new metric. Panics if a metric with the same name
// has already been registered.
func Register(typ protos.MetricType, name string, help string, bounds []float64) *Metric {
m := RegisterMap[struct{}](typ, name, help, bounds)
return m.Get(struct{}{})
}
// newMetric registers and returns a new metric.
func newMetric(config config) *Metric {
metricsMu.Lock()
defer metricsMu.Unlock()
metric := &Metric{
typ: config.Type,
name: config.Name,
help: config.Help,
labelsThunk: config.Labels,
bounds: config.Bounds,
}
if config.Type == protos.MetricType_HISTOGRAM {
metric.counts = make([]atomic.Uint64, len(config.Bounds)+1)
}
metrics = append(metrics, metric)
return metric
}
// Name returns the name of the metric.
func (m *Metric) Name() string {
return m.name
}
// Inc adds one to the metric value.
func (m *Metric) Inc() {
m.ivalue.Add(1)
}
// Add adds the provided delta to the metric's value.
func (m *Metric) Add(delta float64) {
m.fvalue.add(delta)
}
// Sub subtracts the provided delta from the metric's value.
func (m *Metric) Sub(delta float64) {
m.fvalue.add(-delta)
}
// Set sets the metric's value.
func (m *Metric) Set(val float64) {
m.fvalue.set(val)
}
// Put adds the provided value to the metric's histogram.
func (m *Metric) Put(val float64) {
var idx int
if len(m.bounds) == 0 || val < m.bounds[0] {
// Skip binary search for values that fall in the first bucket
// (often true for short latency operations).
} else {
idx = sort.SearchFloat64s(m.bounds, val)
if idx < len(m.bounds) && val == m.bounds[idx] {
idx++
}
}
m.counts[idx].Add(1)
// Microsecond latencies are often zero for very fast functions.
if val != 0 {
m.fvalue.add(val)
}
m.putCount.Add(1)
}
// initIdAndLabels initializes the id and labels of a metric.
// We delay this initialization until the first time we export a
// metric to avoid slowing down a Get() call.
func (m *Metric) initIdAndLabels() {
m.once.Do(func() {
if labels := m.labelsThunk(); len(labels) > 0 {
m.labels = labels
}
var id [16]byte = uuid.New()
m.id = binary.LittleEndian.Uint64(id[:8])
})
}
// get returns the current value (sum of all added values for histograms).
func (m *Metric) get() float64 {
return m.fvalue.get() + float64(m.ivalue.Load())
}
// Snapshot returns a snapshot of the metric. You must call Init at least once
// before calling Snapshot.
func (m *Metric) Snapshot() *MetricSnapshot {
var counts []uint64
if n := len(m.counts); n > 0 {
counts = make([]uint64, n)
for i := range m.counts {
counts[i] = m.counts[i].Load()
}
}
return &MetricSnapshot{
Id: m.id,
Name: m.name,
Type: m.typ,
Help: m.help,
Labels: maps.Clone(m.labels),
Value: m.get(),
Bounds: slices.Clone(m.bounds),
Counts: counts,
}
}
// MetricDef returns a MetricDef derived from the metric. You must call Init at
// least once before calling Snapshot.
func (m *Metric) MetricDef() *protos.MetricDef {
return &protos.MetricDef{
Id: m.id,
Name: m.name,
Typ: m.typ,
Help: m.help,
Labels: maps.Clone(m.labels),
Bounds: slices.Clone(m.bounds),
}
}
// MetricValue returns a MetricValue derived from the metric.
func (m *Metric) MetricValue() *protos.MetricValue {
var counts []uint64
if n := len(m.counts); n > 0 {
counts = make([]uint64, n)
for i := range m.counts {
counts[i] = m.counts[i].Load()
}
}
return &protos.MetricValue{
Id: m.id,
Value: m.get(),
Counts: counts,
}
}
// MetricMap is a collection of metrics with the same name and label schema
// but with different label values. See public metric documentation for
// an explanation of labels.
//
// TODO(mwhittaker): Understand the behavior of prometheus and Google Cloud
// Metrics when we add or remove metric labels over time.
type MetricMap[L comparable] struct {
config config // configures the metrics returned by Get
extractor *labelExtractor[L] // extracts labels from a value of type L
mu sync.Mutex // guards metrics
metrics map[L]*Metric // cache of metrics, by label
}
func RegisterMap[L comparable](typ protos.MetricType, name string, help string, bounds []float64) *MetricMap[L] {
if err := typecheckLabels[L](); err != nil {
panic(err)
}
if name == "" {
panic(fmt.Errorf("empty metric name"))
}
if typ == protos.MetricType_INVALID {
panic(fmt.Errorf("metric %q: invalid metric type %v", name, typ))
}
for _, x := range bounds {
if math.IsNaN(x) {
panic(fmt.Errorf("metric %q: NaN histogram bound", name))
}
}
for i := 0; i < len(bounds)-1; i++ {
if bounds[i] >= bounds[i+1] {
panic(fmt.Errorf("metric %q: non-ascending histogram bounds %v", name, bounds))
}
}
metricNamesMu.Lock()
defer metricNamesMu.Unlock()
if metricNames[name] {
panic(fmt.Errorf("metric %q already exists", name))
}
metricNames[name] = true
return &MetricMap[L]{
config: config{Type: typ, Name: name, Help: help, Bounds: bounds},
extractor: newLabelExtractor[L](),
metrics: map[L]*Metric{},
}
}
// Name returns the name of the metricMap.
func (mm *MetricMap[L]) Name() string {
return mm.config.Name
}
// Get returns the metric with the provided labels, constructing it if it
// doesn't already exist. Multiple calls to Get with the same labels will
// return the same metric.
func (mm *MetricMap[L]) Get(labels L) *Metric {
mm.mu.Lock()
defer mm.mu.Unlock()
if metric, ok := mm.metrics[labels]; ok {
return metric
}
config := mm.config
config.Labels = func() map[string]string {
return mm.extractor.Extract(labels)
}
metric := newMetric(config)
mm.metrics[labels] = metric
return metric
}
// Snapshot returns a snapshot of all currently registered metrics. The
// snapshot is not guaranteed to be atomic.
func Snapshot() []*MetricSnapshot {
metricsMu.RLock()
defer metricsMu.RUnlock()
snapshots := make([]*MetricSnapshot, 0, len(metrics))
for _, metric := range metrics {
metric.initIdAndLabels()
snapshots = append(snapshots, metric.Snapshot())
}
return snapshots
}