Skip to content

feat: implement gauge and counter support for OpenMetrics 2.0 - #894

Open
dashpole wants to merge 9 commits into
prometheus:mainfrom
dashpole:om2_1
Open

feat: implement gauge and counter support for OpenMetrics 2.0#894
dashpole wants to merge 9 commits into
prometheus:mainfrom
dashpole:om2_1

Conversation

@dashpole

@dashpole dashpole commented Apr 7, 2026

Copy link
Copy Markdown
Contributor

Part 1 of #893

Histograms and summaries are left for follow-ups.

Open Questions:

  • How do we indicate that OM 2.0 support is experimental?

@ywwg @bwplotka @krajorama

@bwplotka bwplotka left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For common, given v0 I would just comment on the method. If we want to be fancy we can generate comment in the exposition format too (:

Looks good so far, good start (see comments) - only question is do we iterate on this on main or some feature branch - no strong opinion, assuming commentary is clear.

Thanks

Comment thread expfmt/openmetrics_2_0_create.go
Comment thread expfmt/openmetrics_2_0_create.go
Comment thread expfmt/openmetrics_2_0_create.go
Comment thread expfmt/openmetrics_2_0_create.go
Comment thread expfmt/openmetrics_2_0_create_test.go Outdated
Comment thread expfmt/openmetrics_2_0_create_test.go
@dashpole
dashpole marked this pull request as ready for review April 7, 2026 19:09

@ywwg ywwg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good to me so far. My general opinion is that if some scraper asks for openmetrics 2.0, they should get it... Hypothetically I could see requiring the version number "2.0.0rc" or "1.9.9" just in case?

@dashpole

Copy link
Copy Markdown
Contributor Author

Lots of discussion at the OM wg. We will add a NegotiateIncluding() function that takes a list of supported protocols so we don't end up with NegotiateIncludingOpenMetricsAndOpenMetrics2 ... 😆 . Then the client can decide when it wants to add support, and how it wants to gate it (behind HandlerOpts.EnableOpenMetrics?).

@dashpole

Copy link
Copy Markdown
Contributor Author

I added a NegotiateIncluding function. I didn't deprecate the old functions (e.g. NegotiateIncludingOpenMetrics), but we can do that in a follow-up if we want to.

@dashpole
dashpole force-pushed the om2_1 branch 2 times, most recently from 873bb38 to 1bcdef1 Compare May 27, 2026 15:39
@dashpole
dashpole requested review from bwplotka and ywwg June 4, 2026 13:21
@bwplotka

Copy link
Copy Markdown
Member

I think that's great, the experimental factor at this level should be a commentary.

As per discussion on Slack I think we decided to follow:

  1. Exposer should eventually switch to OM2 default, but only once ready (which means we recommend opt-in "exposed protocol" option)
  2. Scrapers should be opt-in for now, while OM2 is experimental

Comment thread expfmt/encode.go
@krajorama
krajorama self-requested a review June 24, 2026 13:33
Comment thread expfmt/openmetrics_2_0_create_test.go
Comment thread expfmt/openmetrics_2_0_create.go Outdated
Comment thread expfmt/openmetrics_2_0_create_test.go
Comment thread expfmt/openmetrics_2_0_create_test.go Outdated
Comment thread expfmt/encode.go
@dashpole
dashpole force-pushed the om2_1 branch 16 times, most recently from ab106e7 to 2f87680 Compare July 7, 2026 17:13
@dashpole

dashpole commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

In the spirit of @krajorama's feedback, i've updated the implementation to perform some additional validation. It checks for newline characters, NaN and negative counter values, valid timestamps, empty exemplar label keys. It does not validate UTF-8 for strings, or duplicate label keys or duplicate label sets because those are expensive to validate. This does more validation than OM 1.0, but still leaves some things not validated. I also ended up writing benchmarks as part of that work, which i've split into #943.

The benchmark results from #943 run against this PR are:

goos: linux
goarch: amd64
pkg: github.com/prometheus/common/expfmt
cpu: Intel(R) Xeon(R) CPU @ 2.20GHz
                                             │ /tmp/bench.txt │
                                             │     sec/op     │
ConvertMetricFamily/TEXT/COUNTER-24               775.5n ± 1%
ConvertMetricFamily/OM1.0/COUNTER-24              1.664µ ± 1%
ConvertMetricFamily/OM2.0/COUNTER-24              1.617µ ± 1%
ConvertMetricFamily/TEXT/GAUGE-24                 761.5n ± 1%
ConvertMetricFamily/OM1.0/GAUGE-24                1.091µ ± 6%
ConvertMetricFamily/OM2.0/GAUGE-24                1.003µ ± 3%
ConvertMetricFamily/TEXT/UNTYPED-24               680.6n ± 1%
ConvertMetricFamily/OM1.0/UNTYPED-24              923.8n ± 3%
ConvertMetricFamily/OM2.0/UNTYPED-24              883.1n ± 1%
ConvertMetricFamily/TEXT/SUMMARY-24               3.207µ ± 1%
ConvertMetricFamily/OM1.0/SUMMARY-24              3.965µ ± 1%
ConvertMetricFamily/TEXT/HISTOGRAM-24             4.532µ ± 2%
ConvertMetricFamily/OM1.0/HISTOGRAM-24            5.582µ ± 2%
ConvertMetricFamily/TEXT/GAUGE_HISTOGRAM-24       3.414µ ± 1%
ConvertMetricFamily/OM1.0/GAUGE_HISTOGRAM-24      3.852µ ± 1%
geomean                                           1.741µ

                                             │ /tmp/bench.txt │
                                             │      B/op      │
ConvertMetricFamily/TEXT/COUNTER-24              0.000 ± 0%
ConvertMetricFamily/OM1.0/COUNTER-24             33.00 ± 0%
ConvertMetricFamily/OM2.0/COUNTER-24             0.000 ± 0%
ConvertMetricFamily/TEXT/GAUGE-24                0.000 ± 0%
ConvertMetricFamily/OM1.0/GAUGE-24               1.000 ± 0%
ConvertMetricFamily/OM2.0/GAUGE-24               0.000 ± 0%
ConvertMetricFamily/TEXT/UNTYPED-24              0.000 ± 0%
ConvertMetricFamily/OM1.0/UNTYPED-24             1.000 ± 0%
ConvertMetricFamily/OM2.0/UNTYPED-24             0.000 ± 0%
ConvertMetricFamily/TEXT/SUMMARY-24              56.00 ± 0%
ConvertMetricFamily/OM1.0/SUMMARY-24             57.00 ± 0%
ConvertMetricFamily/TEXT/HISTOGRAM-24            288.0 ± 0%
ConvertMetricFamily/OM1.0/HISTOGRAM-24           289.0 ± 0%
ConvertMetricFamily/TEXT/GAUGE_HISTOGRAM-24      160.0 ± 0%
ConvertMetricFamily/OM1.0/GAUGE_HISTOGRAM-24     161.0 ± 0%
geomean                                                     ¹
¹ summaries must be >0 to compute geomean

                                             │ /tmp/bench.txt │
                                             │   allocs/op    │
ConvertMetricFamily/TEXT/COUNTER-24              0.000 ± 0%
ConvertMetricFamily/OM1.0/COUNTER-24             2.000 ± 0%
ConvertMetricFamily/OM2.0/COUNTER-24             0.000 ± 0%
ConvertMetricFamily/TEXT/GAUGE-24                0.000 ± 0%
ConvertMetricFamily/OM1.0/GAUGE-24               1.000 ± 0%
ConvertMetricFamily/OM2.0/GAUGE-24               0.000 ± 0%
ConvertMetricFamily/TEXT/UNTYPED-24              0.000 ± 0%
ConvertMetricFamily/OM1.0/UNTYPED-24             1.000 ± 0%
ConvertMetricFamily/OM2.0/UNTYPED-24             0.000 ± 0%
ConvertMetricFamily/TEXT/SUMMARY-24              2.000 ± 0%
ConvertMetricFamily/OM1.0/SUMMARY-24             3.000 ± 0%
ConvertMetricFamily/TEXT/HISTOGRAM-24            6.000 ± 0%
ConvertMetricFamily/OM1.0/HISTOGRAM-24           7.000 ± 0%
ConvertMetricFamily/TEXT/GAUGE_HISTOGRAM-24      5.000 ± 0%
ConvertMetricFamily/OM1.0/GAUGE_HISTOGRAM-24     6.000 ± 0%
geomean                                                     ¹
¹ summaries must be >0 to compute geomean

dashpole added 5 commits July 31, 2026 20:32
Signed-off-by: David Ashpole <dashpole@google.com>
Signed-off-by: David Ashpole <dashpole@google.com>
Signed-off-by: David Ashpole <dashpole@google.com>
…rt timestamps

Signed-off-by: David Ashpole <dashpole@google.com>
Signed-off-by: David Ashpole <dashpole@google.com>
dashpole added 4 commits July 31, 2026 20:36
Signed-off-by: David Ashpole <dashpole@google.com>
…ion loss

Signed-off-by: David Ashpole <dashpole@google.com>
…sion loss

Signed-off-by: David Ashpole <dashpole@google.com>
…cision

Signed-off-by: David Ashpole <dashpole@google.com>

@bwplotka bwplotka left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, just one last API question

Comment thread expfmt/expfmt.go
//nolint:revive // Allow for underscores.
FmtOpenMetrics_1_0_0 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_1_0_0 + `; charset=utf-8`
//nolint:revive // Allow for underscores.
FmtOpenMetrics_2_0_0 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_2_0_0 + `; charset=utf-8`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR introduces FmtOpenMetrics_2_0_0 as an exported constant. Adjacent older formats are explicitly marked as // Deprecated: Use expfmt.NewFormat(expfmt.TypeOpenMetrics) instead.. Introducing a new format-specific exported constant reproduces the very API footprint that maintainers actively deprecated. Consider changing to unexported fmtOpenMetrics_2_0_0.

}
}

func TestCreateOpenMetrics20_Errors(t *testing.T) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test for Counter CreatedTimestamp validation (Edge case coverage). Add a test case in TestCreateOpenMetrics20_Errors where Metric.Counter.CreatedTimestamp is mathematically invalid (e.g., negative seconds or out-of-bounds) to assert it cleanly returns the validation error message.

return strings.IndexByte(s, '\n') >= 0 || strings.IndexByte(s, '\r') >= 0
}

func validateExemplar20(e *dto.Exemplar) error {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test for Exemplar Timestamp validation (Edge case coverage). Add a test case with an invalid Exemplar Timestamp (e.g. invalid protobuf timestamp values) to ensure validateExemplar20 correctly captures and bubbles up the CheckValid() error.

t.Run(tc.name, func(t *testing.T) {
var buf bytes.Buffer
_, err := MetricFamilyToOpenMetrics20(&buf, tc.in)
if err == nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update the err == nil logic to strictly check if the actual err.Error() matches the exact expected error message or a substring representing it. Currently, the test blindly accepts any generic non-nil error, masking regressions.

for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
var buf bytes.Buffer
_, err := MetricFamilyToOpenMetrics20(&buf, scenario.in)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weak Unit Test Assertions on Written Bytes count. The functions return the number of bytes written, but this test suppresses it with _, err := ... . Verify that the returned byte count equals len(buf.String()) / len(expected) to ensure the returned byte counting arithmetic functions properly.

Comment thread expfmt/encode.go
// FmtOpenMetrics as an option for the result. Note that this function is
// temporary and will disappear once FmtOpenMetrics is fully supported and as
// such may be negotiated by the normal Negotiate function.
func NegotiateIncludingOpenMetrics(h http.Header) Format {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we deprecate this in favor of the new function?

Comment thread expfmt/encode.go
// Additional formats provided in the arguments are also included as options,
// and are checked in the order they are provided, respecting the preference
// order in the Accept header.
func NegotiateIncluding(h http.Header, additionalFormats ...Format) Format {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Since we bake new API, what about something more explicit like: NegotiateAccept(h http.Header, accepted ...Format) Format { that would allow us to provide full list vs speculating what's default what's not (especially when default change?)

Comment thread expfmt/encode.go
return false
}

// Default OpenMetrics version to OpenMetricsVersion_0_0_1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Default OpenMetrics version to OpenMetricsVersion_0_0_1
// Default OpenMetrics version to OpenMetricsVersion_0_0_1.

Comment thread expfmt/encode.go
acVersion = OpenMetricsVersion_0_0_1
}

// General param matching

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// General param matching
// General param matching.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants