Skip to content

Commit c2fbf25

Browse files
bwplotkaaknuds1
authored andcommitted
feature: type-and-unit-labels (extended MetricIdentity)
Experimental implementation of prometheus/proposals#39 Previous (unmerged) experiments: * prometheus/prometheus@main...dashpole:prometheus:type_and_unit_labels * prometheus#16025 Signed-off-by: bwplotka <[email protected]>
1 parent 4aee718 commit c2fbf25

File tree

15 files changed

+417
-136
lines changed

15 files changed

+417
-136
lines changed

cmd/prometheus/main.go

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -283,19 +283,9 @@ func (c *flagConfig) setFeatureListOptions(logger *slog.Logger) error {
283283
case "otlp-deltatocumulative":
284284
c.web.ConvertOTLPDelta = true
285285
logger.Info("Converting delta OTLP metrics to cumulative")
286-
case "otlp-native-delta-ingestion":
287-
// Experimental OTLP native delta ingestion.
288-
// This currently just stores the raw delta value as-is with unknown metric type. Better typing and
289-
// type-aware functions may come later.
290-
// See proposal: https://github.com/prometheus/proposals/pull/48
291-
c.web.NativeOTLPDeltaIngestion = true
292-
logger.Info("Enabling native ingestion of delta OTLP metrics, storing the raw sample values without conversion. WARNING: Delta support is in an early stage of development. The ingestion and querying process is likely to change over time.")
293286
case "type-and-unit-labels":
294287
c.scrape.EnableTypeAndUnitLabels = true
295288
logger.Info("Experimental type and unit labels enabled")
296-
case "use-uncached-io":
297-
c.tsdb.UseUncachedIO = true
298-
logger.Info("Experimental Uncached IO is enabled.")
299289
default:
300290
logger.Warn("Unknown option for --enable-feature", "option", o)
301291
}

docs/feature_flags.md

Lines changed: 9 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -217,70 +217,23 @@ Examples of equivalent durations:
217217

218218
[d2c]: https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/deltatocumulativeprocessor
219219

220-
## OTLP Native Delta Support
221-
222-
`--enable-feature=otlp-native-delta-ingestion`
223-
224-
When enabled, allows for the native ingestion of delta OTLP metrics, storing the raw sample values without conversion. This cannot be enabled in conjunction with `otlp-deltatocumulative`.
225-
226-
Currently, the StartTimeUnixNano field is ignored, and deltas are given the unknown metric metadata type.
227-
228-
Delta support is in a very early stage of development and the ingestion and querying process my change over time. For the open proposal see [prometheus/proposals#48](https://github.com/prometheus/proposals/pull/48).
229-
230-
### Querying
231-
232-
We encourage users to experiment with deltas and existing PromQL functions; we will collect feedback and likely build features to improve the experience around querying deltas.
233-
234-
Note that standard PromQL counter functions like `rate()` and `increase()` are designed for cumulative metrics and will produce incorrect results when used with delta metrics. This may change in the future, but for now, to get similar results for delta metrics, you need `sum_over_time()`:
235-
236-
* `sum_over_time(delta_metric[<range>])`: Calculates the sum of delta values over the specified time range.
237-
* `sum_over_time(delta_metric[<range>]) / <range>`: Calculates the per-second rate of the delta metric.
238-
239-
These may not work well if the `<range>` is not a multiple of the collection interval of the metric. For example, if you do `sum_over_time(delta_metric[1m]) / 1m` range query (with a 1m step), but the collection interval of a metric is 10m, the graph will show a single point every 10 minutes with a high rate value, rather than 10 points with a lower, constant value.
240-
241-
### Current gotchas
242-
243-
* If delta metrics are exposed via [federation](https://prometheus.io/docs/prometheus/latest/federation/), data can be incorrectly collected if the ingestion interval is not the same as the scrape interval for the federated endpoint.
244-
245-
* It is difficult to figure out whether a metric has delta or cumulative temporality, since there's no indication of temporality in metric names or labels. For now, if you are ingesting a mix of delta and cumulative metrics we advise you to explicitly add your own labels to distinguish them. In the future, we plan to introduce type labels to consistently distinguish metric types and potentially make PromQL functions type-aware (e.g. providing warnings when cumulative-only functions are used with delta metrics).
246-
247-
* If there are multiple samples being ingested at the same timestamp, only one of the points is kept - the samples are **not** summed together (this is how Prometheus works in general - duplicate timestamp samples are rejected). Any aggregation will have to be done before sending samples to Prometheus.
248-
249220
## Type and Unit Labels
250221

251222
`--enable-feature=type-and-unit-labels`
252223

253-
When enabled, Prometheus will start injecting additional, reserved `__type__`
254-
and `__unit__` labels as designed in the [PROM-39 proposal](https://github.com/prometheus/proposals/pull/39).
255-
256-
Those labels are sourced from the metadata structured of the existing scrape and ingestion formats
257-
like OpenMetrics Text, Prometheus Text, Prometheus Proto, Remote Write 2 and OTLP. All the user provided labels with
258-
`__type__` and `__unit__` will be overridden.
259-
260-
PromQL layer will handle those labels the same way __name__ is handled, e.g. dropped
261-
on certain operations like `-` or `+` and affected by `promql-delayed-name-removal` feature.
224+
When enabled, Prometheus will start injecting additional, special `__type__`
225+
and `__unit__` labels that extends the existing `__name__` metric identity.
262226

263-
This feature enables important metadata information to be accessible directly with samples and PromQL layer.
264-
265-
It's especially useful for users who:
227+
Those labels are injected from the metadata parts of OpenMetrics and other scrape expositions
228+
, as well as Remote Write 2.0 and OTLP receive. All user provided labels with
229+
`__type__` and `__unit__` will be dropped or overridden.
266230

231+
This is useful for users who:
267232
* Want to be able to select metrics based on type or unit.
268233
* Want to handle cases of series with the same metric name and different type and units.
269-
e.g. native histogram migrations or OpenTelemetry metrics from OTLP endpoint, without translation.
234+
e.g. native histogram migrations or OpenTelemetry metrics from OTLP endpoint, without translation.
270235

271-
In future more [work is planned](https://github.com/prometheus/prometheus/issues/16610) that will depend on this e.g. rich PromQL UX that helps
236+
In future more work is planned that will depend on this e.g. rich PromQL UX that helps
272237
when wrong types are used on wrong functions, automatic renames, delta types and more.
273238

274-
## Use Uncached IO
275-
276-
`--enable-feature=use-uncached-io`
277-
278-
Experimental and only available on Linux.
279-
280-
When enabled, it makes chunks writing bypass the page cache. Its primary
281-
goal is to reduce confusion around page‐cache behavior and to prevent over‑allocation of
282-
memory in response to misleading cache growth.
283-
284-
This is currently implemented using direct I/O.
285-
286-
For more details, see the [proposal](https://github.com/prometheus/proposals/pull/45).
239+
See [proposal](https://github.com/prometheus/proposals/pull/39)

model/labels/labels.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,25 @@ func (ls Labels) DropReserved(shouldDropFn func(name string) bool) Labels {
364364
return ls
365365
}
366366

367+
// DropMetricIdentity is like DropMetricName but drops all parts of MetricIdentity.
368+
func (ls Labels) DropMetricIdentity() Labels {
369+
rm := 0
370+
for i, l := range ls {
371+
if IsMetricIdentityLabel(l.Name) {
372+
i := i - rm // Offsetting after removals.
373+
if i == 0 { // Make common case fast with no allocations.
374+
ls = ls[1:]
375+
} else {
376+
// Avoid modifying original Labels - use [:i:i] so that left slice would not
377+
// have any spare capacity and append would have to allocate a new slice for the result.
378+
ls = append(ls[:i:i], ls[i+1:]...)
379+
}
380+
rm++
381+
}
382+
}
383+
return ls
384+
}
385+
367386
// InternStrings calls intern on every string value inside ls, replacing them with what it returns.
368387
func (ls *Labels) InternStrings(intern func(string) string) {
369388
for i, l := range *ls {

model/labels/labels_common.go

Lines changed: 112 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,138 @@ import (
1818
"encoding/json"
1919
"slices"
2020
"strconv"
21+
"strings"
2122
"unsafe"
2223

2324
"github.com/prometheus/common/model"
2425
)
2526

2627
const (
27-
// MetricName is a special label name that represent a metric name.
28-
// Deprecated: Use schema.Metadata structure and its methods.
28+
// MetricName is a special label name and selector for MetricIdentity.Name.
2929
MetricName = "__name__"
3030

31-
AlertName = "alertname"
32-
BucketLabel = "le"
31+
// metricType is a special label name and selector for MetricIdentity.Type.
32+
// Private to ensure __name__, __type__ and __unit__ are used together
33+
// and remain extensible in Prometheus. See Labels.MetricIdentity,
34+
// Builder.SetMetricIdentity and ScratchBuilder.AddMetricIdentity for access.
35+
metricType = "__type__"
36+
// MetricUnit is a special label name and selector for MetricIdentity.Unit,
37+
// which in the past used to be stored in metadata.
38+
// Private to ensure __name__, __type__ and __unit__ are used together
39+
// and remain extensible in Prometheus. See Labels.MetricIdentity,
40+
// Builder.SetMetricIdentity and ScratchBuilder.AddMetricIdentity for access.
41+
metricUnit = "__unit__"
42+
43+
AlertName = "alertname"
44+
BucketLabel = "le"
45+
InstanceName = "instance"
3346

3447
labelSep = '\xfe' // Used at beginning of `Bytes` return.
3548
sep = '\xff' // Used between labels in `Bytes` and `Hash`.
3649
)
3750

51+
// IsMetricIdentityLabel returns true if the given label name is a special
52+
// metric identity label.
53+
func IsMetricIdentityLabel(name string) bool {
54+
return name == MetricName || name == metricType || name == metricUnit
55+
}
56+
57+
// MetricIdentity represents extended metric identity parts beyond the metric name.
58+
// Each "time series" is identifiable by MetricIdentity and other labels e.g. job.
59+
type MetricIdentity struct {
60+
// Name represents metric name (not always the same as metric family, until we
61+
// have native, structured metric representation for all types).
62+
// Empty means nameless metric (e.g. result of the PromQL function).
63+
Name string
64+
// Type, empty ("") is equivalent to model.UnknownMetricType.
65+
// In the past Prometheus used to be stored it n metadata.
66+
Type model.MetricType
67+
// Unit of the metric, regardless if encoded in the metric name. Empty means
68+
// unitless metric (e.g. result of the PromQL function).
69+
// In the past Prometheus used to be stored it n metadata.
70+
Unit string
71+
}
72+
73+
func (m MetricIdentity) String() string {
74+
b := strings.Builder{}
75+
b.WriteString(m.Name)
76+
if m.Unit != "" {
77+
b.WriteString("~")
78+
b.WriteString(m.Unit)
79+
}
80+
if m.Type != "" && m.Type != model.MetricTypeUnknown {
81+
b.WriteString(".")
82+
b.WriteString(string(m.Type))
83+
}
84+
return b.String()
85+
}
86+
3887
var seps = []byte{sep} // Used with Hash, which has no WriteByte method.
3988

4089
// Label is a key/value a pair of strings.
4190
type Label struct {
4291
Name, Value string
4392
}
4493

94+
// MetricIdentity returns the metric identity parts.
95+
func (ls Labels) MetricIdentity() MetricIdentity {
96+
typ := model.MetricTypeUnknown
97+
if got := ls.Get(metricType); got != "" {
98+
typ = model.MetricType(got)
99+
}
100+
return MetricIdentity{
101+
Name: ls.Get(MetricName),
102+
Type: typ,
103+
Unit: ls.Get(metricUnit),
104+
}
105+
}
106+
107+
// SetMetricIdentity injects metric identity parts into labels.
108+
// Empty fields of the given MetricIdentity (or unknown metric type), will
109+
// cause removal of the existing part labels.
110+
func (b *Builder) SetMetricIdentity(mid MetricIdentity) *Builder {
111+
b.Set(MetricName, mid.Name)
112+
if mid.Type == model.MetricTypeUnknown {
113+
// Unknown equals empty semantically, so remove the label on unknown too as per
114+
// method signature comment.
115+
mid.Type = ""
116+
}
117+
b.Set(metricType, string(mid.Type))
118+
b.Set(metricUnit, mid.Unit)
119+
return b
120+
}
121+
122+
// IgnoreIdentityLabelsScratchBuilder is a wrapper over scratch builder
123+
// that ignores subsequent additions of special metric identity labels.
124+
type IgnoreIdentityLabelsScratchBuilder struct {
125+
*ScratchBuilder
126+
}
127+
128+
// Add a name/value pair, unless it's a special metric identity label e.g. __name__, __type__, __unit__.
129+
// Note if you Add the same name twice you will get a duplicate label, which is invalid.
130+
func (b IgnoreIdentityLabelsScratchBuilder) Add(name, value string) {
131+
if IsMetricIdentityLabel(name) {
132+
return
133+
}
134+
b.ScratchBuilder.Add(name, value)
135+
}
136+
137+
// AddMetricIdentity adds metric identity parts into labels.
138+
// Empty fields of the given MetricIdentity (or unknown metric type), will be ignored.
139+
//
140+
//nolint:revive // unexported type
141+
func (b *ScratchBuilder) AddMetricIdentity(mid MetricIdentity) {
142+
if mid.Name != "" {
143+
b.Add(MetricName, mid.Name)
144+
}
145+
if mid.Type != "" && mid.Type != model.MetricTypeUnknown {
146+
b.Add(metricType, string(mid.Type))
147+
}
148+
if mid.Unit != "" {
149+
b.Add(metricUnit, mid.Unit)
150+
}
151+
}
152+
45153
func (ls Labels) String() string {
46154
var bytea [1024]byte // On stack to avoid memory allocation while building the output.
47155
b := bytes.NewBuffer(bytea[:0])

model/labels/labels_dedupelabels.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -554,8 +554,8 @@ func (ls Labels) ReleaseStrings(release func(string)) {
554554
// TODO: remove these calls as there is nothing to do.
555555
}
556556

557-
// DropMetricName returns Labels with the "__name__" removed.
558-
// Deprecated: Use DropReserved instead.
557+
// DropMetricName returns Labels with "__name__" removed.
558+
// Deprecate: Use DropMetric instead to handle type and unit correctly.
559559
func (ls Labels) DropMetricName() Labels {
560560
return ls.DropReserved(func(n string) bool { return n == MetricName })
561561
}
@@ -581,6 +581,27 @@ func (ls Labels) DropReserved(shouldDropFn func(name string) bool) Labels {
581581
return ls
582582
}
583583

584+
// DropMetricIdentity is like DropMetricName but drops all parts of MetricIdentity.
585+
func (ls Labels) DropMetricIdentity() Labels {
586+
for i := 0; i < len(ls.data); {
587+
lName, i2 := decodeString(ls.syms, ls.data, i)
588+
_, i2 = decodeVarint(ls.data, i2)
589+
if lName[0] > '_' { // Stop looking if we've gone past special labels.
590+
break
591+
}
592+
if IsMetricIdentityLabel(lName) {
593+
if i == 0 { // Make common case fast with no allocations.
594+
ls.data = ls.data[i2:]
595+
} else {
596+
ls.data = ls.data[:i] + ls.data[i2:]
597+
}
598+
continue
599+
}
600+
i = i2
601+
}
602+
return ls
603+
}
604+
584605
// Builder allows modifying Labels.
585606
type Builder struct {
586607
syms *SymbolTable

model/labels/labels_stringlabels.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,28 @@ func (ls Labels) DropReserved(shouldDropFn func(name string) bool) Labels {
441441
return ls
442442
}
443443

444+
// DropMetricIdentity is like DropMetricName but drops all parts of MetricIdentity.
445+
func (ls Labels) DropMetricIdentity() Labels {
446+
for i := 0; i < len(ls.data); {
447+
lName, i2 := decodeString(ls.data, i)
448+
size, i2 := decodeSize(ls.data, i2)
449+
i2 += size
450+
if lName[0] > '_' { // Stop looking if we've gone past special labels.
451+
break
452+
}
453+
if IsMetricIdentityLabel(lName) {
454+
if i == 0 { // Make common case fast with no allocations.
455+
ls.data = ls.data[i2:]
456+
} else {
457+
ls.data = ls.data[:i] + ls.data[i2:]
458+
}
459+
continue
460+
}
461+
i = i2
462+
}
463+
return ls
464+
}
465+
444466
// InternStrings is a no-op because it would only save when the whole set of labels is identical.
445467
func (ls *Labels) InternStrings(_ func(string) string) {
446468
}

model/labels/labels_test.go

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -528,17 +528,15 @@ func TestLabels_DropMetricName(t *testing.T) {
528528
require.True(t, Equal(original, check))
529529
}
530530

531-
func TestLabels_DropReserved(t *testing.T) {
532-
shouldDropFn := func(n string) bool {
533-
return n == MetricName || n == "__something__"
534-
}
535-
require.True(t, Equal(FromStrings("aaa", "111", "bbb", "222"), FromStrings("aaa", "111", "bbb", "222").DropReserved(shouldDropFn)))
536-
require.True(t, Equal(FromStrings("aaa", "111"), FromStrings(MetricName, "myname", "aaa", "111").DropReserved(shouldDropFn)))
537-
require.True(t, Equal(FromStrings("aaa", "111"), FromStrings(MetricName, "myname", "__something__", string(model.MetricTypeCounter), "aaa", "111").DropReserved(shouldDropFn)))
531+
func TestLabels_DropMetricIdentity(t *testing.T) {
532+
require.True(t, Equal(FromStrings("aaa", "111", "bbb", "222"), FromStrings("aaa", "111", "bbb", "222").DropMetricIdentity()))
533+
require.True(t, Equal(FromStrings("aaa", "111"), FromStrings(MetricName, "myname", "aaa", "111").DropMetricIdentity()))
534+
require.True(t, Equal(FromStrings("aaa", "111"), FromStrings(MetricName, "myname", metricType, string(model.MetricTypeCounter), "aaa", "111").DropMetricIdentity()))
535+
require.True(t, Equal(FromStrings("aaa", "111"), FromStrings(MetricName, "myname", metricType, string(model.MetricTypeCounter), metricUnit, "seconds", "aaa", "111").DropMetricIdentity()))
538536

539537
original := FromStrings("__aaa__", "111", MetricName, "myname", "bbb", "222")
540538
check := original.Copy()
541-
require.True(t, Equal(FromStrings("__aaa__", "111", "bbb", "222"), check.DropReserved(shouldDropFn)))
539+
require.True(t, Equal(FromStrings("__aaa__", "111", "bbb", "222"), check.DropMetricIdentity()))
542540
require.True(t, Equal(original, check))
543541
}
544542

model/textparse/openmetricsparse.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,15 @@ func (p *OpenMetricsParser) Labels(l *labels.Labels) {
219219

220220
p.builder.Reset()
221221
metricName := unreplace(s[p.offsets[0]-p.start : p.offsets[1]-p.start])
222+
if p.enableTypeAndUnitLabels {
223+
p.builder.AddMetricIdentity(labels.MetricIdentity{
224+
Name: metricName,
225+
Type: p.mtype,
226+
Unit: p.unit,
227+
})
228+
} else {
229+
p.builder.Add(labels.MetricName, metricName)
230+
}
222231

223232
m := schema.Metadata{
224233
Name: metricName,
@@ -234,8 +243,8 @@ func (p *OpenMetricsParser) Labels(l *labels.Labels) {
234243
a := p.offsets[i] - p.start
235244
b := p.offsets[i+1] - p.start
236245
label := unreplace(s[a:b])
237-
if p.enableTypeAndUnitLabels && !m.IsEmptyFor(label) {
238-
// Dropping user provided metadata labels, if found in the OM metadata.
246+
if p.enableTypeAndUnitLabels && labels.IsMetricIdentityLabel(label) {
247+
// Dropping user provided id labels if needed.
239248
continue
240249
}
241250
c := p.offsets[i+2] - p.start

0 commit comments

Comments
 (0)