From 098669c08af8eee3da33d1d4a4f242a5242cccb8 Mon Sep 17 00:00:00 2001 From: PrometheusBot Date: Sat, 17 May 2025 12:06:11 +0200 Subject: [PATCH 1/4] Update common Prometheus files (#789) Signed-off-by: prombot --- Makefile.common | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) 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 From 1de8cfa8452bf1c800b1e7a7705c1c37410d3326 Mon Sep 17 00:00:00 2001 From: Arthur Silva Sens Date: Tue, 27 May 2025 14:54:38 -0300 Subject: [PATCH 2/4] Remove otlptranslator package Signed-off-by: Arthur Silva Sens --- otlptranslator/constants.go | 58 --- otlptranslator/doc.go | 26 -- otlptranslator/label_builder.go | 54 --- otlptranslator/label_builder_bench_test.go | 35 -- otlptranslator/label_builder_test.go | 44 -- otlptranslator/metric_name_builder.go | 286 ------------ .../metric_name_builder_bench_test.go | 134 ------ otlptranslator/metric_name_builder_test.go | 428 ------------------ 8 files changed, 1065 deletions(-) delete mode 100644 otlptranslator/constants.go delete mode 100644 otlptranslator/doc.go delete mode 100644 otlptranslator/label_builder.go delete mode 100644 otlptranslator/label_builder_bench_test.go delete mode 100644 otlptranslator/label_builder_test.go delete mode 100644 otlptranslator/metric_name_builder.go delete mode 100644 otlptranslator/metric_name_builder_bench_test.go delete mode 100644 otlptranslator/metric_name_builder_test.go 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) - } - }) - } -} From 7bd5fff888acb05c04a857998772b9d7ffe3e8e4 Mon Sep 17 00:00:00 2001 From: TJ Hoplock <33664289+tjhop@users.noreply.github.com> Date: Sat, 21 Jun 2025 11:33:24 -0400 Subject: [PATCH 3/4] feat(promslog): add Level() method to get slog.Level (#795) Adds a `Level()` method to the promslog.Level type. We already expose the configured level with the `String()` method, but only in a specifically processed format. This means that downstream projects which need a true `slog.Level` value need to do shenanigans like we do in the blackbox_exporter here[1]. The slog.Level type is really just an int behind the scenes, but buys us access to the type's methods, obviously. If we want to make this a bit more flexible/"future proof", we could instead have the method return an slog.Leveler interface type. I don't think it's necessary though, as we'd probably only want to do that if we intended to expose access to promslog.Level's internal slog.LevelVar (since that also satisfies the slog.Leveler interface), and I don't think that's a good idea -- giving direct access would allow users to interact with the level var directly, providing an avenue to change level configurations outside of promslog's knowledge, resulting in unexpected behavior at best and incompatible behavior at worst. I think providing access to the value of the level as a proper slog.Level value as done here is useful enough on it's own and the safer option. 1. https://github.com/prometheus/blackbox_exporter/blob/f77c50ed7c0f39b734235931e773cf7b5af1fc8a/prober/handler.go#L236-L249 2. https://pkg.go.dev/log/slog#Level Signed-off-by: TJ Hoplock --- promslog/slog.go | 5 +++++ 1 file changed, 5 insertions(+) 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() { From 75c3814dc66c571cc82cee2d3a6bf5f37ee73f1a Mon Sep 17 00:00:00 2001 From: Dmitry Ponomaryov Date: Mon, 23 Jun 2025 03:58:37 +0500 Subject: [PATCH 4/4] feat: Support negative duration in new function ParseDurationAllowNegative (#793) Signed-off-by: Dmitry Ponomaryov --- model/time.go | 25 +++++- model/time_test.go | 203 +++++++++++++++++++++++++++++++++------------ 2 files changed, 173 insertions(+), 55 deletions(-) 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) } - } }