Skip to content

Commit 2edaccb

Browse files
authored
Escape exemplar keys to fix invalid key errors (#5995)
Fixes #5936
1 parent 99c3c66 commit 2edaccb

File tree

3 files changed

+69
-18
lines changed

3 files changed

+69
-18
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
2121
- Fix inconsistent request body closing in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp`. (#5954)
2222
- Fix inconsistent request body closing in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp`. (#5954)
2323
- Fix inconsistent request body closing in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp`. (#5954)
24+
- Fix invalid exemplar keys in `go.opentelemetry.io/otel/exporters/prometheus`. (#5995)
2425

2526
<!-- Released section -->
2627
<!-- Don't change this section unless doing release -->

exporters/prometheus/exporter.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,8 @@ func addExemplars[N int64 | float64](m prometheus.Metric, exemplars []metricdata
547547
func attributesToLabels(attrs []attribute.KeyValue) prometheus.Labels {
548548
labels := make(map[string]string)
549549
for _, attr := range attrs {
550-
labels[string(attr.Key)] = attr.Value.Emit()
550+
key := model.EscapeName(string(attr.Key), model.NameEscapingScheme)
551+
labels[key] = attr.Value.Emit()
551552
}
552553
return labels
553554
}

exporters/prometheus/exporter_test.go

+66-17
Original file line numberDiff line numberDiff line change
@@ -949,37 +949,94 @@ func TestShutdownExporter(t *testing.T) {
949949

950950
func TestExemplars(t *testing.T) {
951951
attrsOpt := otelmetric.WithAttributes(
952-
attribute.Key("A").String("B"),
953-
attribute.Key("C").String("D"),
954-
attribute.Key("E").Bool(true),
955-
attribute.Key("F").Int(42),
952+
attribute.Key("A.1").String("B"),
953+
attribute.Key("C.2").String("D"),
954+
attribute.Key("E.3").Bool(true),
955+
attribute.Key("F.4").Int(42),
956956
)
957+
expectedNonEscapedLabels := map[string]string{
958+
traceIDExemplarKey: "01000000000000000000000000000000",
959+
spanIDExemplarKey: "0100000000000000",
960+
"A.1": "B",
961+
"C.2": "D",
962+
"E.3": "true",
963+
"F.4": "42",
964+
}
965+
expectedEscapedLabels := map[string]string{
966+
traceIDExemplarKey: "01000000000000000000000000000000",
967+
spanIDExemplarKey: "0100000000000000",
968+
"A_1": "B",
969+
"C_2": "D",
970+
"E_3": "true",
971+
"F_4": "42",
972+
}
957973
for _, tc := range []struct {
958974
name string
959975
recordMetrics func(ctx context.Context, meter otelmetric.Meter)
960976
expectedExemplarValue float64
977+
expectedLabels map[string]string
978+
escapingScheme model.EscapingScheme
979+
validationScheme model.ValidationScheme
961980
}{
962981
{
963-
name: "counter",
982+
name: "escaped counter",
964983
recordMetrics: func(ctx context.Context, meter otelmetric.Meter) {
965984
counter, err := meter.Float64Counter("foo")
966985
require.NoError(t, err)
967986
counter.Add(ctx, 9, attrsOpt)
968987
},
969988
expectedExemplarValue: 9,
989+
expectedLabels: expectedEscapedLabels,
990+
escapingScheme: model.UnderscoreEscaping,
991+
validationScheme: model.LegacyValidation,
970992
},
971993
{
972-
name: "histogram",
994+
name: "escaped histogram",
973995
recordMetrics: func(ctx context.Context, meter otelmetric.Meter) {
974996
hist, err := meter.Int64Histogram("foo")
975997
require.NoError(t, err)
976998
hist.Record(ctx, 9, attrsOpt)
977999
},
9781000
expectedExemplarValue: 9,
1001+
expectedLabels: expectedEscapedLabels,
1002+
escapingScheme: model.UnderscoreEscaping,
1003+
validationScheme: model.LegacyValidation,
1004+
},
1005+
{
1006+
name: "non-escaped counter",
1007+
recordMetrics: func(ctx context.Context, meter otelmetric.Meter) {
1008+
counter, err := meter.Float64Counter("foo")
1009+
require.NoError(t, err)
1010+
counter.Add(ctx, 9, attrsOpt)
1011+
},
1012+
expectedExemplarValue: 9,
1013+
expectedLabels: expectedNonEscapedLabels,
1014+
escapingScheme: model.NoEscaping,
1015+
validationScheme: model.UTF8Validation,
1016+
},
1017+
{
1018+
name: "non-escaped histogram",
1019+
recordMetrics: func(ctx context.Context, meter otelmetric.Meter) {
1020+
hist, err := meter.Int64Histogram("foo")
1021+
require.NoError(t, err)
1022+
hist.Record(ctx, 9, attrsOpt)
1023+
},
1024+
expectedExemplarValue: 9,
1025+
expectedLabels: expectedNonEscapedLabels,
1026+
escapingScheme: model.NoEscaping,
1027+
validationScheme: model.UTF8Validation,
9791028
},
9801029
} {
9811030
t.Run(tc.name, func(t *testing.T) {
982-
t.Setenv("OTEL_GO_X_EXEMPLAR", "true")
1031+
originalEscapingScheme := model.NameEscapingScheme
1032+
originalValidationScheme := model.NameValidationScheme
1033+
model.NameEscapingScheme = tc.escapingScheme
1034+
model.NameValidationScheme = tc.validationScheme
1035+
// Restore original value after the test is complete
1036+
defer func() {
1037+
model.NameEscapingScheme = originalEscapingScheme
1038+
model.NameValidationScheme = originalValidationScheme
1039+
}()
9831040
// initialize registry exporter
9841041
ctx := context.Background()
9851042
registry := prometheus.NewRegistry()
@@ -1044,17 +1101,9 @@ func TestExemplars(t *testing.T) {
10441101
}
10451102
require.NotNil(t, exemplar)
10461103
require.Equal(t, tc.expectedExemplarValue, exemplar.GetValue())
1047-
expectedLabels := map[string]string{
1048-
traceIDExemplarKey: "01000000000000000000000000000000",
1049-
spanIDExemplarKey: "0100000000000000",
1050-
"A": "B",
1051-
"C": "D",
1052-
"E": "true",
1053-
"F": "42",
1054-
}
1055-
require.Equal(t, len(expectedLabels), len(exemplar.GetLabel()))
1104+
require.Equal(t, len(tc.expectedLabels), len(exemplar.GetLabel()))
10561105
for _, label := range exemplar.GetLabel() {
1057-
val, ok := expectedLabels[label.GetName()]
1106+
val, ok := tc.expectedLabels[label.GetName()]
10581107
require.True(t, ok)
10591108
require.Equal(t, label.GetValue(), val)
10601109
}

0 commit comments

Comments
 (0)