|
| 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 | +} |
0 commit comments