From 5c73e4bb0650ea944fb63357ebcc6a226d56b68d Mon Sep 17 00:00:00 2001 From: David Ashpole Date: Mon, 24 Aug 2026 15:50:59 +0000 Subject: [PATCH 1/2] fix: drop invalid OpenMetrics 2.0 exemplars instead of failing exposition Per the OpenMetrics 2.0 specification failure modes, failures specific to exemplars should not cause the entire exposition to fail. Invalid exemplars (e.g. invalid timestamps, malformed labels) are now dropped so that the rest of the metric exposition succeeds. Signed-off-by: David Ashpole --- expfmt/openmetrics_2_0_create.go | 15 ++-- expfmt/openmetrics_2_0_create_test.go | 120 ++++++++++++++++++++------ 2 files changed, 103 insertions(+), 32 deletions(-) diff --git a/expfmt/openmetrics_2_0_create.go b/expfmt/openmetrics_2_0_create.go index a218226b..2fe2b880 100644 --- a/expfmt/openmetrics_2_0_create.go +++ b/expfmt/openmetrics_2_0_create.go @@ -263,7 +263,7 @@ func writeOpenMetrics20Sample(w enhancedWriter, name string, metric *dto.Metric, } } - if exemplar != nil && exemplar.Timestamp != nil { + if exemplar != nil { n, err = writeExemplar20(w, exemplar) written += n if err != nil { @@ -280,13 +280,13 @@ func writeOpenMetrics20Sample(w enhancedWriter, name string, metric *dto.Metric, } // writeExemplar20 writes the provided exemplar in OpenMetrics 2.0 format to w. -// In OpenMetrics 2.0, exemplars without a timestamp are dropped. +// In OpenMetrics 2.0, invalid exemplars or exemplars without a timestamp are dropped. func writeExemplar20(w enhancedWriter, e *dto.Exemplar) (int, error) { - if e == nil || e.Timestamp == nil { + if e == nil { return 0, nil } if err := validateExemplar20(e); err != nil { - return 0, err + return 0, nil } written := 0 n, err := w.WriteString(" # ") @@ -318,10 +318,6 @@ func writeExemplar20(w enhancedWriter, e *dto.Exemplar) (int, error) { if err != nil { return written, err } - err = e.Timestamp.CheckValid() - if err != nil { - return written, err - } ts := e.Timestamp n, err = writeProtoTimestamp(w, ts) written += n @@ -387,6 +383,9 @@ func containsRawNewline(s string) bool { } func validateExemplar20(e *dto.Exemplar) error { + if e.Timestamp == nil { + return errors.New("exemplar timestamp is required") + } if err := e.Timestamp.CheckValid(); err != nil { return err } diff --git a/expfmt/openmetrics_2_0_create_test.go b/expfmt/openmetrics_2_0_create_test.go index 747bff4c..5a2fa005 100644 --- a/expfmt/openmetrics_2_0_create_test.go +++ b/expfmt/openmetrics_2_0_create_test.go @@ -205,6 +205,102 @@ http_requests_total 1027.0 # {} 1.0 1234567890.5 }, out: `# TYPE http_requests_total counter http_requests_total 1027.0 +`, + }, + { + name: "CounterWithInvalidExemplarTimestamp", + in: &dto.MetricFamily{ + Name: proto.String("http_requests_total"), + Type: dto.MetricType_COUNTER.Enum(), + Metric: []*dto.Metric{ + { + Counter: &dto.Counter{ + Value: proto.Float64(1027), + Exemplar: &dto.Exemplar{ + Label: []*dto.LabelPair{ + {Name: proto.String("trace_id"), Value: proto.String("1234")}, + }, + Value: proto.Float64(1), + Timestamp: ×tamppb.Timestamp{ + Nanos: -1, + }, + }, + }, + }, + }, + }, + out: `# TYPE http_requests_total counter +http_requests_total 1027 +`, + }, + { + name: "CounterWithInvalidExemplarLabel", + in: &dto.MetricFamily{ + Name: proto.String("http_requests_total"), + Type: dto.MetricType_COUNTER.Enum(), + Metric: []*dto.Metric{ + { + Counter: &dto.Counter{ + Value: proto.Float64(1027), + Exemplar: &dto.Exemplar{ + Label: []*dto.LabelPair{ + {Name: proto.String(""), Value: proto.String("1234")}, + }, + Value: proto.Float64(1), + Timestamp: ×tamppb.Timestamp{Seconds: 1234567890}, + }, + }, + }, + }, + }, + out: `# TYPE http_requests_total counter +http_requests_total 1027 +`, + }, + { + name: "CounterWithNewlineInExemplarLabelName", + in: &dto.MetricFamily{ + Name: proto.String("http_requests_total"), + Type: dto.MetricType_COUNTER.Enum(), + Metric: []*dto.Metric{ + { + Counter: &dto.Counter{ + Value: proto.Float64(1027), + Exemplar: &dto.Exemplar{ + Label: []*dto.LabelPair{ + {Name: proto.String("trace\nid"), Value: proto.String("1234")}, + }, + Value: proto.Float64(1), + Timestamp: ×tamppb.Timestamp{Seconds: 1234567890}, + }, + }, + }, + }, + }, + out: `# TYPE http_requests_total counter +http_requests_total 1027 +`, + }, + { + name: "CounterWithNilExemplarLabelPair", + in: &dto.MetricFamily{ + Name: proto.String("http_requests_total"), + Type: dto.MetricType_COUNTER.Enum(), + Metric: []*dto.Metric{ + { + Counter: &dto.Counter{ + Value: proto.Float64(1027), + Exemplar: &dto.Exemplar{ + Label: []*dto.LabelPair{nil}, + Value: proto.Float64(1), + Timestamp: ×tamppb.Timestamp{Seconds: 1234567890}, + }, + }, + }, + }, + }, + out: `# TYPE http_requests_total counter +http_requests_total 1027 `, }, { @@ -556,30 +652,6 @@ func TestCreateOpenMetrics20_Errors(t *testing.T) { }, expectedErr: "invalid created timestamp in metric test_counter_total", }, - { - name: "ExemplarInvalidTimestamp", - in: &dto.MetricFamily{ - Name: proto.String("test_counter_total"), - Type: dto.MetricType_COUNTER.Enum(), - Metric: []*dto.Metric{ - { - Counter: &dto.Counter{ - Value: proto.Float64(1.0), - Exemplar: &dto.Exemplar{ - Label: []*dto.LabelPair{ - {Name: proto.String("trace_id"), Value: proto.String("1234")}, - }, - Value: proto.Float64(1.0), - Timestamp: ×tamppb.Timestamp{ - Nanos: -1, - }, - }, - }, - }, - }, - }, - expectedErr: "has out-of-range nanos", - }, } for _, tc := range tests { From 223e26bee54be3c13eb7c5b581f1cc1c62958259 Mon Sep 17 00:00:00 2001 From: David Ashpole Date: Mon, 24 Aug 2026 17:34:28 +0000 Subject: [PATCH 2/2] expfmt: add comment explaining why invalid OpenMetrics 2.0 exemplars are dropped Signed-off-by: David Ashpole --- expfmt/openmetrics_2_0_create.go | 1 + expfmt/openmetrics_2_0_create_test.go | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/expfmt/openmetrics_2_0_create.go b/expfmt/openmetrics_2_0_create.go index 2fe2b880..aefdaf4f 100644 --- a/expfmt/openmetrics_2_0_create.go +++ b/expfmt/openmetrics_2_0_create.go @@ -285,6 +285,7 @@ func writeExemplar20(w enhancedWriter, e *dto.Exemplar) (int, error) { if e == nil { return 0, nil } + // In OpenMetrics 2.0, invalid exemplars are dropped rather than failing the entire exposition. if err := validateExemplar20(e); err != nil { return 0, nil } diff --git a/expfmt/openmetrics_2_0_create_test.go b/expfmt/openmetrics_2_0_create_test.go index 5a2fa005..e3e1766b 100644 --- a/expfmt/openmetrics_2_0_create_test.go +++ b/expfmt/openmetrics_2_0_create_test.go @@ -230,7 +230,7 @@ http_requests_total 1027.0 }, }, out: `# TYPE http_requests_total counter -http_requests_total 1027 +http_requests_total 1027.0 `, }, { @@ -254,7 +254,7 @@ http_requests_total 1027 }, }, out: `# TYPE http_requests_total counter -http_requests_total 1027 +http_requests_total 1027.0 `, }, { @@ -278,7 +278,7 @@ http_requests_total 1027 }, }, out: `# TYPE http_requests_total counter -http_requests_total 1027 +http_requests_total 1027.0 `, }, { @@ -300,7 +300,7 @@ http_requests_total 1027 }, }, out: `# TYPE http_requests_total counter -http_requests_total 1027 +http_requests_total 1027.0 `, }, {