From 11eb5abcdaf662bfdb309c7c18837a3cda5e43b2 Mon Sep 17 00:00:00 2001 From: David Ashpole Date: Wed, 2 Sep 2026 16:20:20 +0000 Subject: [PATCH 1/5] feat: implement summary support for OpenMetrics 2.0 Implement serialization and strict validation for summaries in OpenMetrics 2.0 format according to the specification and ABNF grammar. Signed-off-by: David Ashpole --- expfmt/openmetrics_2_0_create.go | 160 ++++++++- expfmt/openmetrics_2_0_create_test.go | 457 +++++++++++++++++++++++++- 2 files changed, 606 insertions(+), 11 deletions(-) diff --git a/expfmt/openmetrics_2_0_create.go b/expfmt/openmetrics_2_0_create.go index 7ab6a212..dd52a809 100644 --- a/expfmt/openmetrics_2_0_create.go +++ b/expfmt/openmetrics_2_0_create.go @@ -31,7 +31,7 @@ import ( // It returns the number of bytes written and any error encountered. // // NOTE: This method targets OpenMetrics 2.0.0 (currently aligned with 2.0-rc.0) which is experimental and -// encode-only (currently supporting counter, gauge, and untyped metric types). +// encode-only (currently supporting counter, gauge, summary, untyped, histogram, and gaugehistogram metric types). // Breaking changes might happen in the future. This implementation is still a // work-in-progress, and does not yet support all features of the format. // EncoderOptions are accepted for signature compatibility with @@ -340,13 +340,159 @@ func writeOpenMetrics20Timestamp(w enhancedWriter, f float64) (int, error) { return written, err } -// Stubs for Summary - func writeCompositeSummary(w enhancedWriter, name string, metric *dto.Metric) (int, error) { - _ = w - _ = name - _ = metric - return 0, errors.New("summary not implemented yet") + s := metric.Summary + if s == nil { + return 0, fmt.Errorf("expected summary in metric %s", name) + } + + if err := validateLabels20(metric.Label); err != nil { + return 0, err + } + for _, lp := range metric.Label { + if lp.GetName() == "quantile" { + return 0, fmt.Errorf("metric %s is a summary but label set contains %q label", name, "quantile") + } + } + + sum := s.GetSampleSum() + if math.IsNaN(sum) { + return 0, fmt.Errorf("summary sum cannot be NaN in metric %s", name) + } + if sum < 0 { + return 0, fmt.Errorf("summary sum cannot be negative (%g) in metric %s", sum, name) + } + + var prevQuantile float64 + for i, q := range s.Quantile { + if q == nil { + return 0, fmt.Errorf("expected non-nil quantile in metric %s", name) + } + qv := q.GetQuantile() + if math.IsNaN(qv) { + return 0, fmt.Errorf("summary quantile cannot be NaN in metric %s", name) + } + if math.IsInf(qv, 0) || qv < 0 || qv > 1 { + return 0, fmt.Errorf("summary quantile %g must be between 0 and 1 in metric %s", qv, name) + } + if i > 0 && qv <= prevQuantile { + return 0, fmt.Errorf("summary quantiles must be strictly increasing: %g <= %g in metric %s", qv, prevQuantile, name) + } + prevQuantile = qv + + v := q.GetValue() + if !math.IsNaN(v) && v < 0 { + return 0, fmt.Errorf("summary quantile value cannot be negative (%g) in metric %s", v, name) + } + } + + if s.CreatedTimestamp != nil { + if err := s.CreatedTimestamp.CheckValid(); err != nil { + return 0, fmt.Errorf("invalid created timestamp in metric %s: %w", name, err) + } + } + + written := 0 + n, err := writeOpenMetricsNameAndLabelPairs(w, name, metric.Label, "", 0) + written += n + if err != nil { + return written, err + } + + n, err = w.WriteString(" {count:") + written += n + if err != nil { + return written, err + } + + n, err = writeUint(w, s.GetSampleCount()) + written += n + if err != nil { + return written, err + } + + n, err = w.WriteString(",sum:") + written += n + if err != nil { + return written, err + } + + n, err = writeFloat(w, sum) + written += n + if err != nil { + return written, err + } + + n, err = w.WriteString(",quantile:[") + written += n + if err != nil { + return written, err + } + + for i, q := range s.Quantile { + if i > 0 { + err = w.WriteByte(',') + written++ + if err != nil { + return written, err + } + } + n, err = writeFloat(w, q.GetQuantile()) + written += n + if err != nil { + return written, err + } + err = w.WriteByte(':') + written++ + if err != nil { + return written, err + } + n, err = writeFloat(w, q.GetValue()) + written += n + if err != nil { + return written, err + } + } + + n, err = w.WriteString("]}") + written += n + if err != nil { + return written, err + } + + if metric.TimestampMs != nil { + err = w.WriteByte(' ') + written++ + if err != nil { + return written, err + } + n, err = writeOpenMetrics20Timestamp(w, float64(*metric.TimestampMs)/1000) + written += n + if err != nil { + return written, err + } + } + + if s.CreatedTimestamp != nil { + n, err = w.WriteString(" st@") + written += n + if err != nil { + return written, err + } + n, err = writeProtoTimestamp(w, s.CreatedTimestamp) + written += n + if err != nil { + return written, err + } + } + + err = w.WriteByte('\n') + written++ + if err != nil { + return written, err + } + + return written, nil } func writeCompositeHistogram(w enhancedWriter, name string, metric *dto.Metric, isGauge bool) (int, error) { diff --git a/expfmt/openmetrics_2_0_create_test.go b/expfmt/openmetrics_2_0_create_test.go index 167406c7..b99a03e5 100644 --- a/expfmt/openmetrics_2_0_create_test.go +++ b/expfmt/openmetrics_2_0_create_test.go @@ -923,6 +923,244 @@ empty_histogram {count:0,sum:0,bucket:[+Inf:0]} }, out: `# TYPE test_histogram histogram test_histogram {count:1,sum:0.1,bucket:[0.1:1,+Inf:1]} st@-0.5 +`, + }, + { + name: "Summary", + in: &dto.MetricFamily{ + Name: proto.String("rpc_duration_seconds"), + Help: proto.String("RPC latency in seconds."), + Type: dto.MetricType_SUMMARY.Enum(), + Unit: proto.String("seconds"), + Metric: []*dto.Metric{ + { + Label: []*dto.LabelPair{ + {Name: proto.String("service"), Value: proto.String("auth")}, + }, + Summary: &dto.Summary{ + SampleCount: proto.Uint64(100), + SampleSum: proto.Float64(25.5), + Quantile: []*dto.Quantile{ + {Quantile: proto.Float64(0.5), Value: proto.Float64(0.12)}, + {Quantile: proto.Float64(0.9), Value: proto.Float64(0.45)}, + {Quantile: proto.Float64(0.99), Value: proto.Float64(0.89)}, + }, + }, + }, + }, + }, + out: `# HELP rpc_duration_seconds RPC latency in seconds. +# TYPE rpc_duration_seconds summary +# UNIT rpc_duration_seconds seconds +rpc_duration_seconds{service="auth"} {count:100,sum:25.5,quantile:[0.5:0.12,0.9:0.45,0.99:0.89]} +`, + }, + { + name: "Summary_EmptyQuantiles", + in: &dto.MetricFamily{ + Name: proto.String("foo"), + Type: dto.MetricType_SUMMARY.Enum(), + Metric: []*dto.Metric{ + { + Summary: &dto.Summary{ + SampleCount: proto.Uint64(17), + SampleSum: proto.Float64(324789.3), + }, + }, + }, + }, + out: `# TYPE foo summary +foo {count:17,sum:324789.3,quantile:[]} +`, + }, + { + name: "Summary_ZeroCountAndSum", + in: &dto.MetricFamily{ + Name: proto.String("foo"), + Type: dto.MetricType_SUMMARY.Enum(), + Metric: []*dto.Metric{ + { + Summary: &dto.Summary{ + SampleCount: proto.Uint64(0), + SampleSum: proto.Float64(0), + Quantile: []*dto.Quantile{ + {Quantile: proto.Float64(0.95), Value: proto.Float64(123.7)}, + {Quantile: proto.Float64(0.99), Value: proto.Float64(150)}, + }, + }, + }, + }, + }, + out: `# TYPE foo summary +foo {count:0,sum:0,quantile:[0.95:123.7,0.99:150]} +`, + }, + { + name: "Summary_WithCreatedTimestamp", + in: &dto.MetricFamily{ + Name: proto.String("foo"), + Type: dto.MetricType_SUMMARY.Enum(), + Metric: []*dto.Metric{ + { + Summary: &dto.Summary{ + SampleCount: proto.Uint64(17), + SampleSum: proto.Float64(324789.3), + CreatedTimestamp: ×tamppb.Timestamp{Seconds: 1234567890}, + }, + }, + }, + }, + out: `# TYPE foo summary +foo {count:17,sum:324789.3,quantile:[]} st@1234567890 +`, + }, + { + name: "Summary_WithSubsecondCreatedTimestamp", + in: &dto.MetricFamily{ + Name: proto.String("foo"), + Type: dto.MetricType_SUMMARY.Enum(), + Metric: []*dto.Metric{ + { + Summary: &dto.Summary{ + SampleCount: proto.Uint64(17), + SampleSum: proto.Float64(324789.3), + CreatedTimestamp: ×tamppb.Timestamp{Seconds: 1234567890, Nanos: 987654321}, + }, + }, + }, + }, + out: `# TYPE foo summary +foo {count:17,sum:324789.3,quantile:[]} st@1234567890.987654321 +`, + }, + { + name: "Summary_WithTimestamp", + in: &dto.MetricFamily{ + Name: proto.String("foo"), + Type: dto.MetricType_SUMMARY.Enum(), + Metric: []*dto.Metric{ + { + Summary: &dto.Summary{ + SampleCount: proto.Uint64(17), + SampleSum: proto.Float64(324789.3), + }, + TimestampMs: proto.Int64(1234567891000), + }, + }, + }, + out: `# TYPE foo summary +foo {count:17,sum:324789.3,quantile:[]} 1234567891 +`, + }, + { + name: "Summary_WithTimestampAndCreatedTimestamp", + in: &dto.MetricFamily{ + Name: proto.String("foo"), + Type: dto.MetricType_SUMMARY.Enum(), + Metric: []*dto.Metric{ + { + Summary: &dto.Summary{ + SampleCount: proto.Uint64(17), + SampleSum: proto.Float64(324789.3), + CreatedTimestamp: ×tamppb.Timestamp{Seconds: 1234567890}, + }, + TimestampMs: proto.Int64(1234567891000), + }, + }, + }, + out: `# TYPE foo summary +foo {count:17,sum:324789.3,quantile:[]} 1234567891 st@1234567890 +`, + }, + { + name: "Summary_NaNAndInfQuantileValues", + in: &dto.MetricFamily{ + Name: proto.String("foo"), + Type: dto.MetricType_SUMMARY.Enum(), + Metric: []*dto.Metric{ + { + Summary: &dto.Summary{ + SampleCount: proto.Uint64(5), + SampleSum: proto.Float64(10), + Quantile: []*dto.Quantile{ + {Quantile: proto.Float64(0.5), Value: proto.Float64(math.NaN())}, + {Quantile: proto.Float64(0.99), Value: proto.Float64(math.Inf(+1))}, + }, + }, + }, + }, + }, + out: `# TYPE foo summary +foo {count:5,sum:10,quantile:[0.5:NaN,0.99:+Inf]} +`, + }, + { + name: "Summary_UTF8", + in: &dto.MetricFamily{ + Name: proto.String("my.app/duration"), + Type: dto.MetricType_SUMMARY.Enum(), + Metric: []*dto.Metric{ + { + Label: []*dto.LabelPair{ + {Name: proto.String("service.name"), Value: proto.String("my_service")}, + }, + Summary: &dto.Summary{ + SampleCount: proto.Uint64(2), + SampleSum: proto.Float64(3.4), + Quantile: []*dto.Quantile{ + {Quantile: proto.Float64(0.95), Value: proto.Float64(1.7)}, + }, + }, + }, + }, + }, + out: `# TYPE "my.app/duration" summary +{"my.app/duration","service.name"="my_service"} {count:2,sum:3.4,quantile:[0.95:1.7]} +`, + }, + { + name: "Summary_MultipleMetrics", + in: &dto.MetricFamily{ + Name: proto.String("acme_http_router_request_seconds"), + Help: proto.String("Latency though all of ACME's HTTP request router."), + Type: dto.MetricType_SUMMARY.Enum(), + Unit: proto.String("seconds"), + Metric: []*dto.Metric{ + { + Label: []*dto.LabelPair{ + {Name: proto.String("path"), Value: proto.String("/api/v1")}, + {Name: proto.String("method"), Value: proto.String("GET")}, + }, + Summary: &dto.Summary{ + SampleCount: proto.Uint64(807283), + SampleSum: proto.Float64(9036.32), + Quantile: []*dto.Quantile{ + {Quantile: proto.Float64(0.95), Value: proto.Float64(2)}, + {Quantile: proto.Float64(0.99), Value: proto.Float64(20)}, + }, + }, + }, + { + Label: []*dto.LabelPair{ + {Name: proto.String("path"), Value: proto.String("/api/v2")}, + {Name: proto.String("method"), Value: proto.String("GET")}, + }, + Summary: &dto.Summary{ + SampleCount: proto.Uint64(34), + SampleSum: proto.Float64(479.3), + Quantile: []*dto.Quantile{ + {Quantile: proto.Float64(0.95), Value: proto.Float64(2.5)}, + {Quantile: proto.Float64(0.99), Value: proto.Float64(2.9)}, + }, + }, + }, + }, + }, + out: `# HELP acme_http_router_request_seconds Latency though all of ACME's HTTP request router. +# TYPE acme_http_router_request_seconds summary +# UNIT acme_http_router_request_seconds seconds +acme_http_router_request_seconds{path="/api/v1",method="GET"} {count:807283,sum:9036.32,quantile:[0.95:2,0.99:20]} +acme_http_router_request_seconds{path="/api/v2",method="GET"} {count:34,sum:479.3,quantile:[0.95:2.5,0.99:2.9]} `, }, } @@ -1053,15 +1291,201 @@ func TestCreateOpenMetrics20_Errors(t *testing.T) { expectedErr: "expected histogram in metric", }, { - name: "SummaryNotImplemented", + name: "Summary_LabelContainsQuantile", in: &dto.MetricFamily{ - Name: proto.String("test_metric"), + Name: proto.String("test_summary"), + Type: dto.MetricType_SUMMARY.Enum(), + Metric: []*dto.Metric{ + { + Label: []*dto.LabelPair{ + {Name: proto.String("quantile"), Value: proto.String("0.9")}, + }, + Summary: &dto.Summary{}, + }, + }, + }, + expectedErr: `metric test_summary is a summary but label set contains "quantile" label`, + }, + { + name: "Summary_SumNaN", + in: &dto.MetricFamily{ + Name: proto.String("test_summary"), + Type: dto.MetricType_SUMMARY.Enum(), + Metric: []*dto.Metric{ + { + Summary: &dto.Summary{ + SampleSum: proto.Float64(math.NaN()), + }, + }, + }, + }, + expectedErr: "summary sum cannot be NaN", + }, + { + name: "Summary_SumNegative", + in: &dto.MetricFamily{ + Name: proto.String("test_summary"), + Type: dto.MetricType_SUMMARY.Enum(), + Metric: []*dto.Metric{ + { + Summary: &dto.Summary{ + SampleSum: proto.Float64(-1.0), + }, + }, + }, + }, + expectedErr: "summary sum cannot be negative", + }, + { + name: "Summary_NilQuantile", + in: &dto.MetricFamily{ + Name: proto.String("test_summary"), Type: dto.MetricType_SUMMARY.Enum(), Metric: []*dto.Metric{ - {Summary: &dto.Summary{}}, + { + Summary: &dto.Summary{ + Quantile: []*dto.Quantile{nil}, + }, + }, + }, + }, + expectedErr: "expected non-nil quantile", + }, + { + name: "Summary_QuantileNaN", + in: &dto.MetricFamily{ + Name: proto.String("test_summary"), + Type: dto.MetricType_SUMMARY.Enum(), + Metric: []*dto.Metric{ + { + Summary: &dto.Summary{ + Quantile: []*dto.Quantile{ + {Quantile: proto.Float64(math.NaN()), Value: proto.Float64(1.0)}, + }, + }, + }, + }, + }, + expectedErr: "summary quantile cannot be NaN", + }, + { + name: "Summary_QuantileNegative", + in: &dto.MetricFamily{ + Name: proto.String("test_summary"), + Type: dto.MetricType_SUMMARY.Enum(), + Metric: []*dto.Metric{ + { + Summary: &dto.Summary{ + Quantile: []*dto.Quantile{ + {Quantile: proto.Float64(-0.1), Value: proto.Float64(1.0)}, + }, + }, + }, + }, + }, + expectedErr: "must be between 0 and 1", + }, + { + name: "Summary_QuantileGreaterThanOne", + in: &dto.MetricFamily{ + Name: proto.String("test_summary"), + Type: dto.MetricType_SUMMARY.Enum(), + Metric: []*dto.Metric{ + { + Summary: &dto.Summary{ + Quantile: []*dto.Quantile{ + {Quantile: proto.Float64(1.1), Value: proto.Float64(1.0)}, + }, + }, + }, }, }, - expectedErr: "summary not implemented yet", + expectedErr: "must be between 0 and 1", + }, + { + name: "Summary_QuantilePosInf", + in: &dto.MetricFamily{ + Name: proto.String("test_summary"), + Type: dto.MetricType_SUMMARY.Enum(), + Metric: []*dto.Metric{ + { + Summary: &dto.Summary{ + Quantile: []*dto.Quantile{ + {Quantile: proto.Float64(math.Inf(+1)), Value: proto.Float64(1.0)}, + }, + }, + }, + }, + }, + expectedErr: "must be between 0 and 1", + }, + { + name: "Summary_QuantilesUnsorted", + in: &dto.MetricFamily{ + Name: proto.String("test_summary"), + Type: dto.MetricType_SUMMARY.Enum(), + Metric: []*dto.Metric{ + { + Summary: &dto.Summary{ + Quantile: []*dto.Quantile{ + {Quantile: proto.Float64(0.9), Value: proto.Float64(1.0)}, + {Quantile: proto.Float64(0.5), Value: proto.Float64(0.5)}, + }, + }, + }, + }, + }, + expectedErr: "summary quantiles must be strictly increasing", + }, + { + name: "Summary_QuantilesDuplicate", + in: &dto.MetricFamily{ + Name: proto.String("test_summary"), + Type: dto.MetricType_SUMMARY.Enum(), + Metric: []*dto.Metric{ + { + Summary: &dto.Summary{ + Quantile: []*dto.Quantile{ + {Quantile: proto.Float64(0.5), Value: proto.Float64(1.0)}, + {Quantile: proto.Float64(0.5), Value: proto.Float64(2.0)}, + }, + }, + }, + }, + }, + expectedErr: "summary quantiles must be strictly increasing", + }, + { + name: "Summary_QuantileValueNegative", + in: &dto.MetricFamily{ + Name: proto.String("test_summary"), + Type: dto.MetricType_SUMMARY.Enum(), + Metric: []*dto.Metric{ + { + Summary: &dto.Summary{ + Quantile: []*dto.Quantile{ + {Quantile: proto.Float64(0.5), Value: proto.Float64(-1.0)}, + }, + }, + }, + }, + }, + expectedErr: "summary quantile value cannot be negative", + }, + { + name: "Summary_InvalidCreatedTimestamp", + in: &dto.MetricFamily{ + Name: proto.String("test_summary"), + Type: dto.MetricType_SUMMARY.Enum(), + Metric: []*dto.Metric{ + { + Summary: &dto.Summary{ + CreatedTimestamp: ×tamppb.Timestamp{Nanos: -1}, + }, + }, + }, + }, + expectedErr: "invalid created timestamp in metric test_summary", }, { name: "HistogramCountNegative", @@ -1909,3 +2333,28 @@ func TestCreateOpenMetrics20_HistogramError_NoPartialBytes(t *testing.T) { t.Fatalf("expected 0 bytes written on validation error, got %q", buf.String()) } } + +func TestCreateOpenMetrics20_SummaryError_NoPartialBytes(t *testing.T) { + in := &dto.MetricFamily{ + Name: proto.String("test_summary"), + Type: dto.MetricType_SUMMARY.Enum(), + Metric: []*dto.Metric{ + { + Summary: &dto.Summary{ + SampleCount: proto.Uint64(1), + SampleSum: proto.Float64(-1.0), // invalid sum + }, + }, + }, + } + + var buf bytes.Buffer + w := enhancedWriter(&buf) + _, err := writeCompositeSummary(w, in.GetName(), in.Metric[0]) + if err == nil { + t.Fatal("expected error, got nil") + } + if buf.Len() != 0 { + t.Fatalf("expected 0 bytes written on validation error, got %q", buf.String()) + } +} From 4925ca55000be1a5937466dd78b4fa50e31ba323 Mon Sep 17 00:00:00 2001 From: David Ashpole Date: Tue, 8 Sep 2026 15:46:34 +0000 Subject: [PATCH 2/5] Address review comment: document that OpenMetrics 2.0 may reject metric families accepted by other formats Signed-off-by: David Ashpole --- expfmt/openmetrics_2_0_create.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/expfmt/openmetrics_2_0_create.go b/expfmt/openmetrics_2_0_create.go index dd52a809..d68504e7 100644 --- a/expfmt/openmetrics_2_0_create.go +++ b/expfmt/openmetrics_2_0_create.go @@ -36,6 +36,11 @@ import ( // work-in-progress, and does not yet support all features of the format. // EncoderOptions are accepted for signature compatibility with // MetricFamilyToOpenMetrics and are currently ignored. +// +// OpenMetrics 2.0 enforces stricter validation rules defined in the specification +// than Prometheus text or OpenMetrics 1.0 formats (such as requiring non-negative +// count and sum, and non-negative quantile values for summaries). Consequently, +// MetricFamilyToOpenMetrics20 may reject metric families that the other encoders accept. func MetricFamilyToOpenMetrics20(out io.Writer, in *dto.MetricFamily, options ...EncoderOption) (written int, err error) { // Options are accepted for signature compatibility and ignored. _ = options From d5fc4725b117e19bad1343abbfbe2f0d8a6abfe8 Mon Sep 17 00:00:00 2001 From: David Ashpole Date: Fri, 18 Sep 2026 15:11:15 -0400 Subject: [PATCH 3/5] Update expfmt/openmetrics_2_0_create.go Co-authored-by: Bartlomiej Plotka Signed-off-by: David Ashpole --- expfmt/openmetrics_2_0_create.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/expfmt/openmetrics_2_0_create.go b/expfmt/openmetrics_2_0_create.go index d68504e7..d5d8f880 100644 --- a/expfmt/openmetrics_2_0_create.go +++ b/expfmt/openmetrics_2_0_create.go @@ -377,7 +377,7 @@ func writeCompositeSummary(w enhancedWriter, name string, metric *dto.Metric) (i if math.IsNaN(qv) { return 0, fmt.Errorf("summary quantile cannot be NaN in metric %s", name) } - if math.IsInf(qv, 0) || qv < 0 || qv > 1 { + if qv < 0 || qv > 1 { return 0, fmt.Errorf("summary quantile %g must be between 0 and 1 in metric %s", qv, name) } if i > 0 && qv <= prevQuantile { From 29b91d71dc7c0f59519f3952eccc187f330f32e4 Mon Sep 17 00:00:00 2001 From: David Ashpole Date: Fri, 18 Sep 2026 19:14:13 +0000 Subject: [PATCH 4/5] Address review comment: include quantile 0 and 1 in Summary_ZeroCountAndSum test Signed-off-by: David Ashpole --- expfmt/openmetrics_2_0_create_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/expfmt/openmetrics_2_0_create_test.go b/expfmt/openmetrics_2_0_create_test.go index b99a03e5..5a0ce697 100644 --- a/expfmt/openmetrics_2_0_create_test.go +++ b/expfmt/openmetrics_2_0_create_test.go @@ -984,15 +984,17 @@ foo {count:17,sum:324789.3,quantile:[]} SampleCount: proto.Uint64(0), SampleSum: proto.Float64(0), Quantile: []*dto.Quantile{ + {Quantile: proto.Float64(0), Value: proto.Float64(0)}, {Quantile: proto.Float64(0.95), Value: proto.Float64(123.7)}, {Quantile: proto.Float64(0.99), Value: proto.Float64(150)}, + {Quantile: proto.Float64(1), Value: proto.Float64(160)}, }, }, }, }, }, out: `# TYPE foo summary -foo {count:0,sum:0,quantile:[0.95:123.7,0.99:150]} +foo {count:0,sum:0,quantile:[0:0,0.95:123.7,0.99:150,1:160]} `, }, { From 2a75737f107edbcd35bcce81a37764b0e0790207 Mon Sep 17 00:00:00 2001 From: David Ashpole Date: Fri, 18 Sep 2026 19:15:22 +0000 Subject: [PATCH 5/5] Address review comment: assert n == 0 and test invalid CreatedTimestamp in SummaryError_NoPartialBytes Signed-off-by: David Ashpole --- expfmt/openmetrics_2_0_create_test.go | 50 +++++++++++++++++++-------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/expfmt/openmetrics_2_0_create_test.go b/expfmt/openmetrics_2_0_create_test.go index 5a0ce697..ff08eee4 100644 --- a/expfmt/openmetrics_2_0_create_test.go +++ b/expfmt/openmetrics_2_0_create_test.go @@ -2337,26 +2337,48 @@ func TestCreateOpenMetrics20_HistogramError_NoPartialBytes(t *testing.T) { } func TestCreateOpenMetrics20_SummaryError_NoPartialBytes(t *testing.T) { - in := &dto.MetricFamily{ - Name: proto.String("test_summary"), - Type: dto.MetricType_SUMMARY.Enum(), - Metric: []*dto.Metric{ - { + tests := []struct { + name string + metric *dto.Metric + }{ + { + name: "invalid sum", + metric: &dto.Metric{ + Summary: &dto.Summary{ + SampleCount: proto.Uint64(1), + SampleSum: proto.Float64(-1.0), + }, + }, + }, + { + name: "invalid created timestamp", + metric: &dto.Metric{ Summary: &dto.Summary{ SampleCount: proto.Uint64(1), - SampleSum: proto.Float64(-1.0), // invalid sum + SampleSum: proto.Float64(1.0), + Quantile: []*dto.Quantile{ + {Quantile: proto.Float64(0.5), Value: proto.Float64(0.5)}, + }, + CreatedTimestamp: ×tamppb.Timestamp{Nanos: -1}, }, }, }, } - var buf bytes.Buffer - w := enhancedWriter(&buf) - _, err := writeCompositeSummary(w, in.GetName(), in.Metric[0]) - if err == nil { - t.Fatal("expected error, got nil") - } - if buf.Len() != 0 { - t.Fatalf("expected 0 bytes written on validation error, got %q", buf.String()) + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var buf bytes.Buffer + w := enhancedWriter(&buf) + n, err := writeCompositeSummary(w, "test_summary", tc.metric) + if err == nil { + t.Fatal("expected error, got nil") + } + if n != 0 { + t.Fatalf("expected 0 returned bytes on validation error, got %d", n) + } + if buf.Len() != 0 { + t.Fatalf("expected 0 bytes written on validation error, got %q", buf.String()) + } + }) } }