Skip to content

Optionally add OM unit #1392

Open
vesari wants to merge 51 commits intoprometheus:mainfrom
vesari:add-unit
Open

Optionally add OM unit #1392
vesari wants to merge 51 commits intoprometheus:mainfrom
vesari:add-unit

Conversation

@vesari
Copy link
Copy Markdown
Contributor

@vesari vesari commented Nov 26, 2023

This PR adds support for unit for Open Metrics. It is ready for review, but not ready to merge (see my long comment below). Fixes #684.

Signed-off-by: Arianna Vespri <[email protected]>
Signed-off-by: Arianna Vespri <[email protected]>
Signed-off-by: Arianna Vespri <[email protected]>
Signed-off-by: Arianna Vespri <[email protected]>
Signed-off-by: Arianna Vespri <[email protected]>
Signed-off-by: Arianna Vespri <[email protected]>
@ArthurSens
Copy link
Copy Markdown
Member

Hey @vesari , just passing by to ask if there is anything we could do to unblock you 👋

@vesari
Copy link
Copy Markdown
Contributor Author

vesari commented Mar 15, 2024

Hey @vesari , just passing by to ask if there is anything we could do to unblock you 👋

@ArthurSens hello! I think I'm just using a Go version which is too new, I plan on fixing that later today. Thanks a lot for checking on me, much appreciated! :)

Copy link
Copy Markdown
Member

@bwplotka bwplotka left a comment

Choose a reason for hiding this comment

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

This is amazing. Solid work, thanks for finding way to not break compatibility.

Added a few suggestions and ideas, hope those help! Overall this goes in a good direction 👍🏽

enc = expfmt.NewEncoder(w, contentType)
encOpts = append(encOpts, expfmt.WithCreatedLines())
}
if opts.EnableOpenMetricsUnit {
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.

Do we need this manual flag?

Why can't we simply add UNIT line if someone enabled OM? Shouldn't this be safe?

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.

We added flag for EnableOpenMetricsTextCreatedSamples because we believe CT line is potentially breaking to the ecosystem (lot's of problems). But with UNIT metadata, I'd expect OM compatible parsers to either ignore it or parse. Even TEXT formats should work, because it's just a # .. comment.

The only potential risk is the slight increase of payload size, but this is actually limited by the user who need to manually add Unit: <....> fields to their metric definitions, no?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I see your point. However, as things stand, the flag is necessary because prometheus/common's expfmt encoder requires explicit opt-in via WithUnit() to output units. Without it, units are silently ignored even if set in the MetricFamily. I think this was to avoid potential changes to metric names to happen automatically. In fact, because of this logic

if we:

  • Opt in with WithUnit()
  • Have a metric foo_total with Unit: "seconds"
  • The metric name doesn't already end with _seconds

The metric name becomes foo_seconds_total. I believe the consensus back then was this should be the result of a very explicit decision by the user.
Of course, if - for example - we decide we’re allowed not to enforce the unit suffix anymore or that we deem acceptable to silently add unit suffixes, then the flag becomes superfluous, and can be removed as soon as the related code in common gets modified accordingly. I’ll be happy to do that myself, if consensus is ever reached in that sense.

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.

I think we agree it's a bit toxic to change name depending on the unit, even though it's what OM 1.0 kind of requires (but there tons of other way to handle this e.g. return error on registration e.g. using pedantic registry).

To proceed I think we need to simplify this for users and make sure there's a mode in expfmt that do not change metric names, perhaps even to _total. Then we also need to reduce risk this mode will be not alter.

The problem with adding a feature flag "for now", it's because it's for forever (v1) and it's also adding a flag with something that we don't recommend and can have consequences. So let's try to alter common then?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sure thing. Opened the corresponding PR on common today.

encOpts = append(encOpts, expfmt.WithCreatedLines())
}
if opts.EnableOpenMetricsUnit {
encOpts = append(encOpts, expfmt.WithUnit())
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.

I wonder, does this print UNIT line if UNIT is unknown (empty string?)

Can we ensure this does NOT add UNIT if it's unknown? Just to safe space and limit the risks mentioned in https://github.com/prometheus/client_golang/pull/1392/files#r2580622660

EnableOpenMetricsTextCreatedSamples bool
// EnableOpenMetricsUnit enables unit metadata in the OpenMetrics output format.
// This is only applicable when OpenMetrics format is negotiated.
EnableOpenMetricsUnit bool
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.

again, I wonder if we could skip this (reduce complexity and effort for everyone, users already need to manually add Unit field).

}

wantMsg := `error gathering metrics: error collecting metric Desc{fqName: "invalid_metric", help: "not helpful", constLabels: {}, variableLabels: {}}: collect error
wantMsg := `error gathering metrics: error collecting metric Desc{fqName: "invalid_metric", help: "not helpful", unit: "", constLabels: {}, variableLabels: {}}: collect 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.

Can we ensure everywhere we don't print empty string? (and just skip it?). It does not give a lot of info in an empty string form

// For constLabels, the label values are constant. Therefore, they are fully
// specified in the Desc. See the Collector example for a usage pattern.
func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, constLabels Labels) *Desc {
func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, constLabels Labels, unit ...string) *Desc {
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.

Here, I wonder if we don't want to break compatibility. 🤔 It's V2 and I wished we marked it as experimental somewhere visibility (it was meant to experiment on a new API). However, I bet downstream ecosystem already started using it.

I'd suggest, (see A, B options in https://github.com/prometheus/client_golang/pull/1392/files#r2580668173)

  • For NewDesc we do (B)
  • For V2 we do (A) ?

// label names.
xxh.Reset()
xxh.WriteString(help)
xxh.WriteString(optionalUnitValue(unit...))
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.

let's eval unit as soon as we have it from descV2Opts or something

}
return fmt.Sprintf(
"Desc{fqName: %q, help: %q, constLabels: {%s}, variableLabels: {%s}}",
"Desc{fqName: %q, help: %q, unit: %q, constLabels: {%s}, variableLabels: {%s}}",
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.

ditto, optionally print it?

sampleBuf = append(sampleBuf, metrics.Sample{Name: d.Name})
sampleMap[d.Name] = &sampleBuf[len(sampleBuf)-1]

// Extract unit from the runtime/metrics name (e.g., "/gc/heap/allocs:bytes" -> "bytes")
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.

That's nice!

vesari and others added 4 commits December 14, 2025 09:53
Co-authored-by: Bartlomiej Plotka <[email protected]>
Signed-off-by: Arianna Vespri <[email protected]>
Co-authored-by: Bartlomiej Plotka <[email protected]>
Signed-off-by: Arianna Vespri <[email protected]>
Signed-off-by: Arianna Vespri <[email protected]>
@vesari
Copy link
Copy Markdown
Contributor Author

vesari commented Dec 14, 2025

This is amazing. Solid work, thanks for finding way to not break compatibility.

Added a few suggestions and ideas, hope those help! Overall this goes in a good direction 👍🏽

Thank you very much, I'm working on addressing your requests for changes and your observations. I'll get back to you once I'm done :)

@vesari vesari requested a review from bwplotka January 11, 2026 10:14
@vesari
Copy link
Copy Markdown
Contributor Author

vesari commented Jan 11, 2026

This is now ready for review :)

Copy link
Copy Markdown
Member

@bwplotka bwplotka left a comment

Choose a reason for hiding this comment

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

Early review, looks good!

err error
}

type DescOpt func(*Desc)
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.

Probably needs commentary (public structure)

for _, opt := range opts {
opt(d)
}
unit := d.unit
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.

I'd inline this

}
unitStr := ""
if d.unit != "" {
unitStr = fmt.Sprintf(", unit: %q", d.unit)
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.

I'd add this inline and show unit is empty? We expanded the data structure so we can expand the error too


func NewInfoVec(name, help string, labelNames []string) *InfoVec {
desc := prometheus.NewDesc(name, help, labelNames, nil)
func NewInfoVec(name, help string, labelNames []string, unit ...string) *InfoVec {
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.

Info likely does not have unit?

sampleMap[d.Name] = &sampleBuf[len(sampleBuf)-1]

// Extract unit from the runtime/metrics name (e.g., "/gc/heap/allocs:bytes" -> "bytes")
unit := d.Name[strings.IndexRune(d.Name, ':')+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.

As found in https://gist.github.com/bwplotka/a38c33a74fad88a46c4623e924fa58c8#file-prometheusclient_golang1392-md, I think indeed we should handle the case of no unit here gracefully (-1 index response):

> Unsafe Substring Slice during Runtime Metric Unit Extraction

File with exact line number: prometheus/go_collector_latest.go:L213
Link to a file line on GitHub PR: https://github.com/prometheus/client_golang/pull/1392/files#diff-8360f0c0ae2f0d98fb9ecdf37119d6d333dc17ec23f0547055c1ddfdf4ed0759R213
Problem: The unit is extracted using d.Name[strings.IndexRune(d.Name, ':')+1:]. If a runtime metric is introduced without a :, strings.IndexRune evaluates to -1 resulting in -1+1 = 0. The unit silently turns into the entire metric name, bypassing obvious fault detection. (Note: this extraction assumption existed before the PR for histograms, but was generalized here).
Suggestion: Consider checking if strings.ContainsRune(d.Name, ':') before slicing to future-proof the collector against misconfigured runtime metrics.

vesari and others added 4 commits March 19, 2026 11:04
Co-authored-by: Bartlomiej Plotka <[email protected]>
Signed-off-by: Arianna Vespri <[email protected]>
Signed-off-by: Arianna Vespri <[email protected]>
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.

OpenMetrics unit support (in v1)

4 participants