diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/exemplar/ExemplarReservoir.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/exemplar/ExemplarReservoir.java index cebd4683fbb..c0de6e0df6a 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/exemplar/ExemplarReservoir.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/exemplar/ExemplarReservoir.java @@ -8,6 +8,7 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.context.Context; import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.metrics.Aggregation; import io.opentelemetry.sdk.metrics.data.DoubleExemplarData; import io.opentelemetry.sdk.metrics.data.ExemplarData; import io.opentelemetry.sdk.metrics.data.LongExemplarData; @@ -25,6 +26,19 @@ */ public interface ExemplarReservoir<T extends ExemplarData> { + /** + * Wraps an {@link ExemplarReservoir}, casting calls from {@link + * ExemplarReservoir#offerLongMeasurement(long, Attributes, Context)} to {@link + * ExemplarReservoir#offerDoubleMeasurement(double, Attributes, Context)} such that {@link + * ExemplarReservoir#collectAndReset(Attributes)} only returns {@link DoubleExemplarData}. + * + * <p>This is used for {@link Aggregation#explicitBucketHistogram()} and {@link + * Aggregation#base2ExponentialBucketHistogram()} which only support double measurements. + */ + static <T extends ExemplarData> ExemplarReservoir<T> longToDouble(ExemplarReservoir<T> delegate) { + return new LongToDoubleExemplarReservoir<>(delegate); + } + /** Wraps a {@link ExemplarReservoir} with a measurement pre-filter. */ static <T extends ExemplarData> ExemplarReservoir<T> filtered( ExemplarFilter filter, ExemplarReservoir<T> original) { diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/exemplar/LongToDoubleExemplarReservoir.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/exemplar/LongToDoubleExemplarReservoir.java new file mode 100644 index 00000000000..c3a6b98fced --- /dev/null +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/exemplar/LongToDoubleExemplarReservoir.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.internal.exemplar; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.metrics.data.ExemplarData; +import java.util.List; + +class LongToDoubleExemplarReservoir<T extends ExemplarData> implements ExemplarReservoir<T> { + + private final ExemplarReservoir<T> delegate; + + LongToDoubleExemplarReservoir(ExemplarReservoir<T> delegate) { + this.delegate = delegate; + } + + @Override + public void offerDoubleMeasurement(double value, Attributes attributes, Context context) { + delegate.offerDoubleMeasurement(value, attributes, context); + } + + @Override + public void offerLongMeasurement(long value, Attributes attributes, Context context) { + offerDoubleMeasurement((double) value, attributes, context); + } + + @Override + public List<T> collectAndReset(Attributes pointAttributes) { + return delegate.collectAndReset(pointAttributes); + } +} diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/view/Base2ExponentialHistogramAggregation.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/view/Base2ExponentialHistogramAggregation.java index a0408bc32fd..0604f591327 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/view/Base2ExponentialHistogramAggregation.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/view/Base2ExponentialHistogramAggregation.java @@ -72,10 +72,11 @@ public <T extends PointData, U extends ExemplarData> Aggregator<T, U> createAggr () -> ExemplarReservoir.filtered( exemplarFilter, - ExemplarReservoir.doubleFixedSizeReservoir( - Clock.getDefault(), - Runtime.getRuntime().availableProcessors(), - RandomSupplier.platformDefault())), + ExemplarReservoir.longToDouble( + ExemplarReservoir.doubleFixedSizeReservoir( + Clock.getDefault(), + Runtime.getRuntime().availableProcessors(), + RandomSupplier.platformDefault()))), maxBuckets, maxScale); } diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/view/ExplicitBucketHistogramAggregation.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/view/ExplicitBucketHistogramAggregation.java index 1ec3867dc7e..1ad32b698ba 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/view/ExplicitBucketHistogramAggregation.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/view/ExplicitBucketHistogramAggregation.java @@ -57,8 +57,9 @@ public <T extends PointData, U extends ExemplarData> Aggregator<T, U> createAggr () -> ExemplarReservoir.filtered( exemplarFilter, - ExemplarReservoir.histogramBucketReservoir( - Clock.getDefault(), bucketBoundaries))); + ExemplarReservoir.longToDouble( + ExemplarReservoir.histogramBucketReservoir( + Clock.getDefault(), bucketBoundaries)))); } @Override diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleHistogramTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleHistogramTest.java index dd060736ed6..dd210faad46 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleHistogramTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleHistogramTest.java @@ -14,12 +14,16 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.DoubleHistogram; import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; import io.opentelemetry.internal.testing.slf4j.SuppressLogger; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.metrics.internal.state.DefaultSynchronousMetricStorage; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; import io.opentelemetry.sdk.testing.time.TestClock; +import io.opentelemetry.sdk.trace.SdkTracerProvider; import java.time.Duration; import java.util.Arrays; import java.util.Collections; @@ -275,6 +279,97 @@ void doubleHistogramRecord_NaN() { assertThat(sdkMeterReader.collectAllMetrics()).hasSize(0); } + @Test + void collectMetrics_ExemplarsWithExponentialHistogram() { + InMemoryMetricReader reader = InMemoryMetricReader.create(); + SdkMeterProvider sdkMeterProvider = + SdkMeterProvider.builder() + .setClock(testClock) + .setResource(RESOURCE) + .registerView( + InstrumentSelector.builder().setType(InstrumentType.HISTOGRAM).build(), + View.builder() + .setAggregation(Aggregation.base2ExponentialBucketHistogram()) + .setAttributeFilter(Collections.emptySet()) + .build()) + .registerMetricReader(reader) + .build(); + Meter sdkMeter = sdkMeterProvider.get(getClass().getName()); + DoubleHistogram histogram = sdkMeter.histogramBuilder("testHistogram").build(); + + SdkTracerProvider tracerProvider = SdkTracerProvider.builder().build(); + Tracer tracer = tracerProvider.get("foo"); + + Span span = tracer.spanBuilder("span").startSpan(); + try (Scope unused = span.makeCurrent()) { + histogram.record(10, Attributes.builder().put("key", "value").build()); + } + + assertThat(reader.collectAllMetrics()) + .satisfiesExactly( + metric -> + assertThat(metric) + .hasExponentialHistogramSatisfying( + exponentialHistogram -> + exponentialHistogram.hasPointsSatisfying( + point -> + point + .hasSum(10.0) + .hasAttributes(Attributes.empty()) + .hasExemplarsSatisfying( + exemplar -> + exemplar + .hasValue(10.0) + .hasFilteredAttributes( + Attributes.builder() + .put("key", "value") + .build()))))); + } + + @Test + void collectMetrics_ExemplarsWithExplicitBucketHistogram() { + InMemoryMetricReader reader = InMemoryMetricReader.create(); + SdkMeterProvider sdkMeterProvider = + SdkMeterProvider.builder() + .setClock(testClock) + .setResource(RESOURCE) + .registerView( + InstrumentSelector.builder().setName("*").build(), + View.builder().setAttributeFilter(Collections.emptySet()).build()) + .registerMetricReader(reader) + .build(); + Meter sdkMeter = sdkMeterProvider.get(getClass().getName()); + DoubleHistogram histogram = sdkMeter.histogramBuilder("testHistogram").build(); + + SdkTracerProvider tracerProvider = SdkTracerProvider.builder().build(); + Tracer tracer = tracerProvider.get("foo"); + + Span span = tracer.spanBuilder("span").startSpan(); + try (Scope unused = span.makeCurrent()) { + histogram.record(10, Attributes.builder().put("key", "value").build()); + } + + assertThat(reader.collectAllMetrics()) + .satisfiesExactly( + metric -> + assertThat(metric) + .hasHistogramSatisfying( + explicitHistogram -> + explicitHistogram.hasPointsSatisfying( + point -> + point + .hasSum(10) + .hasAttributes(Attributes.empty()) + .hasExemplarsSatisfying( + exemplar -> + exemplar + .hasValue(10.0) + .hasFilteredAttributes( + Attributes.builder() + .put("key", "value") + .build()))))); + } + @Test void stressTest() { DoubleHistogram doubleHistogram = sdkMeter.histogramBuilder("testHistogram").build(); diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkLongHistogramTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkLongHistogramTest.java index 09e33faf2cc..dd802ff613f 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkLongHistogramTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkLongHistogramTest.java @@ -14,11 +14,15 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.LongHistogram; import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; import io.opentelemetry.internal.testing.slf4j.SuppressLogger; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; import io.opentelemetry.sdk.testing.time.TestClock; +import io.opentelemetry.sdk.trace.SdkTracerProvider; import java.time.Duration; import java.util.Arrays; import java.util.Collections; @@ -435,6 +439,97 @@ void longHistogramRecord_NonNegativeCheck() { "Histograms can only record non-negative values. Instrument testHistogram has recorded a negative value."); } + @Test + void collectMetrics_ExemplarsWithExponentialHistogram() { + InMemoryMetricReader reader = InMemoryMetricReader.create(); + SdkMeterProvider sdkMeterProvider = + SdkMeterProvider.builder() + .setClock(testClock) + .setResource(RESOURCE) + .registerView( + InstrumentSelector.builder().setType(InstrumentType.HISTOGRAM).build(), + View.builder() + .setAggregation(Aggregation.base2ExponentialBucketHistogram()) + .setAttributeFilter(Collections.emptySet()) + .build()) + .registerMetricReader(reader) + .build(); + Meter sdkMeter = sdkMeterProvider.get(getClass().getName()); + LongHistogram histogram = sdkMeter.histogramBuilder("testHistogram").ofLongs().build(); + + SdkTracerProvider tracerProvider = SdkTracerProvider.builder().build(); + Tracer tracer = tracerProvider.get("foo"); + + Span span = tracer.spanBuilder("span").startSpan(); + try (Scope unused = span.makeCurrent()) { + histogram.record(10, Attributes.builder().put("key", "value").build()); + } + + assertThat(reader.collectAllMetrics()) + .satisfiesExactly( + metric -> + assertThat(metric) + .hasExponentialHistogramSatisfying( + exponentialHistogram -> + exponentialHistogram.hasPointsSatisfying( + point -> + point + .hasSum(10.0) + .hasAttributes(Attributes.empty()) + .hasExemplarsSatisfying( + exemplar -> + exemplar + .hasValue(10.0) + .hasFilteredAttributes( + Attributes.builder() + .put("key", "value") + .build()))))); + } + + @Test + void collectMetrics_ExemplarsWithExplicitBucketHistogram() { + InMemoryMetricReader reader = InMemoryMetricReader.create(); + SdkMeterProvider sdkMeterProvider = + SdkMeterProvider.builder() + .setClock(testClock) + .setResource(RESOURCE) + .registerView( + InstrumentSelector.builder().setName("*").build(), + View.builder().setAttributeFilter(Collections.emptySet()).build()) + .registerMetricReader(reader) + .build(); + Meter sdkMeter = sdkMeterProvider.get(getClass().getName()); + LongHistogram histogram = sdkMeter.histogramBuilder("testHistogram").ofLongs().build(); + + SdkTracerProvider tracerProvider = SdkTracerProvider.builder().build(); + Tracer tracer = tracerProvider.get("foo"); + + Span span = tracer.spanBuilder("span").startSpan(); + try (Scope unused = span.makeCurrent()) { + histogram.record(10, Attributes.builder().put("key", "value").build()); + } + + assertThat(reader.collectAllMetrics()) + .satisfiesExactly( + metric -> + assertThat(metric) + .hasHistogramSatisfying( + explicitHistogram -> + explicitHistogram.hasPointsSatisfying( + point -> + point + .hasSum(10) + .hasAttributes(Attributes.empty()) + .hasExemplarsSatisfying( + exemplar -> + exemplar + .hasValue(10.0) + .hasFilteredAttributes( + Attributes.builder() + .put("key", "value") + .build()))))); + } + @Test void stressTest() { LongHistogram longHistogram = sdkMeter.histogramBuilder("testHistogram").ofLongs().build(); diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/exemplar/LongToDoubleExemplarReservoirTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/exemplar/LongToDoubleExemplarReservoirTest.java new file mode 100644 index 00000000000..fc7a5a3c07a --- /dev/null +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/exemplar/LongToDoubleExemplarReservoirTest.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.internal.exemplar; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.context.Context; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class LongToDoubleExemplarReservoirTest { + @Mock ExemplarReservoir<?> delegate; + + @Test + void offerDoubleMeasurement() { + ExemplarReservoir<?> filtered = new LongToDoubleExemplarReservoir<>(delegate); + filtered.offerDoubleMeasurement(1.0, Attributes.empty(), Context.root()); + verify(delegate).offerDoubleMeasurement(1.0, Attributes.empty(), Context.root()); + verify(delegate, never()).offerLongMeasurement(anyLong(), any(), any()); + } + + @Test + void offerLongMeasurement() { + ExemplarReservoir<?> filtered = new LongToDoubleExemplarReservoir<>(delegate); + filtered.offerLongMeasurement(1L, Attributes.empty(), Context.root()); + verify(delegate).offerDoubleMeasurement(1.0, Attributes.empty(), Context.root()); + verify(delegate, never()).offerLongMeasurement(anyLong(), any(), any()); + } +}