Skip to content

Commit cf3c56f

Browse files
authored
Merge pull request #768 from prometheus/otlp-translator
otlptranslator: Add dependency free package that translates OTLP data into Prometheus metric/label names
2 parents a9cc7f7 + b35ad99 commit cf3c56f

8 files changed

+1063
-0
lines changed

otlptranslator/constants.go

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright 2025 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
package otlptranslator
14+
15+
const (
16+
// MetricMetadataTypeKey is the key used to store the original Prometheus
17+
// type in metric metadata:
18+
// https://github.com/open-telemetry/opentelemetry-specification/blob/e6eccba97ebaffbbfad6d4358408a2cead0ec2df/specification/compatibility/prometheus_and_openmetrics.md#metric-metadata
19+
MetricMetadataTypeKey = "prometheus.type"
20+
// ExemplarTraceIDKey is the key used to store the trace ID in Prometheus
21+
// exemplars:
22+
// https://github.com/open-telemetry/opentelemetry-specification/blob/e6eccba97ebaffbbfad6d4358408a2cead0ec2df/specification/compatibility/prometheus_and_openmetrics.md#exemplars
23+
ExemplarTraceIDKey = "trace_id"
24+
// ExemplarSpanIDKey is the key used to store the Span ID in Prometheus
25+
// exemplars:
26+
// https://github.com/open-telemetry/opentelemetry-specification/blob/e6eccba97ebaffbbfad6d4358408a2cead0ec2df/specification/compatibility/prometheus_and_openmetrics.md#exemplars
27+
ExemplarSpanIDKey = "span_id"
28+
// ScopeInfoMetricName is the name of the metric used to preserve scope
29+
// attributes in Prometheus format:
30+
// https://github.com/open-telemetry/opentelemetry-specification/blob/e6eccba97ebaffbbfad6d4358408a2cead0ec2df/specification/compatibility/prometheus_and_openmetrics.md#instrumentation-scope
31+
ScopeInfoMetricName = "otel_scope_info"
32+
// ScopeNameLabelKey is the name of the label key used to identify the name
33+
// of the OpenTelemetry scope which produced the metric:
34+
// https://github.com/open-telemetry/opentelemetry-specification/blob/e6eccba97ebaffbbfad6d4358408a2cead0ec2df/specification/compatibility/prometheus_and_openmetrics.md#instrumentation-scope
35+
ScopeNameLabelKey = "otel_scope_name"
36+
// ScopeVersionLabelKey is the name of the label key used to identify the
37+
// version of the OpenTelemetry scope which produced the metric:
38+
// https://github.com/open-telemetry/opentelemetry-specification/blob/e6eccba97ebaffbbfad6d4358408a2cead0ec2df/specification/compatibility/prometheus_and_openmetrics.md#instrumentation-scope
39+
ScopeVersionLabelKey = "otel_scope_version"
40+
// TargetInfoMetricName is the name of the metric used to preserve resource
41+
// attributes in Prometheus format:
42+
// https://github.com/open-telemetry/opentelemetry-specification/blob/e6eccba97ebaffbbfad6d4358408a2cead0ec2df/specification/compatibility/prometheus_and_openmetrics.md#resource-attributes-1
43+
// It originates from OpenMetrics:
44+
// https://github.com/OpenObservability/OpenMetrics/blob/1386544931307dff279688f332890c31b6c5de36/specification/OpenMetrics.md#supporting-target-metadata-in-both-push-based-and-pull-based-systems
45+
TargetInfoMetricName = "target_info"
46+
)
47+
48+
type MetricType string
49+
50+
const (
51+
MetricTypeNonMonotonicCounter MetricType = "non-monotonic-counter"
52+
MetricTypeMonotonicCounter MetricType = "monotonic-counter"
53+
MetricTypeGauge MetricType = "gauge"
54+
MetricTypeHistogram MetricType = "histogram"
55+
MetricTypeExponentialHistogram MetricType = "exponential-histogram"
56+
MetricTypeSummary MetricType = "summary"
57+
MetricTypeUnknown MetricType = "unknown"
58+
)

otlptranslator/doc.go

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright 2025 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
// otlptranslator is a dependency free package that contains the logic for translating information, such as metric name, unit and type,
15+
// from OpenTelemetry metrics to valid Prometheus metric and label names.
16+
//
17+
// Use BuildCompliantMetricName to build a metric name that complies with traditional Prometheus naming conventions.
18+
// Such conventions exist from a time when Prometheus didn't have support for full UTF-8 characters in metric names.
19+
// For more details see: https://prometheus.io/docs/practices/naming/
20+
//
21+
// Use BuildMetricName to build a metric name that will be accepted by Prometheus with full UTF-8 support.
22+
//
23+
// Use NormalizeLabel to normalize a label name to a valid format that can be used in Prometheus before UTF-8 characters were supported.
24+
package otlptranslator

otlptranslator/label_builder.go

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright 2025 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
package otlptranslator
14+
15+
import (
16+
"regexp"
17+
"strings"
18+
"unicode"
19+
)
20+
21+
var invalidLabelCharRE = regexp.MustCompile(`[^a-zA-Z0-9_]`)
22+
23+
// Normalizes the specified label to follow Prometheus label names standard.
24+
//
25+
// See rules at https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels.
26+
//
27+
// Labels that start with non-letter rune will be prefixed with "key_".
28+
// An exception is made for double-underscores which are allowed.
29+
func NormalizeLabel(label string) string {
30+
// Trivial case.
31+
if len(label) == 0 {
32+
return label
33+
}
34+
35+
label = SanitizeLabelName(label)
36+
37+
// If label starts with a number, prepend with "key_".
38+
if unicode.IsDigit(rune(label[0])) {
39+
label = "key_" + label
40+
} else if strings.HasPrefix(label, "_") && !strings.HasPrefix(label, "__") {
41+
label = "key" + label
42+
}
43+
44+
return label
45+
}
46+
47+
// SanitizeLabelName replaces anything that doesn't match
48+
// client_label.LabelNameRE with an underscore.
49+
// Note: this does not handle all Prometheus label name restrictions (such as
50+
// not starting with a digit 0-9), and hence should only be used if the label
51+
// name is prefixed with a known valid string.
52+
func SanitizeLabelName(name string) string {
53+
return invalidLabelCharRE.ReplaceAllString(name, "_")
54+
}
+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright 2025 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
package otlptranslator
14+
15+
import "testing"
16+
17+
var labelBenchmarkInputs = []string{
18+
"",
19+
"label:with:colons",
20+
"LabelWithCapitalLetters",
21+
"label!with&special$chars)",
22+
"label_with_foreign_characters_字符",
23+
"label.with.dots",
24+
"123label",
25+
"_label_starting_with_underscore",
26+
"__label_starting_with_2underscores",
27+
}
28+
29+
func BenchmarkNormalizeLabel(b *testing.B) {
30+
for i := 0; i < b.N; i++ {
31+
for _, input := range labelBenchmarkInputs {
32+
NormalizeLabel(input)
33+
}
34+
}
35+
}

otlptranslator/label_builder_test.go

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright 2025 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
package otlptranslator
14+
15+
import (
16+
"fmt"
17+
"testing"
18+
)
19+
20+
func TestNormalizeLabel(t *testing.T) {
21+
tests := []struct {
22+
label string
23+
expected string
24+
}{
25+
{"", ""},
26+
{"label:with:colons", "label_with_colons"},
27+
{"LabelWithCapitalLetters", "LabelWithCapitalLetters"},
28+
{"label!with&special$chars)", "label_with_special_chars_"},
29+
{"label_with_foreign_characters_字符", "label_with_foreign_characters___"},
30+
{"label.with.dots", "label_with_dots"},
31+
{"123label", "key_123label"},
32+
{"_label_starting_with_underscore", "key_label_starting_with_underscore"},
33+
{"__label_starting_with_2underscores", "__label_starting_with_2underscores"},
34+
}
35+
36+
for i, test := range tests {
37+
t.Run(fmt.Sprintf("test_%d", i), func(t *testing.T) {
38+
result := NormalizeLabel(test.label)
39+
if test.expected != result {
40+
t.Errorf("expected %s, got %s", test.expected, result)
41+
}
42+
})
43+
}
44+
}

0 commit comments

Comments
 (0)