diff --git a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/OtelInstrumentType.java b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/OtelInstrumentType.java index 9a072c95e57..d8b7ea9ac22 100644 --- a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/OtelInstrumentType.java +++ b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/OtelInstrumentType.java @@ -2,11 +2,21 @@ public enum OtelInstrumentType { // same order as io.opentelemetry.sdk.metrics.InstrumentType - COUNTER, - UP_DOWN_COUNTER, - HISTOGRAM, - OBSERVABLE_COUNTER, - OBSERVABLE_UP_DOWN_COUNTER, - OBSERVABLE_GAUGE, - GAUGE, + COUNTER(false), + UP_DOWN_COUNTER(false), + HISTOGRAM(false), + OBSERVABLE_COUNTER(true), + OBSERVABLE_UP_DOWN_COUNTER(true), + OBSERVABLE_GAUGE(true), + GAUGE(false); + + private final boolean observable; + + OtelInstrumentType(boolean observable) { + this.observable = observable; + } + + public boolean isObservable() { + return observable; + } } diff --git a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/data/OtelDoubleDelta.java b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/data/OtelDoubleDelta.java new file mode 100644 index 00000000000..9544fe44f04 --- /dev/null +++ b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/data/OtelDoubleDelta.java @@ -0,0 +1,25 @@ +package datadog.trace.bootstrap.otel.metrics.data; + +import datadog.trace.bootstrap.otlp.metrics.OtlpDataPoint; +import datadog.trace.bootstrap.otlp.metrics.OtlpDoublePoint; + +/** Reports the delta value since the last reset. */ +final class OtelDoubleDelta extends OtelAggregator { + private volatile double value; + private double lastValue; + + @Override + void doRecordDouble(double value) { + this.value = value; + } + + @Override + OtlpDataPoint doCollect(boolean reset) { + double collectedValue = value; + double delta = collectedValue - lastValue; + if (reset) { + lastValue = collectedValue; + } + return new OtlpDoublePoint(delta); + } +} diff --git a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/data/OtelDoubleSum.java b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/data/OtelDoubleSum.java index 602cdad0a87..7e5bd48bc6a 100644 --- a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/data/OtelDoubleSum.java +++ b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/data/OtelDoubleSum.java @@ -4,6 +4,7 @@ import datadog.trace.bootstrap.otlp.metrics.OtlpDoublePoint; import java.util.concurrent.atomic.DoubleAdder; +/** Reports the sum of values since the last reset. */ final class OtelDoubleSum extends OtelAggregator { private final DoubleAdder total = new DoubleAdder(); diff --git a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/data/OtelDoubleValue.java b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/data/OtelDoubleValue.java index 509516b62db..6e1269e73a5 100644 --- a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/data/OtelDoubleValue.java +++ b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/data/OtelDoubleValue.java @@ -3,6 +3,7 @@ import datadog.trace.bootstrap.otlp.metrics.OtlpDataPoint; import datadog.trace.bootstrap.otlp.metrics.OtlpDoublePoint; +/** Always reports the latest value. */ final class OtelDoubleValue extends OtelAggregator { private volatile double value; diff --git a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/data/OtelHistogramSketch.java b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/data/OtelHistogramSketch.java index b322d3f75e5..35092bc55a0 100644 --- a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/data/OtelHistogramSketch.java +++ b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/data/OtelHistogramSketch.java @@ -6,6 +6,7 @@ import datadog.trace.bootstrap.otlp.metrics.OtlpHistogramPoint; import java.util.List; +/** Reports the histogram of values since the last reset. */ final class OtelHistogramSketch extends OtelAggregator { private final HistogramWithSum histogram; diff --git a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/data/OtelLongDelta.java b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/data/OtelLongDelta.java new file mode 100644 index 00000000000..a644ee6085d --- /dev/null +++ b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/data/OtelLongDelta.java @@ -0,0 +1,25 @@ +package datadog.trace.bootstrap.otel.metrics.data; + +import datadog.trace.bootstrap.otlp.metrics.OtlpDataPoint; +import datadog.trace.bootstrap.otlp.metrics.OtlpLongPoint; + +/** Reports the delta value since the last reset. */ +final class OtelLongDelta extends OtelAggregator { + private volatile long value; + private long lastValue; + + @Override + void doRecordLong(long value) { + this.value = value; + } + + @Override + OtlpDataPoint doCollect(boolean reset) { + long collectedValue = value; + long delta = collectedValue - lastValue; + if (reset) { + lastValue = collectedValue; + } + return new OtlpLongPoint(delta); + } +} diff --git a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/data/OtelLongSum.java b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/data/OtelLongSum.java index d4693954ba4..773e107a613 100644 --- a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/data/OtelLongSum.java +++ b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/data/OtelLongSum.java @@ -4,6 +4,7 @@ import datadog.trace.bootstrap.otlp.metrics.OtlpLongPoint; import java.util.concurrent.atomic.LongAdder; +/** Reports the sum of values since the last reset. */ final class OtelLongSum extends OtelAggregator { private final LongAdder total = new LongAdder(); diff --git a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/data/OtelLongValue.java b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/data/OtelLongValue.java index 790ce640364..5f6967fca84 100644 --- a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/data/OtelLongValue.java +++ b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/data/OtelLongValue.java @@ -3,6 +3,7 @@ import datadog.trace.bootstrap.otlp.metrics.OtlpDataPoint; import datadog.trace.bootstrap.otlp.metrics.OtlpLongPoint; +/** Always reports the latest value. */ final class OtelLongValue extends OtelAggregator { private volatile long value; diff --git a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/data/OtelMetricStorage.java b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/data/OtelMetricStorage.java index 8863b21bd1b..15e953016bf 100644 --- a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/data/OtelMetricStorage.java +++ b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/metrics/data/OtelMetricStorage.java @@ -11,6 +11,7 @@ import datadog.trace.bootstrap.otel.metrics.OtelInstrumentDescriptor; import datadog.trace.bootstrap.otel.metrics.OtelInstrumentType; import datadog.trace.bootstrap.otlp.common.OtlpAttributeVisitor; +import datadog.trace.bootstrap.otlp.metrics.OtlpDataPoint; import datadog.trace.bootstrap.otlp.metrics.OtlpMetricVisitor; import io.opentelemetry.api.common.Attributes; import java.util.Collections; @@ -46,6 +47,7 @@ public final class OtelMetricStorage { private final OtelInstrumentDescriptor descriptor; private final boolean resetOnCollect; + private final boolean toggleRecordings; private final Function aggregatorSupplier; private volatile Recording currentRecording; @@ -56,9 +58,12 @@ private OtelMetricStorage( OtelInstrumentDescriptor descriptor, Supplier aggregatorSupplier) { this.descriptor = descriptor; this.resetOnCollect = shouldResetOnCollect(descriptor.getType()); + // no need to toggle if not resetting on collect, or if it's an observable instrument + // (observables are always invoked within the collect cycle, so no concurrent writers) + this.toggleRecordings = resetOnCollect && !descriptor.getType().isObservable(); this.aggregatorSupplier = unused -> aggregatorSupplier.get(); this.currentRecording = new Recording(); - if (resetOnCollect) { + if (toggleRecordings) { this.previousRecording = new Recording(); } } @@ -86,6 +91,10 @@ public static OtelMetricStorage newDoubleValueStorage(OtelInstrumentDescriptor d return new OtelMetricStorage(descriptor, OtelDoubleValue::new); } + public static OtelMetricStorage newDoubleDeltaStorage(OtelInstrumentDescriptor descriptor) { + return new OtelMetricStorage(descriptor, OtelDoubleDelta::new); + } + public static OtelMetricStorage newLongSumStorage(OtelInstrumentDescriptor descriptor) { return new OtelMetricStorage(descriptor, OtelLongSum::new); } @@ -94,6 +103,10 @@ public static OtelMetricStorage newLongValueStorage(OtelInstrumentDescriptor des return new OtelMetricStorage(descriptor, OtelLongValue::new); } + public static OtelMetricStorage newLongDeltaStorage(OtelInstrumentDescriptor descriptor) { + return new OtelMetricStorage(descriptor, OtelLongDelta::new); + } + public static OtelMetricStorage newHistogramStorage( OtelInstrumentDescriptor descriptor, List bucketBoundaries) { return new OtelMetricStorage(descriptor, () -> new OtelHistogramSketch(bucketBoundaries)); @@ -108,7 +121,7 @@ public OtelInstrumentDescriptor getDescriptor() { } public void recordLong(long value, Object attributes) { - if (resetOnCollect) { + if (toggleRecordings) { Recording recording = acquireRecordingForWrite(); try { aggregator(recording.aggregators, attributes).recordLong(value); @@ -129,7 +142,7 @@ public void recordDouble(double value, Object attributes) { attributes); return; } - if (resetOnCollect) { + if (toggleRecordings) { Recording recording = acquireRecordingForWrite(); try { aggregator(recording.aggregators, attributes).recordDouble(value); @@ -173,26 +186,8 @@ public static void registerAttributeReader( /** Collect data for CUMULATIVE temporality, keeping aggregators for future writes. */ private void doCollect(OtlpMetricVisitor visitor) { - BiConsumer attributesReader = null; - ClassLoader attributesClassLoader = null; - // no need to hold writers back if we are not resetting metrics on collect - for (Map.Entry entry : currentRecording.aggregators.entrySet()) { - OtelAggregator aggregator = entry.getValue(); - if (!aggregator.isEmpty()) { - Object attributes = entry.getKey(); - ClassLoader cl = attributes.getClass().getClassLoader(); - // avoid repeated lookups when attribute class-loader is same for all records - if (attributesReader == null || cl != attributesClassLoader) { - attributesReader = ATTRIBUTE_READERS.get(cl); - attributesClassLoader = cl; - } - if (attributesReader != null) { - attributesReader.accept(attributes, visitor); - } - visitor.visitDataPoint(aggregator.collect()); - } - } + collectDataPoints(currentRecording.aggregators, visitor, OtelAggregator::collect); } /** @@ -205,13 +200,15 @@ private void doCollectAndReset(OtlpMetricVisitor visitor) { // capture _current_ recording for collection, its aggregators will be reset at the end final Recording recording = currentRecording; - // publish fresh recording for new writers, using aggregators from _previous_ recording - currentRecording = new Recording(previousRecording); + if (toggleRecordings) { + // publish fresh recording for new writers, using aggregators from _previous_ recording + currentRecording = new Recording(previousRecording); - // notify writers that the captured recording is about to be reset - ACTIVITY.addAndGet(recording, RESET_PENDING); - while (recording.activity > 1) { - Thread.yield(); // other threads are still writing to this recording + // notify writers that the captured recording is about to be reset + ACTIVITY.addAndGet(recording, RESET_PENDING); + while (recording.activity > 1) { + Thread.yield(); // other threads are still writing to this recording + } } Map aggregators = recording.aggregators; @@ -221,6 +218,17 @@ private void doCollectAndReset(OtlpMetricVisitor visitor) { aggregators.values().removeIf(OtelAggregator::isEmpty); } + collectDataPoints(aggregators, visitor, OtelAggregator::collectAndReset); + + if (toggleRecordings) { + previousRecording = recording; + } + } + + private void collectDataPoints( + Map aggregators, + OtlpMetricVisitor visitor, + Function collect) { BiConsumer attributesReader = null; ClassLoader attributesClassLoader = null; @@ -237,11 +245,9 @@ private void doCollectAndReset(OtlpMetricVisitor visitor) { if (attributesReader != null) { attributesReader.accept(attributes, visitor); } - visitor.visitDataPoint(aggregator.collectAndReset()); + visitor.visitDataPoint(collect.apply(aggregator)); } } - - previousRecording = recording; } private Recording acquireRecordingForWrite() { diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelDoubleCounter.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelDoubleCounter.java index d2ab4dedbc2..1395dbbe8ad 100644 --- a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelDoubleCounter.java +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelDoubleCounter.java @@ -79,7 +79,7 @@ public DoubleCounter build() { @Override public ObservableDoubleMeasurement buildObserver() { - return meter.registerObservableStorage(builder, OtelMetricStorage::newDoubleSumStorage); + return meter.registerObservableStorage(builder, OtelMetricStorage::newDoubleDeltaStorage); } @Override diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelDoubleUpDownCounter.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelDoubleUpDownCounter.java index 4a25c3f095a..f66a09d2d69 100644 --- a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelDoubleUpDownCounter.java +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelDoubleUpDownCounter.java @@ -65,7 +65,7 @@ public DoubleUpDownCounter build() { @Override public ObservableDoubleMeasurement buildObserver() { - return meter.registerObservableStorage(builder, OtelMetricStorage::newDoubleSumStorage); + return meter.registerObservableStorage(builder, OtelMetricStorage::newDoubleDeltaStorage); } @Override diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelLongCounter.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelLongCounter.java index 949c10733c4..76507eed9d8 100644 --- a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelLongCounter.java +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelLongCounter.java @@ -85,7 +85,7 @@ public LongCounter build() { @Override public ObservableLongMeasurement buildObserver() { - return meter.registerObservableStorage(builder, OtelMetricStorage::newLongSumStorage); + return meter.registerObservableStorage(builder, OtelMetricStorage::newLongDeltaStorage); } @Override diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelLongUpDownCounter.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelLongUpDownCounter.java index 209b9e7265d..2bd9833a6bd 100644 --- a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelLongUpDownCounter.java +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelLongUpDownCounter.java @@ -71,7 +71,7 @@ public LongUpDownCounter build() { @Override public ObservableLongMeasurement buildObserver() { - return meter.registerObservableStorage(builder, OtelMetricStorage::newLongSumStorage); + return meter.registerObservableStorage(builder, OtelMetricStorage::newLongDeltaStorage); } @Override diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.47/src/test/java/opentelemetry147/metrics/OpenTelemetryMetricsCumulativeForkedTest.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.47/src/test/java/opentelemetry147/metrics/OpenTelemetryMetricsCumulativeForkedTest.java new file mode 100644 index 00000000000..8ab40d1f738 --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.47/src/test/java/opentelemetry147/metrics/OpenTelemetryMetricsCumulativeForkedTest.java @@ -0,0 +1,147 @@ +package opentelemetry147.metrics; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import datadog.metrics.impl.DDSketchHistograms; +import datadog.opentelemetry.shim.metrics.OtelMeterProvider; +import datadog.trace.agent.test.AbstractInstrumentationTest; +import datadog.trace.bootstrap.otel.metrics.data.OtelMetricRegistry; +import datadog.trace.junit.utils.config.WithConfig; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.Meter; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +// Forked test: runs in an isolated JVM with cumulative temporality, verifying that observable +// counters report absolute values on each collect (no delta computation). +@WithConfig(key = "metrics.otel.enabled", value = "true") +@WithConfig(key = "otlp.metrics.temporality.preference", value = "cumulative") +class OpenTelemetryMetricsCumulativeForkedTest extends AbstractInstrumentationTest { + + private static final Attributes SOME_ATTRIBUTES = Attributes.of(stringKey("some"), "thing"); + private static final String WITH_ATTRS = "@{some=thing}"; + + private OtelMeterProvider meterProvider; + private Meter meter; + private Map points; + private OpenTelemetryMetricsTest.MeterReader meterReader; + + @BeforeAll + static void registerHistogramFactory() { + datadog.metrics.api.Histograms.register(DDSketchHistograms.FACTORY); + } + + @BeforeEach + void setUpMetrics() { + meterProvider = (OtelMeterProvider) GlobalOpenTelemetry.get().getMeterProvider(); + meter = meterProvider.get("test"); + points = new HashMap<>(); + meterReader = new OpenTelemetryMetricsTest.MeterReader(points); + } + + @Test + void testObservableLongCounterCumulative() { + long[] absoluteValue = {0L}; + AutoCloseable observable = + meter + .counterBuilder("cumulative-observable-long-counter") + .buildWithCallback(m -> m.record(absoluteValue[0])); + + absoluteValue[0] = 5L; + OtelMetricRegistry.INSTANCE.collectMetrics(meterReader); + assertEquals(5L, points.get("test:cumulative-observable-long-counter")); + + // cumulative: same absolute value is reported as-is (not as delta=0) + points.clear(); + OtelMetricRegistry.INSTANCE.collectMetrics(meterReader); + assertEquals(5L, points.get("test:cumulative-observable-long-counter")); + + // absolute value increases: reports new absolute value + points.clear(); + absoluteValue[0] = 12L; + OtelMetricRegistry.INSTANCE.collectMetrics(meterReader); + assertEquals(12L, points.get("test:cumulative-observable-long-counter")); + + closeQuietly(observable); + } + + @Test + void testObservableDoubleCounterCumulative() { + double[] absoluteValue = {0.0}; + AutoCloseable observable = + meter + .counterBuilder("cumulative-observable-double-counter") + .ofDoubles() + .buildWithCallback(m -> m.record(absoluteValue[0])); + + absoluteValue[0] = 3.5; + OtelMetricRegistry.INSTANCE.collectMetrics(meterReader); + assertEquals(3.5, points.get("test:cumulative-observable-double-counter")); + + // cumulative: same absolute value is reported as-is (not as delta=0.0) + points.clear(); + OtelMetricRegistry.INSTANCE.collectMetrics(meterReader); + assertEquals(3.5, points.get("test:cumulative-observable-double-counter")); + + // absolute value increases: reports new absolute value + points.clear(); + absoluteValue[0] = 8.0; + OtelMetricRegistry.INSTANCE.collectMetrics(meterReader); + assertEquals(8.0, points.get("test:cumulative-observable-double-counter")); + + closeQuietly(observable); + } + + @Test + void testSynchronousLongCounterCumulative() { + io.opentelemetry.api.metrics.LongCounter counter = + meter.counterBuilder("cumulative-long-counter").build(); + + counter.add(1); + counter.add(2, SOME_ATTRIBUTES); + OtelMetricRegistry.INSTANCE.collectMetrics(meterReader); + assertEquals(1L, points.get("test:cumulative-long-counter")); + assertEquals(2L, points.get("test:cumulative-long-counter" + WITH_ATTRS)); + + // cumulative: values accumulate without reset between collects + points.clear(); + counter.add(3); + counter.add(4, SOME_ATTRIBUTES); + OtelMetricRegistry.INSTANCE.collectMetrics(meterReader); + assertEquals(4L, points.get("test:cumulative-long-counter")); + assertEquals(6L, points.get("test:cumulative-long-counter" + WITH_ATTRS)); + } + + @Test + void testSynchronousDoubleCounterCumulative() { + io.opentelemetry.api.metrics.DoubleCounter counter = + meter.counterBuilder("cumulative-double-counter").ofDoubles().build(); + + counter.add(1.0); + counter.add(2.0, SOME_ATTRIBUTES); + OtelMetricRegistry.INSTANCE.collectMetrics(meterReader); + assertEquals(1.0, points.get("test:cumulative-double-counter")); + assertEquals(2.0, points.get("test:cumulative-double-counter" + WITH_ATTRS)); + + // cumulative: values accumulate without reset between collects + points.clear(); + counter.add(0.5); + counter.add(1.5, SOME_ATTRIBUTES); + OtelMetricRegistry.INSTANCE.collectMetrics(meterReader); + assertEquals(1.5, (double) points.get("test:cumulative-double-counter"), 0.001); + assertEquals(3.5, (double) points.get("test:cumulative-double-counter" + WITH_ATTRS), 0.001); + } + + private static void closeQuietly(AutoCloseable closeable) { + try { + closeable.close(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.47/src/test/java/opentelemetry147/metrics/OpenTelemetryMetricsTest.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.47/src/test/java/opentelemetry147/metrics/OpenTelemetryMetricsTest.java index 3a4c6187fb2..11fcff87612 100644 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.47/src/test/java/opentelemetry147/metrics/OpenTelemetryMetricsTest.java +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.47/src/test/java/opentelemetry147/metrics/OpenTelemetryMetricsTest.java @@ -212,6 +212,13 @@ void testObservableLongCounter() { assertEquals(1L, points.get("test:observable-long-counter")); assertEquals(2L, points.get("test:observable-long-counter" + WITH_ATTRS)); + // second collect: absolute values are reported, not accumulated + points.clear(); + OtelMetricRegistry.INSTANCE.collectMetrics(meterReader); + + assertEquals(0L, points.get("test:observable-long-counter")); + assertEquals(0L, points.get("test:observable-long-counter" + WITH_ATTRS)); + closeQuietly(observable); } @@ -231,6 +238,13 @@ void testObservableDoubleCounter() { assertEquals(1.2, points.get("test:observable-double-counter")); assertEquals(3.4, points.get("test:observable-double-counter" + WITH_ATTRS)); + // second collect: absolute values are reported, not accumulated + points.clear(); + OtelMetricRegistry.INSTANCE.collectMetrics(meterReader); + + assertEquals(0.0, points.get("test:observable-double-counter")); + assertEquals(0.0, points.get("test:observable-double-counter" + WITH_ATTRS)); + closeQuietly(observable); } @@ -249,6 +263,13 @@ void testObservableLongUpDownCounter() { assertEquals(1L, points.get("test:observable-long-up-down-counter")); assertEquals(2L, points.get("test:observable-long-up-down-counter" + WITH_ATTRS)); + // second collect: absolute values are reported, not accumulated + points.clear(); + OtelMetricRegistry.INSTANCE.collectMetrics(meterReader); + + assertEquals(1L, points.get("test:observable-long-up-down-counter")); + assertEquals(2L, points.get("test:observable-long-up-down-counter" + WITH_ATTRS)); + closeQuietly(observable); } @@ -268,6 +289,13 @@ void testObservableDoubleUpDownCounter() { assertEquals(1.2, points.get("test:observable-double-up-down-counter")); assertEquals(3.4, points.get("test:observable-double-up-down-counter" + WITH_ATTRS)); + // second collect: absolute values are reported, not accumulated + points.clear(); + OtelMetricRegistry.INSTANCE.collectMetrics(meterReader); + + assertEquals(1.2, points.get("test:observable-double-up-down-counter")); + assertEquals(3.4, points.get("test:observable-double-up-down-counter" + WITH_ATTRS)); + closeQuietly(observable); } @@ -308,6 +336,108 @@ void testObservableDoubleGauge() { closeQuietly(observable); } + @Test + void testObservableLongCounterDeltaWithChangingValues() { + long[] absoluteValue = {0L}; + AutoCloseable observable = + meter + .counterBuilder("observable-long-counter-delta-changing") + .buildWithCallback(m -> m.record(absoluteValue[0])); + + absoluteValue[0] = 5L; + OtelMetricRegistry.INSTANCE.collectMetrics(meterReader); + assertEquals(5L, points.get("test:observable-long-counter-delta-changing")); + + // delta since last collect: 12 - 5 = 7 + points.clear(); + absoluteValue[0] = 12L; + OtelMetricRegistry.INSTANCE.collectMetrics(meterReader); + assertEquals(7L, points.get("test:observable-long-counter-delta-changing")); + + // no change in absolute value: delta = 0 + points.clear(); + OtelMetricRegistry.INSTANCE.collectMetrics(meterReader); + assertEquals(0L, points.get("test:observable-long-counter-delta-changing")); + + closeQuietly(observable); + } + + @Test + void testObservableDoubleCounterDeltaWithChangingValues() { + double[] absoluteValue = {0.0}; + AutoCloseable observable = + meter + .counterBuilder("observable-double-counter-delta-changing") + .ofDoubles() + .buildWithCallback(m -> m.record(absoluteValue[0])); + + absoluteValue[0] = 2.5; + OtelMetricRegistry.INSTANCE.collectMetrics(meterReader); + assertEquals(2.5, points.get("test:observable-double-counter-delta-changing")); + + // delta since last collect: 5.0 - 2.5 = 2.5 + points.clear(); + absoluteValue[0] = 5.0; + OtelMetricRegistry.INSTANCE.collectMetrics(meterReader); + assertEquals(2.5, points.get("test:observable-double-counter-delta-changing")); + + // no change in absolute value: delta = 0.0 + points.clear(); + OtelMetricRegistry.INSTANCE.collectMetrics(meterReader); + assertEquals(0.0, points.get("test:observable-double-counter-delta-changing")); + + closeQuietly(observable); + } + + @Test + void testObservableLongUpDownCounterReportsAbsoluteValue() { + long[] absoluteValue = {0L}; + AutoCloseable observable = + meter + .upDownCounterBuilder("observable-long-up-down-counter-absolute") + .buildWithCallback(m -> m.record(absoluteValue[0])); + + absoluteValue[0] = 10L; + OtelMetricRegistry.INSTANCE.collectMetrics(meterReader); + assertEquals(10L, points.get("test:observable-long-up-down-counter-absolute")); + + // value decreases: should report new absolute value, not a delta + points.clear(); + absoluteValue[0] = 3L; + OtelMetricRegistry.INSTANCE.collectMetrics(meterReader); + assertEquals(3L, points.get("test:observable-long-up-down-counter-absolute")); + + // value increases again + points.clear(); + absoluteValue[0] = 15L; + OtelMetricRegistry.INSTANCE.collectMetrics(meterReader); + assertEquals(15L, points.get("test:observable-long-up-down-counter-absolute")); + + closeQuietly(observable); + } + + @Test + void testObservableDoubleUpDownCounterReportsAbsoluteValue() { + double[] absoluteValue = {0.0}; + AutoCloseable observable = + meter + .upDownCounterBuilder("observable-double-up-down-counter-absolute") + .ofDoubles() + .buildWithCallback(m -> m.record(absoluteValue[0])); + + absoluteValue[0] = 8.0; + OtelMetricRegistry.INSTANCE.collectMetrics(meterReader); + assertEquals(8.0, points.get("test:observable-double-up-down-counter-absolute")); + + // value decreases: should report new absolute value + points.clear(); + absoluteValue[0] = 2.5; + OtelMetricRegistry.INSTANCE.collectMetrics(meterReader); + assertEquals(2.5, points.get("test:observable-double-up-down-counter-absolute")); + + closeQuietly(observable); + } + @Test void testBatchCallback() { ObservableLongMeasurement longCounterObserver = @@ -385,18 +515,18 @@ void testBatchCallback() { points.clear(); OtelMetricRegistry.INSTANCE.collectMetrics(meterReader); - // delta mode: counters show values added during last collect - assertEquals(1L, points.get("test:long-counter-observer")); - assertEquals(10L, points.get("test:long-counter-observer" + WITH_ATTRS)); - assertEquals(2.3, points.get("test:double-counter-observer")); - assertEquals(20.3, points.get("test:double-counter-observer" + WITH_ATTRS)); - // up-down counters stay cumulative: they show the running total - assertEquals(8L, points.get("test:long-up-down-counter-observer")); - assertEquals(80L, points.get("test:long-up-down-counter-observer" + WITH_ATTRS)); - assertEquals(11.2, (double) points.get("test:double-up-down-counter-observer"), 0.001); + // async counters show delta since last collect (same value was recorded) + assertEquals(0L, points.get("test:long-counter-observer")); + assertEquals(0L, points.get("test:long-counter-observer" + WITH_ATTRS)); + assertEquals(0.0, points.get("test:double-counter-observer")); + assertEquals(0.0, points.get("test:double-counter-observer" + WITH_ATTRS)); + // async up-down counters stay cumulative and show the latest value + assertEquals(4L, points.get("test:long-up-down-counter-observer")); + assertEquals(40L, points.get("test:long-up-down-counter-observer" + WITH_ATTRS)); + assertEquals(5.6, (double) points.get("test:double-up-down-counter-observer"), 0.001); assertEquals( - 101.2, (double) points.get("test:double-up-down-counter-observer" + WITH_ATTRS), 0.001); - // gauges also stay cumulative: they only show latest value + 50.6, (double) points.get("test:double-up-down-counter-observer" + WITH_ATTRS), 0.001); + // gauges continue to only show the latest value assertEquals(7L, points.get("test:long-gauge-observer")); assertEquals(70L, points.get("test:long-gauge-observer" + WITH_ATTRS)); assertEquals(8.9, points.get("test:double-gauge-observer")); @@ -407,18 +537,18 @@ void testBatchCallback() { points.clear(); OtelMetricRegistry.INSTANCE.collectMetrics(meterReader); - // delta mode: no values were added as batchCallback is closed + // delta mode: no counts were set as batchCallback is closed, so no data point assertNull(points.get("test:long-counter-observer")); assertNull(points.get("test:long-counter-observer" + WITH_ATTRS)); assertNull(points.get("test:double-counter-observer")); assertNull(points.get("test:double-counter-observer" + WITH_ATTRS)); - // up-down counters stay cumulative: they show the running total - assertEquals(8L, points.get("test:long-up-down-counter-observer")); - assertEquals(80L, points.get("test:long-up-down-counter-observer" + WITH_ATTRS)); - assertEquals(11.2, (double) points.get("test:double-up-down-counter-observer"), 0.001); + // up-down counters stay cumulative: they continue to show the last count set + assertEquals(4L, points.get("test:long-up-down-counter-observer")); + assertEquals(40L, points.get("test:long-up-down-counter-observer" + WITH_ATTRS)); + assertEquals(5.6, (double) points.get("test:double-up-down-counter-observer"), 0.001); assertEquals( - 101.2, (double) points.get("test:double-up-down-counter-observer" + WITH_ATTRS), 0.001); - // gauges also stay cumulative: they only show latest value + 50.6, (double) points.get("test:double-up-down-counter-observer" + WITH_ATTRS), 0.001); + // gauges also stay cumulative: they continue to show the latest value set assertEquals(7L, points.get("test:long-gauge-observer")); assertEquals(70L, points.get("test:long-gauge-observer" + WITH_ATTRS)); assertEquals(8.9, points.get("test:double-gauge-observer"));