Skip to content

Commit b7145de

Browse files
authored
Speed up build metric name. (#1671)
Signed-off-by: Karsten Jeschkies <[email protected]>
1 parent 70fcc27 commit b7145de

File tree

4 files changed

+130
-66
lines changed

4 files changed

+130
-66
lines changed

Diff for: pkg/promutil/migrate.go

+18-7
Original file line numberDiff line numberDiff line change
@@ -31,27 +31,38 @@ var Percentile = regexp.MustCompile(`^p(\d{1,2}(\.\d{0,2})?|100)$`)
3131

3232
func BuildMetricName(namespace, metricName, statistic string) string {
3333
sb := strings.Builder{}
34-
promNs := PromString(strings.ToLower(namespace))
34+
3535
// Some namespaces have a leading forward slash like
36-
// /aws/sagemaker/TrainingJobs, which gets converted to
37-
// a leading _ by PromString().
38-
promNs = strings.TrimPrefix(promNs, "_")
36+
// /aws/sagemaker/TrainingJobs, which should be removed.
37+
var promNs string
38+
if strings.HasPrefix(namespace, "/") {
39+
promNs = PromString(strings.ToLower(namespace[1:]))
40+
} else {
41+
promNs = PromString(strings.ToLower(namespace))
42+
}
43+
3944
if !strings.HasPrefix(promNs, "aws") {
4045
sb.WriteString("aws_")
4146
}
4247
sb.WriteString(promNs)
48+
4349
sb.WriteString("_")
50+
4451
promMetricName := PromString(metricName)
4552
// Some metric names duplicate parts of the namespace as a prefix,
4653
// For example, the `Glue` namespace metrics have names prefixed also by `glue``
54+
skip := 0
4755
for _, part := range strings.Split(promNs, "_") {
48-
promMetricName = strings.TrimPrefix(promMetricName, part)
56+
if strings.HasPrefix(promMetricName[skip:], part) {
57+
skip = len(part)
58+
}
4959
}
50-
promMetricName = strings.TrimPrefix(promMetricName, "_")
60+
promMetricName = strings.TrimPrefix(promMetricName[skip:], "_")
61+
5162
sb.WriteString(promMetricName)
5263
if statistic != "" {
5364
sb.WriteString("_")
54-
sb.WriteString(PromString(statistic))
65+
PromStringToBuilder(statistic, &sb)
5566
}
5667
return sb.String()
5768
}

Diff for: pkg/promutil/migrate_test.go

+88
Original file line numberDiff line numberDiff line change
@@ -1124,6 +1124,94 @@ func Benchmark_BuildMetrics(b *testing.B) {
11241124
require.Equal(b, expectedLabels, labels)
11251125
}
11261126

1127+
func TestBuildMetricName(t *testing.T) {
1128+
type testCase struct {
1129+
name string
1130+
namespace string
1131+
metric string
1132+
statistic string
1133+
expected string
1134+
}
1135+
1136+
testCases := []testCase{
1137+
{
1138+
name: "standard AWS namespace",
1139+
namespace: "AWS/ElastiCache",
1140+
metric: "CPUUtilization",
1141+
statistic: "Average",
1142+
expected: "aws_elasticache_cpuutilization_average",
1143+
},
1144+
{
1145+
name: "nonstandard namespace with slashes",
1146+
namespace: "/aws/sagemaker/TrainingJobs",
1147+
metric: "CPUUtilization",
1148+
statistic: "Average",
1149+
expected: "aws_sagemaker_trainingjobs_cpuutilization_average",
1150+
},
1151+
{
1152+
name: "metric name duplicating namespace",
1153+
namespace: "Glue",
1154+
metric: "glue.driver.aggregate.bytesRead",
1155+
statistic: "Average",
1156+
expected: "aws_glue_driver_aggregate_bytes_read_average",
1157+
},
1158+
{
1159+
name: "metric name not duplicating namespace",
1160+
namespace: "Glue",
1161+
metric: "aggregate.glue.jobs.bytesRead",
1162+
statistic: "Average",
1163+
expected: "aws_glue_aggregate_glue_jobs_bytes_read_average",
1164+
},
1165+
}
1166+
1167+
for _, tc := range testCases {
1168+
t.Run(tc.name, func(t *testing.T) {
1169+
result := BuildMetricName(tc.namespace, tc.metric, tc.statistic)
1170+
require.Equal(t, tc.expected, result)
1171+
})
1172+
}
1173+
}
1174+
1175+
func Benchmark_BuildMetricName(b *testing.B) {
1176+
testCases := []struct {
1177+
namespace string
1178+
metric string
1179+
statistic string
1180+
}{
1181+
{
1182+
namespace: "AWS/ElastiCache",
1183+
metric: "CPUUtilization",
1184+
statistic: "Average",
1185+
},
1186+
{
1187+
namespace: "/aws/sagemaker/TrainingJobs",
1188+
metric: "CPUUtilization",
1189+
statistic: "Average",
1190+
},
1191+
{
1192+
namespace: "Glue",
1193+
metric: "glue.driver.aggregate.bytesRead",
1194+
statistic: "Average",
1195+
},
1196+
{
1197+
namespace: "Glue",
1198+
metric: "aggregate.glue.jobs.bytesRead",
1199+
statistic: "Average",
1200+
},
1201+
}
1202+
1203+
for _, tc := range testCases {
1204+
testName := BuildMetricName(tc.namespace, tc.metric, tc.statistic)
1205+
b.ResetTimer()
1206+
b.ReportAllocs()
1207+
b.Run(testName, func(b *testing.B) {
1208+
for i := 0; i < b.N; i++ {
1209+
BuildMetricName(tc.namespace, tc.metric, tc.statistic)
1210+
}
1211+
})
1212+
}
1213+
}
1214+
11271215
// replaceNaNValues replaces any NaN floating-point values with a marker value (54321.0)
11281216
// so that require.Equal() can compare them. By default, require.Equal() will fail if any
11291217
// struct values are NaN because NaN != NaN

Diff for: pkg/promutil/prometheus.go

+24-35
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ package promutil
1515
import (
1616
"strings"
1717
"time"
18+
"unicode"
1819

1920
"github.com/prometheus/client_golang/prometheus"
2021
"github.com/prometheus/common/model"
@@ -178,8 +179,29 @@ func toConstMetrics(metrics []*PrometheusMetric) []prometheus.Metric {
178179
}
179180

180181
func PromString(text string) string {
181-
text = splitString(text)
182-
return strings.ToLower(sanitize(text))
182+
var buf strings.Builder
183+
PromStringToBuilder(text, &buf)
184+
return buf.String()
185+
}
186+
187+
func PromStringToBuilder(text string, buf *strings.Builder) {
188+
buf.Grow(len(text))
189+
190+
var prev rune
191+
for _, c := range text {
192+
switch c {
193+
case ' ', ',', '\t', '/', '\\', '.', '-', ':', '=', '@', '<', '>', '(', ')', '“':
194+
buf.WriteRune('_')
195+
case '%':
196+
buf.WriteString("_percent")
197+
default:
198+
if unicode.IsUpper(c) && (unicode.IsLower(prev) || unicode.IsDigit(prev)) {
199+
buf.WriteRune('_')
200+
}
201+
buf.WriteRune(unicode.ToLower(c))
202+
}
203+
prev = c
204+
}
183205
}
184206

185207
func PromStringTag(text string, labelsSnakeCase bool) (bool, string) {
@@ -210,36 +232,3 @@ func sanitize(text string) string {
210232
}
211233
return string(b)
212234
}
213-
214-
// splitString replaces consecutive occurrences of a lowercase and uppercase letter,
215-
// or a number and an upper case letter, by putting a dot between the two chars.
216-
//
217-
// This is an optimised version of the original implementation:
218-
//
219-
// var splitRegexp = regexp.MustCompile(`([a-z0-9])([A-Z])`)
220-
//
221-
// func splitString(text string) string {
222-
// return splitRegexp.ReplaceAllString(text, `$1.$2`)
223-
// }
224-
func splitString(text string) string {
225-
sb := strings.Builder{}
226-
sb.Grow(len(text) + 4) // make some room for replacements
227-
228-
i := 0
229-
for i < len(text) {
230-
c := text[i]
231-
sb.WriteByte(c)
232-
if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') {
233-
if i < (len(text) - 1) {
234-
c = text[i+1]
235-
if c >= 'A' && c <= 'Z' {
236-
sb.WriteByte('.')
237-
sb.WriteByte(c)
238-
i++
239-
}
240-
}
241-
}
242-
i++
243-
}
244-
return sb.String()
245-
}

Diff for: pkg/promutil/prometheus_test.go

-24
Original file line numberDiff line numberDiff line change
@@ -23,30 +23,6 @@ import (
2323
"github.com/stretchr/testify/require"
2424
)
2525

26-
func TestSplitString(t *testing.T) {
27-
testCases := []struct {
28-
input string
29-
output string
30-
}{
31-
{
32-
input: "GlobalTopicCount",
33-
output: "Global.Topic.Count",
34-
},
35-
{
36-
input: "CPUUtilization",
37-
output: "CPUUtilization",
38-
},
39-
{
40-
input: "StatusCheckFailed_Instance",
41-
output: "Status.Check.Failed_Instance",
42-
},
43-
}
44-
45-
for _, tc := range testCases {
46-
assert.Equal(t, tc.output, splitString(tc.input))
47-
}
48-
}
49-
5026
func TestSanitize(t *testing.T) {
5127
testCases := []struct {
5228
input string

0 commit comments

Comments
 (0)