From 71baf95d587c33243626423612588da3fccee690 Mon Sep 17 00:00:00 2001 From: navneet singh Date: Sun, 8 Nov 2026 12:11:34 +0530 Subject: [PATCH] feat(metrics): add composable metric views support Implement experimental composable view matching mode according to the OpenTelemetry Metrics SDK specification and declarative config schema. - Introduce ViewMatchingMode (INDEPENDENT, COMPOSABLE). - Track explicit View properties in ViewBuilder, View, and RegisteredView to distinguish configured fields from defaults. - Implement composable view resolution in ViewRegistry: stream grouping by configured name, last-wins attribute/cardinality merging, backward-scanning aggregation compatibility fallback, and attribute filter conjunction. - Add reflection bridge in SdkMeterProviderUtil to safely configure ViewMatchingMode on SdkMeterProviderBuilder without modifying public 1.x APIs. - Wire view_matching_mode/development in MeterProviderFactory for declarative configuration. - Add comprehensive unit and integration tests. Fixes #9013 --- .../MeterProviderFactory.java | 12 + .../MeterProviderFactoryTest.java | 8 + .../sdk/metrics/SdkMeterProvider.java | 11 +- .../sdk/metrics/SdkMeterProviderBuilder.java | 16 +- .../io/opentelemetry/sdk/metrics/View.java | 24 +- .../sdk/metrics/ViewBuilder.java | 31 +- .../internal/SdkMeterProviderUtil.java | 57 +++ .../metrics/internal/view/RegisteredView.java | 19 + .../internal/view/ViewMatchingMode.java | 24 + .../metrics/internal/view/ViewRegistry.java | 263 +++++++++- .../opentelemetry/sdk/metrics/ViewTest.java | 26 + .../internal/view/ViewRegistryTest.java | 474 ++++++++++++++++++ 12 files changed, 942 insertions(+), 23 deletions(-) create mode 100644 sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/view/ViewMatchingMode.java diff --git a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/MeterProviderFactory.java b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/MeterProviderFactory.java index 4eb8a061b01..0490fbaaa83 100644 --- a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/MeterProviderFactory.java +++ b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/MeterProviderFactory.java @@ -16,6 +16,7 @@ import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.internal.ExperimentalMeterConfigModel; import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.internal.ExperimentalMeterConfiguratorModel; import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.internal.ExperimentalMeterMatcherAndConfigModel; +import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.internal.ExperimentalViewMatchingModeModel; import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.internal.MeterProviderModelAccessor; import io.opentelemetry.sdk.common.internal.ScopeConfigurator; import io.opentelemetry.sdk.common.internal.ScopeConfiguratorBuilder; @@ -24,6 +25,7 @@ import io.opentelemetry.sdk.metrics.export.CardinalityLimitSelector; import io.opentelemetry.sdk.metrics.internal.MeterConfig; import io.opentelemetry.sdk.metrics.internal.SdkMeterProviderUtil; +import io.opentelemetry.sdk.metrics.internal.view.ViewMatchingMode; import java.util.List; final class MeterProviderFactory implements Factory { @@ -97,6 +99,16 @@ public SdkMeterProviderBuilder create( SdkMeterProviderUtil.setMeterConfigurator(builder, configuratorBuilder.build()); } + ExperimentalViewMatchingModeModel viewMatchingModeModel = + MeterProviderModelAccessor.getViewMatchingMode(model); + if (viewMatchingModeModel != null) { + ViewMatchingMode viewMatchingMode = + viewMatchingModeModel == ExperimentalViewMatchingModeModel.COMPOSABLE + ? ViewMatchingMode.COMPOSABLE + : ViewMatchingMode.INDEPENDENT; + SdkMeterProviderUtil.setViewMatchingMode(builder, viewMatchingMode); + } + ExemplarFilterModel exemplarFilterModel = model.getExemplarFilter(); if (exemplarFilterModel != null) { builder.setExemplarFilter( diff --git a/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/MeterProviderFactoryTest.java b/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/MeterProviderFactoryTest.java index 584bdfa22f4..81e1b4329c5 100644 --- a/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/MeterProviderFactoryTest.java +++ b/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/MeterProviderFactoryTest.java @@ -6,6 +6,7 @@ package io.opentelemetry.sdk.autoconfigure.declarativeconfig; import static io.opentelemetry.sdk.metrics.internal.SdkMeterProviderUtil.setMeterConfigurator; +import static io.opentelemetry.sdk.metrics.internal.SdkMeterProviderUtil.setViewMatchingMode; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import io.opentelemetry.common.ComponentLoader; @@ -23,6 +24,7 @@ import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.internal.ExperimentalMeterConfigModel; import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.internal.ExperimentalMeterConfiguratorModel; import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.internal.ExperimentalMeterMatcherAndConfigModel; +import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.internal.ExperimentalViewMatchingModeModel; import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.internal.MeterProviderModelAccessor; import io.opentelemetry.sdk.common.internal.ScopeConfigurator; import io.opentelemetry.sdk.common.internal.ScopeConfiguratorBuilder; @@ -32,6 +34,7 @@ import io.opentelemetry.sdk.metrics.View; import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; import io.opentelemetry.sdk.metrics.internal.MeterConfig; +import io.opentelemetry.sdk.metrics.internal.view.ViewMatchingMode; import java.io.Closeable; import java.util.ArrayList; import java.util.Collections; @@ -73,6 +76,11 @@ private static Stream createArguments() { return Stream.of( Arguments.argumentSet( "defaults", new MeterProviderModel(), SdkMeterProvider.builder().build()), + Arguments.argumentSet( + "with view matching mode composable", + MeterProviderModelAccessor.setViewMatchingMode( + new MeterProviderModel(), ExperimentalViewMatchingModeModel.COMPOSABLE), + setViewMatchingMode(SdkMeterProvider.builder(), ViewMatchingMode.COMPOSABLE).build()), Arguments.argumentSet( "with reader view and meter configurator", MeterProviderModelAccessor.setMeterConfigurator( diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProvider.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProvider.java index 3851fbe4ed8..a71640809cd 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProvider.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProvider.java @@ -26,6 +26,7 @@ import io.opentelemetry.sdk.metrics.internal.export.RegisteredReader; import io.opentelemetry.sdk.metrics.internal.state.MeterProviderSharedState; import io.opentelemetry.sdk.metrics.internal.view.RegisteredView; +import io.opentelemetry.sdk.metrics.internal.view.ViewMatchingMode; import io.opentelemetry.sdk.metrics.internal.view.ViewRegistry; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.resources.internal.SdkResourceProvider; @@ -52,6 +53,7 @@ public final class SdkMeterProvider implements MeterProvider, Closeable { static final String DEFAULT_METER_NAME = "unknown"; private final List registeredViews; + private final ViewMatchingMode viewMatchingMode; private final List registeredReaders; private final List metricProducers; private final MeterProviderSharedState sharedState; @@ -73,16 +75,19 @@ public static SdkMeterProviderBuilder builder() { Clock clock, SdkResourceProvider resourceProvider, ExemplarFilterInternal exemplarFilter, - ScopeConfigurator meterConfigurator) { + ScopeConfigurator meterConfigurator, + ViewMatchingMode viewMatchingMode) { this.resourceProvider = resourceProvider; this.registeredViews = registeredViews; + this.viewMatchingMode = viewMatchingMode; this.registeredReaders = metricReaders.entrySet().stream() .map( entry -> RegisteredReader.create( entry.getKey(), - ViewRegistry.create(entry.getKey(), entry.getValue(), registeredViews))) + ViewRegistry.create( + entry.getKey(), entry.getValue(), registeredViews, viewMatchingMode))) .collect(toList()); this.metricProducers = metricProducers; this.sharedState = MeterProviderSharedState.create(clock, resourceProvider, exemplarFilter); @@ -206,6 +211,8 @@ public String toString() { + metricProducers + ", views=" + registeredViews + + ", viewMatchingMode=" + + viewMatchingMode + ", meterConfigurator=" + meterConfigurator + "}"; 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..069f087dfbb 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 @@ -18,6 +18,7 @@ import io.opentelemetry.sdk.metrics.internal.exemplar.ExemplarFilterInternal; import io.opentelemetry.sdk.metrics.internal.view.AttributesFilters; import io.opentelemetry.sdk.metrics.internal.view.RegisteredView; +import io.opentelemetry.sdk.metrics.internal.view.ViewMatchingMode; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.resources.internal.SdkResourceProvider; import java.util.ArrayList; @@ -50,6 +51,7 @@ public final class SdkMeterProviderBuilder { new IdentityHashMap<>(); private final List metricProducers = new ArrayList<>(); private final List registeredViews = new ArrayList<>(); + private ViewMatchingMode viewMatchingMode = ViewMatchingMode.INDEPENDENT; private ExemplarFilterInternal exemplarFilter = DEFAULT_EXEMPLAR_FILTER; private ScopeConfiguratorBuilder meterConfiguratorBuilder = MeterConfig.configuratorBuilder(); @@ -240,6 +242,17 @@ SdkMeterProviderBuilder addMeterConfiguratorCondition( return this; } + /** + * Set the view matching mode. + * + *

This method is experimental so not public. You may reflectively call it using {@link + * SdkMeterProviderUtil#setViewMatchingMode(SdkMeterProviderBuilder, ViewMatchingMode)}. + */ + SdkMeterProviderBuilder setViewMatchingMode(ViewMatchingMode viewMatchingMode) { + this.viewMatchingMode = Objects.requireNonNull(viewMatchingMode, "viewMatchingMode"); + return this; + } + /** Returns an {@link SdkMeterProvider} built with the configuration of this builder. */ public SdkMeterProvider build() { SdkResourceProvider effectiveSdkResourceProvider = @@ -251,6 +264,7 @@ public SdkMeterProvider build() { clock, effectiveSdkResourceProvider, exemplarFilter, - meterConfiguratorBuilder.build()); + meterConfiguratorBuilder.build(), + viewMatchingMode); } } 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..d9ec7175cac 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,19 @@ static View create( @Nullable String description, Aggregation aggregation, Predicate attributeFilter, - int cardinalityLimit) { - return new AutoValue_View(name, description, aggregation, attributeFilter, cardinalityLimit); + int cardinalityLimit, + @Nullable Aggregation explicitAggregation, + @Nullable Predicate explicitAttributeFilter, + @Nullable Integer explicitCardinalityLimit) { + return new AutoValue_View( + name, + description, + aggregation, + attributeFilter, + cardinalityLimit, + explicitAggregation, + explicitAttributeFilter, + explicitCardinalityLimit); } View() {} @@ -71,6 +82,15 @@ static View create( */ public abstract int getCardinalityLimit(); + @Nullable + abstract Aggregation getExplicitAggregation(); + + @Nullable + abstract Predicate getExplicitAttributeFilter(); + + @Nullable + abstract Integer getExplicitCardinalityLimit(); + @Override public final String toString() { StringJoiner joiner = new StringJoiner(", ", "View{", "}"); 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..4e35945121e 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 @@ -25,9 +25,9 @@ public final class ViewBuilder { @Nullable private String name; @Nullable private String description; - private Aggregation aggregation = Aggregation.defaultAggregation(); - private Predicate attributeFilter = StringPredicates.ALL; - private int cardinalityLimit = MetricStorage.DEFAULT_MAX_CARDINALITY; + @Nullable private Aggregation explicitAggregation; + @Nullable private Predicate explicitAttributeFilter; + @Nullable private Integer explicitCardinalityLimit; ViewBuilder() {} @@ -58,12 +58,13 @@ public ViewBuilder setDescription(String description) { * @param aggregation aggregation to use. */ public ViewBuilder setAggregation(Aggregation aggregation) { + Objects.requireNonNull(aggregation, "aggregation"); if (!(aggregation instanceof AggregatorFactory)) { throw new IllegalArgumentException( "Custom Aggregation implementations are currently not supported. " + "Use one of the standard implementations returned by the static factories in the Aggregation class."); } - this.aggregation = aggregation; + this.explicitAggregation = aggregation; return this; } @@ -104,7 +105,7 @@ public ViewBuilder setAttributeFilter(Set keysToRetain) { */ public ViewBuilder setAttributeFilter(Predicate keyFilter) { Objects.requireNonNull(keyFilter, "keyFilter"); - this.attributeFilter = keyFilter; + this.explicitAttributeFilter = keyFilter; return this; } @@ -121,12 +122,28 @@ public ViewBuilder setCardinalityLimit(int cardinalityLimit) { if (cardinalityLimit <= 0) { throw new IllegalArgumentException("cardinalityLimit must be > 0"); } - this.cardinalityLimit = cardinalityLimit; + this.explicitCardinalityLimit = cardinalityLimit; return this; } /** Returns a {@link View} with the configuration of this builder. */ public View build() { - return View.create(name, description, aggregation, attributeFilter, cardinalityLimit); + Aggregation aggregation = + explicitAggregation != null ? explicitAggregation : Aggregation.defaultAggregation(); + Predicate attributeFilter = + explicitAttributeFilter != null ? explicitAttributeFilter : StringPredicates.ALL; + int cardinalityLimit = + explicitCardinalityLimit != null + ? explicitCardinalityLimit + : MetricStorage.DEFAULT_MAX_CARDINALITY; + return View.create( + name, + description, + aggregation, + attributeFilter, + cardinalityLimit, + explicitAggregation, + explicitAttributeFilter, + explicitCardinalityLimit); } } 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..1180c7254ec 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 @@ -7,12 +7,16 @@ import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.common.internal.ScopeConfigurator; +import io.opentelemetry.sdk.metrics.Aggregation; import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; +import io.opentelemetry.sdk.metrics.View; +import io.opentelemetry.sdk.metrics.internal.view.ViewMatchingMode; import io.opentelemetry.sdk.resources.internal.SdkResourceProvider; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.function.Predicate; +import javax.annotation.Nullable; /** * A collection of methods that allow use of experimental features prior to availability in public @@ -111,4 +115,57 @@ public static void resetForTest(SdkMeterProvider sdkMeterProvider) { throw new IllegalStateException("Error calling resetForTest on SdkMeterProvider", e); } } + + /** Reflectively set the {@link ViewMatchingMode} on the {@link SdkMeterProviderBuilder}. */ + public static SdkMeterProviderBuilder setViewMatchingMode( + SdkMeterProviderBuilder sdkMeterProviderBuilder, ViewMatchingMode viewMatchingMode) { + try { + Method method = + SdkMeterProviderBuilder.class.getDeclaredMethod( + "setViewMatchingMode", ViewMatchingMode.class); + method.setAccessible(true); + method.invoke(sdkMeterProviderBuilder, viewMatchingMode); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException( + "Error calling setViewMatchingMode on SdkMeterProviderBuilder", e); + } + return sdkMeterProviderBuilder; + } + + /** Reflectively get the explicit aggregation configured on a {@link View}. */ + @Nullable + public static Aggregation getExplicitAggregation(View view) { + try { + Method method = View.class.getDeclaredMethod("getExplicitAggregation"); + method.setAccessible(true); + return (Aggregation) method.invoke(view); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException("Error calling getExplicitAggregation on View", e); + } + } + + /** Reflectively get the explicit attribute filter configured on a {@link View}. */ + @Nullable + @SuppressWarnings("unchecked") + public static Predicate getExplicitAttributeFilter(View view) { + try { + Method method = View.class.getDeclaredMethod("getExplicitAttributeFilter"); + method.setAccessible(true); + return (Predicate) method.invoke(view); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException("Error calling getExplicitAttributeFilter on View", e); + } + } + + /** Reflectively get the explicit cardinality limit configured on a {@link View}. */ + @Nullable + public static Integer getExplicitCardinalityLimit(View view) { + try { + Method method = View.class.getDeclaredMethod("getExplicitCardinalityLimit"); + method.setAccessible(true); + return (Integer) method.invoke(view); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException("Error calling getExplicitCardinalityLimit on View", e); + } + } } 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..e928d2516cc 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 @@ -7,10 +7,14 @@ import com.google.auto.value.AutoValue; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.metrics.Aggregation; import io.opentelemetry.sdk.metrics.InstrumentSelector; import io.opentelemetry.sdk.metrics.View; +import io.opentelemetry.sdk.metrics.internal.SdkMeterProviderUtil; import io.opentelemetry.sdk.metrics.internal.debug.SourceInfo; +import java.util.function.Predicate; import java.util.function.UnaryOperator; +import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; /** @@ -54,6 +58,21 @@ public static RegisteredView create( /** The {@link SourceInfo} from where the view was registered. */ public abstract SourceInfo getViewSourceInfo(); + @Nullable + public Aggregation getExplicitAggregation() { + return SdkMeterProviderUtil.getExplicitAggregation(getView()); + } + + @Nullable + public Predicate getExplicitAttributeFilter() { + return SdkMeterProviderUtil.getExplicitAttributeFilter(getView()); + } + + @Nullable + public Integer getExplicitCardinalityLimit() { + return SdkMeterProviderUtil.getExplicitCardinalityLimit(getView()); + } + @Override public final String toString() { return "RegisteredView{" diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/view/ViewMatchingMode.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/view/ViewMatchingMode.java new file mode 100644 index 00000000000..374cbde436a --- /dev/null +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/view/ViewMatchingMode.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.internal.view; + +/** + * Controls how multiple matching {@link io.opentelemetry.sdk.metrics.View}s are applied to an + * instrument. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public enum ViewMatchingMode { + /** + * Each matching view creates a separate metric stream independently of any other views registered + * for the same matching instrument. + */ + INDEPENDENT, + + /** Matching views are combined (merged) to modify metric streams rather than duplicating them. */ + COMPOSABLE +} 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..16db7dc5583 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 @@ -7,6 +7,7 @@ import static java.util.Objects.requireNonNull; +import io.opentelemetry.api.common.Attributes; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.common.internal.GlobUtil; import io.opentelemetry.sdk.metrics.Aggregation; @@ -14,6 +15,7 @@ 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.aggregator.AggregationUtil; @@ -25,10 +27,14 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; import java.util.logging.Level; import java.util.logging.Logger; +import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; /** @@ -50,36 +56,77 @@ public final class ViewRegistry { SourceInfo.noSourceInfo()); private static final Logger logger = Logger.getLogger(ViewRegistry.class.getName()); + private final DefaultAggregationSelector defaultAggregationSelector; + private final CardinalityLimitSelector cardinalityLimitSelector; + private final ViewMatchingMode viewMatchingMode; private final Map instrumentDefaultRegisteredView; private final List registeredViews; ViewRegistry( DefaultAggregationSelector defaultAggregationSelector, CardinalityLimitSelector cardinalityLimitSelector, - List registeredViews) { - instrumentDefaultRegisteredView = new HashMap<>(); + List registeredViews, + ViewMatchingMode viewMatchingMode) { + this.defaultAggregationSelector = defaultAggregationSelector; + this.cardinalityLimitSelector = cardinalityLimitSelector; + this.viewMatchingMode = viewMatchingMode; + this.instrumentDefaultRegisteredView = new HashMap<>(); for (InstrumentType instrumentType : InstrumentType.values()) { + Aggregation defaultAgg = defaultAggregationSelector.getDefaultAggregation(instrumentType); + ViewBuilder vb = View.builder(); + if (!defaultAgg.equals(Aggregation.defaultAggregation())) { + vb.setAggregation(defaultAgg); + } + int cardinalityLimit = cardinalityLimitSelector.getCardinalityLimit(instrumentType); + if (cardinalityLimit != MetricStorage.DEFAULT_MAX_CARDINALITY) { + vb.setCardinalityLimit(cardinalityLimit); + } instrumentDefaultRegisteredView.put( instrumentType, RegisteredView.create( InstrumentSelector.builder().setName("*").build(), - View.builder() - .setAggregation(defaultAggregationSelector.getDefaultAggregation(instrumentType)) - .build(), + vb.build(), AttributesFilters.ALLOW_ALL, - cardinalityLimitSelector.getCardinalityLimit(instrumentType), + cardinalityLimit, SourceInfo.noSourceInfo())); } this.registeredViews = registeredViews; } + ViewRegistry( + DefaultAggregationSelector defaultAggregationSelector, + CardinalityLimitSelector cardinalityLimitSelector, + List registeredViews) { + this( + defaultAggregationSelector, + cardinalityLimitSelector, + registeredViews, + ViewMatchingMode.INDEPENDENT); + } + /** Returns a {@link ViewRegistry}. */ public static ViewRegistry create( DefaultAggregationSelector defaultAggregationSelector, CardinalityLimitSelector cardinalityLimitSelector, - List registeredViews) { + List registeredViews, + ViewMatchingMode viewMatchingMode) { return new ViewRegistry( - defaultAggregationSelector, cardinalityLimitSelector, new ArrayList<>(registeredViews)); + defaultAggregationSelector, + cardinalityLimitSelector, + new ArrayList<>(registeredViews), + viewMatchingMode); + } + + /** Returns a {@link ViewRegistry}. */ + public static ViewRegistry create( + DefaultAggregationSelector defaultAggregationSelector, + CardinalityLimitSelector cardinalityLimitSelector, + List registeredViews) { + return create( + defaultAggregationSelector, + cardinalityLimitSelector, + registeredViews, + ViewMatchingMode.INDEPENDENT); } /** Return a {@link ViewRegistry} using the default aggregation and no views registered. */ @@ -87,7 +134,8 @@ public static ViewRegistry create() { return create( unused -> Aggregation.defaultAggregation(), CardinalityLimitSelector.defaultCardinalityLimitSelector(), - Collections.emptyList()); + Collections.emptyList(), + ViewMatchingMode.INDEPENDENT); } /** @@ -98,6 +146,14 @@ public static ViewRegistry create() { */ public List findViews( InstrumentDescriptor descriptor, InstrumentationScopeInfo meterScope) { + if (viewMatchingMode == ViewMatchingMode.COMPOSABLE) { + return findComposableViews(descriptor, meterScope); + } + return findIndependentViews(descriptor, meterScope); + } + + private List findIndependentViews( + InstrumentDescriptor descriptor, InstrumentationScopeInfo meterScope) { List result = new ArrayList<>(); // Find matching views for the instrument for (RegisteredView entry : registeredViews) { @@ -124,7 +180,192 @@ public List findViews( return Collections.unmodifiableList(result); } - // No views matched, use default view + return Collections.singletonList(getDefaultRegisteredView(descriptor)); + } + + private List findComposableViews( + InstrumentDescriptor descriptor, InstrumentationScopeInfo meterScope) { + List matchingViews = new ArrayList<>(); + for (RegisteredView entry : registeredViews) { + if (matchesSelector(entry.getInstrumentSelector(), descriptor, meterScope)) { + matchingViews.add(entry); + } + } + + if (matchingViews.isEmpty()) { + return Collections.singletonList(getDefaultRegisteredView(descriptor)); + } + + // Collect distinct configured stream names (preserving order of appearance) + LinkedHashSet configuredNames = new LinkedHashSet<>(); + for (RegisteredView entry : matchingViews) { + String viewName = entry.getView().getName(); + if (viewName != null) { + configuredNames.add(viewName); + } + } + + List result = new ArrayList<>(); + + if (configuredNames.isEmpty()) { + // No matching View configures a stream name: single group using instrument's default name + result.add(composeStreamGroup(descriptor, null, matchingViews)); + } else { + for (String configuredName : configuredNames) { + List group = new ArrayList<>(); + for (RegisteredView entry : matchingViews) { + String viewName = entry.getView().getName(); + if (viewName == null || viewName.equals(configuredName)) { + group.add(entry); + } + } + result.add(composeStreamGroup(descriptor, configuredName, group)); + } + } + + return Collections.unmodifiableList(result); + } + + private RegisteredView composeStreamGroup( + InstrumentDescriptor descriptor, @Nullable String targetName, List group) { + String effectiveDescription = resolveDescription(descriptor, group); + int effectiveCardinalityLimit = + resolveCardinalityLimit(descriptor, group, cardinalityLimitSelector); + Aggregation effectiveAggregation = + resolveAggregation(descriptor, group, defaultAggregationSelector); + AttributeFilterComposition filterComposition = composeAttributeFilter(descriptor, group); + + RegisteredView lastEntry = group.get(group.size() - 1); + ViewBuilder viewBuilder = View.builder(); + if (targetName != null) { + viewBuilder.setName(targetName); + } + if (effectiveDescription != null) { + viewBuilder.setDescription(effectiveDescription); + } + viewBuilder.setAggregation(effectiveAggregation); + if (filterComposition.explicitPredicate != null) { + viewBuilder.setAttributeFilter(filterComposition.explicitPredicate); + } + viewBuilder.setCardinalityLimit(effectiveCardinalityLimit); + View composedView = viewBuilder.build(); + + return RegisteredView.create( + lastEntry.getInstrumentSelector(), + composedView, + filterComposition.attributesFilter, + effectiveCardinalityLimit, + lastEntry.getViewSourceInfo()); + } + + private static String resolveDescription( + InstrumentDescriptor descriptor, List group) { + String effectiveDescription = descriptor.getDescription(); + for (RegisteredView entry : group) { + String desc = entry.getView().getDescription(); + if (desc != null) { + effectiveDescription = desc; + } + } + return effectiveDescription; + } + + private static int resolveCardinalityLimit( + InstrumentDescriptor descriptor, + List group, + CardinalityLimitSelector cardinalityLimitSelector) { + int effectiveCardinalityLimit = + cardinalityLimitSelector.getCardinalityLimit(descriptor.getType()); + for (RegisteredView entry : group) { + Integer limit = entry.getExplicitCardinalityLimit(); + if (limit != null) { + effectiveCardinalityLimit = limit; + } + } + return effectiveCardinalityLimit; + } + + private static Aggregation resolveAggregation( + InstrumentDescriptor descriptor, + List group, + DefaultAggregationSelector defaultAggregationSelector) { + Aggregation effectiveAggregation = null; + for (int i = group.size() - 1; i >= 0; i--) { + RegisteredView entry = group.get(i); + Aggregation agg = entry.getExplicitAggregation(); + if (agg != null) { + AggregatorFactory factory = (AggregatorFactory) agg; + if (factory.isCompatibleWithInstrument(descriptor)) { + effectiveAggregation = agg; + break; + } else { + logger.log( + Level.WARNING, + "View aggregation " + + AggregationUtil.aggregationName(agg) + + " is incompatible with instrument " + + descriptor.getName() + + " of type " + + descriptor.getType()); + } + } + } + + if (effectiveAggregation == null) { + Aggregation defaultAgg = + defaultAggregationSelector.getDefaultAggregation(descriptor.getType()); + AggregatorFactory defaultFactory = (AggregatorFactory) defaultAgg; + if (!defaultFactory.isCompatibleWithInstrument(descriptor)) { + logger.log( + Level.WARNING, + "Instrument default aggregation " + + AggregationUtil.aggregationName(defaultAgg) + + " is incompatible with instrument " + + descriptor.getName() + + " of type " + + descriptor.getType()); + effectiveAggregation = DEFAULT_VIEW.getAggregation(); + } else { + effectiveAggregation = defaultAgg; + } + } + return effectiveAggregation; + } + + private static AttributeFilterComposition composeAttributeFilter( + InstrumentDescriptor descriptor, List group) { + Predicate composedPredicate = null; + for (RegisteredView entry : group) { + Predicate explicitFilter = entry.getExplicitAttributeFilter(); + if (explicitFilter != null) { + composedPredicate = + composedPredicate == null ? explicitFilter : composedPredicate.and(explicitFilter); + } + } + + if (composedPredicate != null) { + return new AttributeFilterComposition( + composedPredicate, AttributesFilters.byKeyName(composedPredicate)); + } + if (descriptor.getAdvice().hasAttributes()) { + return new AttributeFilterComposition( + null, AttributesFilters.byAdvice(requireNonNull(descriptor.getAdvice().getAttributes()))); + } + return new AttributeFilterComposition(null, AttributesFilters.ALLOW_ALL); + } + + private static final class AttributeFilterComposition { + @Nullable private final Predicate explicitPredicate; + private final UnaryOperator attributesFilter; + + AttributeFilterComposition( + @Nullable Predicate explicitPredicate, UnaryOperator attributesFilter) { + this.explicitPredicate = explicitPredicate; + this.attributesFilter = attributesFilter; + } + } + + private RegisteredView getDefaultRegisteredView(InstrumentDescriptor descriptor) { RegisteredView instrumentDefaultView = requireNonNull(instrumentDefaultRegisteredView.get(descriptor.getType())); @@ -151,7 +392,7 @@ public List findViews( applyAdviceToDefaultView(instrumentDefaultView, descriptor.getAdvice()); } - return Collections.singletonList(instrumentDefaultView); + return instrumentDefaultView; } // Matches an instrument selector against an instrument + meter. 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..a69ceafdf07 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.view.StringPredicates; import java.util.Arrays; import java.util.HashSet; import org.junit.jupiter.api.Test; @@ -36,4 +37,29 @@ void stringRepresentation() { + "cardinalityLimit=10" + "}"); } + + @Test + void equalsAndHashCode() { + View defaultView = View.builder().build(); + View defaultViewCopy = View.builder().build(); + View explicitDefaultAggregation = + View.builder().setAggregation(Aggregation.defaultAggregation()).build(); + View explicitDefaultCardinality = View.builder().setCardinalityLimit(2000).build(); + View explicitDefaultFilter = View.builder().setAttributeFilter(StringPredicates.ALL).build(); + + assertThat(defaultView) + .isEqualTo(defaultViewCopy) + .hasSameHashCodeAs(defaultViewCopy) + .isNotEqualTo(explicitDefaultAggregation) + .isNotEqualTo(explicitDefaultCardinality) + .isNotEqualTo(explicitDefaultFilter); + + assertThat(explicitDefaultAggregation.hashCode()).isNotEqualTo(defaultView.hashCode()); + assertThat(explicitDefaultCardinality.hashCode()).isNotEqualTo(defaultView.hashCode()); + assertThat(explicitDefaultFilter.hashCode()).isNotEqualTo(defaultView.hashCode()); + + View sum1 = View.builder().setAggregation(Aggregation.sum()).build(); + View sum2 = View.builder().setAggregation(Aggregation.sum()).build(); + assertThat(sum1).isEqualTo(sum2).hasSameHashCodeAs(sum2); + } } 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..03fcdb69a5a 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 @@ -28,6 +28,8 @@ import io.opentelemetry.sdk.metrics.internal.state.MetricStorage; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; +import java.util.List; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -538,4 +540,476 @@ private static void assertAdviceFilter(RegisteredView view, AttributeKey... e .asInstanceOf(InstanceOfAssertFactories.collection(AttributeKey.class)) .containsExactlyInAnyOrder(expectedKeys); } + + @Test + void findComposableViews_SingleMatchingView() { + RegisteredView registeredView = + registeredView( + InstrumentSelector.builder().setName("my_counter").build(), + View.builder().setDescription("custom description").build()); + ViewRegistry viewRegistry = + ViewRegistry.create( + DefaultAggregationSelector.getDefault(), + CardinalityLimitSelector.defaultCardinalityLimitSelector(), + Collections.singletonList(registeredView), + ViewMatchingMode.COMPOSABLE); + + List views = + viewRegistry.findViews( + InstrumentDescriptor.create( + "my_counter", + "desc", + "1", + InstrumentType.COUNTER, + InstrumentValueType.LONG, + Advice.empty()), + INSTRUMENTATION_SCOPE_INFO); + + assertThat(views).hasSize(1); + assertThat(views.get(0).getView().getDescription()).isEqualTo("custom description"); + assertThat(views.get(0).getView().getAggregation()).isEqualTo(Aggregation.defaultAggregation()); + } + + @Test + void findComposableViews_LastWinsPrecedence() { + RegisteredView view1 = + registeredView( + InstrumentSelector.builder().setName("my_counter").build(), + View.builder() + .setDescription("description 1") + .setCardinalityLimit(100) + .setAggregation(Aggregation.sum()) + .build()); + RegisteredView view2 = + registeredView( + InstrumentSelector.builder().setName("my_counter").build(), + View.builder().setDescription("description 2").setCardinalityLimit(500).build()); + + ViewRegistry viewRegistry = + ViewRegistry.create( + DefaultAggregationSelector.getDefault(), + CardinalityLimitSelector.defaultCardinalityLimitSelector(), + Arrays.asList(view1, view2), + ViewMatchingMode.COMPOSABLE); + + List views = + viewRegistry.findViews( + InstrumentDescriptor.create( + "my_counter", + "desc", + "1", + InstrumentType.COUNTER, + InstrumentValueType.LONG, + Advice.empty()), + INSTRUMENTATION_SCOPE_INFO); + + assertThat(views).hasSize(1); + assertThat(views.get(0).getView().getDescription()).isEqualTo("description 2"); + assertThat(views.get(0).getCardinalityLimit()).isEqualTo(500); + assertThat(views.get(0).getView().getAggregation()).isEqualTo(Aggregation.sum()); + } + + @Test + void findComposableViews_AttributeFilterConjunction() { + RegisteredView policyView = + registeredView( + InstrumentSelector.builder().setName("*").build(), + View.builder() + .setAttributeFilter(new HashSet<>(Arrays.asList("keep1", "keep2", "keep3"))) + .build()); + RegisteredView serviceView = + registeredView( + InstrumentSelector.builder().setName("my_counter").build(), + View.builder() + .setAttributeFilter(new HashSet<>(Arrays.asList("keep1", "keep2"))) + .build()); + + ViewRegistry viewRegistry = + ViewRegistry.create( + DefaultAggregationSelector.getDefault(), + CardinalityLimitSelector.defaultCardinalityLimitSelector(), + Arrays.asList(policyView, serviceView), + ViewMatchingMode.COMPOSABLE); + + List views = + viewRegistry.findViews( + InstrumentDescriptor.create( + "my_counter", + "desc", + "1", + InstrumentType.COUNTER, + InstrumentValueType.LONG, + Advice.empty()), + INSTRUMENTATION_SCOPE_INFO); + + assertThat(views).hasSize(1); + Attributes attributes = + Attributes.builder() + .put("keep1", "v1") + .put("keep2", "v2") + .put("keep3", "v3") + .put("drop", "v4") + .build(); + Attributes filtered = views.get(0).getAttributesFilter().apply(attributes); + assertThat(filtered.asMap()).containsOnlyKeys(stringKey("keep1"), stringKey("keep2")); + } + + @Test + void findComposableViews_AttributeFilterOverridesAdvice() { + RegisteredView view = + registeredView( + InstrumentSelector.builder().setName("my_counter").build(), + View.builder().setAttributeFilter(Collections.singleton("custom_key")).build()); + + ViewRegistry viewRegistry = + ViewRegistry.create( + DefaultAggregationSelector.getDefault(), + CardinalityLimitSelector.defaultCardinalityLimitSelector(), + Collections.singletonList(view), + ViewMatchingMode.COMPOSABLE); + + List views = + viewRegistry.findViews( + InstrumentDescriptor.create( + "my_counter", + "desc", + "1", + InstrumentType.COUNTER, + InstrumentValueType.LONG, + Advice.builder() + .setAttributes(Collections.singletonList(stringKey("advice_key"))) + .build()), + INSTRUMENTATION_SCOPE_INFO); + + assertThat(views).hasSize(1); + Attributes attributes = + Attributes.builder().put("custom_key", "v1").put("advice_key", "v2").build(); + Attributes filtered = views.get(0).getAttributesFilter().apply(attributes); + assertThat(filtered.asMap()).containsOnlyKeys(stringKey("custom_key")); + } + + @Test + void findComposableViews_UnspecifiedAttributeFilterPreservesAdvice() { + RegisteredView view = + registeredView( + InstrumentSelector.builder().setName("my_counter").build(), + View.builder().setDescription("custom description").build()); + + ViewRegistry viewRegistry = + ViewRegistry.create( + DefaultAggregationSelector.getDefault(), + CardinalityLimitSelector.defaultCardinalityLimitSelector(), + Collections.singletonList(view), + ViewMatchingMode.COMPOSABLE); + + List views = + viewRegistry.findViews( + InstrumentDescriptor.create( + "my_counter", + "desc", + "1", + InstrumentType.COUNTER, + InstrumentValueType.LONG, + Advice.builder() + .setAttributes(Collections.singletonList(stringKey("advice_key"))) + .build()), + INSTRUMENTATION_SCOPE_INFO); + + assertThat(views).hasSize(1); + Attributes attributes = + Attributes.builder().put("advice_key", "v1").put("other_key", "v2").build(); + Attributes filtered = views.get(0).getAttributesFilter().apply(attributes); + assertThat(filtered.asMap()).containsOnlyKeys(stringKey("advice_key")); + } + + @Test + void findComposableViews_SemanticIncompatibilityFallback() { + RegisteredView validView = + registeredView( + InstrumentSelector.builder().setName("my_counter").build(), + View.builder().setAggregation(Aggregation.sum()).build()); + RegisteredView invalidView = + registeredView( + InstrumentSelector.builder().setName("my_counter").build(), + View.builder().setAggregation(Aggregation.lastValue()).build()); + + ViewRegistry viewRegistry = + ViewRegistry.create( + DefaultAggregationSelector.getDefault(), + CardinalityLimitSelector.defaultCardinalityLimitSelector(), + Arrays.asList(validView, invalidView), + ViewMatchingMode.COMPOSABLE); + + List views = + viewRegistry.findViews( + InstrumentDescriptor.create( + "my_counter", + "desc", + "1", + InstrumentType.COUNTER, + InstrumentValueType.LONG, + Advice.empty()), + INSTRUMENTATION_SCOPE_INFO); + + assertThat(views).hasSize(1); + assertThat(views.get(0).getView().getAggregation()).isEqualTo(Aggregation.sum()); + assertThat(logs.getEvents()).hasSize(1); + assertThat(logs.getEvents().get(0).getMessage()) + .contains( + "View aggregation last_value is incompatible with instrument my_counter of type COUNTER"); + } + + @Test + void findComposableViews_DistinctConfiguredStreamNames() { + RegisteredView globalPolicy = + registeredView( + InstrumentSelector.builder().setName("*").build(), + View.builder().setAttributeFilter(Collections.singleton("allowed_key")).build()); + RegisteredView streamA = + registeredView( + InstrumentSelector.builder().setName("my_histogram").build(), + View.builder().setName("histogram.stream_a").setAggregation(Aggregation.sum()).build()); + RegisteredView streamB = + registeredView( + InstrumentSelector.builder().setName("my_histogram").build(), + View.builder() + .setName("histogram.stream_b") + .setAggregation(Aggregation.explicitBucketHistogram()) + .build()); + + ViewRegistry viewRegistry = + ViewRegistry.create( + DefaultAggregationSelector.getDefault(), + CardinalityLimitSelector.defaultCardinalityLimitSelector(), + Arrays.asList(globalPolicy, streamA, streamB), + ViewMatchingMode.COMPOSABLE); + + List views = + viewRegistry.findViews( + InstrumentDescriptor.create( + "my_histogram", + "desc", + "ms", + InstrumentType.HISTOGRAM, + InstrumentValueType.DOUBLE, + Advice.empty()), + INSTRUMENTATION_SCOPE_INFO); + + assertThat(views).hasSize(2); + RegisteredView resA = + views.stream() + .filter(v -> "histogram.stream_a".equals(v.getView().getName())) + .findFirst() + .get(); + RegisteredView resB = + views.stream() + .filter(v -> "histogram.stream_b".equals(v.getView().getName())) + .findFirst() + .get(); + + assertThat(resA.getView().getAggregation()).isEqualTo(Aggregation.sum()); + assertThat(resB.getView().getAggregation()).isEqualTo(Aggregation.explicitBucketHistogram()); + + Attributes attrs = + Attributes.builder().put("allowed_key", "v1").put("dropped_key", "v2").build(); + assertThat(resA.getAttributesFilter().apply(attrs).asMap()) + .containsOnlyKeys(stringKey("allowed_key")); + assertThat(resB.getAttributesFilter().apply(attrs).asMap()) + .containsOnlyKeys(stringKey("allowed_key")); + } + + @Test + void findComposableViews_ExplicitDefaultAggregationResetsPreceding() { + RegisteredView view1 = + registeredView( + InstrumentSelector.builder().setName("my_histogram").build(), + View.builder().setAggregation(Aggregation.base2ExponentialBucketHistogram()).build()); + RegisteredView view2ExplicitDefault = + registeredView( + InstrumentSelector.builder().setName("my_histogram").build(), + View.builder().setAggregation(Aggregation.defaultAggregation()).build()); + + InstrumentDescriptor histogramDesc = + InstrumentDescriptor.create( + "my_histogram", + "desc", + "ms", + InstrumentType.HISTOGRAM, + InstrumentValueType.DOUBLE, + Advice.empty()); + + ViewRegistry registryWithExplicitDefault = + ViewRegistry.create( + DefaultAggregationSelector.getDefault(), + CardinalityLimitSelector.defaultCardinalityLimitSelector(), + Arrays.asList(view1, view2ExplicitDefault), + ViewMatchingMode.COMPOSABLE); + + List views = + registryWithExplicitDefault.findViews(histogramDesc, INSTRUMENTATION_SCOPE_INFO); + assertThat(views).hasSize(1); + assertThat(views.get(0).getView().getAggregation()).isEqualTo(Aggregation.defaultAggregation()); + + RegisteredView view2Unspecified = + registeredView( + InstrumentSelector.builder().setName("my_histogram").build(), + View.builder().setDescription("updated description").build()); + + ViewRegistry registryWithUnspecified = + ViewRegistry.create( + DefaultAggregationSelector.getDefault(), + CardinalityLimitSelector.defaultCardinalityLimitSelector(), + Arrays.asList(view1, view2Unspecified), + ViewMatchingMode.COMPOSABLE); + + List viewsUnspecified = + registryWithUnspecified.findViews(histogramDesc, INSTRUMENTATION_SCOPE_INFO); + assertThat(viewsUnspecified).hasSize(1); + assertThat(viewsUnspecified.get(0).getView().getAggregation()) + .isEqualTo(Aggregation.base2ExponentialBucketHistogram()); + assertThat(viewsUnspecified.get(0).getView().getDescription()).isEqualTo("updated description"); + } + + @Test + void findComposableViews_ExplicitDefaultCardinalityResetsPreceding() { + RegisteredView view1 = + registeredView( + InstrumentSelector.builder().setName("my_counter").build(), + View.builder().setCardinalityLimit(100).build()); + RegisteredView view2ExplicitDefault = + registeredView( + InstrumentSelector.builder().setName("my_counter").build(), + View.builder().setCardinalityLimit(2000).build()); + + InstrumentDescriptor counterDesc = + InstrumentDescriptor.create( + "my_counter", + "desc", + "1", + InstrumentType.COUNTER, + InstrumentValueType.LONG, + Advice.empty()); + + ViewRegistry registry = + ViewRegistry.create( + DefaultAggregationSelector.getDefault(), + CardinalityLimitSelector.defaultCardinalityLimitSelector(), + Arrays.asList(view1, view2ExplicitDefault), + ViewMatchingMode.COMPOSABLE); + + List views = registry.findViews(counterDesc, INSTRUMENTATION_SCOPE_INFO); + assertThat(views).hasSize(1); + assertThat(views.get(0).getCardinalityLimit()).isEqualTo(2000); + + RegisteredView view2Unspecified = + registeredView( + InstrumentSelector.builder().setName("my_counter").build(), + View.builder().setDescription("desc2").build()); + + ViewRegistry registryUnspecified = + ViewRegistry.create( + DefaultAggregationSelector.getDefault(), + CardinalityLimitSelector.defaultCardinalityLimitSelector(), + Arrays.asList(view1, view2Unspecified), + ViewMatchingMode.COMPOSABLE); + + List viewsUnspecified = + registryUnspecified.findViews(counterDesc, INSTRUMENTATION_SCOPE_INFO); + assertThat(viewsUnspecified).hasSize(1); + assertThat(viewsUnspecified.get(0).getCardinalityLimit()).isEqualTo(100); + } + + @Test + void findComposableViews_AllConfiguredAggregationsIncompatible() { + RegisteredView view1 = + registeredView( + InstrumentSelector.builder().setName("my_counter").build(), + View.builder().setAggregation(Aggregation.lastValue()).build()); + RegisteredView view2 = + registeredView( + InstrumentSelector.builder().setName("my_counter").build(), + View.builder().setAggregation(Aggregation.lastValue()).build()); + + ViewRegistry viewRegistry = + ViewRegistry.create( + DefaultAggregationSelector.getDefault(), + CardinalityLimitSelector.defaultCardinalityLimitSelector(), + Arrays.asList(view1, view2), + ViewMatchingMode.COMPOSABLE); + + List views = + viewRegistry.findViews( + InstrumentDescriptor.create( + "my_counter", + "desc", + "1", + InstrumentType.COUNTER, + InstrumentValueType.LONG, + Advice.empty()), + INSTRUMENTATION_SCOPE_INFO); + + assertThat(views).hasSize(1); + assertThat(views.get(0).getView().getAggregation()).isEqualTo(Aggregation.defaultAggregation()); + assertThat(logs.getEvents()).hasSize(2); + assertThat(logs.getEvents().get(0).getMessage()) + .contains( + "View aggregation last_value is incompatible with instrument my_counter of type COUNTER"); + assertThat(logs.getEvents().get(1).getMessage()) + .contains( + "View aggregation last_value is incompatible with instrument my_counter of type COUNTER"); + } + + @Test + void findComposableViews_ThreeOrMoreViewsMerged() { + RegisteredView view1 = + registeredView( + InstrumentSelector.builder().setName("my_counter").build(), + View.builder().setAttributeFilter(new HashSet<>(Arrays.asList("a", "b", "c"))).build()); + RegisteredView view2 = + registeredView( + InstrumentSelector.builder().setName("my_counter").build(), + View.builder() + .setAttributeFilter(new HashSet<>(Arrays.asList("b", "c", "d"))) + .setDescription("composed desc") + .build()); + RegisteredView view3 = + registeredView( + InstrumentSelector.builder().setName("my_counter").build(), + View.builder().setCardinalityLimit(500).setAggregation(Aggregation.sum()).build()); + + ViewRegistry viewRegistry = + ViewRegistry.create( + DefaultAggregationSelector.getDefault(), + CardinalityLimitSelector.defaultCardinalityLimitSelector(), + Arrays.asList(view1, view2, view3), + ViewMatchingMode.COMPOSABLE); + + List views = + viewRegistry.findViews( + InstrumentDescriptor.create( + "my_counter", + "desc", + "1", + InstrumentType.COUNTER, + InstrumentValueType.LONG, + Advice.empty()), + INSTRUMENTATION_SCOPE_INFO); + + assertThat(views).hasSize(1); + RegisteredView composed = views.get(0); + assertThat(composed.getView().getDescription()).isEqualTo("composed desc"); + assertThat(composed.getCardinalityLimit()).isEqualTo(500); + assertThat(composed.getView().getAggregation()).isEqualTo(Aggregation.sum()); + + Attributes attrs = + Attributes.builder() + .put("a", "1") + .put("b", "2") + .put("c", "3") + .put("d", "4") + .put("e", "5") + .build(); + assertThat(composed.getAttributesFilter().apply(attrs).asMap()) + .containsOnlyKeys(stringKey("b"), stringKey("c")); + } }