Skip to content

Commit c98d047

Browse files
committed
add exmplar supplier
Signed-off-by: Jay DeLuca <jaydeluca4@gmail.com>
1 parent 107c563 commit c98d047

10 files changed

Lines changed: 454 additions & 8 deletions

File tree

docs/apidiffs/current_vs_latest/prometheus-metrics-core.txt

Lines changed: 17 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/content/otel/tracing.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,43 @@ The [examples/example-exemplar-tail-sampling/](https://github.com/prometheus/cli
7575
directory has a complete end-to-end example, with a distributed Java application with two services,
7676
an OpenTelemetry collector, Prometheus, Tempo as a trace database, and Grafana dashboards. Use
7777
docker-compose as described in the example's readme to run the example and explore the results.
78+
79+
## Adding custom labels to exemplars
80+
81+
Automatically-sampled exemplars carry the `trace_id` and `span_id` labels. You can attach
82+
additional, custom labels (for example an internal identifier) to every automatically-sampled
83+
exemplar. There are two options.
84+
85+
### Global (all metrics)
86+
87+
Register a global supplier to add custom labels to the exemplars of _all_ metrics, including
88+
metrics registered by third-party libraries that you do not control. This is the right option when
89+
you cannot modify the code that creates the metric:
90+
91+
```java
92+
ExemplarLabelsSupplier.setExemplarLabelsSupplier(
93+
() -> Labels.of("management_id", currentManagementId()));
94+
```
95+
96+
### Per metric
97+
98+
If you only want the extra labels on a specific metric you define yourself, use the builder:
99+
100+
```java
101+
Counter counter =
102+
Counter.builder()
103+
.name("requests_total")
104+
.exemplarLabelsSupplier(() -> Labels.of("management_id", currentManagementId()))
105+
.build();
106+
```
107+
108+
### Notes
109+
110+
- The supplier is invoked on the (rate-limited) hot path each time an exemplar is sampled, so it
111+
should be cheap. It may return dynamic, request-scoped values (e.g. read from a thread-local).
112+
- Custom labels are only added when a valid, sampled span context is present; the supplier never
113+
causes an exemplar to be created on its own.
114+
- Precedence on a label-name collision: the reserved `trace_id`/`span_id` labels always win, then
115+
the per-metric supplier, then the global supplier. Colliding labels are silently dropped.
116+
- If the supplier throws, the exception is swallowed and the exemplar is created without the
117+
additional labels, so a misbehaving supplier never breaks metric collection.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package io.prometheus.metrics.core.exemplars;
2+
3+
import io.prometheus.metrics.annotations.StableApi;
4+
import io.prometheus.metrics.model.snapshots.Labels;
5+
import java.util.concurrent.atomic.AtomicReference;
6+
import java.util.function.Supplier;
7+
import javax.annotation.Nullable;
8+
9+
/**
10+
* Global holder for a {@link Supplier} of additional {@link Labels} that are merged into every
11+
* automatically-sampled Exemplar across the entire application.
12+
*
13+
* <p>This is the global counterpart to the per-metric {@code exemplarLabelsSupplier(...)} builder
14+
* method. Registering a supplier here affects <em>all</em> metrics, including metrics registered by
15+
* third-party libraries that the application does not control. This makes it the right tool when
16+
* you cannot modify the code that creates the metrics.
17+
*
18+
* <p>The supplier is invoked on the metric hot path (rate-limited by the exemplar sampler), each
19+
* time an Exemplar is sampled from a valid, sampled span context. It should therefore be cheap and
20+
* non-blocking. It may return dynamic, request-scoped values, for example an identifier read from a
21+
* thread-local:
22+
*
23+
* <pre>{@code
24+
* ExemplarLabelsSupplier.setExemplarLabelsSupplier(
25+
* () -> Labels.of("management_id", currentManagementId()));
26+
* }</pre>
27+
*
28+
* <p>Labels returned by the supplier that collide with {@code trace_id}/{@code span_id} (or, when a
29+
* per-metric supplier is also configured, with that supplier's labels) are silently dropped rather
30+
* than causing an error: the per-metric supplier takes precedence over the global one, and the
31+
* reserved {@code trace_id}/{@code span_id} labels always win. If the supplier throws, the
32+
* exception is swallowed and the Exemplar is created without the additional labels, so a
33+
* misbehaving supplier never breaks metric collection.
34+
*/
35+
@StableApi
36+
public class ExemplarLabelsSupplier {
37+
38+
private static final AtomicReference<Supplier<Labels>> supplierRef = new AtomicReference<>();
39+
40+
private ExemplarLabelsSupplier() {}
41+
42+
/**
43+
* Register a global supplier of additional exemplar labels. Pass {@code null} to remove a
44+
* previously registered supplier. The most recently registered supplier wins.
45+
*/
46+
public static void setExemplarLabelsSupplier(@Nullable Supplier<Labels> supplier) {
47+
supplierRef.set(supplier);
48+
}
49+
50+
/** Returns the registered global supplier, or {@code null} if none has been set. */
51+
@Nullable
52+
public static Supplier<Labels> getExemplarLabelsSupplier() {
53+
return supplierRef.get();
54+
}
55+
}

prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/exemplars/ExemplarSampler.java

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import java.util.concurrent.TimeUnit;
1414
import java.util.concurrent.atomic.AtomicBoolean;
1515
import java.util.function.LongSupplier;
16+
import java.util.function.Supplier;
1617
import javax.annotation.Nullable;
1718

1819
/**
@@ -47,8 +48,10 @@ public class ExemplarSampler {
4748
private final SpanContext
4849
spanContext; // may be null, in that case SpanContextSupplier.getSpanContext() is used.
4950

51+
@Nullable private final Supplier<Labels> additionalLabelsSupplier;
52+
5053
public ExemplarSampler(ExemplarSamplerConfig config) {
51-
this(config, null);
54+
this(config, null, null);
5255
}
5356

5457
/**
@@ -60,10 +63,24 @@ public ExemplarSampler(ExemplarSamplerConfig config) {
6063
* SpanContextSupplier.getSpanContext()} is called to find a span context.
6164
*/
6265
public ExemplarSampler(ExemplarSamplerConfig config, @Nullable SpanContext spanContext) {
66+
this(config, spanContext, null);
67+
}
68+
69+
/**
70+
* Constructor that additionally accepts a supplier of labels to be merged into every
71+
* automatically-sampled exemplar. The supplier is called each time an exemplar is sampled from a
72+
* span context, so it can return dynamic values (e.g. a request-scoped identifier). The supplier
73+
* is only called when a valid, sampled span context is present.
74+
*/
75+
public ExemplarSampler(
76+
ExemplarSamplerConfig config,
77+
@Nullable SpanContext spanContext,
78+
@Nullable Supplier<Labels> additionalLabelsSupplier) {
6379
this.config = config;
6480
this.exemplars = new Exemplar[config.getNumberOfExemplars()];
6581
this.customExemplars = new Exemplar[exemplars.length];
6682
this.spanContext = spanContext;
83+
this.additionalLabelsSupplier = additionalLabelsSupplier;
6784
}
6885

6986
public Exemplars collect() {
@@ -322,7 +339,7 @@ private long durationUntilNextExemplarExpires(long now) {
322339

323340
private long updateCustomExemplar(int index, double value, Labels labels, long now) {
324341
if (!labels.contains(Exemplar.TRACE_ID) && !labels.contains(Exemplar.SPAN_ID)) {
325-
labels = labels.merge(doSampleExemplar());
342+
labels = mergeLabels(labels, doSampleExemplar());
326343
}
327344
customExemplars[index] =
328345
Exemplar.builder().value(value).labels(labels).timestampMillis(now).build();
@@ -357,7 +374,14 @@ private Labels doSampleExemplar() {
357374
String traceId = spanContext.getCurrentTraceId();
358375
if (spanId != null && traceId != null) {
359376
spanContext.markCurrentSpanAsExemplar();
360-
return Labels.of(Exemplar.TRACE_ID, traceId, Exemplar.SPAN_ID, spanId);
377+
Labels labels = Labels.of(Exemplar.TRACE_ID, traceId, Exemplar.SPAN_ID, spanId);
378+
// Per-metric supplier first (more specific), then the global supplier. On a name
379+
// collision the earlier (more specific) value is kept; the reserved trace_id/span_id
380+
// labels always win over both.
381+
labels = mergeAdditionalLabels(labels, additionalLabelsSupplier);
382+
labels =
383+
mergeAdditionalLabels(labels, ExemplarLabelsSupplier.getExemplarLabelsSupplier());
384+
return labels;
361385
}
362386
}
363387
}
@@ -366,4 +390,68 @@ private Labels doSampleExemplar() {
366390
}
367391
return Labels.EMPTY;
368392
}
393+
394+
/**
395+
* Merge labels from {@code supplier} into {@code base}, dropping any label whose name already
396+
* exists in {@code base}. Never throws: a {@code null} supplier, a {@code null}/empty result, a
397+
* colliding label name, or an exception thrown by the supplier all result in {@code base} being
398+
* returned unchanged (minus the offending labels). A misbehaving supplier must never break metric
399+
* collection.
400+
*/
401+
private static Labels mergeAdditionalLabels(Labels base, @Nullable Supplier<Labels> supplier) {
402+
if (supplier == null) {
403+
return base;
404+
}
405+
Labels extra;
406+
try {
407+
extra = supplier.get();
408+
} catch (Throwable ignored) {
409+
// A misbehaving supplier (any RuntimeException or Error) must never break metric collection.
410+
return base;
411+
}
412+
if (extra == null || extra.isEmpty()) {
413+
return base;
414+
}
415+
return mergeLabels(base, extra);
416+
}
417+
418+
/**
419+
* Merge {@code extra} into {@code base}, dropping any label whose name already exists in {@code
420+
* base}.
421+
*/
422+
private static Labels mergeLabels(Labels base, Labels extra) {
423+
if (extra.isEmpty()) {
424+
return base;
425+
}
426+
// Count name collisions with base in a single pass so we can merge exactly once below: base
427+
// (trace_id/span_id and any more-specific supplier) always wins, so colliding labels are
428+
// dropped. extra is itself a valid Labels (no internal duplicates), so the surviving labels
429+
// never collide with each other and merge() cannot throw on a duplicate name.
430+
int size = extra.size();
431+
int collisions = 0;
432+
for (int i = 0; i < size; i++) {
433+
if (base.contains(extra.getName(i))) {
434+
collisions++;
435+
}
436+
}
437+
if (collisions == 0) {
438+
return base.merge(extra);
439+
}
440+
if (collisions == size) {
441+
return base;
442+
}
443+
int kept = size - collisions;
444+
String[] names = new String[kept];
445+
String[] values = new String[kept];
446+
int j = 0;
447+
for (int i = 0; i < size; i++) {
448+
String name = extra.getName(i);
449+
if (!base.contains(name)) {
450+
names[j] = name;
451+
values[j] = extra.getValue(i);
452+
j++;
453+
}
454+
}
455+
return base.merge(names, values);
456+
}
369457
}

prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Counter.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import java.util.List;
1616
import java.util.concurrent.atomic.DoubleAdder;
1717
import java.util.concurrent.atomic.LongAdder;
18+
import java.util.function.Supplier;
1819
import javax.annotation.Nullable;
1920

2021
/**
@@ -37,6 +38,7 @@ public class Counter extends StatefulMetric<CounterDataPoint, Counter.DataPoint>
3738
implements CounterDataPoint {
3839

3940
@Nullable private final ExemplarSamplerConfig exemplarSamplerConfig;
41+
@Nullable private final Supplier<Labels> exemplarLabelsSupplier;
4042

4143
private Counter(Builder builder, PrometheusProperties prometheusProperties) {
4244
super(builder);
@@ -49,6 +51,7 @@ private Counter(Builder builder, PrometheusProperties prometheusProperties) {
4951
} else {
5052
exemplarSamplerConfig = null;
5153
}
54+
exemplarLabelsSupplier = builder.exemplarLabelsSupplier;
5255
}
5356

5457
@Override
@@ -108,7 +111,8 @@ public MetricType getMetricType() {
108111
@Override
109112
protected DataPoint newDataPoint() {
110113
if (exemplarSamplerConfig != null) {
111-
return new DataPoint(new ExemplarSampler(exemplarSamplerConfig));
114+
return new DataPoint(
115+
new ExemplarSampler(exemplarSamplerConfig, null, exemplarLabelsSupplier));
112116
} else {
113117
return new DataPoint(null);
114118
}

prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Gauge.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.util.Collections;
1515
import java.util.List;
1616
import java.util.concurrent.atomic.AtomicLong;
17+
import java.util.function.Supplier;
1718
import javax.annotation.Nullable;
1819

1920
/**
@@ -44,6 +45,7 @@ public class Gauge extends StatefulMetric<GaugeDataPoint, Gauge.DataPoint>
4445
implements GaugeDataPoint {
4546

4647
@Nullable private final ExemplarSamplerConfig exemplarSamplerConfig;
48+
@Nullable private final Supplier<Labels> exemplarLabelsSupplier;
4749

4850
private Gauge(Builder builder, PrometheusProperties prometheusProperties) {
4951
super(builder);
@@ -56,6 +58,7 @@ private Gauge(Builder builder, PrometheusProperties prometheusProperties) {
5658
} else {
5759
exemplarSamplerConfig = null;
5860
}
61+
exemplarLabelsSupplier = builder.exemplarLabelsSupplier;
5962
}
6063

6164
@Override
@@ -110,7 +113,8 @@ public MetricType getMetricType() {
110113
@Override
111114
protected DataPoint newDataPoint() {
112115
if (exemplarSamplerConfig != null) {
113-
return new DataPoint(new ExemplarSampler(exemplarSamplerConfig));
116+
return new DataPoint(
117+
new ExemplarSampler(exemplarSamplerConfig, null, exemplarLabelsSupplier));
114118
} else {
115119
return new DataPoint(null);
116120
}

prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Histogram.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.util.concurrent.atomic.AtomicBoolean;
2727
import java.util.concurrent.atomic.DoubleAdder;
2828
import java.util.concurrent.atomic.LongAdder;
29+
import java.util.function.Supplier;
2930
import javax.annotation.Nullable;
3031

3132
/**
@@ -73,6 +74,7 @@ public class Histogram extends StatefulMetric<DistributionDataPoint, Histogram.D
7374
private static final double[][] NATIVE_BOUNDS;
7475

7576
@Nullable private final ExemplarSamplerConfig exemplarSamplerConfig;
77+
@Nullable private final Supplier<Labels> exemplarLabelsSupplier;
7678

7779
// Upper bounds for the classic histogram buckets. Contains at least +Inf.
7880
// An empty array indicates that this is a native histogram only.
@@ -171,6 +173,7 @@ private Histogram(Histogram.Builder builder, PrometheusProperties prometheusProp
171173
} else {
172174
exemplarSamplerConfig = null;
173175
}
176+
exemplarLabelsSupplier = builder.exemplarLabelsSupplier;
174177
}
175178

176179
@Override
@@ -212,7 +215,7 @@ public class DataPoint implements DistributionDataPoint {
212215

213216
private DataPoint() {
214217
if (exemplarSamplerConfig != null) {
215-
exemplarSampler = new ExemplarSampler(exemplarSamplerConfig);
218+
exemplarSampler = new ExemplarSampler(exemplarSamplerConfig, null, exemplarLabelsSupplier);
216219
} else {
217220
exemplarSampler = null;
218221
}

prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/StatefulMetric.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.util.Objects;
1515
import java.util.concurrent.ConcurrentHashMap;
1616
import java.util.function.Function;
17+
import java.util.function.Supplier;
1718
import javax.annotation.Nullable;
1819

1920
/**
@@ -200,11 +201,31 @@ abstract static class Builder<B extends Builder<B, M>, M extends StatefulMetric<
200201
extends MetricWithFixedMetadata.Builder<B, M> {
201202

202203
@Nullable protected Boolean exemplarsEnabled;
204+
@Nullable protected Supplier<Labels> exemplarLabelsSupplier;
203205

204206
protected Builder(List<String> illegalLabelNames, PrometheusProperties config) {
205207
super(illegalLabelNames, config);
206208
}
207209

210+
/**
211+
* Provide additional labels to be merged into every automatically-sampled exemplar of <em>this
212+
* metric</em>. The supplier is called each time an exemplar is sampled, so it can return
213+
* dynamic values (e.g. a request-scoped identifier from a thread-local). The supplier is only
214+
* invoked when a valid, sampled span context is present; it has no effect when tracing is not
215+
* active.
216+
*
217+
* <p>For a global supplier that applies to all metrics (including metrics registered by
218+
* third-party libraries you do not control), see {@link
219+
* io.prometheus.metrics.core.exemplars.ExemplarLabelsSupplier}. When both are configured, this
220+
* per-metric supplier takes precedence over the global one on a label-name collision, and the
221+
* reserved {@code trace_id}/{@code span_id} labels always win over both. Labels that collide
222+
* are silently dropped.
223+
*/
224+
public B exemplarLabelsSupplier(Supplier<Labels> supplier) {
225+
this.exemplarLabelsSupplier = supplier;
226+
return self();
227+
}
228+
208229
/** Allow Exemplars for this metric. */
209230
public B withExemplars() {
210231
this.exemplarsEnabled = true;

0 commit comments

Comments
 (0)