Skip to content

Commit 07ce2d7

Browse files
committed
Create basic bound instrument API shape
1 parent 8d01d31 commit 07ce2d7

5 files changed

Lines changed: 207 additions & 1 deletion

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.api.incubator.metrics;
7+
8+
import io.opentelemetry.api.common.Attributes;
9+
import io.opentelemetry.api.metrics.LongCounter;
10+
import io.opentelemetry.context.Context;
11+
import javax.annotation.concurrent.ThreadSafe;
12+
13+
/**
14+
* A {@link LongCounter} bound to a fixed set of {@link Attributes}, obtained via {@link
15+
* ExtendedLongCounter#bind(Attributes)}.
16+
*
17+
* <p>Binding resolves the underlying timeseries once, so subsequent {@link #add(long)} calls record
18+
* directly without the per-recording attribute processing and map lookup that {@link
19+
* LongCounter#add(long, Attributes)} performs. This is useful when the set of attribute
20+
* combinations is known ahead of time and the same series is recorded to repeatedly.
21+
*/
22+
@ThreadSafe
23+
public interface BoundLongCounter {
24+
25+
/**
26+
* Records a value against the bound attributes.
27+
*
28+
* <p>Note: This may use {@code Context.current()} to pull the context associated with this
29+
* measurement.
30+
*
31+
* @param value The increment amount. MUST be non-negative.
32+
*/
33+
void add(long value);
34+
35+
/**
36+
* Records a value against the bound attributes, with an explicit {@link Context}.
37+
*
38+
* @param value The increment amount. MUST be non-negative.
39+
* @param context The explicit context to associate with this measurement.
40+
*/
41+
void add(long value, Context context);
42+
}

api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDefaultMeter.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,22 @@ public void add(long value, Attributes attributes) {}
107107

108108
@Override
109109
public void add(long value) {}
110+
111+
@Override
112+
public BoundLongCounter bind(Attributes attributes) {
113+
return NOOP_BOUND_LONG_COUNTER;
114+
}
110115
}
111116

117+
private static final BoundLongCounter NOOP_BOUND_LONG_COUNTER =
118+
new BoundLongCounter() {
119+
@Override
120+
public void add(long value) {}
121+
122+
@Override
123+
public void add(long value, Context context) {}
124+
};
125+
112126
private static class NoopDoubleCounter implements ExtendedDoubleCounter {
113127
@Override
114128
public boolean isEnabled() {

api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongCounter.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,20 @@
55

66
package io.opentelemetry.api.incubator.metrics;
77

8+
import io.opentelemetry.api.common.Attributes;
89
import io.opentelemetry.api.metrics.DoubleCounter;
910
import io.opentelemetry.api.metrics.LongCounter;
1011

1112
/** Extended {@link DoubleCounter} with experimental APIs. */
1213
public interface ExtendedLongCounter extends LongCounter {
1314

14-
// keep this class even if it is empty, since experimental methods may be added in the future.
15+
/**
16+
* Binds this counter to the given {@code attributes}, returning a {@link BoundLongCounter} that
17+
* records to the corresponding timeseries directly.
18+
*
19+
* <p>Binding resolves the underlying timeseries once, eliminating the per-recording attribute
20+
* processing and map lookup performed by {@link #add(long, Attributes)}. Prefer this when the set
21+
* of attribute combinations is known ahead of time and the same series is recorded to repeatedly.
22+
*/
23+
BoundLongCounter bind(Attributes attributes);
1524
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.api.incubator.metrics;
7+
8+
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
9+
10+
import io.opentelemetry.api.common.AttributeKey;
11+
import io.opentelemetry.api.common.Attributes;
12+
import io.opentelemetry.api.metrics.LongCounter;
13+
import io.opentelemetry.api.metrics.Meter;
14+
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
15+
import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
16+
import java.util.Random;
17+
import org.junit.jupiter.api.Test;
18+
19+
/**
20+
* Demonstrates usage of bound instruments for a dice-rolling scenario.
21+
*
22+
* <p>When the full set of attribute combinations is known ahead of time — as it is here, with 6
23+
* fixed die faces — bound instruments eliminate the per-recording overhead of the CHM lookup
24+
* (bucket traversal, {@link Attributes} equality comparison) and attribute processing by resolving
25+
* the underlying timeseries once at bind time.
26+
*/
27+
class BoundInstrumentUsageTest {
28+
29+
private static final AttributeKey<Long> ROLL_VALUE = AttributeKey.longKey("roll.value");
30+
31+
// One Attributes object per die face, constructed once and reused across all recordings.
32+
// With unbound instruments each call would look these up on every add().
33+
private static final Attributes ROLL_1 = Attributes.of(ROLL_VALUE, 1L);
34+
private static final Attributes ROLL_2 = Attributes.of(ROLL_VALUE, 2L);
35+
private static final Attributes ROLL_3 = Attributes.of(ROLL_VALUE, 3L);
36+
private static final Attributes ROLL_4 = Attributes.of(ROLL_VALUE, 4L);
37+
private static final Attributes ROLL_5 = Attributes.of(ROLL_VALUE, 5L);
38+
private static final Attributes ROLL_6 = Attributes.of(ROLL_VALUE, 6L);
39+
40+
@Test
41+
void rollTheDice() {
42+
InMemoryMetricReader reader = InMemoryMetricReader.create();
43+
try (SdkMeterProvider meterProvider =
44+
SdkMeterProvider.builder().registerMetricReader(reader).build()) {
45+
46+
Meter meter = meterProvider.get("io.opentelemetry.example.dice");
47+
48+
// The bind() API lives on the incubating ExtendedLongCounter, reached by casting the
49+
// instrument returned from the (stable) builder.
50+
ExtendedLongCounter rolls =
51+
(ExtendedLongCounter)
52+
meter
53+
.counterBuilder("dice.rolls")
54+
.setDescription("The number of times each side of the die was rolled")
55+
.setUnit("{roll}")
56+
.build();
57+
58+
// Bind one BoundLongCounter per die face. Each bind() call resolves the underlying timeseries
59+
// once, so subsequent add() calls record directly without any attribute lookup.
60+
//
61+
// Equivalent unbound setup (no bind calls needed, but per-recording overhead is higher):
62+
// // no setup — just call rolls.add(1, ROLL_N) inline below
63+
BoundLongCounter face1 = rolls.bind(ROLL_1);
64+
BoundLongCounter face2 = rolls.bind(ROLL_2);
65+
BoundLongCounter face3 = rolls.bind(ROLL_3);
66+
BoundLongCounter face4 = rolls.bind(ROLL_4);
67+
BoundLongCounter face5 = rolls.bind(ROLL_5);
68+
BoundLongCounter face6 = rolls.bind(ROLL_6);
69+
70+
// Simulate 600 rolls with a fixed seed for a reproducible distribution.
71+
Random random = new Random(42);
72+
long[] counts = new long[7]; // indexed 1..6; index 0 unused
73+
74+
for (int i = 0; i < 600; i++) {
75+
int result = random.nextInt(6) + 1;
76+
counts[result]++;
77+
switch (result) {
78+
case 1:
79+
face1.add(1);
80+
// Equivalent unbound: rolls.add(1, ROLL_1);
81+
break;
82+
case 2:
83+
face2.add(1);
84+
// Equivalent unbound: rolls.add(1, ROLL_2);
85+
break;
86+
case 3:
87+
face3.add(1);
88+
// Equivalent unbound: rolls.add(1, ROLL_3);
89+
break;
90+
case 4:
91+
face4.add(1);
92+
// Equivalent unbound: rolls.add(1, ROLL_4);
93+
break;
94+
case 5:
95+
face5.add(1);
96+
// Equivalent unbound: rolls.add(1, ROLL_5);
97+
break;
98+
case 6:
99+
face6.add(1);
100+
// Equivalent unbound: rolls.add(1, ROLL_6);
101+
break;
102+
default:
103+
break;
104+
}
105+
}
106+
107+
// A bound counter and the unbound instrument it came from record to the same timeseries; the
108+
// two styles are interchangeable and can be mixed.
109+
LongCounter unbound = rolls;
110+
unbound.add(0, ROLL_1);
111+
112+
// One cumulative data point per die face, each with the exact roll count recorded above.
113+
assertThat(reader.collectAllMetrics())
114+
.satisfiesExactly(
115+
metric ->
116+
assertThat(metric)
117+
.hasName("dice.rolls")
118+
.hasDescription("The number of times each side of the die was rolled")
119+
.hasUnit("{roll}")
120+
.hasLongSumSatisfying(
121+
sum ->
122+
sum.isMonotonic()
123+
.hasPointsSatisfying(
124+
point -> point.hasAttributes(ROLL_1).hasValue(counts[1]),
125+
point -> point.hasAttributes(ROLL_2).hasValue(counts[2]),
126+
point -> point.hasAttributes(ROLL_3).hasValue(counts[3]),
127+
point -> point.hasAttributes(ROLL_4).hasValue(counts[4]),
128+
point -> point.hasAttributes(ROLL_5).hasValue(counts[5]),
129+
point -> point.hasAttributes(ROLL_6).hasValue(counts[6]))));
130+
}
131+
}
132+
}

sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongCounter.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
package io.opentelemetry.sdk.metrics;
77

88
import io.opentelemetry.api.common.AttributeKey;
9+
import io.opentelemetry.api.common.Attributes;
10+
import io.opentelemetry.api.incubator.metrics.BoundLongCounter;
911
import io.opentelemetry.api.incubator.metrics.ExtendedDoubleCounterBuilder;
1012
import io.opentelemetry.api.incubator.metrics.ExtendedLongCounter;
1113
import io.opentelemetry.api.incubator.metrics.ExtendedLongCounterBuilder;
@@ -20,6 +22,13 @@ private ExtendedSdkLongCounter(
2022
super(descriptor, sdkMeter, storage);
2123
}
2224

25+
@Override
26+
public BoundLongCounter bind(Attributes attributes) {
27+
// TODO(bound-instruments): implement against WriteableMetricStorage in the implementation phase
28+
// (includes fresh-eyes work on DeltaSynchronousMetricStorage).
29+
throw new UnsupportedOperationException("bind is not yet implemented");
30+
}
31+
2332
static final class ExtendedSdkLongCounterBuilder extends SdkLongCounterBuilder
2433
implements ExtendedLongCounterBuilder {
2534

0 commit comments

Comments
 (0)