9
9
10
10
"github.com/coder/coder/v2/coderd/agentmetrics"
11
11
"github.com/prometheus/client_golang/prometheus"
12
+ "github.com/prometheus/common/model"
12
13
"golang.org/x/xerrors"
13
14
14
15
"cdr.dev/slog"
@@ -107,8 +108,9 @@ var _ prometheus.Collector = new(MetricsAggregator)
107
108
108
109
func (am * annotatedMetric )asPrometheus () (prometheus.Metric ,error ) {
109
110
var (
110
- baseLabelNames [] string = am .aggregateByLabels
111
+ baseLabelNames = am .aggregateByLabels
111
112
baseLabelValues []string
113
+ extraLabels = am .Labels
112
114
)
113
115
114
116
for _ ,label := range am .aggregateByLabels {
@@ -120,19 +122,17 @@ func (am *annotatedMetric) asPrometheus() (prometheus.Metric, error) {
120
122
baseLabelValues = append (baseLabelValues ,val )
121
123
}
122
124
123
- labels := make ([]string ,0 ,len (baseLabelNames )+ len (am . Labels ))
124
- labelValues := make ([]string ,0 ,len (baseLabelNames )+ len (am . Labels ))
125
+ labels := make ([]string ,0 ,len (baseLabelNames )+ len (extraLabels ))
126
+ labelValues := make ([]string ,0 ,len (baseLabelNames )+ len (extraLabels ))
125
127
126
128
labels = append (labels ,baseLabelNames ... )
127
129
labelValues = append (labelValues ,baseLabelValues ... )
128
130
129
- for _ ,l := range am . Labels {
131
+ for _ ,l := range extraLabels {
130
132
labels = append (labels ,l .Name )
131
133
labelValues = append (labelValues ,l .Value )
132
134
}
133
135
134
- //fmt.Printf(">>>>[%s] [%s] %s [%q] [%q]: %v\n", time.Now().Format(time.RFC3339Nano), am.Type, am.Name, labels, labelValues, am.Value)
135
-
136
136
desc := prometheus .NewDesc (am .Name ,metricHelpForAgent ,labels ,nil )
137
137
valueType ,err := asPrometheusValueType (am .Type )
138
138
if err != nil {
@@ -237,50 +237,56 @@ func NewMetricsAggregator(logger slog.Logger, registerer prometheus.Registerer,
237
237
},nil
238
238
}
239
239
240
- type MetricAggregator struct {
240
+ // labelAggregator is used to control cardinality of collected Prometheus metrics by pre-aggregating series based on given labels.
241
+ type labelAggregator struct {
241
242
aggregations map [string ]float64
242
243
metrics map [string ]annotatedMetric
243
244
}
244
245
245
- func NewMetricAggregator (size int )* MetricAggregator {
246
- return & MetricAggregator {
246
+ func newLabelAggregator (size int )* labelAggregator {
247
+ return & labelAggregator {
247
248
aggregations :make (map [string ]float64 ,size ),
248
249
metrics :make (map [string ]annotatedMetric ,size ),
249
250
}
250
251
}
251
252
252
- func (a * MetricAggregator )Aggregate (am annotatedMetric ,labels []string )error {
253
- // if we already have an entry for this key, don't clone this am afresh - rather use the existing one
254
- // this will be a bit more memory efficient
255
- // ...do this after unit-test is written
256
-
257
- clone := am .clone ()
258
-
259
- fields := make (map [string ]string ,len (labels ))
253
+ func (a * labelAggregator )aggregate (am annotatedMetric ,labels []string )error {
254
+ // Use a LabelSet because it can give deterministic fingerprints of label combinations regardless of map ordering.
255
+ labelSet := make (model.LabelSet ,len (labels ))
260
256
labelValues := make ([]string ,0 ,len (labels ))
261
257
262
258
for _ ,label := range labels {
263
- val ,err := clone .getFieldByLabel (label )
259
+ val ,err := am .getFieldByLabel (label )
264
260
if err != nil {
265
261
return err
266
262
}
267
263
268
- fields [ label ]= val
264
+ labelSet [ model . LabelName ( label ) ]= model . LabelValue ( val )
269
265
labelValues = append (labelValues ,val )
270
266
}
271
267
272
- key := fmt .Sprintf ("%s:%v" ,clone .Stats_Metric .Name ,fields )
268
+ // Memoize based on the metric name & the unique combination of labels.
269
+ key := fmt .Sprintf ("%s:%v" ,am .Stats_Metric .Name ,labelSet .FastFingerprint ())
270
+
271
+ // Aggregate the value based on the key.
272
+ a .aggregations [key ]+= am .Value
273
+
274
+ metric ,found := a .metrics [key ]
275
+ if ! found {
276
+ // Take a copy of the given annotatedMetric because it may be manipulated later and contains pointers.
277
+ metric = am .clone ()
278
+ }
273
279
274
- clone .aggregateByLabels = labels
275
- a .aggregations [key ]+= clone .Value
280
+ // Store the metric.
281
+ metric .aggregateByLabels = labels
282
+ metric .Value = a .aggregations [key ]
276
283
277
- clone .Value = a .aggregations [key ]
278
- a .metrics [key ]= clone
284
+ a .metrics [key ]= metric
279
285
280
286
return nil
281
287
}
282
288
283
- func (a * MetricAggregator ) asMetrics () (out []annotatedMetric ) {
289
+ func (a * labelAggregator ) toMetrics () (out []annotatedMetric ) {
284
290
for _ ,am := range a .metrics {
285
291
out = append (out ,am )
286
292
}
@@ -331,24 +337,25 @@ func (ma *MetricsAggregator) Run(ctx context.Context) func() {
331
337
332
338
// If custom aggregation labels have not been chosen, generate Prometheus metrics without any pre-aggregation.
333
339
// This results in higher cardinality, but may be desirable in larger deployments.
340
+ // Default behaviour.
334
341
if len (ma .aggregateByLabels )== 0 {
335
342
for _ ,m := range ma .store {
336
- // Aggregate byhigh cardinality labels .
337
- m .aggregateByLabels = agentMetricsLabels
343
+ // Aggregate byall available metrics .
344
+ m .aggregateByLabels = defaultAgentMetricsLabels
338
345
input = append (input ,m )
339
346
}
340
347
}else {
341
348
// However, if custom aggregations have been chosen, we need to aggregate the values from the annotated
342
349
// metrics because we cannot register multiple metric series with the same labels.
343
- aggregator := NewMetricAggregator (len (ma .store ) * len ( ma . aggregateByLabels ))
350
+ la := newLabelAggregator (len (ma .store ))
344
351
345
352
for _ ,m := range ma .store {
346
- if err := aggregator . Aggregate (m ,ma .aggregateByLabels );err != nil {
353
+ if err := la . aggregate (m ,ma .aggregateByLabels );err != nil {
347
354
ma .log .Error (ctx ,"can't aggregate labels" ,slog .F ("labels" ,strings .Join (ma .aggregateByLabels ,"," )),slog .Error (err ))
348
355
}
349
356
}
350
357
351
- input = aggregator . asMetrics ()
358
+ input = la . toMetrics ()
352
359
}
353
360
354
361
for _ ,m := range input {
@@ -395,7 +402,7 @@ func (ma *MetricsAggregator) Run(ctx context.Context) func() {
395
402
func (* MetricsAggregator )Describe (_ chan <- * prometheus.Desc ) {
396
403
}
397
404
398
- var agentMetricsLabels = []string {agentmetrics .UsernameLabel ,agentmetrics .WorkspaceNameLabel ,agentmetrics .AgentNameLabel ,agentmetrics .TemplateNameLabel }
405
+ var defaultAgentMetricsLabels = []string {agentmetrics .UsernameLabel ,agentmetrics .WorkspaceNameLabel ,agentmetrics .AgentNameLabel ,agentmetrics .TemplateNameLabel }
399
406
400
407
// AgentMetricLabels are the labels used to decorate an agent's metrics.
401
408
// This list should match the list of labels in agentMetricsLabels.