diff --git a/Makefile.common b/Makefile.common index d8b79890..4de21512 100644 --- a/Makefile.common +++ b/Makefile.common @@ -62,6 +62,7 @@ SKIP_GOLANGCI_LINT := GOLANGCI_LINT := GOLANGCI_LINT_OPTS ?= GOLANGCI_LINT_VERSION ?= v2.1.5 +GOLANGCI_FMT_OPTS ?= # golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64. # windows isn't included here because of the path separator being different. ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin)) @@ -156,9 +157,13 @@ $(GOTEST_DIR): @mkdir -p $@ .PHONY: common-format -common-format: +common-format: $(GOLANGCI_LINT) @echo ">> formatting code" $(GO) fmt $(pkgs) +ifdef GOLANGCI_LINT + @echo ">> formatting code with golangci-lint" + $(GOLANGCI_LINT) fmt $(GOLANGCI_FMT_OPTS) +endif .PHONY: common-vet common-vet: @@ -248,8 +253,8 @@ $(PROMU): cp $(PROMU_TMP)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM)/promu $(FIRST_GOPATH)/bin/promu rm -r $(PROMU_TMP) -.PHONY: proto -proto: +.PHONY: common-proto +common-proto: @echo ">> generating code from proto files" @./scripts/genproto.sh diff --git a/model/time.go b/model/time.go index 5727452c..fed9e87b 100644 --- a/model/time.go +++ b/model/time.go @@ -201,6 +201,7 @@ var unitMap = map[string]struct { // ParseDuration parses a string into a time.Duration, assuming that a year // always has 365d, a week always has 7d, and a day always has 24h. +// Negative durations are not supported. func ParseDuration(s string) (Duration, error) { switch s { case "0": @@ -253,18 +254,36 @@ func ParseDuration(s string) (Duration, error) { return 0, errors.New("duration out of range") } } + return Duration(dur), nil } +// ParseDurationAllowNegative is like ParseDuration but also accepts negative durations. +func ParseDurationAllowNegative(s string) (Duration, error) { + if s == "" || s[0] != '-' { + return ParseDuration(s) + } + + d, err := ParseDuration(s[1:]) + + return -d, err +} + func (d Duration) String() string { var ( - ms = int64(time.Duration(d) / time.Millisecond) - r = "" + ms = int64(time.Duration(d) / time.Millisecond) + r = "" + sign = "" ) + if ms == 0 { return "0s" } + if ms < 0 { + sign, ms = "-", -ms + } + f := func(unit string, mult int64, exact bool) { if exact && ms%mult != 0 { return @@ -286,7 +305,7 @@ func (d Duration) String() string { f("s", 1000, false) f("ms", 1, false) - return r + return sign + r } // MarshalJSON implements the json.Marshaler interface. diff --git a/model/time_test.go b/model/time_test.go index 70f38394..a4e9069f 100644 --- a/model/time_test.go +++ b/model/time_test.go @@ -68,70 +68,171 @@ func TestDuration(t *testing.T) { } func TestParseDuration(t *testing.T) { - cases := []struct { - in string - out time.Duration + type testCase struct { + in string + out time.Duration + expectedString string + allowedNegative bool + } - expectedString string - }{ + baseCases := []testCase{ { - in: "0", - out: 0, - expectedString: "0s", - }, { - in: "0w", - out: 0, - expectedString: "0s", - }, { - in: "0s", - out: 0, - }, { - in: "324ms", - out: 324 * time.Millisecond, - }, { - in: "3s", - out: 3 * time.Second, - }, { - in: "5m", - out: 5 * time.Minute, - }, { - in: "1h", - out: time.Hour, - }, { - in: "4d", - out: 4 * 24 * time.Hour, - }, { - in: "4d1h", - out: 4*24*time.Hour + time.Hour, - }, { - in: "14d", - out: 14 * 24 * time.Hour, - expectedString: "2w", - }, { - in: "3w", - out: 3 * 7 * 24 * time.Hour, - }, { - in: "3w2d1h", - out: 3*7*24*time.Hour + 2*24*time.Hour + time.Hour, - expectedString: "23d1h", - }, { - in: "10y", - out: 10 * 365 * 24 * time.Hour, + in: "0", + out: 0, + expectedString: "0s", + allowedNegative: false, + }, + { + in: "0w", + out: 0, + expectedString: "0s", + allowedNegative: false, + }, + { + in: "0s", + out: 0, + expectedString: "", + allowedNegative: false, + }, + { + in: "324ms", + out: 324 * time.Millisecond, + expectedString: "", + allowedNegative: false, + }, + { + in: "3s", + out: 3 * time.Second, + expectedString: "", + allowedNegative: false, + }, + { + in: "5m", + out: 5 * time.Minute, + expectedString: "", + allowedNegative: false, + }, + { + in: "1h", + out: time.Hour, + expectedString: "", + allowedNegative: false, + }, + { + in: "4d", + out: 4 * 24 * time.Hour, + expectedString: "", + allowedNegative: false, + }, + { + in: "4d1h", + out: 4*24*time.Hour + time.Hour, + expectedString: "", + allowedNegative: false, + }, + { + in: "14d", + out: 14 * 24 * time.Hour, + expectedString: "2w", + allowedNegative: false, + }, + { + in: "3w", + out: 3 * 7 * 24 * time.Hour, + expectedString: "", + allowedNegative: false, + }, + { + in: "3w2d1h", + out: 3*7*24*time.Hour + 2*24*time.Hour + time.Hour, + expectedString: "23d1h", + allowedNegative: false, + }, + { + in: "10y", + out: 10 * 365 * 24 * time.Hour, + expectedString: "", + allowedNegative: false, }, } - for _, c := range cases { - d, err := ParseDuration(c.in) + negativeCases := []testCase{ + { + in: "-3s", + out: -3 * time.Second, + expectedString: "", + allowedNegative: true, + }, + { + in: "-5m", + out: -5 * time.Minute, + expectedString: "", + allowedNegative: true, + }, + { + in: "-1h", + out: -1 * time.Hour, + expectedString: "", + allowedNegative: true, + }, + { + in: "-2d", + out: -2 * 24 * time.Hour, + expectedString: "", + allowedNegative: true, + }, + { + in: "-1w", + out: -7 * 24 * time.Hour, + expectedString: "", + allowedNegative: true, + }, + { + in: "-3w2d1h", + out: -(3*7*24*time.Hour + 2*24*time.Hour + time.Hour), + expectedString: "-23d1h", + allowedNegative: true, + }, + { + in: "-10y", + out: -10 * 365 * 24 * time.Hour, + expectedString: "", + allowedNegative: true, + }, + } + + for _, c := range baseCases { + c.allowedNegative = true + negativeCases = append(negativeCases, c) + } + + allCases := append(baseCases, negativeCases...) + + for _, c := range allCases { + var ( + d Duration + err error + ) + + if c.allowedNegative { + d, err = ParseDurationAllowNegative(c.in) + } else { + d, err = ParseDuration(c.in) + } + if err != nil { t.Errorf("Unexpected error on input %q", c.in) } + if time.Duration(d) != c.out { t.Errorf("Expected %v but got %v", c.out, d) } + expectedString := c.expectedString - if c.expectedString == "" { + if expectedString == "" { expectedString = c.in } + if d.String() != expectedString { t.Errorf("Expected duration string %q but got %q", c.in, d.String()) } @@ -307,7 +408,6 @@ func TestParseBadDuration(t *testing.T) { cases := []string{ "1", "1y1m1d", - "-1w", "1.5d", "d", "294y", @@ -322,7 +422,6 @@ func TestParseBadDuration(t *testing.T) { if err == nil { t.Errorf("Expected error on input %s", c) } - } } diff --git a/otlptranslator/constants.go b/otlptranslator/constants.go deleted file mode 100644 index d719daa1..00000000 --- a/otlptranslator/constants.go +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2025 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 otlptranslator - -const ( - // MetricMetadataTypeKey is the key used to store the original Prometheus - // type in metric metadata: - // https://github.com/open-telemetry/opentelemetry-specification/blob/e6eccba97ebaffbbfad6d4358408a2cead0ec2df/specification/compatibility/prometheus_and_openmetrics.md#metric-metadata - MetricMetadataTypeKey = "prometheus.type" - // ExemplarTraceIDKey is the key used to store the trace ID in Prometheus - // exemplars: - // https://github.com/open-telemetry/opentelemetry-specification/blob/e6eccba97ebaffbbfad6d4358408a2cead0ec2df/specification/compatibility/prometheus_and_openmetrics.md#exemplars - ExemplarTraceIDKey = "trace_id" - // ExemplarSpanIDKey is the key used to store the Span ID in Prometheus - // exemplars: - // https://github.com/open-telemetry/opentelemetry-specification/blob/e6eccba97ebaffbbfad6d4358408a2cead0ec2df/specification/compatibility/prometheus_and_openmetrics.md#exemplars - ExemplarSpanIDKey = "span_id" - // ScopeInfoMetricName is the name of the metric used to preserve scope - // attributes in Prometheus format: - // https://github.com/open-telemetry/opentelemetry-specification/blob/e6eccba97ebaffbbfad6d4358408a2cead0ec2df/specification/compatibility/prometheus_and_openmetrics.md#instrumentation-scope - ScopeInfoMetricName = "otel_scope_info" - // ScopeNameLabelKey is the name of the label key used to identify the name - // of the OpenTelemetry scope which produced the metric: - // https://github.com/open-telemetry/opentelemetry-specification/blob/e6eccba97ebaffbbfad6d4358408a2cead0ec2df/specification/compatibility/prometheus_and_openmetrics.md#instrumentation-scope - ScopeNameLabelKey = "otel_scope_name" - // ScopeVersionLabelKey is the name of the label key used to identify the - // version of the OpenTelemetry scope which produced the metric: - // https://github.com/open-telemetry/opentelemetry-specification/blob/e6eccba97ebaffbbfad6d4358408a2cead0ec2df/specification/compatibility/prometheus_and_openmetrics.md#instrumentation-scope - ScopeVersionLabelKey = "otel_scope_version" - // TargetInfoMetricName is the name of the metric used to preserve resource - // attributes in Prometheus format: - // https://github.com/open-telemetry/opentelemetry-specification/blob/e6eccba97ebaffbbfad6d4358408a2cead0ec2df/specification/compatibility/prometheus_and_openmetrics.md#resource-attributes-1 - // It originates from OpenMetrics: - // https://github.com/OpenObservability/OpenMetrics/blob/1386544931307dff279688f332890c31b6c5de36/specification/OpenMetrics.md#supporting-target-metadata-in-both-push-based-and-pull-based-systems - TargetInfoMetricName = "target_info" -) - -type MetricType string - -const ( - MetricTypeNonMonotonicCounter MetricType = "non-monotonic-counter" - MetricTypeMonotonicCounter MetricType = "monotonic-counter" - MetricTypeGauge MetricType = "gauge" - MetricTypeHistogram MetricType = "histogram" - MetricTypeExponentialHistogram MetricType = "exponential-histogram" - MetricTypeSummary MetricType = "summary" - MetricTypeUnknown MetricType = "unknown" -) diff --git a/otlptranslator/doc.go b/otlptranslator/doc.go deleted file mode 100644 index e5088064..00000000 --- a/otlptranslator/doc.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2025 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. - -// otlptranslator is a dependency free package that contains the logic for translating information, such as metric name, unit and type, -// from OpenTelemetry metrics to valid Prometheus metric and label names. -// -// Use BuildCompliantMetricName to build a metric name that complies with traditional Prometheus naming conventions. -// Such conventions exist from a time when Prometheus didn't have support for full UTF-8 characters in metric names. -// For more details see: https://prometheus.io/docs/practices/naming/ -// -// Use BuildMetricName to build a metric name that will be accepted by Prometheus with full UTF-8 support. -// -// Use NormalizeLabel to normalize a label name to a valid format that can be used in Prometheus before UTF-8 characters were supported. -// -// Deprecated: Use github.com/prometheus/otlptranslator instead. -package otlptranslator diff --git a/otlptranslator/label_builder.go b/otlptranslator/label_builder.go deleted file mode 100644 index deb3bbd0..00000000 --- a/otlptranslator/label_builder.go +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2025 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 otlptranslator - -import ( - "regexp" - "strings" - "unicode" -) - -var invalidLabelCharRE = regexp.MustCompile(`[^a-zA-Z0-9_]`) - -// Normalizes the specified label to follow Prometheus label names standard. -// -// See rules at https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels. -// -// Labels that start with non-letter rune will be prefixed with "key_". -// An exception is made for double-underscores which are allowed. -func NormalizeLabel(label string) string { - // Trivial case. - if len(label) == 0 { - return label - } - - label = SanitizeLabelName(label) - - // If label starts with a number, prepend with "key_". - if unicode.IsDigit(rune(label[0])) { - label = "key_" + label - } else if strings.HasPrefix(label, "_") && !strings.HasPrefix(label, "__") { - label = "key" + label - } - - return label -} - -// SanitizeLabelName replaces anything that doesn't match -// client_label.LabelNameRE with an underscore. -// Note: this does not handle all Prometheus label name restrictions (such as -// not starting with a digit 0-9), and hence should only be used if the label -// name is prefixed with a known valid string. -func SanitizeLabelName(name string) string { - return invalidLabelCharRE.ReplaceAllString(name, "_") -} diff --git a/otlptranslator/label_builder_bench_test.go b/otlptranslator/label_builder_bench_test.go deleted file mode 100644 index 6f2ba120..00000000 --- a/otlptranslator/label_builder_bench_test.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2025 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 otlptranslator - -import "testing" - -var labelBenchmarkInputs = []string{ - "", - "label:with:colons", - "LabelWithCapitalLetters", - "label!with&special$chars)", - "label_with_foreign_characters_字符", - "label.with.dots", - "123label", - "_label_starting_with_underscore", - "__label_starting_with_2underscores", -} - -func BenchmarkNormalizeLabel(b *testing.B) { - for i := 0; i < b.N; i++ { - for _, input := range labelBenchmarkInputs { - NormalizeLabel(input) - } - } -} diff --git a/otlptranslator/label_builder_test.go b/otlptranslator/label_builder_test.go deleted file mode 100644 index 48856a21..00000000 --- a/otlptranslator/label_builder_test.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2025 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 otlptranslator - -import ( - "fmt" - "testing" -) - -func TestNormalizeLabel(t *testing.T) { - tests := []struct { - label string - expected string - }{ - {"", ""}, - {"label:with:colons", "label_with_colons"}, - {"LabelWithCapitalLetters", "LabelWithCapitalLetters"}, - {"label!with&special$chars)", "label_with_special_chars_"}, - {"label_with_foreign_characters_字符", "label_with_foreign_characters___"}, - {"label.with.dots", "label_with_dots"}, - {"123label", "key_123label"}, - {"_label_starting_with_underscore", "key_label_starting_with_underscore"}, - {"__label_starting_with_2underscores", "__label_starting_with_2underscores"}, - } - - for i, test := range tests { - t.Run(fmt.Sprintf("test_%d", i), func(t *testing.T) { - result := NormalizeLabel(test.label) - if test.expected != result { - t.Errorf("expected %s, got %s", test.expected, result) - } - }) - } -} diff --git a/otlptranslator/metric_name_builder.go b/otlptranslator/metric_name_builder.go deleted file mode 100644 index a6b7d275..00000000 --- a/otlptranslator/metric_name_builder.go +++ /dev/null @@ -1,286 +0,0 @@ -// Copyright 2025 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 otlptranslator - -import ( - "regexp" - "slices" - "strings" - "unicode" -) - -// The map to translate OTLP units to Prometheus units -// OTLP metrics use the c/s notation as specified at https://ucum.org/ucum.html -// (See also https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/README.md#instrument-units) -// Prometheus best practices for units: https://prometheus.io/docs/practices/naming/#base-units -// OpenMetrics specification for units: https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#units-and-base-units -var unitMap = map[string]string{ - // Time - "d": "days", - "h": "hours", - "min": "minutes", - "s": "seconds", - "ms": "milliseconds", - "us": "microseconds", - "ns": "nanoseconds", - - // Bytes - "By": "bytes", - "KiBy": "kibibytes", - "MiBy": "mebibytes", - "GiBy": "gibibytes", - "TiBy": "tibibytes", - "KBy": "kilobytes", - "MBy": "megabytes", - "GBy": "gigabytes", - "TBy": "terabytes", - - // SI - "m": "meters", - "V": "volts", - "A": "amperes", - "J": "joules", - "W": "watts", - "g": "grams", - - // Misc - "Cel": "celsius", - "Hz": "hertz", - "1": "", - "%": "percent", -} - -// The map that translates the "per" unit -// Example: s => per second (singular) -var perUnitMap = map[string]string{ - "s": "second", - "m": "minute", - "h": "hour", - "d": "day", - "w": "week", - "mo": "month", - "y": "year", -} - -var ( - nonMetricNameCharRE = regexp.MustCompile(`[^a-zA-Z0-9:]`) - // Regexp for metric name characters that should be replaced with _. - invalidMetricCharRE = regexp.MustCompile(`[^a-zA-Z0-9:_]`) - multipleUnderscoresRE = regexp.MustCompile(`__+`) -) - -// BuildMetricName builds a valid metric name but without following Prometheus naming conventions. -// It doesn't do any character transformation, it only prefixes the metric name with the namespace, if any, -// and adds metric type suffixes, e.g. "_total" for counters and unit suffixes. -// -// Differently from BuildCompliantMetricName, it doesn't check for the presence of unit and type suffixes. -// If "addMetricSuffixes" is true, it will add them anyway. -// -// Please use BuildCompliantMetricName for a metric name that follows Prometheus naming conventions. -func BuildMetricName(name, unit string, metricType MetricType, addMetricSuffixes bool) string { - if addMetricSuffixes { - mainUnitSuffix, perUnitSuffix := buildUnitSuffixes(unit) - if mainUnitSuffix != "" { - name = name + "_" + mainUnitSuffix - } - if perUnitSuffix != "" { - name = name + "_" + perUnitSuffix - } - - // Append _total for Counters - if metricType == MetricTypeMonotonicCounter { - name = name + "_total" - } - - // Append _ratio for metrics with unit "1" - // Some OTel receivers improperly use unit "1" for counters of objects - // See https://github.com/open-telemetry/opentelemetry-collector-contrib/issues?q=is%3Aissue+some+metric+units+don%27t+follow+otel+semantic+conventions - // Until these issues have been fixed, we're appending `_ratio` for gauges ONLY - // Theoretically, counters could be ratios as well, but it's absurd (for mathematical reasons) - if unit == "1" && metricType == MetricTypeGauge { - name = name + "_ratio" - } - } - return name -} - -// BuildCompliantMetricName builds a Prometheus-compliant metric name for the specified metric. -// -// Metric name is prefixed with specified namespace and underscore (if any). -// Namespace is not cleaned up. Make sure specified namespace follows Prometheus -// naming convention. -// -// See rules at https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels, -// https://prometheus.io/docs/practices/naming/#metric-and-label-naming -// and https://github.com/open-telemetry/opentelemetry-specification/blob/v1.38.0/specification/compatibility/prometheus_and_openmetrics.md#otlp-metric-points-to-prometheus. -func BuildCompliantMetricName(name, unit string, metricType MetricType, addMetricSuffixes bool) string { - // Full normalization following standard Prometheus naming conventions - if addMetricSuffixes { - return normalizeName(name, unit, metricType) - } - - // Simple case (no full normalization, no units, etc.). - metricName := strings.Join(strings.FieldsFunc(name, func(r rune) bool { - return invalidMetricCharRE.MatchString(string(r)) - }), "_") - - // Metric name starts with a digit? Prefix it with an underscore. - if metricName != "" && unicode.IsDigit(rune(metricName[0])) { - metricName = "_" + metricName - } - - return metricName -} - -// Build a normalized name for the specified metric. -func normalizeName(metric, unit string, metricType MetricType) string { - // Split metric name into "tokens" (of supported metric name runes). - // Note that this has the side effect of replacing multiple consecutive underscores with a single underscore. - // This is part of the OTel to Prometheus specification: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.38.0/specification/compatibility/prometheus_and_openmetrics.md#otlp-metric-points-to-prometheus. - nameTokens := strings.FieldsFunc( - metric, - func(r rune) bool { return nonMetricNameCharRE.MatchString(string(r)) }, - ) - - mainUnitSuffix, perUnitSuffix := buildUnitSuffixes(unit) - nameTokens = addUnitTokens(nameTokens, CleanUpString(mainUnitSuffix), CleanUpString(perUnitSuffix)) - - // Append _total for Counters - if metricType == MetricTypeMonotonicCounter { - nameTokens = append(removeItem(nameTokens, "total"), "total") - } - - // Append _ratio for metrics with unit "1" - // Some OTel receivers improperly use unit "1" for counters of objects - // See https://github.com/open-telemetry/opentelemetry-collector-contrib/issues?q=is%3Aissue+some+metric+units+don%27t+follow+otel+semantic+conventions - // Until these issues have been fixed, we're appending `_ratio` for gauges ONLY - // Theoretically, counters could be ratios as well, but it's absurd (for mathematical reasons) - if unit == "1" && metricType == MetricTypeGauge { - nameTokens = append(removeItem(nameTokens, "ratio"), "ratio") - } - - // Build the string from the tokens, separated with underscores - normalizedName := strings.Join(nameTokens, "_") - - // Metric name cannot start with a digit, so prefix it with "_" in this case - if normalizedName != "" && unicode.IsDigit(rune(normalizedName[0])) { - normalizedName = "_" + normalizedName - } - - return normalizedName -} - -// buildUnitSuffixes builds the main and per unit suffixes for the specified unit -// but doesn't do any special character transformation to accommodate Prometheus naming conventions. -// Removing trailing underscores or appending suffixes is done in the caller. -func buildUnitSuffixes(unit string) (mainUnitSuffix, perUnitSuffix string) { - // Split unit at the '/' if any - unitTokens := strings.SplitN(unit, "/", 2) - - if len(unitTokens) > 0 { - // Main unit - // Update if not blank and doesn't contain '{}' - mainUnitOTel := strings.TrimSpace(unitTokens[0]) - if mainUnitOTel != "" && !strings.ContainsAny(mainUnitOTel, "{}") { - mainUnitSuffix = unitMapGetOrDefault(mainUnitOTel) - } - - // Per unit - // Update if not blank and doesn't contain '{}' - if len(unitTokens) > 1 && unitTokens[1] != "" { - perUnitOTel := strings.TrimSpace(unitTokens[1]) - if perUnitOTel != "" && !strings.ContainsAny(perUnitOTel, "{}") { - perUnitSuffix = perUnitMapGetOrDefault(perUnitOTel) - } - if perUnitSuffix != "" { - perUnitSuffix = "per_" + perUnitSuffix - } - } - } - - return mainUnitSuffix, perUnitSuffix -} - -// Retrieve the Prometheus "basic" unit corresponding to the specified "basic" unit -// Returns the specified unit if not found in unitMap -func unitMapGetOrDefault(unit string) string { - if promUnit, ok := unitMap[unit]; ok { - return promUnit - } - return unit -} - -// Retrieve the Prometheus "per" unit corresponding to the specified "per" unit -// Returns the specified unit if not found in perUnitMap -func perUnitMapGetOrDefault(perUnit string) string { - if promPerUnit, ok := perUnitMap[perUnit]; ok { - return promPerUnit - } - return perUnit -} - -// addUnitTokens will add the suffixes to the nameTokens if they are not already present. -// It will also remove trailing underscores from the main suffix to avoid double underscores -// when joining the tokens. -// -// If the 'per' unit ends with underscore, the underscore will be removed. If the per unit is just -// 'per_', it will be entirely removed. -func addUnitTokens(nameTokens []string, mainUnitSuffix, perUnitSuffix string) []string { - if slices.Contains(nameTokens, mainUnitSuffix) { - mainUnitSuffix = "" - } - - if perUnitSuffix == "per_" { - perUnitSuffix = "" - } else { - perUnitSuffix = strings.TrimSuffix(perUnitSuffix, "_") - if slices.Contains(nameTokens, perUnitSuffix) { - perUnitSuffix = "" - } - } - - if perUnitSuffix != "" { - mainUnitSuffix = strings.TrimSuffix(mainUnitSuffix, "_") - } - - if mainUnitSuffix != "" { - nameTokens = append(nameTokens, mainUnitSuffix) - } - if perUnitSuffix != "" { - nameTokens = append(nameTokens, perUnitSuffix) - } - return nameTokens -} - -// CleanUpString cleans up a string so it matches model.LabelNameRE. -// CleanUpString is usually used to clean up unit strings, but can be used for any string, e.g. namespaces. -func CleanUpString(s string) string { - // Multiple consecutive underscores are replaced with a single underscore. - // This is part of the OTel to Prometheus specification: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.38.0/specification/compatibility/prometheus_and_openmetrics.md#otlp-metric-points-to-prometheus. - return strings.TrimPrefix(multipleUnderscoresRE.ReplaceAllString( - nonMetricNameCharRE.ReplaceAllString(s, "_"), - "_", - ), "_") -} - -// Remove the specified value from the slice -func removeItem(slice []string, value string) []string { - newSlice := make([]string, 0, len(slice)) - for _, sliceEntry := range slice { - if sliceEntry != value { - newSlice = append(newSlice, sliceEntry) - } - } - return newSlice -} diff --git a/otlptranslator/metric_name_builder_bench_test.go b/otlptranslator/metric_name_builder_bench_test.go deleted file mode 100644 index bc0851d3..00000000 --- a/otlptranslator/metric_name_builder_bench_test.go +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright 2025 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 otlptranslator - -import ( - "fmt" - "testing" -) - -var benchmarkInputs = []struct { - name string - metricName string - unit string - metricType MetricType -}{ - { - name: "simple_metric", - metricName: "http_requests", - unit: "", - metricType: MetricTypeGauge, - }, - { - name: "compound_unit", - metricName: "request_throughput", - unit: "By/s", - metricType: MetricTypeMonotonicCounter, - }, - { - name: "complex_unit", - metricName: "disk_usage", - unit: "KiBy/m", - metricType: MetricTypeGauge, - }, - { - name: "ratio_metric", - metricName: "cpu_utilization", - unit: "1", - metricType: MetricTypeGauge, - }, - { - name: "metric_with_dots", - metricName: "system.cpu.usage.idle", - unit: "%", - metricType: MetricTypeGauge, - }, - { - name: "metric_with_unicode", - metricName: "メモリ使用率", - unit: "By", - metricType: MetricTypeGauge, - }, - { - name: "metric_with_special_chars", - metricName: "error-rate@host{instance}/service#component", - unit: "ms", - metricType: MetricTypeMonotonicCounter, - }, - { - name: "metric_with_multiple_slashes", - metricName: "network/throughput/total", - unit: "By/s/min", - metricType: MetricTypeGauge, - }, - { - name: "metric_with_spaces", - metricName: "api response time total", - unit: "ms", - metricType: MetricTypeMonotonicCounter, - }, - { - name: "metric_with_curly_braces", - metricName: "custom_{tag}_metric", - unit: "{custom}/s", - metricType: MetricTypeGauge, - }, - { - name: "metric_starting_with_digit", - metricName: "5xx_error_count", - unit: "1", - metricType: MetricTypeMonotonicCounter, - }, - { - name: "empty_metric", - metricName: "", - unit: "", - metricType: MetricTypeGauge, - }, - { - name: "metric_with_SI_units", - metricName: "power_consumption", - unit: "W", - metricType: MetricTypeGauge, - }, - { - name: "metric_with_temperature", - metricName: "server_temperature", - unit: "Cel", - metricType: MetricTypeGauge, - }, -} - -func BenchmarkBuildMetricName(b *testing.B) { - for _, addSuffixes := range []bool{true, false} { - b.Run(fmt.Sprintf("with_metric_suffixes=%t", addSuffixes), func(b *testing.B) { - for _, input := range benchmarkInputs { - for i := 0; i < b.N; i++ { - BuildMetricName(input.metricName, input.unit, input.metricType, addSuffixes) - } - } - }) - } -} - -func BenchmarkBuildCompliantMetricName(b *testing.B) { - for _, addSuffixes := range []bool{true, false} { - b.Run(fmt.Sprintf("with_metric_suffixes=%t", addSuffixes), func(b *testing.B) { - for _, input := range benchmarkInputs { - for i := 0; i < b.N; i++ { - BuildCompliantMetricName(input.metricName, input.unit, input.metricType, addSuffixes) - } - } - }) - } -} diff --git a/otlptranslator/metric_name_builder_test.go b/otlptranslator/metric_name_builder_test.go deleted file mode 100644 index 49b729b4..00000000 --- a/otlptranslator/metric_name_builder_test.go +++ /dev/null @@ -1,428 +0,0 @@ -// Copyright 2025 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 otlptranslator - -import ( - "reflect" - "testing" -) - -func TestBuildMetricName(t *testing.T) { - tests := []struct { - name string - metricName string - unit string - metricType MetricType - addMetricSuffixes bool - expected string - }{ - { - name: "simple metric without suffixes", - metricName: "http_requests", - unit: "", - metricType: MetricTypeGauge, - addMetricSuffixes: false, - expected: "http_requests", - }, - { - name: "counter with total suffix", - metricName: "http_requests", - unit: "", - metricType: MetricTypeMonotonicCounter, - addMetricSuffixes: true, - expected: "http_requests_total", - }, - { - name: "gauge with time unit", - metricName: "request_duration", - unit: "s", - metricType: MetricTypeGauge, - addMetricSuffixes: true, - expected: "request_duration_seconds", - }, - { - name: "counter with time unit", - metricName: "request_duration", - unit: "ms", - metricType: MetricTypeMonotonicCounter, - addMetricSuffixes: true, - expected: "request_duration_milliseconds_total", - }, - { - name: "gauge with compound unit", - metricName: "throughput", - unit: "By/s", - metricType: MetricTypeGauge, - addMetricSuffixes: true, - expected: "throughput_bytes_per_second", - }, - { - name: "ratio metric", - metricName: "cpu_utilization", - unit: "1", - metricType: MetricTypeGauge, - addMetricSuffixes: true, - expected: "cpu_utilization_ratio", - }, - { - name: "counter with unit 1 (no ratio suffix)", - metricName: "error_count", - unit: "1", - metricType: MetricTypeMonotonicCounter, - addMetricSuffixes: true, - expected: "error_count_total", - }, - { - name: "metric with byte units", - metricName: "memory_usage", - unit: "MiBy", - metricType: MetricTypeGauge, - addMetricSuffixes: true, - expected: "memory_usage_mebibytes", - }, - { - name: "metric with SI units", - metricName: "temperature", - unit: "Cel", - metricType: MetricTypeGauge, - addMetricSuffixes: true, - expected: "temperature_celsius", - }, - { - name: "metric with dots", - metricName: "system.cpu.usage", - unit: "1", - metricType: MetricTypeGauge, - addMetricSuffixes: true, - expected: "system.cpu.usage_ratio", - }, - { - name: "metric with japanese characters (memory usage rate)", - metricName: "メモリ使用率", // memori shiyouritsu (memory usage rate) xD - unit: "By", - metricType: MetricTypeGauge, - addMetricSuffixes: true, - expected: "メモリ使用率_bytes", - }, - { - name: "metric with mixed special characters (system.memory.usage.rate)", - metricName: "system.メモリ.usage.率", // system.memory.usage.rate - unit: "By/s", - metricType: MetricTypeGauge, - addMetricSuffixes: true, - expected: "system.メモリ.usage.率_bytes_per_second", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := BuildMetricName(tt.metricName, tt.unit, tt.metricType, tt.addMetricSuffixes) - if tt.expected != result { - t.Errorf("expected %s, got %s", tt.expected, result) - } - }) - } -} - -func TestBuildUnitSuffixes(t *testing.T) { - tests := []struct { - name string - unit string - expectedMain string - expectedPerUnit string - }{ - { - name: "empty unit", - unit: "", - expectedMain: "", - expectedPerUnit: "", - }, - { - name: "simple time unit", - unit: "s", - expectedMain: "seconds", - expectedPerUnit: "", - }, - { - name: "compound unit", - unit: "By/s", - expectedMain: "bytes", - expectedPerUnit: "per_second", - }, - { - name: "complex compound unit", - unit: "KiBy/m", - expectedMain: "kibibytes", - expectedPerUnit: "per_minute", - }, - { - name: "unit with spaces", - unit: " ms / s ", - expectedMain: "milliseconds", - expectedPerUnit: "per_second", - }, - { - name: "invalid unit", - unit: "invalid", - expectedMain: "invalid", - expectedPerUnit: "", - }, - { - name: "unit with curly braces", - unit: "{custom}/s", - expectedMain: "", - expectedPerUnit: "per_second", - }, - { - name: "multiple slashes", - unit: "By/s/h", - expectedMain: "bytes", - expectedPerUnit: "per_s/h", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mainUnit, perUnit := buildUnitSuffixes(tt.unit) - if tt.expectedMain != mainUnit { - t.Errorf("expected main unit %s, got %s", tt.expectedMain, mainUnit) - } - if tt.expectedPerUnit != perUnit { - t.Errorf("expected per unit %s, got %s", tt.expectedPerUnit, perUnit) - } - }) - } -} - -func TestBuildCompliantMetricName(t *testing.T) { - tests := []struct { - name string - metricName string - unit string - metricType MetricType - addMetricSuffixes bool - expected string - }{ - { - name: "simple valid metric name", - metricName: "http_requests", - unit: "", - metricType: MetricTypeGauge, - addMetricSuffixes: false, - expected: "http_requests", - }, - { - name: "metric name with invalid characters", - metricName: "http-requests@in_flight", - unit: "", - metricType: MetricTypeNonMonotonicCounter, - addMetricSuffixes: false, - expected: "http_requests_in_flight", - }, - { - name: "metric name starting with digit", - metricName: "5xx_errors", - unit: "", - metricType: MetricTypeGauge, - addMetricSuffixes: false, - expected: "_5xx_errors", - }, - { - name: "metric name starting with digit, with suffixes", - metricName: "5xx_errors", - unit: "", - metricType: MetricTypeMonotonicCounter, - addMetricSuffixes: true, - expected: "_5xx_errors_total", - }, - { - name: "metric name with multiple consecutive invalid chars", - metricName: "api..//request--time", - unit: "", - metricType: MetricTypeGauge, - addMetricSuffixes: false, - expected: "api_request_time", - }, - { - name: "full normalization with units and type", - metricName: "system.cpu-utilization", - unit: "ms/s", - metricType: MetricTypeMonotonicCounter, - addMetricSuffixes: true, - expected: "system_cpu_utilization_milliseconds_per_second_total", - }, - { - name: "metric with special characters and ratio", - metricName: "memory.usage%rate", - unit: "1", - metricType: MetricTypeGauge, - addMetricSuffixes: true, - expected: "memory_usage_rate_ratio", - }, - { - name: "metric with unicode characters", - metricName: "error_rate_£_€_¥", - unit: "", - metricType: MetricTypeGauge, - addMetricSuffixes: false, - expected: "error_rate_____", - }, - { - name: "metric with multiple spaces", - metricName: "api response time", - unit: "ms", - metricType: MetricTypeGauge, - addMetricSuffixes: true, - expected: "api_response_time_milliseconds", - }, - { - name: "metric with colons (valid prometheus chars)", - metricName: "app:request:latency", - unit: "s", - metricType: MetricTypeGauge, - addMetricSuffixes: true, - expected: "app:request:latency_seconds", - }, - { - name: "empty metric name", - metricName: "", - unit: "", - metricType: MetricTypeGauge, - addMetricSuffixes: false, - expected: "", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := BuildCompliantMetricName(tt.metricName, tt.unit, tt.metricType, tt.addMetricSuffixes) - if tt.expected != result { - t.Errorf("expected %s, got %s", tt.expected, result) - } - }) - } -} - -func TestAddUnitTokens(t *testing.T) { - tests := []struct { - nameTokens []string - mainUnitSuffix string - perUnitSuffix string - expected []string - }{ - {[]string{}, "", "", []string{}}, - {[]string{"token1"}, "main", "", []string{"token1", "main"}}, - {[]string{"token1"}, "", "per", []string{"token1", "per"}}, - {[]string{"token1"}, "main", "per", []string{"token1", "main", "per"}}, - {[]string{"token1", "per"}, "main", "per", []string{"token1", "per", "main"}}, - {[]string{"token1", "main"}, "main", "per", []string{"token1", "main", "per"}}, - {[]string{"token1"}, "main_", "per", []string{"token1", "main", "per"}}, - {[]string{"token1"}, "main_unit", "per_seconds_", []string{"token1", "main_unit", "per_seconds"}}, // trailing underscores are removed - {[]string{"token1"}, "main_unit", "per_", []string{"token1", "main_unit"}}, // 'per_' is removed entirely - } - - for _, test := range tests { - result := addUnitTokens(test.nameTokens, test.mainUnitSuffix, test.perUnitSuffix) - if !reflect.DeepEqual(test.expected, result) { - t.Errorf("expected %v, got %v", test.expected, result) - } - } -} - -func TestRemoveItem(t *testing.T) { - if !reflect.DeepEqual([]string{}, removeItem([]string{}, "test")) { - t.Errorf("expected %v, got %v", []string{}, removeItem([]string{}, "test")) - } - if !reflect.DeepEqual([]string{}, removeItem([]string{}, "")) { - t.Errorf("expected %v, got %v", []string{}, removeItem([]string{}, "")) - } - if !reflect.DeepEqual([]string{"a", "b", "c"}, removeItem([]string{"a", "b", "c"}, "d")) { - t.Errorf("expected %v, got %v", []string{"a", "b", "c"}, removeItem([]string{"a", "b", "c"}, "d")) - } - if !reflect.DeepEqual([]string{"a", "b", "c"}, removeItem([]string{"a", "b", "c"}, "")) { - t.Errorf("expected %v, got %v", []string{"a", "b", "c"}, removeItem([]string{"a", "b", "c"}, "")) - } - if !reflect.DeepEqual([]string{"a", "b"}, removeItem([]string{"a", "b", "c"}, "c")) { - t.Errorf("expected %v, got %v", []string{"a", "b"}, removeItem([]string{"a", "b", "c"}, "c")) - } - if !reflect.DeepEqual([]string{"a", "c"}, removeItem([]string{"a", "b", "c"}, "b")) { - t.Errorf("expected %v, got %v", []string{"a", "c"}, removeItem([]string{"a", "b", "c"}, "b")) - } - if !reflect.DeepEqual([]string{"b", "c"}, removeItem([]string{"a", "b", "c"}, "a")) { - t.Errorf("expected %v, got %v", []string{"b", "c"}, removeItem([]string{"a", "b", "c"}, "a")) - } -} - -func TestCleanUpStrings(t *testing.T) { - tests := []struct { - name string - input string - expected string - }{ - { - name: "already valid string", - input: "valid_metric_name", - expected: "valid_metric_name", - }, - { - name: "invalid characters", - input: "metric-name@with#special$chars", - expected: "metric_name_with_special_chars", - }, - { - name: "multiple consecutive invalid chars", - input: "metric---name###special", - expected: "metric_name_special", - }, - { - name: "leading invalid chars", - input: "@#$metric_name", - expected: "metric_name", - }, - { - name: "trailing invalid chars", - input: "metric_name@#$", - expected: "metric_name_", - }, - { - name: "multiple consecutive underscores", - input: "metric___name____test", - expected: "metric_name_test", - }, - { - name: "empty string", - input: "", - expected: "", - }, - { - name: "only invalid chars", - input: "@#$%^&", - expected: "", - }, - { - name: "colons are valid", - input: "system.cpu:usage.rate", - expected: "system_cpu:usage_rate", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := CleanUpString(tt.input) - if result != tt.expected { - t.Errorf("expected %q, got %q", tt.expected, result) - } - }) - } -} diff --git a/promslog/slog.go b/promslog/slog.go index 3bd81732..8da43aef 100644 --- a/promslog/slog.go +++ b/promslog/slog.go @@ -76,6 +76,11 @@ func (l *Level) UnmarshalYAML(unmarshal func(interface{}) error) error { return nil } +// Level returns the value of the logging level as an slog.Level. +func (l *Level) Level() slog.Level { + return l.lvl.Level() +} + // String returns the current level. func (l *Level) String() string { switch l.lvl.Level() {