From 54304919c1cebcad58ceb9f1c9e5e001f9d2ca7d Mon Sep 17 00:00:00 2001 From: amalsaidov Date: Fri, 13 Nov 2026 08:34:04 +0700 Subject: [PATCH] Test that exemplars retain attributes dropped by the Attributes advice The Attributes instrument advisory parameter filters attributes out of a metric point. Per the metrics SDK spec, those dropped attributes must still be recorded on sampled exemplars as filtered attributes, so joining an exemplar's filtered attributes with its point's attributes reconstructs the original measurement. View attribute filtering already has this coverage, but the advice path did not. Add end-to-end tests covering a counter and a histogram, plus the case where a matching View supersedes the advice and the exemplar retains the attributes dropped by the View instead. Signed-off-by: amalsaidov --- .../sdk/metrics/AttributesAdviceTest.java | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/sdk/metrics/src/testIncubating/java/io/opentelemetry/sdk/metrics/AttributesAdviceTest.java b/sdk/metrics/src/testIncubating/java/io/opentelemetry/sdk/metrics/AttributesAdviceTest.java index ddfff8d611a..a228827b558 100644 --- a/sdk/metrics/src/testIncubating/java/io/opentelemetry/sdk/metrics/AttributesAdviceTest.java +++ b/sdk/metrics/src/testIncubating/java/io/opentelemetry/sdk/metrics/AttributesAdviceTest.java @@ -48,6 +48,7 @@ import java.util.stream.Stream; import javax.annotation.Nullable; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -168,6 +169,102 @@ void instrumentWithAdviceAndDescriptionViews( equalTo(stringKey("key3"), "3")))); } + // Per spec, attributes dropped from a metric point by the Attributes advisory parameter are + // still retained on sampled Exemplars as filtered attributes, so that joining an Exemplar's + // filtered attributes with its metric point's attributes reconstructs the original measurement. + // See https://github.com/open-telemetry/opentelemetry-specification/pull/5392. + @Test + void adviceDroppedAttributesRetainedAsExemplarFilteredAttributes_counter() { + InMemoryMetricReader reader = InMemoryMetricReader.create(); + meterProvider = + SdkMeterProvider.builder() + .setExemplarFilter(ExemplarFilter.alwaysOn()) + .registerMetricReader(reader) + .build(); + + LongCounter counter = + ((ExtendedLongCounterBuilder) meterProvider.get("meter").counterBuilder("test")) + .setAttributesAdvice(asList(stringKey("key1"), stringKey("key2"))) + .build(); + counter.add(1, ATTRIBUTES); + + assertThat(reader.collectAllMetrics()) + .satisfiesExactly(metric -> + assertThat(metric) + .hasLongSumSatisfying(sum -> + sum.hasPointsSatisfying(point -> + point + .hasAttributesSatisfyingExactly( + equalTo(stringKey("key1"), "1"), equalTo(stringKey("key2"), "2")) + .hasExemplarsSatisfying(exemplar -> + exemplar.hasFilteredAttributes( + Attributes.builder().put(stringKey("key3"), "3").build()))))); + } + + @Test + void adviceDroppedAttributesRetainedAsExemplarFilteredAttributes_histogram() { + InMemoryMetricReader reader = InMemoryMetricReader.create(); + meterProvider = + SdkMeterProvider.builder() + .setExemplarFilter(ExemplarFilter.alwaysOn()) + .registerMetricReader(reader) + .build(); + + DoubleHistogram histogram = + ((ExtendedDoubleHistogramBuilder) meterProvider.get("meter").histogramBuilder("test")) + .setAttributesAdvice(asList(stringKey("key1"), stringKey("key2"))) + .build(); + histogram.record(1.0, ATTRIBUTES); + + assertThat(reader.collectAllMetrics()) + .satisfiesExactly(metric -> + assertThat(metric) + .hasHistogramSatisfying(h -> + h.hasPointsSatisfying(point -> + point + .hasAttributesSatisfyingExactly( + equalTo(stringKey("key1"), "1"), equalTo(stringKey("key2"), "2")) + .hasExemplarsSatisfying(exemplar -> + exemplar.hasFilteredAttributes( + Attributes.builder().put(stringKey("key3"), "3").build()))))); + } + + // When a View matches, it supersedes the instrument's Attributes advice for the metric point. + // The Exemplar's filtered attributes must still reconstruct the original measurement, so they + // reflect the attributes dropped by the View rather than by the superseded advice. + @Test + void adviceAndView_exemplarRetainsAttributesDroppedByView() { + InMemoryMetricReader reader = InMemoryMetricReader.create(); + meterProvider = + SdkMeterProvider.builder() + .setExemplarFilter(ExemplarFilter.alwaysOn()) + .registerMetricReader(reader) + .registerView( + InstrumentSelector.builder().setName("test").build(), + View.builder() + .setAttributeFilter(key -> "key2".equals(key) || "key3".equals(key)) + .build()) + .build(); + + LongCounter counter = + ((ExtendedLongCounterBuilder) meterProvider.get("meter").counterBuilder("test")) + .setAttributesAdvice(asList(stringKey("key1"), stringKey("key2"))) + .build(); + counter.add(1, ATTRIBUTES); + + assertThat(reader.collectAllMetrics()) + .satisfiesExactly(metric -> + assertThat(metric) + .hasLongSumSatisfying(sum -> + sum.hasPointsSatisfying(point -> + point + .hasAttributesSatisfyingExactly( + equalTo(stringKey("key2"), "2"), equalTo(stringKey("key3"), "3")) + .hasExemplarsSatisfying(exemplar -> + exemplar.hasFilteredAttributes( + Attributes.builder().put(stringKey("key1"), "1").build()))))); + } + static final class InstrumentsProvider implements ArgumentsProvider { @Override