Skip to content

Commit f8a888c

Browse files
committed
Reimplement PrometheusMetric to use slices for label pairs
This refactoring stems from an attempt to optimise memory usage in BuildMetrics and createPrometheusLabels, where labels are copied across various maps. The new PrometheusMetric uses slices to store label pairs and is implemented to guarantee that labels are always sorted by key. The rationale is that slices _might_ be more memory efficient than maps for large preallocation sizes. Moreover, the fact that label keys are promptly available (no need to iterate over the map) comes handy in a bunch of places where we save additional allocations. Lastly, while we spend cycles to do explicit sorting in yace now, it should save us some comparisons when prometheus sorts labels internally. The refactoring also comes with a reimplementation of signature for labels, since the prometheus models only work with maps. I've added a bunch of benchmarks of specific methods. They show that sometimes the change is noticeable, sometimes it's not (but the overall impact is hard to judge in synthetic benchs due to the variety of input one can get at runtime fromcoming from large aws responses). Benchmark_EnsureLabelConsistencyAndRemoveDuplicates: ``` │ before.txt │ after.txt │ │ sec/op │ sec/op vs base │ _EnsureLabelConsistencyAndRemoveDuplicates-12 14.203µ ± 2% 9.115µ ± 1% -35.82% (p=0.000 n=10) │ before.txt │ after.txt │ │ B/op │ B/op vs base │ _EnsureLabelConsistencyAndRemoveDuplicates-12 448.0 ± 0% 256.0 ± 0% -42.86% (p=0.000 n=10) │ before.txt │ after.txt │ │ allocs/op │ allocs/op vs base │ _EnsureLabelConsistencyAndRemoveDuplicates-12 17.000 ± 0% 9.000 ± 0% -47.06% (p=0.000 n=10) ``` Benchmark_createPrometheusLabels: ``` │ before.txt │ after.txt │ │ sec/op │ sec/op vs base │ _createPrometheusLabels-12 41.86m ± 5% 41.40m ± 9% ~ (p=0.481 n=10) │ before.txt │ after.txt │ │ B/op │ B/op vs base │ _createPrometheusLabels-12 2.867Mi ± 0% 1.531Mi ± 0% -46.59% (p=0.000 n=10) │ before.txt │ after.txt │ │ allocs/op │ allocs/op vs base │ _createPrometheusLabels-12 40.00k ± 0% 40.00k ± 0% -0.00% (p=0.000 n=10) ``` Benchmark_BuildMetrics: ``` │ before.txt │ after.txt │ │ sec/op │ sec/op vs base │ _BuildMetrics-12 110.4µ ± 1% 114.1µ ± 1% +3.35% (p=0.000 n=10) │ before.txt │ after.txt │ │ B/op │ B/op vs base │ _BuildMetrics-12 4.344Ki ± 0% 3.797Ki ± 0% -12.59% (p=0.000 n=10) │ before.txt │ after.txt │ │ allocs/op │ allocs/op vs base │ _BuildMetrics-12 95.00 ± 0% 99.00 ± 0% +4.21% (p=0.000 n=10) ``` Benchmark_NewPrometheusCollector: ``` │ before.txt │ after.txt │ │ sec/op │ sec/op vs base │ _NewPrometheusCollector-12 154.8µ ± 1% 143.5µ ± 1% -7.26% (p=0.000 n=10) │ before.txt │ after.txt │ │ B/op │ B/op vs base │ _NewPrometheusCollector-12 4.516Ki ± 0% 4.281Ki ± 0% -5.19% (p=0.000 n=10) │ before.txt │ after.txt │ │ allocs/op │ allocs/op vs base │ _NewPrometheusCollector-12 142.0 ± 0% 127.0 ± 0% -10.56% (p=0.000 n=10) ```
1 parent 3966e65 commit f8a888c

File tree

6 files changed

+637
-441
lines changed

6 files changed

+637
-441
lines changed

go.mod

+1-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ require (
2020
github.com/aws/aws-sdk-go-v2/service/storagegateway v1.32.0
2121
github.com/aws/aws-sdk-go-v2/service/sts v1.30.7
2222
github.com/aws/smithy-go v1.20.4
23+
github.com/cespare/xxhash/v2 v2.3.0
2324
github.com/go-kit/log v0.2.1
2425
github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db
2526
github.com/prometheus/client_golang v1.20.4
@@ -28,7 +29,6 @@ require (
2829
github.com/r3labs/diff/v3 v3.0.1
2930
github.com/stretchr/testify v1.9.0
3031
github.com/urfave/cli/v2 v2.27.4
31-
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948
3232
golang.org/x/sync v0.8.0
3333
gopkg.in/yaml.v2 v2.4.0
3434
)
@@ -43,7 +43,6 @@ require (
4343
github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 // indirect
4444
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 // indirect
4545
github.com/beorn7/perks v1.0.1 // indirect
46-
github.com/cespare/xxhash/v2 v2.3.0 // indirect
4746
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
4847
github.com/davecgh/go-spew v1.1.1 // indirect
4948
github.com/go-logfmt/logfmt v0.5.1 // indirect

go.sum

-2
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,6 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh
108108
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
109109
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
110110
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
111-
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA=
112-
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
113111
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
114112
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
115113
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=

pkg/promutil/migrate.go

+58-50
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@ package promutil
22

33
import (
44
"fmt"
5-
"maps"
65
"math"
76
"sort"
7+
"strconv"
88
"strings"
99
"time"
1010

1111
"github.com/grafana/regexp"
12-
prom_model "github.com/prometheus/common/model"
1312

1413
"github.com/nerdswords/yet-another-cloudwatch-exporter/pkg/logging"
1514
"github.com/nerdswords/yet-another-cloudwatch-exporter/pkg/model"
@@ -46,30 +45,30 @@ func BuildMetricName(namespace, metricName, statistic string) string {
4645

4746
func BuildNamespaceInfoMetrics(tagData []model.TaggedResourceResult, metrics []*PrometheusMetric, observedMetricLabels map[string]model.LabelSet, labelsSnakeCase bool, logger logging.Logger) ([]*PrometheusMetric, map[string]model.LabelSet) {
4847
for _, tagResult := range tagData {
49-
contextLabels := contextToLabels(tagResult.Context, labelsSnakeCase, logger)
48+
contextLabelKeys, contextLabelValues := contextToLabels(tagResult.Context, labelsSnakeCase, logger)
5049
for _, d := range tagResult.Data {
51-
metricName := BuildMetricName(d.Namespace, "info", "")
50+
size := len(d.Tags) + len(contextLabelKeys) + 1
51+
promLabelKeys, promLabelValues := make([]string, 0, size), make([]string, 0, size)
52+
53+
promLabelKeys = append(promLabelKeys, "name")
54+
promLabelKeys = append(promLabelKeys, contextLabelKeys...)
55+
promLabelValues = append(promLabelValues, d.ARN)
56+
promLabelValues = append(promLabelValues, contextLabelValues...)
5257

53-
promLabels := make(map[string]string, len(d.Tags)+len(contextLabels)+1)
54-
maps.Copy(promLabels, contextLabels)
55-
promLabels["name"] = d.ARN
5658
for _, tag := range d.Tags {
5759
ok, promTag := PromStringTag(tag.Key, labelsSnakeCase)
5860
if !ok {
5961
logger.Warn("tag name is an invalid prometheus label name", "tag", tag.Key)
6062
continue
6163
}
6264

63-
labelName := "tag_" + promTag
64-
promLabels[labelName] = tag.Value
65+
promLabelKeys = append(promLabelKeys, "tag_"+promTag)
66+
promLabelValues = append(promLabelValues, tag.Value)
6567
}
6668

67-
observedMetricLabels = recordLabelsForMetric(metricName, promLabels, observedMetricLabels)
68-
metrics = append(metrics, &PrometheusMetric{
69-
Name: metricName,
70-
Labels: promLabels,
71-
Value: 0,
72-
})
69+
metricName := BuildMetricName(d.Namespace, "info", "")
70+
observedMetricLabels = recordLabelsForMetric(metricName, promLabelKeys, observedMetricLabels)
71+
metrics = append(metrics, NewPrometheusMetric(metricName, promLabelKeys, promLabelValues, 0))
7372
}
7473
}
7574

@@ -81,7 +80,7 @@ func BuildMetrics(results []model.CloudwatchMetricResult, labelsSnakeCase bool,
8180
observedMetricLabels := make(map[string]model.LabelSet)
8281

8382
for _, result := range results {
84-
contextLabels := contextToLabels(result.Context, labelsSnakeCase, logger)
83+
contextLabelKeys, contextLabelValues := contextToLabels(result.Context, labelsSnakeCase, logger)
8584
for _, metric := range result.Data {
8685
// This should not be possible but check just in case
8786
if metric.GetMetricStatisticsResult == nil && metric.GetMetricDataResult == nil {
@@ -112,17 +111,17 @@ func BuildMetrics(results []model.CloudwatchMetricResult, labelsSnakeCase bool,
112111

113112
name := BuildMetricName(metric.Namespace, metric.MetricName, statistic)
114113

115-
promLabels := createPrometheusLabels(metric, labelsSnakeCase, contextLabels, logger)
116-
observedMetricLabels = recordLabelsForMetric(name, promLabels, observedMetricLabels)
117-
118-
output = append(output, &PrometheusMetric{
119-
Name: name,
120-
Labels: promLabels,
121-
Value: exportedDatapoint,
122-
Timestamp: ts,
123-
IncludeTimestamp: metric.MetricMigrationParams.AddCloudwatchTimestamp,
124-
})
125-
114+
labelKeys, labelValues := createPrometheusLabels(metric, labelsSnakeCase, contextLabelKeys, contextLabelValues, logger)
115+
observedMetricLabels = recordLabelsForMetric(name, labelKeys, observedMetricLabels)
116+
117+
output = append(output, NewPrometheusMetricWithTimestamp(
118+
name,
119+
labelKeys,
120+
labelValues,
121+
exportedDatapoint,
122+
metric.MetricMigrationParams.AddCloudwatchTimestamp,
123+
ts,
124+
))
126125
}
127126
}
128127
}
@@ -209,9 +208,12 @@ func sortByTimestamp(datapoints []*model.Datapoint) []*model.Datapoint {
209208
return datapoints
210209
}
211210

212-
func createPrometheusLabels(cwd *model.CloudwatchData, labelsSnakeCase bool, contextLabels map[string]string, logger logging.Logger) map[string]string {
213-
labels := make(map[string]string, len(cwd.Dimensions)+len(cwd.Tags)+len(contextLabels))
214-
labels["name"] = cwd.ResourceName
211+
func createPrometheusLabels(cwd *model.CloudwatchData, labelsSnakeCase bool, contextLabelsKeys []string, contextLabelsValues []string, logger logging.Logger) ([]string, []string) {
212+
size := len(cwd.Dimensions) + len(cwd.Tags) + len(contextLabelsKeys) + 1
213+
labelKeys, labelValues := make([]string, 0, size), make([]string, 0, size)
214+
215+
labelKeys = append(labelKeys, "name")
216+
labelValues = append(labelValues, cwd.ResourceName)
215217

216218
// Inject the sfn name back as a label
217219
for _, dimension := range cwd.Dimensions {
@@ -220,7 +222,8 @@ func createPrometheusLabels(cwd *model.CloudwatchData, labelsSnakeCase bool, con
220222
logger.Warn("dimension name is an invalid prometheus label name", "dimension", dimension.Name)
221223
continue
222224
}
223-
labels["dimension_"+promTag] = dimension.Value
225+
labelKeys = append(labelKeys, "dimension_"+promTag)
226+
labelValues = append(labelValues, dimension.Value)
224227
}
225228

226229
for _, tag := range cwd.Tags {
@@ -229,25 +232,31 @@ func createPrometheusLabels(cwd *model.CloudwatchData, labelsSnakeCase bool, con
229232
logger.Warn("metric tag name is an invalid prometheus label name", "tag", tag.Key)
230233
continue
231234
}
232-
labels["tag_"+promTag] = tag.Value
235+
labelKeys = append(labelKeys, "tag_"+promTag)
236+
labelValues = append(labelValues, tag.Value)
233237
}
234238

235-
maps.Copy(labels, contextLabels)
239+
labelKeys = append(labelKeys, contextLabelsKeys...)
240+
labelValues = append(labelValues, contextLabelsValues...)
236241

237-
return labels
242+
return labelKeys, labelValues
238243
}
239244

240-
func contextToLabels(context *model.ScrapeContext, labelsSnakeCase bool, logger logging.Logger) map[string]string {
245+
func contextToLabels(context *model.ScrapeContext, labelsSnakeCase bool, logger logging.Logger) ([]string, []string) {
241246
if context == nil {
242-
return map[string]string{}
247+
return []string{}, []string{}
243248
}
244249

245-
labels := make(map[string]string, 2+len(context.CustomTags))
246-
labels["region"] = context.Region
247-
labels["account_id"] = context.AccountID
250+
size := 3 + len(context.CustomTags)
251+
keys, values := make([]string, 0, size), make([]string, 0, size)
252+
253+
keys = append(keys, "region", "account_id")
254+
values = append(values, context.Region, context.AccountID)
255+
248256
// If there's no account alias, omit adding an extra label in the series, it will work either way query wise
249257
if context.AccountAlias != "" {
250-
labels["account_alias"] = context.AccountAlias
258+
keys = append(keys, "account_alias")
259+
values = append(values, context.AccountAlias)
251260
}
252261

253262
for _, label := range context.CustomTags {
@@ -256,19 +265,20 @@ func contextToLabels(context *model.ScrapeContext, labelsSnakeCase bool, logger
256265
logger.Warn("custom tag name is an invalid prometheus label name", "tag", label.Key)
257266
continue
258267
}
259-
labels["custom_tag_"+promTag] = label.Value
268+
keys = append(keys, "custom_tag_"+promTag)
269+
values = append(values, label.Value)
260270
}
261271

262-
return labels
272+
return keys, values
263273
}
264274

265275
// recordLabelsForMetric adds any missing labels from promLabels in to the LabelSet for the metric name and returns
266276
// the updated observedMetricLabels
267-
func recordLabelsForMetric(metricName string, promLabels map[string]string, observedMetricLabels map[string]model.LabelSet) map[string]model.LabelSet {
277+
func recordLabelsForMetric(metricName string, labelKeys []string, observedMetricLabels map[string]model.LabelSet) map[string]model.LabelSet {
268278
if _, ok := observedMetricLabels[metricName]; !ok {
269-
observedMetricLabels[metricName] = make(model.LabelSet, len(promLabels))
279+
observedMetricLabels[metricName] = make(model.LabelSet, len(labelKeys))
270280
}
271-
for label := range promLabels {
281+
for _, label := range labelKeys {
272282
if _, ok := observedMetricLabels[metricName][label]; !ok {
273283
observedMetricLabels[metricName][label] = struct{}{}
274284
}
@@ -285,13 +295,11 @@ func EnsureLabelConsistencyAndRemoveDuplicates(metrics []*PrometheusMetric, obse
285295
output := make([]*PrometheusMetric, 0, len(metrics))
286296

287297
for _, metric := range metrics {
288-
for observedLabels := range observedMetricLabels[metric.Name] {
289-
if _, ok := metric.Labels[observedLabels]; !ok {
290-
metric.Labels[observedLabels] = ""
291-
}
298+
for observedLabels := range observedMetricLabels[metric.Name()] {
299+
metric.AddIfMissingLabelPair(observedLabels, "")
292300
}
293301

294-
metricKey := fmt.Sprintf("%s-%d", metric.Name, prom_model.LabelsToSignature(metric.Labels))
302+
metricKey := metric.Name() + "-" + strconv.FormatUint(metric.LabelsSignature(), 10)
295303
if _, exists := metricKeys[metricKey]; !exists {
296304
metricKeys[metricKey] = struct{}{}
297305
output = append(output, metric)

0 commit comments

Comments
 (0)