From e60f77df30317a2bcb5233d78c345fbf5221dc76 Mon Sep 17 00:00:00 2001 From: bakins Date: Thu, 16 Jan 2020 08:48:17 -0500 Subject: [PATCH] Move escapeMetricName to mapper Signed-off-by: bakins --- exporter.go | 61 ++------------------------------ exporter_test.go | 40 --------------------- pkg/mapper/escape.go | 74 +++++++++++++++++++++++++++++++++++++++ pkg/mapper/escape_test.go | 56 +++++++++++++++++++++++++++++ 4 files changed, 133 insertions(+), 98 deletions(-) create mode 100644 pkg/mapper/escape.go create mode 100644 pkg/mapper/escape_test.go diff --git a/exporter.go b/exporter.go index c70a12e..e3984b7 100644 --- a/exporter.go +++ b/exporter.go @@ -54,61 +54,6 @@ type Exporter struct { logger log.Logger } -// Replace invalid characters in the metric name with "_" -// Valid characters are a-z, A-Z, 0-9, and _ -func escapeMetricName(metricName string) string { - metricLen := len(metricName) - if metricLen == 0 { - return "" - } - - escaped := false - var sb strings.Builder - // If a metric starts with a digit, allocate the memory and prepend an - // underscore. - if metricName[0] >= '0' && metricName[0] <= '9' { - escaped = true - sb.Grow(metricLen + 1) - sb.WriteByte('_') - } - - // This is an character replacement method optimized for this limited - // use case. It is much faster than using a regex. - offset := 0 - for i, c := range metricName { - // Seek forward, skipping valid characters until we find one that needs - // to be replaced, then add all the characters we've seen so far to the - // string.Builder. - if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || - (c >= '0' && c <= '9') || (c == '_') { - // Character is valid, so skip over it without doing anything. - } else { - if !escaped { - // Up until now we've been lazy and avoided actually allocating - // memory. Unfortunately we've now determined this string needs - // escaping, so allocate the buffer for the whole string. - escaped = true - sb.Grow(metricLen) - } - sb.WriteString(metricName[offset:i]) - offset = i + utf8.RuneLen(c) - sb.WriteByte('_') - } - } - - if !escaped { - // This is the happy path where nothing had to be escaped, so we can - // avoid doing anything. - return metricName - } - - if offset < metricLen { - sb.WriteString(metricName[offset:]) - } - - return sb.String() -} - // Listen handles all events sent to the given channel sequentially. It // terminates when the channel is closed. func (b *Exporter) Listen(e <-chan Events) { @@ -159,14 +104,14 @@ func (b *Exporter) handleEvent(event Event) { errorEventStats.WithLabelValues("empty_metric_name").Inc() return } - metricName = escapeMetricName(mapping.Name) + metricName = mapper.EscapeMetricName(mapping.Name) for label, value := range labels { prometheusLabels[label] = value } eventsActions.WithLabelValues(string(mapping.Action)).Inc() } else { eventsUnmapped.Inc() - metricName = escapeMetricName(event.MetricName()) + metricName = mapper.EscapeMetricName(event.MetricName()) } switch ev := event.(type) { @@ -298,7 +243,7 @@ func parseTag(component, tag string, separator rune, labels map[string]string, l tagErrors.Inc() level.Debug(logger).Log("msg", "Malformed name tag", "k", k, "v", v, "component", component) } else { - labels[escapeMetricName(k)] = v + labels[mapper.EscapeMetricName(k)] = v } return } diff --git a/exporter_test.go b/exporter_test.go index ffc1d27..c64a5a2 100644 --- a/exporter_test.go +++ b/exporter_test.go @@ -682,25 +682,6 @@ func (ml *mockStatsDTCPListener) handlePacket(packet []byte) { ml.handleConn(sc) } -func TestEscapeMetricName(t *testing.T) { - scenarios := map[string]string{ - "clean": "clean", - "0starts_with_digit": "_0starts_with_digit", - "with_underscore": "with_underscore", - "with.dot": "with_dot", - "with😱emoji": "with_emoji", - "with.*.multiple": "with___multiple", - "test.web-server.foo.bar": "test_web_server_foo_bar", - "": "", - } - - for in, want := range scenarios { - if got := escapeMetricName(in); want != got { - t.Errorf("expected `%s` to be escaped to `%s`, got `%s`", in, want, got) - } - } -} - // TestTtlExpiration validates expiration of time series. // foobar metric without mapping should expire with default ttl of 1s // bazqux metric should expire with ttl of 2s @@ -922,27 +903,6 @@ func getTelemetryCounterValue(counter prometheus.Counter) float64 { return metric.Counter.GetValue() } -func BenchmarkEscapeMetricName(b *testing.B) { - scenarios := []string{ - "clean", - "0starts_with_digit", - "with_underscore", - "with.dot", - "with😱emoji", - "with.*.multiple", - "test.web-server.foo.bar", - "", - } - - for _, s := range scenarios { - b.Run(s, func(b *testing.B) { - for n := 0; n < b.N; n++ { - escapeMetricName(s) - } - }) - } -} - func BenchmarkParseDogStatsDTags(b *testing.B) { scenarios := map[string]string{ "1 tag w/hash": "#test:tag", diff --git a/pkg/mapper/escape.go b/pkg/mapper/escape.go new file mode 100644 index 0000000..fc8d194 --- /dev/null +++ b/pkg/mapper/escape.go @@ -0,0 +1,74 @@ +// Copyright 2020 The Prometheus Authors +// 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 mapper + +import ( + "strings" + "unicode/utf8" +) + +// EscapeMetricName replaces invalid characters in the metric name with "_" +// Valid characters are a-z, A-Z, 0-9, and _ +func EscapeMetricName(metricName string) string { + metricLen := len(metricName) + if metricLen == 0 { + return "" + } + + escaped := false + var sb strings.Builder + // If a metric starts with a digit, allocate the memory and prepend an + // underscore. + if metricName[0] >= '0' && metricName[0] <= '9' { + escaped = true + sb.Grow(metricLen + 1) + sb.WriteByte('_') + } + + // This is an character replacement method optimized for this limited + // use case. It is much faster than using a regex. + offset := 0 + for i, c := range metricName { + // Seek forward, skipping valid characters until we find one that needs + // to be replaced, then add all the characters we've seen so far to the + // string.Builder. + if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || (c == '_') { + // Character is valid, so skip over it without doing anything. + } else { + if !escaped { + // Up until now we've been lazy and avoided actually allocating + // memory. Unfortunately we've now determined this string needs + // escaping, so allocate the buffer for the whole string. + escaped = true + sb.Grow(metricLen) + } + sb.WriteString(metricName[offset:i]) + offset = i + utf8.RuneLen(c) + sb.WriteByte('_') + } + } + + if !escaped { + // This is the happy path where nothing had to be escaped, so we can + // avoid doing anything. + return metricName + } + + if offset < metricLen { + sb.WriteString(metricName[offset:]) + } + + return sb.String() +} diff --git a/pkg/mapper/escape_test.go b/pkg/mapper/escape_test.go new file mode 100644 index 0000000..336692d --- /dev/null +++ b/pkg/mapper/escape_test.go @@ -0,0 +1,56 @@ +// Copyright 2020 The Prometheus Authors +// 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 mapper + +import "testing" + +func TestEscapeMetricName(t *testing.T) { + scenarios := map[string]string{ + "clean": "clean", + "0starts_with_digit": "_0starts_with_digit", + "with_underscore": "with_underscore", + "with.dot": "with_dot", + "with😱emoji": "with_emoji", + "with.*.multiple": "with___multiple", + "test.web-server.foo.bar": "test_web_server_foo_bar", + "": "", + } + + for in, want := range scenarios { + if got := EscapeMetricName(in); want != got { + t.Errorf("expected `%s` to be escaped to `%s`, got `%s`", in, want, got) + } + } +} + +func BenchmarkEscapeMetricName(b *testing.B) { + scenarios := []string{ + "clean", + "0starts_with_digit", + "with_underscore", + "with.dot", + "with😱emoji", + "with.*.multiple", + "test.web-server.foo.bar", + "", + } + + for _, s := range scenarios { + b.Run(s, func(b *testing.B) { + for n := 0; n < b.N; n++ { + EscapeMetricName(s) + } + }) + } +}