Skip to content

Commit ef6e57f

Browse files
committed
add Exemplar support for OpenTelemetry tracing (review)
1 parent a6469c2 commit ef6e57f

File tree

26 files changed

+428
-895
lines changed

26 files changed

+428
-895
lines changed

.circleci/config.yml

+1-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,5 @@ jobs:
1414
paths:
1515
- ~/.m2
1616
key: maven-dependencies-{{ checksum "pom.xml" }}
17-
- run: mvn verify
1817
orbs:
19-
prometheus: prometheus/prometheus@0.11.0
18+
prometheus: prometheus/prometheus@0.11.0

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ target/
1212
simpleclient_pushgateway/mockserver.log
1313
simpleclient_pushgateway/mockserver_request.log
1414
nb-configuration.xml
15-
15+
dependency-reduced-pom.xml

README.md

+19-11
Original file line numberDiff line numberDiff line change
@@ -334,25 +334,25 @@ Note that this is an example application for a unit test, so durations don't rep
334334
By default, Exemplars are enabled if OpenTelemetry tracing is detected. You can disable this globally with:
335335

336336
```java
337-
ExemplarConfig.disableByDefault();
337+
ExemplarConfig.disableExemplars();
338338
```
339339

340-
If you want to enable Exemplars only for a single metric, use `withExemplarSampler()` in the metric builder.
340+
If you want to enable Exemplars only for a single metric, use `withExemplars()` in the metric builder.
341341

342342
```java
343-
// This also works with Gauges, Histograms, and Summaries.
343+
// The same API is also provided by Histograms
344344
Counter labelsCustomExemplar = Counter.build()
345345
.name("number_of_events_total")
346346
.help("help")
347-
.withExemplarSampler(new MyExemplarSampler())
347+
.withExemplars()
348348
...
349349
.register();
350350
```
351351

352352
Likewise, you can disable Exemplars for a single metric like this:
353353

354354
```java
355-
// This also works with Gauges, Histograms, and Summaries.
355+
// The same API is also provided by Histograms
356356
Counter labelsCustomExemplar = Counter.build()
357357
.name("number_of_events_total")
358358
.help("help")
@@ -370,23 +370,31 @@ You might want to implement your own Exemplar sampler that provides more interes
370370
In order to do so, just implement the following interfaces:
371371

372372
* `CounterExemplarSampler`
373-
* `GaugeExemplarSampler`
374373
* `HistogramExemplarSampler`
375-
* `SummaryExemplarSampler`
376374

377375
You can set your implementations as default like this:
378376

379377
```java
380378
ExemplarConfig.setDefaultCounterExemplarSampler(mySampler);
381-
ExemplarConfig.setDefaultGaugeExemplarSampler(mySampler);
382379
ExemplarConfig.setDefaultHistogramExemplarSampler(mySampler);
383-
ExemplarConfig.setDefaultSummaryExemplarSampler(mySampler);
380+
```
381+
382+
If you don't want to use your implementation as a global default, you can also set it for a single metric like this:
383+
384+
```java
385+
// The same API is also provided by Histograms
386+
Counter labelsCustomExemplar = Counter.build()
387+
.name("number_of_events_total")
388+
.help("help")
389+
.withExemplars(new MyExemplarSampler())
390+
...
391+
.register();
384392
```
385393

386394
### Implement Support for Other Tracing Vendors
387395

388-
Version 0.11.0 implements support for OpenTelemetry. If you are a vendor for another distributed tracer,
389-
please create a pull request adding support for your system. The idea is that copy-and-paste the
396+
Version 0.11.0 provides support for OpenTelemetry. If you are a vendor for another distributed tracer,
397+
please create a pull request adding support for your system. The idea is to copy-and-paste the
390398
`tracer_otel` module and modify it for your needs. If this turns out to be too simplistic, please create
391399
a GitHub issue and let us know what you need.
392400

benchmark/pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
<dependency>
5050
<groupId>io.prometheus</groupId>
5151
<artifactId>simpleclient</artifactId>
52-
<version>0.10.1-SNAPSHOT</version>
52+
<version>${project.version}</version>
5353
</dependency>
5454
<dependency>
5555
<groupId>com.codahale.metrics</groupId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package io.prometheus.benchmark;
2+
3+
import io.prometheus.client.Counter;
4+
import io.prometheus.client.exemplars.impl.DefaultExemplarSampler;
5+
import io.prometheus.client.exemplars.tracer.common.SpanContext;
6+
import org.openjdk.jmh.annotations.Benchmark;
7+
import org.openjdk.jmh.annotations.BenchmarkMode;
8+
import org.openjdk.jmh.annotations.Mode;
9+
import org.openjdk.jmh.annotations.OutputTimeUnit;
10+
import org.openjdk.jmh.annotations.Scope;
11+
import org.openjdk.jmh.annotations.Setup;
12+
import org.openjdk.jmh.annotations.State;
13+
14+
import java.util.concurrent.TimeUnit;
15+
16+
@State(Scope.Benchmark)
17+
public class ExemplarsBenchmark {
18+
19+
private Counter counter;
20+
private Counter counterWithExemplars;
21+
private Counter counterWithoutExemplars;
22+
23+
@Setup
24+
public void setup() {
25+
26+
counter = Counter.build()
27+
.name("counter_total")
28+
.help("Total number of requests.")
29+
.labelNames("path")
30+
.create();
31+
32+
counterWithExemplars = Counter.build()
33+
.name("counter_with_exemplars_total")
34+
.help("Total number of requests.")
35+
.labelNames("path")
36+
.withExemplars(new DefaultExemplarSampler(new MockSpanContext()))
37+
.create();
38+
39+
counterWithoutExemplars = Counter.build()
40+
.name("counter_without_exemplars_total")
41+
.help("Total number of requests.")
42+
.labelNames("path")
43+
.withoutExemplars()
44+
.create();
45+
}
46+
47+
@Benchmark
48+
@BenchmarkMode({Mode.AverageTime})
49+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
50+
public void testCounter() {
51+
counter.labels("test").inc();
52+
}
53+
54+
@Benchmark
55+
@BenchmarkMode({Mode.AverageTime})
56+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
57+
public void testCounterWithExemplars() {
58+
counterWithExemplars.labels("test").inc();
59+
}
60+
61+
@Benchmark
62+
@BenchmarkMode({Mode.AverageTime})
63+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
64+
public void testCounterWithoutExemplars() {
65+
counterWithoutExemplars.labels("test").inc();
66+
}
67+
68+
private static class MockSpanContext implements SpanContext {
69+
70+
@Override
71+
public String getTraceId() {
72+
return "trace-id";
73+
}
74+
75+
@Override
76+
public String getSpanId() {
77+
return "span-id";
78+
}
79+
}
80+
}

exemplars_parent/exemplars/src/main/java/io/prometheus/client/exemplars/api/CounterExemplarSampler.java

+6-5
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66
public interface CounterExemplarSampler {
77

88
/**
9-
* @param value the new counter value.
10-
* Note that {@link Value#get()} might be an expensive operation. The implementation
11-
* of {@link CounterExemplarSampler} should call {@link Value#get()} only if needed.
12-
* @param previous the previously sampled exemplar, or {@code null} if there is none.
9+
* @param increment the value added to the counter on this event
10+
* @param newTotalValue the new total counter value after adding the increment.
11+
* Note that {@link Value#get()} might be an expensive operation. The implementation
12+
* of {@link CounterExemplarSampler} should call {@link Value#get()} only if needed.
13+
* @param previous the previously sampled exemplar, or {@code null} if there is none.
1314
* @return an Exemplar to be sampled, or {@code null} if the previous exemplar does not need to be updated.
1415
* Returning {@code null} and returning {@code previous} is equivalent.
1516
*/
16-
Exemplar sample(Value value, Exemplar previous);
17+
Exemplar sample(double increment, Value newTotalValue, Exemplar previous);
1718
}

exemplars_parent/exemplars/src/main/java/io/prometheus/client/exemplars/api/Exemplar.java

+57-21
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,86 @@
11
package io.prometheus.client.exemplars.api;
22

3+
import java.util.Arrays;
4+
35
/**
46
* Immutable data class holding an Exemplar.
57
*/
68
public class Exemplar {
79

8-
private final String traceId;
9-
private final String spanId;
10+
public static final String TRACE_ID = "trace_id";
11+
public static final String SPAN_ID = "span_id";
12+
13+
private final String[] labels;
1014
private final double value;
1115
private final long timestampMs;
1216

13-
public Exemplar(String traceId, String spanId, double value, long timestampMs) {
14-
if (traceId == null) {
15-
throw new NullPointerException("traceId");
16-
}
17-
if (spanId == null) {
18-
throw new NullPointerException("spanId");
19-
}
20-
this.traceId = traceId;
21-
this.spanId = spanId;
17+
/**
18+
* Create an Exemplar without a timestamp
19+
*
20+
* @param value the observed value
21+
* @param labels name/value pairs. Expecting an even number of strings. The combined length of the label names and
22+
* values must not exceed 128 UTF-8 characters. Neither a label name nor a label value may be null.
23+
*/
24+
public Exemplar(double value, String... labels) {
25+
this(value, 0, labels);
26+
}
27+
28+
/**
29+
* Create an Exemplar
30+
*
31+
* @param value the observed value
32+
* @param timestampMs as in {@link System#currentTimeMillis()}
33+
* @param labels name/value pairs. Expecting an even number of strings. The combined length of the
34+
* label names and values must not exceed 128 UTF-8 characters. Neither a label name
35+
* nor a label value may be null.
36+
*/
37+
public Exemplar(double value, long timestampMs, String... labels) {
38+
validateLabels(labels);
39+
this.labels = Arrays.copyOf(labels, labels.length);
2240
this.value = value;
2341
this.timestampMs = timestampMs;
2442
}
2543

26-
public Exemplar(String traceId, String spanId, double value) {
27-
this(traceId, spanId, value, System.currentTimeMillis());
44+
public int getNumberOfLabels() {
45+
return labels.length / 2;
2846
}
2947

30-
public String getTraceId() {
31-
return traceId;
48+
public String getLabelName(int i) {
49+
return labels[2 * i];
3250
}
3351

34-
public String getSpanId() {
35-
return spanId;
52+
public String getLabelValue(int i) {
53+
return labels[2 * i + 1];
3654
}
3755

3856
public double getValue() {
3957
return value;
4058
}
4159

60+
/**
61+
* @return 0 means no timestamp.
62+
*/
4263
public long getTimestampMs() {
4364
return timestampMs;
4465
}
4566

67+
private void validateLabels(String... labels) {
68+
if (labels.length % 2 != 0) {
69+
throw new IllegalArgumentException("labels are name/value pairs, expecting an even number");
70+
}
71+
int charsTotal = 0;
72+
for (int i = 0; i < labels.length; i++) {
73+
if (labels[i] == null) {
74+
throw new IllegalArgumentException("labels[" + i + "] is null");
75+
}
76+
charsTotal += labels[i].length();
77+
}
78+
if (charsTotal > 128) {
79+
throw new IllegalArgumentException(
80+
"the combined length of the label names and values must not exceed 128 UTF-8 characters");
81+
}
82+
}
83+
4684
@Override
4785
public boolean equals(Object obj) {
4886
if (this == obj) {
@@ -52,16 +90,14 @@ public boolean equals(Object obj) {
5290
return false;
5391
}
5492
Exemplar other = (Exemplar) obj;
55-
return traceId.equals(other.traceId) &&
56-
spanId.equals(other.spanId) &&
93+
return Arrays.equals(this.labels, other.labels) &&
5794
Double.compare(other.value, value) == 0 &&
5895
timestampMs == other.timestampMs;
5996
}
6097

6198
@Override
6299
public int hashCode() {
63-
int hash = traceId.hashCode();
64-
hash = 37 * hash + spanId.hashCode();
100+
int hash = Arrays.hashCode(labels);
65101
long d = Double.doubleToLongBits(value);
66102
hash = 37 * hash + (int) (d ^ (d >>> 32));
67103
hash = 37 * hash + (int) timestampMs;

0 commit comments

Comments
 (0)