diff --git a/sdk-extensions/autoconfigure/README.md b/sdk-extensions/autoconfigure/README.md index 56a446bd18d..65dd75ac01f 100644 --- a/sdk-extensions/autoconfigure/README.md +++ b/sdk-extensions/autoconfigure/README.md @@ -40,6 +40,7 @@ environment variables, e.g., `OTEL_TRACES_EXPORTER=zipkin`. * [Periodic Metric Reader](#periodic-metric-reader) * [Metric exporters](#metric-exporters) + [Prometheus exporter](#prometheus-exporter) + * [Cardinality limits](#cardinality-limits) - [Logger provider](#logger-provider) - [Batch log record processor](#batch-log-record-processor) - [Customizing the OpenTelemetry SDK](#customizing-the-opentelemetry-sdk) @@ -319,7 +320,6 @@ The following configuration options are specific to `SdkMeterProvider`. See [gen |-----------------------------|-----------------------------|----------------------------------------------------------------------------------------------| | otel.metric.export.interval | OTEL_METRIC_EXPORT_INTERVAL | The interval, in milliseconds, between the start of two export attempts. Default is `60000`. | - ### Metric exporters The following exporters are only available for the metric signal. See [exporters](#exporters) for general exporter configuration. @@ -337,6 +337,12 @@ The [Prometheus](https://github.com/prometheus/docs/blob/master/content/docs/ins Note that this is a pull exporter - it opens up a server on the local process listening on the specified host and port, which a Prometheus server scrapes from. +### Cardinality Limits + +| System property | Environment variable | Description | +|---------------------------------------------|---------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------| +| otel.experimental.metrics.cardinality.limit | OTEL_EXPERIMENTAL_METRICS_CARDINALITY_LIMIT | If set, configure experimental cardinality limit. The value dictates the maximum number of distinct points per metric. Default is `2000`. | + ## Logger provider The following configuration options are specific to `SdkLoggerProvider`. See [general configuration](#general-configuration) for general configuration. diff --git a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/MeterProviderConfiguration.java b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/MeterProviderConfiguration.java index fc168856c84..74727b09444 100644 --- a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/MeterProviderConfiguration.java +++ b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/MeterProviderConfiguration.java @@ -14,6 +14,7 @@ import io.opentelemetry.sdk.metrics.export.MetricReader; import io.opentelemetry.sdk.metrics.internal.SdkMeterProviderUtil; import io.opentelemetry.sdk.metrics.internal.exemplar.ExemplarFilter; +import io.opentelemetry.sdk.metrics.internal.state.MetricStorage; import java.io.Closeable; import java.util.Collections; import java.util.List; @@ -49,8 +50,18 @@ static void configureMeterProvider( break; } + int cardinalityLimit = + config.getInt( + "otel.experimental.metrics.cardinality.limit", MetricStorage.DEFAULT_MAX_CARDINALITY); + if (cardinalityLimit < 1) { + throw new ConfigurationException("otel.experimental.metrics.cardinality.limit must be >= 1"); + } + configureMetricReaders(config, spiHelper, metricExporterCustomizer, closeables) - .forEach(meterProviderBuilder::registerMetricReader); + .forEach( + reader -> + SdkMeterProviderUtil.registerMetricReaderWithCardinalitySelector( + meterProviderBuilder, reader, instrumentType -> cardinalityLimit)); } static List configureMetricReaders( diff --git a/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/MeterProviderConfigurationTest.java b/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/MeterProviderConfigurationTest.java new file mode 100644 index 00000000000..e1ba6000fd5 --- /dev/null +++ b/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/MeterProviderConfigurationTest.java @@ -0,0 +1,108 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import static org.assertj.core.api.Assertions.as; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.internal.testing.CleanupExtension; +import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; +import io.opentelemetry.sdk.metrics.export.MetricReader; +import io.opentelemetry.sdk.metrics.internal.export.CardinalityLimitSelector; +import java.io.Closeable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class MeterProviderConfigurationTest { + + @RegisterExtension CleanupExtension cleanup = new CleanupExtension(); + + private final SpiHelper spiHelper = + SpiHelper.create(MeterProviderConfigurationTest.class.getClassLoader()); + + @Test + void configureMeterProvider_InvalidCardinalityLimit() { + List closeables = new ArrayList<>(); + + assertThatThrownBy( + () -> { + MeterProviderConfiguration.configureMeterProvider( + SdkMeterProvider.builder(), + DefaultConfigProperties.createForTest( + ImmutableMap.of( + "otel.metrics.exporter", + "logging", + "otel.experimental.metrics.cardinality.limit", + "0")), + spiHelper, + (a, b) -> a, + closeables); + }) + .isInstanceOf(ConfigurationException.class) + .hasMessage("otel.experimental.metrics.cardinality.limit must be >= 1"); + cleanup.addCloseables(closeables); + } + + @Test + void configureMeterProvider_ConfiguresCardinalityLimit() { + List closeables = new ArrayList<>(); + + // Verify default cardinality + SdkMeterProviderBuilder builder = SdkMeterProvider.builder(); + MeterProviderConfiguration.configureMeterProvider( + builder, + DefaultConfigProperties.createForTest( + Collections.singletonMap("otel.metrics.exporter", "logging")), + spiHelper, + (a, b) -> a, + closeables); + cleanup.addCloseables(closeables); + assertCardinalityLimit(builder, 2000); + + // Customized limit cardinality limit to 100 + builder = SdkMeterProvider.builder(); + MeterProviderConfiguration.configureMeterProvider( + builder, + DefaultConfigProperties.createForTest( + ImmutableMap.of( + "otel.metrics.exporter", + "logging", + "otel.experimental.metrics.cardinality.limit", + "100")), + spiHelper, + (a, b) -> a, + closeables); + cleanup.addCloseables(closeables); + assertCardinalityLimit(builder, 100); + } + + private static void assertCardinalityLimit( + SdkMeterProviderBuilder builder, int expectedCardinalityLimit) { + assertThat(builder) + .extracting( + "metricReaders", + as(InstanceOfAssertFactories.map(MetricReader.class, CardinalityLimitSelector.class))) + .hasSizeGreaterThan(0) + .allSatisfy( + (metricReader, cardinalityLimitSelector) -> { + for (InstrumentType instrumentType : InstrumentType.values()) { + assertThat(cardinalityLimitSelector.getCardinalityLimit(instrumentType)) + .isEqualTo(expectedCardinalityLimit); + } + }); + } +}