diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleCounterBuilder.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleCounterBuilder.java index d601318c87f..10ed3b2d9e2 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleCounterBuilder.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleCounterBuilder.java @@ -19,4 +19,12 @@ public interface ExtendedDoubleCounterBuilder extends DoubleCounterBuilder { default ExtendedDoubleCounterBuilder setAttributesAdvice(List> attributes) { return this; } + + /** + * Specify the opt-in advice, which indicates whether this counter should be disabled by default + * and require explicit opt-in via a view to produce metric data. + */ + default ExtendedDoubleCounterBuilder setOptInAdvice(boolean optIn) { + return this; + } } diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleGaugeBuilder.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleGaugeBuilder.java index eb576559609..fe08fc84f6c 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleGaugeBuilder.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleGaugeBuilder.java @@ -19,4 +19,12 @@ public interface ExtendedDoubleGaugeBuilder extends DoubleGaugeBuilder { default ExtendedDoubleGaugeBuilder setAttributesAdvice(List> attributes) { return this; } + + /** + * Specify the opt-in advice, which indicates whether this gauge should be disabled by default and + * require explicit opt-in via a view to produce metric data. + */ + default ExtendedDoubleGaugeBuilder setOptInAdvice(boolean optIn) { + return this; + } } diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleHistogramBuilder.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleHistogramBuilder.java index 8caf8f372f0..630d9923b4f 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleHistogramBuilder.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleHistogramBuilder.java @@ -19,4 +19,12 @@ public interface ExtendedDoubleHistogramBuilder extends DoubleHistogramBuilder { default ExtendedDoubleHistogramBuilder setAttributesAdvice(List> attributes) { return this; } + + /** + * Specify the opt-in advice, which indicates whether this histogram should be disabled by default + * and require explicit opt-in via a view to produce metric data. + */ + default ExtendedDoubleHistogramBuilder setOptInAdvice(boolean optIn) { + return this; + } } diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleUpDownCounterBuilder.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleUpDownCounterBuilder.java index 8262b448b13..e3f8d1c07d5 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleUpDownCounterBuilder.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleUpDownCounterBuilder.java @@ -19,4 +19,12 @@ public interface ExtendedDoubleUpDownCounterBuilder extends DoubleUpDownCounterB default ExtendedDoubleUpDownCounterBuilder setAttributesAdvice(List> attributes) { return this; } + + /** + * Specify the opt-in advice, which indicates whether this up down counter should be disabled by + * default and require explicit opt-in via a view to produce metric data. + */ + default ExtendedDoubleUpDownCounterBuilder setOptInAdvice(boolean optIn) { + return this; + } } diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongCounterBuilder.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongCounterBuilder.java index 04b7ae6f633..de2135b4ab3 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongCounterBuilder.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongCounterBuilder.java @@ -19,4 +19,12 @@ public interface ExtendedLongCounterBuilder extends LongCounterBuilder { default ExtendedLongCounterBuilder setAttributesAdvice(List> attributes) { return this; } + + /** + * Specify the opt-in advice, which indicates whether this counter should be disabled by default + * and require explicit opt-in via a view to produce metric data. + */ + default ExtendedLongCounterBuilder setOptInAdvice(boolean optIn) { + return this; + } } diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongGaugeBuilder.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongGaugeBuilder.java index db05e0958f3..de243af7fbf 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongGaugeBuilder.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongGaugeBuilder.java @@ -19,4 +19,12 @@ public interface ExtendedLongGaugeBuilder extends LongGaugeBuilder { default ExtendedLongGaugeBuilder setAttributesAdvice(List> attributes) { return this; } + + /** + * Specify the opt-in advice, which indicates whether this gauge should be disabled by default and + * require explicit opt-in via a view to produce metric data. + */ + default ExtendedLongGaugeBuilder setOptInAdvice(boolean optIn) { + return this; + } } diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongHistogramBuilder.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongHistogramBuilder.java index 9986443f143..94ea3b782d7 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongHistogramBuilder.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongHistogramBuilder.java @@ -19,4 +19,12 @@ public interface ExtendedLongHistogramBuilder extends LongHistogramBuilder { default ExtendedLongHistogramBuilder setAttributesAdvice(List> attributes) { return this; } + + /** + * Specify the opt-in advice, which indicates whether this histogram should be disabled by default + * and require explicit opt-in via a view to produce metric data. + */ + default ExtendedLongHistogramBuilder setOptInAdvice(boolean optIn) { + return this; + } } diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongUpDownCounterBuilder.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongUpDownCounterBuilder.java index b1d8c2f0b18..1d6b24733d1 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongUpDownCounterBuilder.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongUpDownCounterBuilder.java @@ -19,4 +19,12 @@ public interface ExtendedLongUpDownCounterBuilder extends LongUpDownCounterBuild default ExtendedLongUpDownCounterBuilder setAttributesAdvice(List> attributes) { return this; } + + /** + * Specify the opt-in advice, which indicates whether this up down counter should be disabled by + * default and require explicit opt-in via a view to produce metric data. + */ + default ExtendedLongUpDownCounterBuilder setOptInAdvice(boolean optIn) { + return this; + } } diff --git a/api/incubator/src/test/java/io/opentelemetry/api/incubator/metrics/ExtendedDefaultMeterTest.java b/api/incubator/src/test/java/io/opentelemetry/api/incubator/metrics/ExtendedDefaultMeterTest.java index 97204fc93ea..e7f8719341e 100644 --- a/api/incubator/src/test/java/io/opentelemetry/api/incubator/metrics/ExtendedDefaultMeterTest.java +++ b/api/incubator/src/test/java/io/opentelemetry/api/incubator/metrics/ExtendedDefaultMeterTest.java @@ -65,5 +65,43 @@ void incubatingApiIsLoaded() { assertThat(meter.upDownCounterBuilder("test").ofDoubles().build().isEnabled()).isFalse(); assertThat(meter.upDownCounterBuilder("test").ofDoubles().build()) .isInstanceOf(ExtendedDoubleUpDownCounter.class); + assertThat( + ((ExtendedLongGaugeBuilder) meter.gaugeBuilder("test").ofLongs()) + .setOptInAdvice(true) + .build()) + .isInstanceOf(ExtendedLongGauge.class); + assertThat( + ((ExtendedDoubleGaugeBuilder) meter.gaugeBuilder("test")).setOptInAdvice(true).build()) + .isInstanceOf(ExtendedDoubleGauge.class); + assertThat( + ((ExtendedLongHistogramBuilder) meter.histogramBuilder("test").ofLongs()) + .setOptInAdvice(true) + .build()) + .isInstanceOf(ExtendedLongHistogram.class); + assertThat( + ((ExtendedDoubleHistogramBuilder) meter.histogramBuilder("test")) + .setOptInAdvice(true) + .build()) + .isInstanceOf(ExtendedDoubleHistogram.class); + assertThat( + ((ExtendedLongCounterBuilder) meter.counterBuilder("test")) + .setOptInAdvice(true) + .build()) + .isInstanceOf(ExtendedLongCounter.class); + assertThat( + ((ExtendedDoubleCounterBuilder) meter.counterBuilder("test").ofDoubles()) + .setOptInAdvice(true) + .build()) + .isInstanceOf(ExtendedDoubleCounter.class); + assertThat( + ((ExtendedLongUpDownCounterBuilder) meter.upDownCounterBuilder("test")) + .setOptInAdvice(true) + .build()) + .isInstanceOf(ExtendedLongUpDownCounter.class); + assertThat( + ((ExtendedDoubleUpDownCounterBuilder) meter.upDownCounterBuilder("test").ofDoubles()) + .setOptInAdvice(true) + .build()) + .isInstanceOf(ExtendedDoubleUpDownCounter.class); } } diff --git a/api/incubator/src/test/java/io/opentelemetry/api/incubator/metrics/ExtendedMetricsApiUsageTest.java b/api/incubator/src/test/java/io/opentelemetry/api/incubator/metrics/ExtendedMetricsApiUsageTest.java index fb38819007c..19c481140bf 100644 --- a/api/incubator/src/test/java/io/opentelemetry/api/incubator/metrics/ExtendedMetricsApiUsageTest.java +++ b/api/incubator/src/test/java/io/opentelemetry/api/incubator/metrics/ExtendedMetricsApiUsageTest.java @@ -19,6 +19,7 @@ import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; import io.opentelemetry.sdk.metrics.View; +import io.opentelemetry.sdk.metrics.ViewBuilder; import io.opentelemetry.sdk.metrics.internal.SdkMeterProviderUtil; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; @@ -147,4 +148,48 @@ void attributesAdvice() { .hasAttributes( Attributes.builder().put("key1", "value1").build())))); } + + @Test + void optInAdvice() { + InMemoryMetricReader reader = InMemoryMetricReader.create(); + ViewBuilder enabledViewBuilder = View.builder(); + SdkMeterProviderUtil.setEnabled(enabledViewBuilder, /* enabled= */ true); + SdkMeterProvider meterProvider = + SdkMeterProvider.builder() + .setResource(Resource.getDefault()) + .registerMetricReader(reader) + .registerView( + InstrumentSelector.builder().setName("counter1").build(), + enabledViewBuilder.build()) + .build(); + + Meter meter = meterProvider.get("org.foo.my-scope"); + + ExtendedLongCounter counter1 = + (ExtendedLongCounter) + ((ExtendedLongCounterBuilder) meter.counterBuilder("counter1")) + .setOptInAdvice(true) + .build(); + ExtendedLongCounter counter2 = + (ExtendedLongCounter) + ((ExtendedLongCounterBuilder) meter.counterBuilder("counter2")) + .setOptInAdvice(true) + .build(); + + assertThat(counter1.isEnabled()).isTrue(); + assertThat(counter2.isEnabled()).isFalse(); + + counter1.add(1); + counter2.add(1); + + // Verify that only counter1 is collected and measurements recorded to disabled counter2 are + // dropped + assertThat(reader.collectAllMetrics()) + .satisfiesExactly( + metricData -> + assertThat(metricData) + .hasName("counter1") + .hasLongSumSatisfying( + sumAssert -> sumAssert.hasPointsSatisfying(point -> point.hasValue(1L)))); + } } diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleCounter.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleCounter.java index f5cf1c75b2f..d1b3db8d18f 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleCounter.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleCounter.java @@ -85,5 +85,11 @@ public ExtendedDoubleCounterBuilder setAttributesAdvice(List> at builder.setAdviceAttributes(attributes); return this; } + + @Override + public ExtendedDoubleCounterBuilder setOptInAdvice(boolean optIn) { + builder.setAdviceOptIn(optIn); + return this; + } } } diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleGauge.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleGauge.java index 36007a926b4..c187a83fa6f 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleGauge.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleGauge.java @@ -76,6 +76,12 @@ public ExtendedDoubleGaugeBuilder setAttributesAdvice(List> attr return this; } + @Override + public ExtendedDoubleGaugeBuilder setOptInAdvice(boolean optIn) { + builder.setAdviceOptIn(optIn); + return this; + } + @Override public ExtendedLongGaugeBuilder ofLongs() { return builder.swapBuilder(ExtendedSdkLongGauge.ExtendedSdkLongGaugeBuilder::new); diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleHistogram.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleHistogram.java index f8a401ff9ee..0a24cef5af0 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleHistogram.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleHistogram.java @@ -85,5 +85,11 @@ public ExtendedDoubleHistogramBuilder setAttributesAdvice(List> builder.setAdviceAttributes(attributes); return this; } + + @Override + public ExtendedDoubleHistogramBuilder setOptInAdvice(boolean optIn) { + builder.setAdviceOptIn(optIn); + return this; + } } } diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleUpDownCounter.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleUpDownCounter.java index 99a3972bd3c..d302abff652 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleUpDownCounter.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleUpDownCounter.java @@ -83,5 +83,11 @@ public ExtendedDoubleUpDownCounterBuilder setAttributesAdvice( builder.setAdviceAttributes(attributes); return this; } + + @Override + public ExtendedDoubleUpDownCounterBuilder setOptInAdvice(boolean optIn) { + builder.setAdviceOptIn(optIn); + return this; + } } } diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongCounter.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongCounter.java index 877289dde2b..e131cab92ed 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongCounter.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongCounter.java @@ -84,5 +84,11 @@ public ExtendedLongCounterBuilder setAttributesAdvice(List> attr builder.setAdviceAttributes(attributes); return this; } + + @Override + public ExtendedLongCounterBuilder setOptInAdvice(boolean optIn) { + builder.setAdviceOptIn(optIn); + return this; + } } } diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongGauge.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongGauge.java index c8e4a6f45b5..d31b98f2623 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongGauge.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongGauge.java @@ -80,5 +80,11 @@ public ExtendedLongGaugeBuilder setAttributesAdvice(List> attrib builder.setAdviceAttributes(attributes); return this; } + + @Override + public ExtendedLongGaugeBuilder setOptInAdvice(boolean optIn) { + builder.setAdviceOptIn(optIn); + return this; + } } } diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongHistogram.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongHistogram.java index a2ae99a2211..82b2bedbaf4 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongHistogram.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongHistogram.java @@ -85,5 +85,11 @@ public ExtendedLongHistogramBuilder setAttributesAdvice(List> at builder.setAdviceAttributes(attributes); return this; } + + @Override + public ExtendedLongHistogramBuilder setOptInAdvice(boolean optIn) { + builder.setAdviceOptIn(optIn); + return this; + } } } diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongUpDownCounter.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongUpDownCounter.java index bea60507fe3..a56b4b6c67a 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongUpDownCounter.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongUpDownCounter.java @@ -83,5 +83,11 @@ public ExtendedLongUpDownCounterBuilder setAttributesAdvice(List builder.setAdviceAttributes(attributes); return this; } + + @Override + public ExtendedLongUpDownCounterBuilder setOptInAdvice(boolean optIn) { + builder.setAdviceOptIn(optIn); + return this; + } } } diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/InstrumentBuilder.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/InstrumentBuilder.java index 14fd54d7bd1..eb7f504a16c 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/InstrumentBuilder.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/InstrumentBuilder.java @@ -128,4 +128,8 @@ void setAdviceAttributes(List> attributes) { void setExplicitBucketBoundaries(List bucketBoundaries) { adviceBuilder.setExplicitBucketBoundaries(bucketBoundaries); } + + void setAdviceOptIn(boolean optIn) { + adviceBuilder.setOptIn(optIn); + } } diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilder.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilder.java index b79f7441aa8..290cf665844 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilder.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilder.java @@ -166,7 +166,8 @@ public SdkMeterProviderBuilder registerView(InstrumentSelector selector, View vi view, AttributesFilters.byKeyName(view.getAttributeFilter()), view.getCardinalityLimit(), - SourceInfo.fromCurrentStack())); + SourceInfo.fromCurrentStack(), + view.getEnabled())); return this; } diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/View.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/View.java index ada250561d6..c7e057b249b 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/View.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/View.java @@ -33,8 +33,10 @@ static View create( @Nullable String description, Aggregation aggregation, Predicate attributeFilter, - int cardinalityLimit) { - return new AutoValue_View(name, description, aggregation, attributeFilter, cardinalityLimit); + int cardinalityLimit, + @Nullable Boolean enabled) { + return new AutoValue_View( + name, description, aggregation, attributeFilter, cardinalityLimit, enabled); } View() {} @@ -71,6 +73,13 @@ static View create( */ public abstract int getCardinalityLimit(); + /** + * Returns whether the metric stream from this view is produced, or {@code null} if not explicitly + * configured. + */ + @Nullable + abstract Boolean getEnabled(); + @Override public final String toString() { StringJoiner joiner = new StringJoiner(", ", "View{", "}"); @@ -83,6 +92,9 @@ public final String toString() { joiner.add("aggregation=" + getAggregation()); joiner.add("attributeFilter=" + getAttributeFilter()); joiner.add("cardinalityLimit=" + getCardinalityLimit()); + if (getEnabled() != null) { + joiner.add("enabled=" + getEnabled()); + } return joiner.toString(); } } diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ViewBuilder.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ViewBuilder.java index 9aa7823f9bd..119011a5b7b 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ViewBuilder.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ViewBuilder.java @@ -7,6 +7,7 @@ import io.opentelemetry.sdk.common.export.MemoryMode; import io.opentelemetry.sdk.common.internal.IncludeExcludePredicate; +import io.opentelemetry.sdk.metrics.internal.SdkMeterProviderUtil; import io.opentelemetry.sdk.metrics.internal.aggregator.AggregatorFactory; import io.opentelemetry.sdk.metrics.internal.state.MetricStorage; import io.opentelemetry.sdk.metrics.internal.view.StringPredicates; @@ -28,6 +29,7 @@ public final class ViewBuilder { private Aggregation aggregation = Aggregation.defaultAggregation(); private Predicate attributeFilter = StringPredicates.ALL; private int cardinalityLimit = MetricStorage.DEFAULT_MAX_CARDINALITY; + @Nullable private Boolean enabled; ViewBuilder() {} @@ -125,8 +127,21 @@ public ViewBuilder setCardinalityLimit(int cardinalityLimit) { return this; } + /** + * Sets whether the metric stream from the view is produced. + * + *

This method is experimental so not public. You may reflectively call it using {@link + * SdkMeterProviderUtil#setEnabled(ViewBuilder, boolean)}. + * + * @param enabled whether the metric stream from the view is produced + */ + ViewBuilder setEnabled(boolean enabled) { + this.enabled = enabled; + return this; + } + /** Returns a {@link View} with the configuration of this builder. */ public View build() { - return View.create(name, description, aggregation, attributeFilter, cardinalityLimit); + return View.create(name, description, aggregation, attributeFilter, cardinalityLimit, enabled); } } diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/SdkMeterProviderUtil.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/SdkMeterProviderUtil.java index 6ac78dcbec4..ca57f9bacc9 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/SdkMeterProviderUtil.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/SdkMeterProviderUtil.java @@ -9,6 +9,7 @@ import io.opentelemetry.sdk.common.internal.ScopeConfigurator; import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; +import io.opentelemetry.sdk.metrics.ViewBuilder; import io.opentelemetry.sdk.resources.internal.SdkResourceProvider; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -101,6 +102,18 @@ public static SdkMeterProviderBuilder addMeterConfiguratorCondition( return sdkMeterProviderBuilder; } + /** Reflectively set {@code enabled} on the {@link ViewBuilder}. */ + public static ViewBuilder setEnabled(ViewBuilder viewBuilder, boolean enabled) { + try { + Method method = ViewBuilder.class.getDeclaredMethod("setEnabled", boolean.class); + method.setAccessible(true); + method.invoke(viewBuilder, enabled); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException("Error calling setEnabled on ViewBuilder", e); + } + return viewBuilder; + } + /** Reflectively reset the {@link SdkMeterProvider}, clearing all registered instruments. */ public static void resetForTest(SdkMeterProvider sdkMeterProvider) { try { diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/descriptor/Advice.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/descriptor/Advice.java index 768b3afc297..c36ddee4df1 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/descriptor/Advice.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/descriptor/Advice.java @@ -28,7 +28,7 @@ public static Advice empty() { } public static AdviceBuilder builder() { - return new AutoValue_Advice.Builder(); + return new AutoValue_Advice.Builder().setOptIn(false); } Advice() {} @@ -39,6 +39,8 @@ public static AdviceBuilder builder() { @Nullable public abstract List> getAttributes(); + public abstract boolean isOptIn(); + public boolean hasAttributes() { return getAttributes() != null; } @@ -83,6 +85,17 @@ public AdviceBuilder setAttributes(@Nullable List> attributes) { return attributes(attributes); } + abstract AdviceBuilder optIn(boolean optIn); + + /** + * Sets whether the instrument requires explicit opt-in via a view to produce metric data. + * + * @param optIn whether the instrument is opt-in. + */ + public AdviceBuilder setOptIn(boolean optIn) { + return optIn(optIn); + } + public abstract Advice build(); } } diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/view/RegisteredView.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/view/RegisteredView.java index 296fdadc609..46c598a86de 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/view/RegisteredView.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/view/RegisteredView.java @@ -11,6 +11,7 @@ import io.opentelemetry.sdk.metrics.View; import io.opentelemetry.sdk.metrics.internal.debug.SourceInfo; import java.util.function.UnaryOperator; +import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; /** @@ -29,8 +30,18 @@ public static RegisteredView create( UnaryOperator attributesFilter, int cardinalityLimit, SourceInfo viewSourceInfo) { + return create(selector, view, attributesFilter, cardinalityLimit, viewSourceInfo, null); + } + + public static RegisteredView create( + InstrumentSelector selector, + View view, + UnaryOperator attributesFilter, + int cardinalityLimit, + SourceInfo viewSourceInfo, + @Nullable Boolean enabled) { return new AutoValue_RegisteredView( - selector, view, attributesFilter, cardinalityLimit, viewSourceInfo); + selector, view, attributesFilter, cardinalityLimit, viewSourceInfo, enabled); } RegisteredView() {} @@ -54,6 +65,13 @@ public static RegisteredView create( /** The {@link SourceInfo} from where the view was registered. */ public abstract SourceInfo getViewSourceInfo(); + /** + * Whether the metric stream from this view is enabled, or {@code null} if not explicitly + * configured. + */ + @Nullable + public abstract Boolean getEnabled(); + @Override public final String toString() { return "RegisteredView{" diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/view/ViewRegistry.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/view/ViewRegistry.java index 671b7ab3276..37f7e4196d3 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/view/ViewRegistry.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/view/ViewRegistry.java @@ -14,8 +14,10 @@ import io.opentelemetry.sdk.metrics.InstrumentType; import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; import io.opentelemetry.sdk.metrics.View; +import io.opentelemetry.sdk.metrics.ViewBuilder; import io.opentelemetry.sdk.metrics.export.CardinalityLimitSelector; import io.opentelemetry.sdk.metrics.export.DefaultAggregationSelector; +import io.opentelemetry.sdk.metrics.internal.SdkMeterProviderUtil; import io.opentelemetry.sdk.metrics.internal.aggregator.AggregationUtil; import io.opentelemetry.sdk.metrics.internal.aggregator.AggregatorFactory; import io.opentelemetry.sdk.metrics.internal.debug.SourceInfo; @@ -48,6 +50,13 @@ public final class ViewRegistry { AttributesFilters.ALLOW_ALL, MetricStorage.DEFAULT_MAX_CARDINALITY, SourceInfo.noSourceInfo()); + static final RegisteredView DROP_REGISTERED_VIEW = + RegisteredView.create( + InstrumentSelector.builder().setName("*").build(), + View.builder().setAggregation(Aggregation.drop()).build(), + AttributesFilters.ALLOW_ALL, + MetricStorage.DEFAULT_MAX_CARDINALITY, + SourceInfo.noSourceInfo()); private static final Logger logger = Logger.getLogger(ViewRegistry.class.getName()); private final Map instrumentDefaultRegisteredView; @@ -105,7 +114,22 @@ public List findViews( AggregatorFactory viewAggregatorFactory = (AggregatorFactory) entry.getView().getAggregation(); if (viewAggregatorFactory.isCompatibleWithInstrument(descriptor)) { - result.add(entry); + // Drop the stream if the view explicitly sets enabled=false, or if the instrument is + // opt-in and the view does not explicitly set enabled=true. + if (Boolean.FALSE.equals(entry.getEnabled()) + || (descriptor.getAdvice().isOptIn() && !Boolean.TRUE.equals(entry.getEnabled()))) { + result.add(applyDropAggregation(entry)); + } else if (Boolean.TRUE.equals(entry.getEnabled()) + && descriptor.getAdvice().hasAttributes() + && entry.getView().getAttributeFilter().equals(StringPredicates.ALL)) { + // When a view enables an instrument without configuring an attribute filter, apply the + // instrument's attributes advice so enabling an opt-in instrument respects its other + // advisory parameters. + result.add(applyAdviceToDefaultView(entry, descriptor.getAdvice())); + } else { + // Otherwise, use the matched view as configured. + result.add(entry); + } } else { logger.log( Level.WARNING, @@ -124,6 +148,10 @@ public List findViews( return Collections.unmodifiableList(result); } + if (descriptor.getAdvice().isOptIn()) { + return Collections.singletonList(DROP_REGISTERED_VIEW); + } + // No views matched, use default view RegisteredView instrumentDefaultView = requireNonNull(instrumentDefaultRegisteredView.get(descriptor.getType())); @@ -190,12 +218,41 @@ private static boolean matchesMeter( } private static RegisteredView applyAdviceToDefaultView( - RegisteredView instrumentDefaultView, Advice advice) { + RegisteredView registeredView, Advice advice) { return RegisteredView.create( - instrumentDefaultView.getInstrumentSelector(), - instrumentDefaultView.getView(), + registeredView.getInstrumentSelector(), + registeredView.getView(), AttributesFilters.byAdvice(requireNonNull(advice.getAttributes())), - instrumentDefaultView.getCardinalityLimit(), - instrumentDefaultView.getViewSourceInfo()); + registeredView.getCardinalityLimit(), + registeredView.getViewSourceInfo(), + registeredView.getEnabled()); + } + + private static RegisteredView applyDropAggregation(RegisteredView registeredView) { + if (Aggregation.drop().equals(registeredView.getView().getAggregation())) { + return registeredView; + } + View view = registeredView.getView(); + ViewBuilder viewBuilder = + View.builder() + .setAggregation(Aggregation.drop()) + .setAttributeFilter(view.getAttributeFilter()) + .setCardinalityLimit(view.getCardinalityLimit()); + if (view.getName() != null) { + viewBuilder.setName(view.getName()); + } + if (view.getDescription() != null) { + viewBuilder.setDescription(view.getDescription()); + } + if (registeredView.getEnabled() != null) { + SdkMeterProviderUtil.setEnabled(viewBuilder, registeredView.getEnabled()); + } + return RegisteredView.create( + registeredView.getInstrumentSelector(), + viewBuilder.build(), + registeredView.getAttributesFilter(), + registeredView.getCardinalityLimit(), + registeredView.getViewSourceInfo(), + registeredView.getEnabled()); } } diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/InstrumentBuilderTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/InstrumentBuilderTest.java index 972c9667052..b61af7868de 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/InstrumentBuilderTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/InstrumentBuilderTest.java @@ -48,7 +48,7 @@ void stringRepresentation() { + "unit=instrument-unit, " + "type=COUNTER, " + "valueType=LONG, " - + "advice=Advice{explicitBucketBoundaries=null, attributes=null}" + + "advice=Advice{explicitBucketBoundaries=null, attributes=null, optIn=false}" + "}}"); } @@ -71,7 +71,7 @@ void toStringHelper() { + "unit=instrument-unit, " + "type=HISTOGRAM, " + "valueType=DOUBLE, " - + "advice=Advice{explicitBucketBoundaries=null, attributes=null}" + + "advice=Advice{explicitBucketBoundaries=null, attributes=null, optIn=false}" + "}}"); } } diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleUpDownCounterTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleUpDownCounterTest.java index 244d3d3c5e2..64ee9e4e141 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleUpDownCounterTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkDoubleUpDownCounterTest.java @@ -161,7 +161,7 @@ void collectMetrics_WithMultipleCollects() { @Test void testToString() { String expected = - "SdkDoubleUpDownCounter{descriptor=InstrumentDescriptor{name=testUpDownCounter, description=description, unit=ms, type=UP_DOWN_COUNTER, valueType=DOUBLE, advice=Advice{explicitBucketBoundaries=null, attributes=null}}}"; + "SdkDoubleUpDownCounter{descriptor=InstrumentDescriptor{name=testUpDownCounter, description=description, unit=ms, type=UP_DOWN_COUNTER, valueType=DOUBLE, advice=Advice{explicitBucketBoundaries=null, attributes=null, optIn=false}}}"; DoubleUpDownCounter counter = sdkMeter .upDownCounterBuilder("testUpDownCounter") diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkObservableInstrumentTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkObservableInstrumentTest.java index 385e6f65a8c..1149ae1d605 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkObservableInstrumentTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkObservableInstrumentTest.java @@ -81,7 +81,7 @@ void stringRepresentation() { + "unit=unit, " + "type=COUNTER, " + "valueType=DOUBLE, " - + "advice=Advice{explicitBucketBoundaries=null, attributes=null}}" + + "advice=Advice{explicitBucketBoundaries=null, attributes=null, optIn=false}}" + "]}}"); } } diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/ViewTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/ViewTest.java index 00ec42c6f7c..fe6a75c8105 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/ViewTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/ViewTest.java @@ -7,6 +7,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import io.opentelemetry.sdk.metrics.internal.SdkMeterProviderUtil; import java.util.Arrays; import java.util.HashSet; import org.junit.jupiter.api.Test; @@ -18,15 +19,14 @@ void stringRepresentation() { assertThat(View.builder().build().toString()) .startsWith("View{aggregation=DefaultAggregation, attributeFilter=") .endsWith(", cardinalityLimit=2000}"); - assertThat( - View.builder() - .setName("name") - .setDescription("description") - .setAggregation(Aggregation.sum()) - .setCardinalityLimit(10) - .setAttributeFilter(new HashSet<>(Arrays.asList("key1", "key2"))) - .build() - .toString()) + ViewBuilder viewBuilder = + View.builder() + .setName("name") + .setDescription("description") + .setAggregation(Aggregation.sum()) + .setCardinalityLimit(10) + .setAttributeFilter(new HashSet<>(Arrays.asList("key1", "key2"))); + assertThat(viewBuilder.build().toString()) .isEqualTo( "View{" + "name=name, " @@ -35,5 +35,16 @@ void stringRepresentation() { + "attributeFilter=IncludeExcludePredicate{globMatchingEnabled=false, included=[key1, key2]}, " + "cardinalityLimit=10" + "}"); + SdkMeterProviderUtil.setEnabled(viewBuilder, /* enabled= */ true); + assertThat(viewBuilder.build().toString()) + .isEqualTo( + "View{" + + "name=name, " + + "description=description, " + + "aggregation=SumAggregation, " + + "attributeFilter=IncludeExcludePredicate{globMatchingEnabled=false, included=[key1, key2]}, " + + "cardinalityLimit=10, " + + "enabled=true" + + "}"); } } diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/state/CallbackRegistrationTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/state/CallbackRegistrationTest.java index 47f45028e6e..e88d9384a24 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/state/CallbackRegistrationTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/state/CallbackRegistrationTest.java @@ -109,7 +109,7 @@ void stringRepresentation() { + "unit=unit, " + "type=OBSERVABLE_COUNTER, " + "valueType=LONG, " - + "advice=Advice{explicitBucketBoundaries=null, attributes=null}" + + "advice=Advice{explicitBucketBoundaries=null, attributes=null, optIn=false}" + "}]}"); assertThat( CallbackRegistration.create(Arrays.asList(measurement1, measurement2), callback) @@ -123,14 +123,14 @@ void stringRepresentation() { + "unit=unit, " + "type=OBSERVABLE_COUNTER, " + "valueType=LONG, " - + "advice=Advice{explicitBucketBoundaries=null, attributes=null}}, " + + "advice=Advice{explicitBucketBoundaries=null, attributes=null, optIn=false}}, " + "InstrumentDescriptor{" + "name=long-counter, " + "description=description, " + "unit=unit, " + "type=OBSERVABLE_COUNTER, " + "valueType=LONG, " - + "advice=Advice{explicitBucketBoundaries=null, attributes=null}" + + "advice=Advice{explicitBucketBoundaries=null, attributes=null, optIn=false}" + "}]}"); } diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/view/ViewRegistryTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/view/ViewRegistryTest.java index 492518edb37..c5337d63d47 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/view/ViewRegistryTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/view/ViewRegistryTest.java @@ -7,6 +7,7 @@ import static io.opentelemetry.api.common.AttributeKey.stringKey; import static io.opentelemetry.sdk.metrics.internal.view.ViewRegistry.DEFAULT_REGISTERED_VIEW; +import static io.opentelemetry.sdk.metrics.internal.view.ViewRegistry.DROP_REGISTERED_VIEW; import static org.assertj.core.api.Assertions.assertThat; import io.github.netmikey.logunit.api.LogCapturer; @@ -20,14 +21,17 @@ import io.opentelemetry.sdk.metrics.InstrumentType; import io.opentelemetry.sdk.metrics.InstrumentValueType; import io.opentelemetry.sdk.metrics.View; +import io.opentelemetry.sdk.metrics.ViewBuilder; import io.opentelemetry.sdk.metrics.export.CardinalityLimitSelector; import io.opentelemetry.sdk.metrics.export.DefaultAggregationSelector; +import io.opentelemetry.sdk.metrics.internal.SdkMeterProviderUtil; import io.opentelemetry.sdk.metrics.internal.debug.SourceInfo; import io.opentelemetry.sdk.metrics.internal.descriptor.Advice; import io.opentelemetry.sdk.metrics.internal.descriptor.InstrumentDescriptor; import io.opentelemetry.sdk.metrics.internal.state.MetricStorage; import java.util.Arrays; import java.util.Collections; +import javax.annotation.Nullable; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -43,12 +47,18 @@ class ViewRegistryTest { .build(); private static RegisteredView registeredView(InstrumentSelector instrumentSelector, View view) { + return registeredView(instrumentSelector, view, null); + } + + private static RegisteredView registeredView( + InstrumentSelector instrumentSelector, View view, @Nullable Boolean enabled) { return RegisteredView.create( instrumentSelector, view, AttributesFilters.ALLOW_ALL, MetricStorage.DEFAULT_MAX_CARDINALITY, - SourceInfo.fromCurrentStack()); + SourceInfo.fromCurrentStack(), + enabled); } @Test @@ -519,6 +529,119 @@ void findViews_ApplyAdvice() { .isEqualTo(Collections.singletonList(DEFAULT_REGISTERED_VIEW)); } + @Test + @SuppressLogger(ViewRegistry.class) + void findViews_OptInAndViewEnabled() { + ViewBuilder enabledViewBuilder = View.builder().setDescription("enabled view"); + SdkMeterProviderUtil.setEnabled(enabledViewBuilder, /* enabled= */ true); + RegisteredView enabledView = + registeredView( + InstrumentSelector.builder().setName("opt_in_enabled").build(), + enabledViewBuilder.build(), + /* enabled= */ true); + + ViewBuilder disabledViewBuilder = View.builder().setAggregation(Aggregation.sum()); + SdkMeterProviderUtil.setEnabled(disabledViewBuilder, /* enabled= */ false); + RegisteredView disabledView = + registeredView( + InstrumentSelector.builder().setName("disabled_by_view").build(), + disabledViewBuilder.build(), + /* enabled= */ false); + + ViewBuilder incompatibleDisabledViewBuilder = + View.builder().setAggregation(Aggregation.explicitBucketHistogram()); + SdkMeterProviderUtil.setEnabled(incompatibleDisabledViewBuilder, /* enabled= */ false); + RegisteredView incompatibleDisabledView = + registeredView( + InstrumentSelector.builder().setName("incompatible_disabled").build(), + incompatibleDisabledViewBuilder.build(), + /* enabled= */ false); + + RegisteredView unsetEnabledView = + registeredView( + InstrumentSelector.builder().setName("opt_in_unset_view").build(), + View.builder().setAggregation(Aggregation.sum()).build(), + /* enabled= */ null); + + ViewRegistry viewRegistry = + ViewRegistry.create( + DefaultAggregationSelector.getDefault(), + CardinalityLimitSelector.defaultCardinalityLimitSelector(), + Arrays.asList(enabledView, disabledView, incompatibleDisabledView, unsetEnabledView)); + + // OptIn instrument with no matching view gets DROP_REGISTERED_VIEW + assertThat( + viewRegistry.findViews( + InstrumentDescriptor.create( + "opt_in_no_view", + "", + "", + InstrumentType.COUNTER, + InstrumentValueType.LONG, + Advice.builder().setOptIn(true).build()), + INSTRUMENTATION_SCOPE_INFO)) + .isEqualTo(Collections.singletonList(DROP_REGISTERED_VIEW)); + + // OptIn instrument matching a view with enabled=true is enabled + assertThat( + viewRegistry.findViews( + InstrumentDescriptor.create( + "opt_in_enabled", + "", + "", + InstrumentType.COUNTER, + InstrumentValueType.LONG, + Advice.builder().setOptIn(true).build()), + INSTRUMENTATION_SCOPE_INFO)) + .isEqualTo(Collections.singletonList(enabledView)); + + // OptIn instrument matching a view without enabled=true gets drop aggregation + assertThat( + viewRegistry.findViews( + InstrumentDescriptor.create( + "opt_in_unset_view", + "", + "", + InstrumentType.COUNTER, + InstrumentValueType.LONG, + Advice.builder().setOptIn(true).build()), + INSTRUMENTATION_SCOPE_INFO)) + .hasSize(1) + .element(0) + .satisfies(rv -> assertThat(rv.getView().getAggregation()).isEqualTo(Aggregation.drop())); + + // Non-OptIn instrument matching a view with enabled=false gets drop aggregation regardless of + // view's aggregation + assertThat( + viewRegistry.findViews( + InstrumentDescriptor.create( + "disabled_by_view", + "", + "", + InstrumentType.COUNTER, + InstrumentValueType.LONG, + Advice.empty()), + INSTRUMENTATION_SCOPE_INFO)) + .hasSize(1) + .element(0) + .satisfies(rv -> assertThat(rv.getView().getAggregation()).isEqualTo(Aggregation.drop())); + + // Incompatible view with enabled=false still logs a warning and is ignored + assertThat( + viewRegistry.findViews( + InstrumentDescriptor.create( + "incompatible_disabled", + "", + "", + InstrumentType.OBSERVABLE_COUNTER, + InstrumentValueType.LONG, + Advice.empty()), + INSTRUMENTATION_SCOPE_INFO)) + .isEqualTo(Collections.singletonList(DEFAULT_REGISTERED_VIEW)); + logs.assertContains( + "View aggregation explicit_bucket_histogram is incompatible with instrument incompatible_disabled of type OBSERVABLE_COUNTER"); + } + private static void assertAdviceFilter(RegisteredView view, AttributeKey... expectedKeys) { assertThat(view) .as("is the same as the default view, except the attributes filter") diff --git a/sdk/metrics/src/testIncubating/java/io/opentelemetry/sdk/metrics/OptInAdviceTest.java b/sdk/metrics/src/testIncubating/java/io/opentelemetry/sdk/metrics/OptInAdviceTest.java new file mode 100644 index 00000000000..468798714ed --- /dev/null +++ b/sdk/metrics/src/testIncubating/java/io/opentelemetry/sdk/metrics/OptInAdviceTest.java @@ -0,0 +1,662 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static java.util.Arrays.asList; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.incubator.metrics.ExtendedDoubleCounter; +import io.opentelemetry.api.incubator.metrics.ExtendedDoubleCounterBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedDoubleGauge; +import io.opentelemetry.api.incubator.metrics.ExtendedDoubleGaugeBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedDoubleHistogram; +import io.opentelemetry.api.incubator.metrics.ExtendedDoubleHistogramBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedDoubleUpDownCounter; +import io.opentelemetry.api.incubator.metrics.ExtendedDoubleUpDownCounterBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedLongCounter; +import io.opentelemetry.api.incubator.metrics.ExtendedLongCounterBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedLongGauge; +import io.opentelemetry.api.incubator.metrics.ExtendedLongGaugeBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedLongHistogram; +import io.opentelemetry.api.incubator.metrics.ExtendedLongHistogramBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedLongUpDownCounter; +import io.opentelemetry.api.incubator.metrics.ExtendedLongUpDownCounterBuilder; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.ObservableDoubleMeasurement; +import io.opentelemetry.sdk.metrics.internal.SdkMeterProviderUtil; +import io.opentelemetry.sdk.testing.assertj.AbstractPointAssert; +import io.opentelemetry.sdk.testing.assertj.DoublePointAssert; +import io.opentelemetry.sdk.testing.assertj.HistogramPointAssert; +import io.opentelemetry.sdk.testing.assertj.LongPointAssert; +import io.opentelemetry.sdk.testing.assertj.MetricAssert; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.stream.Stream; +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; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.support.ParameterDeclarations; + +class OptInAdviceTest { + + private static final Attributes ATTRIBUTES = + Attributes.builder() + .put(stringKey("key1"), "1") + .put(stringKey("key2"), "2") + .put(stringKey("key3"), "3") + .build(); + + private SdkMeterProvider meterProvider = SdkMeterProvider.builder().build(); + + @AfterEach + void cleanup() { + meterProvider.close(); + } + + @ParameterizedTest + @ArgumentsSource(InstrumentsProvider.class) + void optInInstrumentDisabledByDefault( + InstrumentFactory instrumentFactory, PointsAssert> unused) { + InMemoryMetricReader reader = InMemoryMetricReader.create(); + meterProvider = SdkMeterProvider.builder().registerMetricReader(reader).build(); + + TestInstrument instrument = instrumentFactory.create(meterProvider, "test", /* optIn= */ true); + assertThat(instrument.isEnabled()).isFalse(); + instrument.record(1, ATTRIBUTES); + + assertThat(reader.collectAllMetrics()).isEmpty(); + } + + @ParameterizedTest + @ArgumentsSource(InstrumentsProvider.class) + void optInFalseInstrumentEnabledByDefault( + InstrumentFactory instrumentFactory, PointsAssert> pointsAssert) { + InMemoryMetricReader reader = InMemoryMetricReader.create(); + meterProvider = SdkMeterProvider.builder().registerMetricReader(reader).build(); + + TestInstrument instrument = instrumentFactory.create(meterProvider, "test", /* optIn= */ false); + assertThat(instrument.isEnabled()).isTrue(); + instrument.record(1, ATTRIBUTES); + + assertThat(reader.collectAllMetrics()) + .satisfiesExactly( + metric -> + pointsAssert.hasPointSatisfying( + assertThat(metric), point -> point.hasAttributes(ATTRIBUTES))); + } + + @ParameterizedTest + @ArgumentsSource(InstrumentsProvider.class) + void optInInstrumentEnabledByView( + InstrumentFactory instrumentFactory, PointsAssert> pointsAssert) { + InMemoryMetricReader reader = InMemoryMetricReader.create(); + ViewBuilder viewBuilder = View.builder(); + SdkMeterProviderUtil.setEnabled(viewBuilder, /* enabled= */ true); + meterProvider = + SdkMeterProvider.builder() + .registerMetricReader(reader) + .registerView(InstrumentSelector.builder().setName("test").build(), viewBuilder.build()) + .build(); + + TestInstrument instrument = instrumentFactory.create(meterProvider, "test", /* optIn= */ true); + assertThat(instrument.isEnabled()).isTrue(); + instrument.record(1, ATTRIBUTES); + + assertThat(reader.collectAllMetrics()) + .satisfiesExactly( + metric -> + pointsAssert.hasPointSatisfying( + assertThat(metric), point -> point.hasAttributes(ATTRIBUTES))); + } + + @ParameterizedTest + @ArgumentsSource(InstrumentsProvider.class) + void optInInstrumentNotEnabledByViewWithoutEnabledTrue( + InstrumentFactory instrumentFactory, PointsAssert> unused) { + InMemoryMetricReader reader = InMemoryMetricReader.create(); + meterProvider = + SdkMeterProvider.builder() + .registerMetricReader(reader) + .registerView( + InstrumentSelector.builder().setName("test").build(), + View.builder().setDescription("custom description").build()) + .build(); + + TestInstrument instrument = instrumentFactory.create(meterProvider, "test", /* optIn= */ true); + assertThat(instrument.isEnabled()).isFalse(); + instrument.record(1, ATTRIBUTES); + + assertThat(reader.collectAllMetrics()).isEmpty(); + } + + @Test + void optInCounterNotEnabledByViewWithAggregationOnly() { + InMemoryMetricReader reader = InMemoryMetricReader.create(); + meterProvider = + SdkMeterProvider.builder() + .registerMetricReader(reader) + .registerView( + InstrumentSelector.builder().setName("test_counter").build(), + View.builder().setAggregation(Aggregation.sum()).build()) + .build(); + + ExtendedLongCounter counter = + (ExtendedLongCounter) + ((ExtendedLongCounterBuilder) meterProvider.get("meter").counterBuilder("test_counter")) + .setOptInAdvice(true) + .build(); + assertThat(counter.isEnabled()).isFalse(); + counter.add(1, ATTRIBUTES); + + assertThat(reader.collectAllMetrics()).isEmpty(); + } + + @ParameterizedTest + @ArgumentsSource(InstrumentsProvider.class) + void nonOptInInstrumentDisabledByViewEnabledFalse( + InstrumentFactory instrumentFactory, PointsAssert> unused) { + InMemoryMetricReader reader = InMemoryMetricReader.create(); + ViewBuilder viewBuilder = View.builder(); + SdkMeterProviderUtil.setEnabled(viewBuilder, /* enabled= */ false); + meterProvider = + SdkMeterProvider.builder() + .registerMetricReader(reader) + .registerView(InstrumentSelector.builder().setName("test").build(), viewBuilder.build()) + .build(); + + TestInstrument instrument = instrumentFactory.create(meterProvider, "test", /* optIn= */ false); + assertThat(instrument.isEnabled()).isFalse(); + instrument.record(1, ATTRIBUTES); + + assertThat(reader.collectAllMetrics()).isEmpty(); + } + + @Test + void nonOptInCounterDisabledByViewEnabledFalseOverridesAggregation() { + InMemoryMetricReader reader = InMemoryMetricReader.create(); + ViewBuilder viewBuilder = + View.builder() + .setAggregation( + Aggregation.explicitBucketHistogram( + ExplicitBucketHistogramOptions.builder().build())); + SdkMeterProviderUtil.setEnabled(viewBuilder, /* enabled= */ false); + meterProvider = + SdkMeterProvider.builder() + .registerMetricReader(reader) + .registerView( + InstrumentSelector.builder().setName("test_counter").build(), viewBuilder.build()) + .build(); + + ExtendedLongCounter counter = + (ExtendedLongCounter) + ((ExtendedLongCounterBuilder) meterProvider.get("meter").counterBuilder("test_counter")) + .setOptInAdvice(false) + .build(); + assertThat(counter.isEnabled()).isFalse(); + counter.add(1, ATTRIBUTES); + + assertThat(reader.collectAllMetrics()).isEmpty(); + } + + @Test + void optInRespectsOtherAdvisoryParametersWhenEnabledByView() { + InMemoryMetricReader reader = InMemoryMetricReader.create(); + ViewBuilder enabledViewBuilder = View.builder(); + SdkMeterProviderUtil.setEnabled(enabledViewBuilder, /* enabled= */ true); + meterProvider = + SdkMeterProvider.builder() + .registerMetricReader(reader) + .registerView( + InstrumentSelector.builder().setName("test_histogram").build(), + enabledViewBuilder.build()) + .build(); + + DoubleHistogram histogram = + ((ExtendedDoubleHistogramBuilder) + meterProvider.get("meter").histogramBuilder("test_histogram")) + .setOptInAdvice(true) + .setAttributesAdvice(asList(stringKey("key1"), stringKey("key2"))) + .setExplicitBucketBoundariesAdvice(asList(5.0, 10.0, 25.0)) + .build(); + + histogram.record(7.0, ATTRIBUTES); + + assertThat(reader.collectAllMetrics()) + .satisfiesExactly( + metric -> + assertThat(metric) + .hasName("test_histogram") + .hasHistogramSatisfying( + hist -> + hist.hasPointsSatisfying( + point -> + point + .hasBucketBoundaries(5.0, 10.0, 25.0) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("key1"), "1"), + equalTo(stringKey("key2"), "2"))))); + } + + @Test + void optInAdvisoryParametersOverriddenByViewWhenConfigured() { + InMemoryMetricReader reader = InMemoryMetricReader.create(); + ViewBuilder enabledViewBuilder = + View.builder() + .setAttributeFilter(Collections.singleton("key3")) + .setAggregation( + Aggregation.explicitBucketHistogram( + ExplicitBucketHistogramOptions.builder() + .setBucketBoundaries(asList(1.0, 2.0)) + .build())); + SdkMeterProviderUtil.setEnabled(enabledViewBuilder, /* enabled= */ true); + meterProvider = + SdkMeterProvider.builder() + .registerMetricReader(reader) + .registerView( + InstrumentSelector.builder().setName("test_histogram").build(), + enabledViewBuilder.build()) + .build(); + + DoubleHistogram histogram = + ((ExtendedDoubleHistogramBuilder) + meterProvider.get("meter").histogramBuilder("test_histogram")) + .setOptInAdvice(true) + .setAttributesAdvice(asList(stringKey("key1"), stringKey("key2"))) + .setExplicitBucketBoundariesAdvice(asList(5.0, 10.0, 25.0)) + .build(); + + histogram.record(1.5, ATTRIBUTES); + + assertThat(reader.collectAllMetrics()) + .satisfiesExactly( + metric -> + assertThat(metric) + .hasName("test_histogram") + .hasHistogramSatisfying( + hist -> + hist.hasPointsSatisfying( + point -> + point + .hasBucketBoundaries(1.0, 2.0) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("key3"), "3"))))); + } + + @Test + void optInAsynchronousInstruments() { + InMemoryMetricReader reader = InMemoryMetricReader.create(); + ViewBuilder enabledViewBuilder = View.builder(); + SdkMeterProviderUtil.setEnabled(enabledViewBuilder, /* enabled= */ true); + ViewBuilder disabledViewBuilder = View.builder(); + SdkMeterProviderUtil.setEnabled(disabledViewBuilder, /* enabled= */ false); + meterProvider = + SdkMeterProvider.builder() + .registerMetricReader(reader) + .registerView( + InstrumentSelector.builder().setName("enabled_opt_in_*").build(), + enabledViewBuilder.build()) + .registerView( + InstrumentSelector.builder().setName("disabled_by_view").build(), + disabledViewBuilder.build()) + .build(); + + Meter meter = meterProvider.get("meter"); + AtomicInteger droppedCallbackCount = new AtomicInteger(); + AtomicInteger enabledCallbackCount = new AtomicInteger(); + + ((ExtendedLongCounterBuilder) meter.counterBuilder("dropped_opt_in_counter")) + .setOptInAdvice(true) + .buildWithCallback( + measurement -> { + droppedCallbackCount.incrementAndGet(); + measurement.record(1); + }); + ((ExtendedDoubleGaugeBuilder) meter.gaugeBuilder("dropped_opt_in_gauge")) + .setOptInAdvice(true) + .buildWithCallback( + measurement -> { + droppedCallbackCount.incrementAndGet(); + measurement.record(1.0); + }); + ((ExtendedLongUpDownCounterBuilder) meter.upDownCounterBuilder("dropped_opt_in_updown")) + .setOptInAdvice(true) + .buildWithCallback( + measurement -> { + droppedCallbackCount.incrementAndGet(); + measurement.record(1); + }); + meter + .counterBuilder("disabled_by_view") + .buildWithCallback( + measurement -> { + droppedCallbackCount.incrementAndGet(); + measurement.record(1); + }); + + ObservableDoubleMeasurement droppedBatchMeasurement = + ((ExtendedDoubleCounterBuilder) meter.counterBuilder("dropped_opt_in_batch").ofDoubles()) + .setOptInAdvice(true) + .buildObserver(); + meter.batchCallback( + () -> { + droppedCallbackCount.incrementAndGet(); + droppedBatchMeasurement.record(1.0); + }, + droppedBatchMeasurement); + + ((ExtendedLongCounterBuilder) meter.counterBuilder("enabled_opt_in_counter")) + .setOptInAdvice(true) + .buildWithCallback( + measurement -> { + enabledCallbackCount.incrementAndGet(); + measurement.record(42); + }); + + assertThat(reader.collectAllMetrics()) + .satisfiesExactly( + metric -> + assertThat(metric) + .hasName("enabled_opt_in_counter") + .hasLongSumSatisfying( + sum -> sum.hasPointsSatisfying(point -> point.hasValue(42L)))); + assertThat(droppedCallbackCount.get()).isEqualTo(0); + assertThat(enabledCallbackCount.get()).isEqualTo(1); + } + + @Test + void optInPreservedAcrossBuilderSwaps() { + InMemoryMetricReader reader = InMemoryMetricReader.create(); + meterProvider = SdkMeterProvider.builder().registerMetricReader(reader).build(); + Meter meter = meterProvider.get("meter"); + + ExtendedDoubleCounter doubleCounter = + (ExtendedDoubleCounter) + ((ExtendedLongCounterBuilder) meter.counterBuilder("double_counter")) + .setOptInAdvice(true) + .ofDoubles() + .build(); + ExtendedLongGauge longGauge = + (ExtendedLongGauge) + ((ExtendedDoubleGaugeBuilder) meter.gaugeBuilder("long_gauge")) + .setOptInAdvice(true) + .ofLongs() + .build(); + ExtendedLongHistogram longHistogram = + (ExtendedLongHistogram) + ((ExtendedDoubleHistogramBuilder) meter.histogramBuilder("long_histogram")) + .setOptInAdvice(true) + .ofLongs() + .build(); + ExtendedDoubleUpDownCounter doubleUpDownCounter = + (ExtendedDoubleUpDownCounter) + ((ExtendedLongUpDownCounterBuilder) meter.upDownCounterBuilder("double_updown")) + .setOptInAdvice(true) + .ofDoubles() + .build(); + + assertThat(doubleCounter.isEnabled()).isFalse(); + assertThat(longGauge.isEnabled()).isFalse(); + assertThat(longHistogram.isEnabled()).isFalse(); + assertThat(doubleUpDownCounter.isEnabled()).isFalse(); + + doubleCounter.add(1.0); + longGauge.set(1L); + longHistogram.record(1L); + doubleUpDownCounter.add(1.0); + + assertThat(reader.collectAllMetrics()).isEmpty(); + } + + static final class InstrumentsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments( + ParameterDeclarations parameters, ExtensionContext context) { + return Stream.of( + // double counter + arguments( + (InstrumentFactory) + (meterProvider, name, optIn) -> { + ExtendedDoubleCounter counter = + (ExtendedDoubleCounter) + ((ExtendedDoubleCounterBuilder) + meterProvider.get("meter").counterBuilder(name).ofDoubles()) + .setOptInAdvice(optIn) + .build(); + return new TestInstrument() { + @Override + public void record(long value, Attributes attributes) { + counter.add((double) value, attributes); + } + + @Override + public boolean isEnabled() { + return counter.isEnabled(); + } + }; + }, + (PointsAssert) + (metricAssert, assertions) -> + metricAssert.hasDoubleSumSatisfying( + sum -> sum.hasPointsSatisfying(assertions))), + // long counter + arguments( + (InstrumentFactory) + (meterProvider, name, optIn) -> { + ExtendedLongCounter counter = + (ExtendedLongCounter) + ((ExtendedLongCounterBuilder) + meterProvider.get("meter").counterBuilder(name)) + .setOptInAdvice(optIn) + .build(); + return new TestInstrument() { + @Override + public void record(long value, Attributes attributes) { + counter.add(value, attributes); + } + + @Override + public boolean isEnabled() { + return counter.isEnabled(); + } + }; + }, + (PointsAssert) + (metricAssert, assertions) -> + metricAssert.hasLongSumSatisfying( + sum -> sum.hasPointsSatisfying(assertions))), + // double gauge + arguments( + (InstrumentFactory) + (meterProvider, name, optIn) -> { + ExtendedDoubleGauge gauge = + (ExtendedDoubleGauge) + ((ExtendedDoubleGaugeBuilder) + meterProvider.get("meter").gaugeBuilder(name)) + .setOptInAdvice(optIn) + .build(); + return new TestInstrument() { + @Override + public void record(long value, Attributes attributes) { + gauge.set((double) value, attributes); + } + + @Override + public boolean isEnabled() { + return gauge.isEnabled(); + } + }; + }, + (PointsAssert) + (metricAssert, assertions) -> + metricAssert.hasDoubleGaugeSatisfying( + sum -> sum.hasPointsSatisfying(assertions))), + // long gauge + arguments( + (InstrumentFactory) + (meterProvider, name, optIn) -> { + ExtendedLongGauge gauge = + (ExtendedLongGauge) + ((ExtendedLongGaugeBuilder) + meterProvider.get("meter").gaugeBuilder(name).ofLongs()) + .setOptInAdvice(optIn) + .build(); + return new TestInstrument() { + @Override + public void record(long value, Attributes attributes) { + gauge.set(value, attributes); + } + + @Override + public boolean isEnabled() { + return gauge.isEnabled(); + } + }; + }, + (PointsAssert) + (metricAssert, assertions) -> + metricAssert.hasLongGaugeSatisfying( + sum -> sum.hasPointsSatisfying(assertions))), + // double histogram + arguments( + (InstrumentFactory) + (meterProvider, name, optIn) -> { + ExtendedDoubleHistogram histogram = + (ExtendedDoubleHistogram) + ((ExtendedDoubleHistogramBuilder) + meterProvider.get("meter").histogramBuilder(name)) + .setOptInAdvice(optIn) + .build(); + return new TestInstrument() { + @Override + public void record(long value, Attributes attributes) { + histogram.record((double) value, attributes); + } + + @Override + public boolean isEnabled() { + return histogram.isEnabled(); + } + }; + }, + (PointsAssert) + (metricAssert, assertions) -> + metricAssert.hasHistogramSatisfying( + sum -> sum.hasPointsSatisfying(assertions))), + // long histogram + arguments( + (InstrumentFactory) + (meterProvider, name, optIn) -> { + ExtendedLongHistogram histogram = + (ExtendedLongHistogram) + ((ExtendedLongHistogramBuilder) + meterProvider.get("meter").histogramBuilder(name).ofLongs()) + .setOptInAdvice(optIn) + .build(); + return new TestInstrument() { + @Override + public void record(long value, Attributes attributes) { + histogram.record(value, attributes); + } + + @Override + public boolean isEnabled() { + return histogram.isEnabled(); + } + }; + }, + (PointsAssert) + (metricAssert, assertions) -> + metricAssert.hasHistogramSatisfying( + sum -> sum.hasPointsSatisfying(assertions))), + // double up down counter + arguments( + (InstrumentFactory) + (meterProvider, name, optIn) -> { + ExtendedDoubleUpDownCounter upDownCounter = + (ExtendedDoubleUpDownCounter) + ((ExtendedDoubleUpDownCounterBuilder) + meterProvider + .get("meter") + .upDownCounterBuilder(name) + .ofDoubles()) + .setOptInAdvice(optIn) + .build(); + return new TestInstrument() { + @Override + public void record(long value, Attributes attributes) { + upDownCounter.add((double) value, attributes); + } + + @Override + public boolean isEnabled() { + return upDownCounter.isEnabled(); + } + }; + }, + (PointsAssert) + (metricAssert, assertions) -> + metricAssert.hasDoubleSumSatisfying( + sum -> sum.hasPointsSatisfying(assertions))), + // long up down counter + arguments( + (InstrumentFactory) + (meterProvider, name, optIn) -> { + ExtendedLongUpDownCounter upDownCounter = + (ExtendedLongUpDownCounter) + ((ExtendedLongUpDownCounterBuilder) + meterProvider.get("meter").upDownCounterBuilder(name)) + .setOptInAdvice(optIn) + .build(); + return new TestInstrument() { + @Override + public void record(long value, Attributes attributes) { + upDownCounter.add(value, attributes); + } + + @Override + public boolean isEnabled() { + return upDownCounter.isEnabled(); + } + }; + }, + (PointsAssert) + (metricAssert, assertions) -> + metricAssert.hasLongSumSatisfying( + sum -> sum.hasPointsSatisfying(assertions)))); + } + } + + @FunctionalInterface + interface InstrumentFactory { + + TestInstrument create(SdkMeterProvider meterProvider, String name, boolean optIn); + } + + interface TestInstrument { + + void record(long value, Attributes attributes); + + boolean isEnabled(); + } + + @FunctionalInterface + interface PointsAssert

> { + + void hasPointSatisfying(MetricAssert metricAssert, Consumer

assertion); + } +}