diff --git a/sdk/all/src/test/java/io/opentelemetry/sdk/ScopeConfiguratorTest.java b/sdk/all/src/test/java/io/opentelemetry/sdk/ScopeConfiguratorTest.java new file mode 100644 index 00000000000..0055df8ce7f --- /dev/null +++ b/sdk/all/src/test/java/io/opentelemetry/sdk/ScopeConfiguratorTest.java @@ -0,0 +1,237 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk; + +import static io.opentelemetry.sdk.internal.ScopeConfiguratorBuilder.nameEquals; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; +import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.opentelemetry.sdk.logs.export.SimpleLogRecordProcessor; +import io.opentelemetry.sdk.logs.internal.LoggerConfig; +import io.opentelemetry.sdk.logs.internal.SdkLoggerProviderUtil; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.internal.MeterConfig; +import io.opentelemetry.sdk.metrics.internal.SdkMeterProviderUtil; +import io.opentelemetry.sdk.testing.exporter.InMemoryLogRecordExporter; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import io.opentelemetry.sdk.trace.internal.SdkTracerProviderUtil; +import io.opentelemetry.sdk.trace.internal.TracerConfig; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +class ScopeConfiguratorTest { + + private final InMemoryLogRecordExporter logRecordExporter = InMemoryLogRecordExporter.create(); + private final InMemoryMetricReader metricReader = InMemoryMetricReader.create(); + private final InMemorySpanExporter spanExporter = InMemorySpanExporter.create(); + + private static final InstrumentationScopeInfo scopeA = InstrumentationScopeInfo.create("scopeA"); + private static final InstrumentationScopeInfo scopeB = InstrumentationScopeInfo.create("scopeB"); + private static final InstrumentationScopeInfo scopeC = InstrumentationScopeInfo.create("scopeC"); + + /** Disable "scopeB". All other scopes are enabled by default. */ + @Test + void disableScopeB() { + // Configuration ergonomics will improve after APIs stabilize + SdkTracerProviderBuilder tracerProviderBuilder = SdkTracerProvider.builder(); + SdkTracerProviderUtil.addTracerConfiguratorCondition( + tracerProviderBuilder, nameEquals(scopeB.getName()), TracerConfig.disabled()); + SdkMeterProviderBuilder meterProviderBuilder = SdkMeterProvider.builder(); + SdkMeterProviderUtil.addMeterConfiguratorCondition( + meterProviderBuilder, nameEquals(scopeB.getName()), MeterConfig.disabled()); + SdkLoggerProviderBuilder loggerProviderBuilder = SdkLoggerProvider.builder(); + SdkLoggerProviderUtil.addLoggerConfiguratorCondition( + loggerProviderBuilder, nameEquals(scopeB.getName()), LoggerConfig.disabled()); + + OpenTelemetrySdk sdk = + OpenTelemetrySdk.builder() + .setTracerProvider( + tracerProviderBuilder + .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) + .build()) + .setMeterProvider(meterProviderBuilder.registerMetricReader(metricReader).build()) + .setLoggerProvider( + loggerProviderBuilder + .addLogRecordProcessor(SimpleLogRecordProcessor.create(logRecordExporter)) + .build()) + .build(); + + simulateInstrumentation(sdk); + + // Collect all the telemetry. Ensure we don't see any from scopeB, and that the telemetry from + // scopeA and scopeC is valid. + assertThat(spanExporter.getFinishedSpanItems()) + .satisfies( + spans -> { + Map> spansByScope = + spans.stream() + .collect(Collectors.groupingBy(SpanData::getInstrumentationScopeInfo)); + assertThat(spansByScope.get(scopeA)).hasSize(1); + assertThat(spansByScope.get(scopeB)).isNull(); + assertThat(spansByScope.get(scopeC)).hasSize(1); + }); + assertThat(metricReader.collectAllMetrics()) + .satisfies( + metrics -> { + Map> metricsByScope = + metrics.stream() + .collect(Collectors.groupingBy(MetricData::getInstrumentationScopeInfo)); + assertThat(metricsByScope.get(scopeA)).hasSize(1); + assertThat(metricsByScope.get(scopeB)).isNull(); + assertThat(metricsByScope.get(scopeC)).hasSize(1); + }); + assertThat(logRecordExporter.getFinishedLogRecordItems()) + .satisfies( + logs -> { + Map> logsByScope = + logs.stream() + .collect(Collectors.groupingBy(LogRecordData::getInstrumentationScopeInfo)); + assertThat(logsByScope.get(scopeA)).hasSize(1); + assertThat(logsByScope.get(scopeB)).isNull(); + assertThat(logsByScope.get(scopeC)).hasSize(1); + }); + } + + /** Disable all scopes by default and enable a single scope. */ + @Test + void disableAllScopesExceptB() { + // Configuration ergonomics will improve after APIs stabilize + SdkTracerProviderBuilder tracerProviderBuilder = SdkTracerProvider.builder(); + SdkTracerProviderUtil.setTracerConfigurator( + tracerProviderBuilder, + TracerConfig.configuratorBuilder() + .setDefault(TracerConfig.disabled()) + .addCondition(nameEquals(scopeB.getName()), TracerConfig.enabled()) + .build()); + SdkMeterProviderBuilder meterProviderBuilder = SdkMeterProvider.builder(); + SdkMeterProviderUtil.setMeterConfigurator( + meterProviderBuilder, + MeterConfig.configuratorBuilder() + .setDefault(MeterConfig.disabled()) + .addCondition(nameEquals(scopeB.getName()), MeterConfig.enabled()) + .build()); + SdkLoggerProviderBuilder loggerProviderBuilder = SdkLoggerProvider.builder(); + SdkLoggerProviderUtil.setLoggerConfigurator( + loggerProviderBuilder, + LoggerConfig.configuratorBuilder() + .setDefault(LoggerConfig.disabled()) + .addCondition(nameEquals(scopeB.getName()), LoggerConfig.enabled()) + .build()); + + OpenTelemetrySdk sdk = + OpenTelemetrySdk.builder() + .setTracerProvider( + tracerProviderBuilder + .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) + .build()) + .setMeterProvider(meterProviderBuilder.registerMetricReader(metricReader).build()) + .setLoggerProvider( + loggerProviderBuilder + .addLogRecordProcessor(SimpleLogRecordProcessor.create(logRecordExporter)) + .build()) + .build(); + + simulateInstrumentation(sdk); + + // Collect all the telemetry. Ensure we only see telemetry from scopeB, since other scopes have + // been disabled by default. + assertThat(spanExporter.getFinishedSpanItems()) + .satisfies( + spans -> { + Map> spansByScope = + spans.stream() + .collect(Collectors.groupingBy(SpanData::getInstrumentationScopeInfo)); + assertThat(spansByScope.get(scopeA)).isNull(); + assertThat(spansByScope.get(scopeB)).hasSize(1); + assertThat(spansByScope.get(scopeC)).isNull(); + }); + assertThat(metricReader.collectAllMetrics()) + .satisfies( + metrics -> { + Map> metricsByScope = + metrics.stream() + .collect(Collectors.groupingBy(MetricData::getInstrumentationScopeInfo)); + assertThat(metricsByScope.get(scopeA)).isNull(); + assertThat(metricsByScope.get(scopeB)).hasSize(1); + assertThat(metricsByScope.get(scopeC)).isNull(); + }); + assertThat(logRecordExporter.getFinishedLogRecordItems()) + .satisfies( + logs -> { + Map> logsByScope = + logs.stream() + .collect(Collectors.groupingBy(LogRecordData::getInstrumentationScopeInfo)); + assertThat(logsByScope.get(scopeA)).isNull(); + assertThat(logsByScope.get(scopeB)).hasSize(1); + assertThat(logsByScope.get(scopeC)).isNull(); + }); + } + + /** + * Emit spans, metrics and logs in a hierarchy of 3 scopes: scopeA -> scopeB -> scopeC. Exercise + * the scope config which is common across all signals. + */ + private static void simulateInstrumentation(OpenTelemetry openTelemetry) { + // Start scopeA + Tracer scopeATracer = openTelemetry.getTracer(scopeA.getName()); + Meter scopeAMeter = openTelemetry.getMeter(scopeA.getName()); + Logger scopeALogger = openTelemetry.getLogsBridge().get(scopeA.getName()); + Span spanA = scopeATracer.spanBuilder("spanA").startSpan(); + try (Scope spanAScope = spanA.makeCurrent()) { + scopeALogger.logRecordBuilder().setBody("scopeA log message").emit(); + + // Start scopeB + Tracer scopeBTracer = openTelemetry.getTracer(scopeB.getName()); + Meter scopeBMeter = openTelemetry.getMeter(scopeB.getName()); + Logger scopeBLogger = openTelemetry.getLogsBridge().get(scopeB.getName()); + Span spanB = scopeBTracer.spanBuilder("spanB").startSpan(); + try (Scope spanBScope = spanB.makeCurrent()) { + scopeBLogger.logRecordBuilder().setBody("scopeB log message").emit(); + + // Start scopeC + Tracer scopeCTracer = openTelemetry.getTracer(scopeC.getName()); + Meter scopeCMeter = openTelemetry.getMeter(scopeC.getName()); + Logger scopeCLogger = openTelemetry.getLogsBridge().get(scopeC.getName()); + Span spanC = scopeCTracer.spanBuilder("spanC").startSpan(); + try (Scope spanCScope = spanB.makeCurrent()) { + scopeCLogger.logRecordBuilder().setBody("scopeC log message").emit(); + } finally { + spanC.end(); + scopeCMeter.counterBuilder("scopeCCounter").build().add(1); + } + // End scopeC + + } finally { + spanB.end(); + scopeBMeter.counterBuilder("scopeBCounter").build().add(1); + } + // End scopeB + + } finally { + spanA.end(); + scopeAMeter.counterBuilder("scopeACounter").build().add(1); + } + // End scopeA + } +} diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/internal/GlobUtil.java b/sdk/common/src/main/java/io/opentelemetry/sdk/internal/GlobUtil.java new file mode 100644 index 00000000000..9c914055d7d --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/internal/GlobUtil.java @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.internal; + +import java.util.function.Predicate; +import java.util.regex.Pattern; + +/** + * Utilities for glob pattern matching. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class GlobUtil { + + private GlobUtil() {} + + /** + * Return a predicate that returns {@code true} if a string matches the {@code globPattern}. + * + *

{@code globPattern} may contain the wildcard characters {@code *} and {@code ?} with the + * following matching criteria: + * + *

    + *
  • {@code *} matches 0 or more instances of any character + *
  • {@code ?} matches exactly one instance of any character + *
+ */ + public static Predicate toGlobPatternPredicate(String globPattern) { + // Match all + if (globPattern.equals("*")) { + return unused -> true; + } + + // If globPattern contains '*' or '?', convert it to a regex and return corresponding predicate + for (int i = 0; i < globPattern.length(); i++) { + char c = globPattern.charAt(i); + if (c == '*' || c == '?') { + Pattern pattern = toRegexPattern(globPattern); + return string -> pattern.matcher(string).matches(); + } + } + + // Exact match, ignoring case + return globPattern::equalsIgnoreCase; + } + + /** + * Transform the {@code globPattern} to a regex by converting {@code *} to {@code .*}, {@code ?} + * to {@code .}, and escaping other regex special characters. + */ + private static Pattern toRegexPattern(String globPattern) { + int tokenStart = -1; + StringBuilder patternBuilder = new StringBuilder(); + for (int i = 0; i < globPattern.length(); i++) { + char c = globPattern.charAt(i); + if (c == '*' || c == '?') { + if (tokenStart != -1) { + patternBuilder.append(Pattern.quote(globPattern.substring(tokenStart, i))); + tokenStart = -1; + } + if (c == '*') { + patternBuilder.append(".*"); + } else { + // c == '?' + patternBuilder.append("."); + } + } else { + if (tokenStart == -1) { + tokenStart = i; + } + } + } + if (tokenStart != -1) { + patternBuilder.append(Pattern.quote(globPattern.substring(tokenStart))); + } + return Pattern.compile(patternBuilder.toString()); + } +} diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/internal/ScopeConfigurator.java b/sdk/common/src/main/java/io/opentelemetry/sdk/internal/ScopeConfigurator.java new file mode 100644 index 00000000000..cb1fdaeed43 --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/internal/ScopeConfigurator.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.internal; + +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import java.util.function.Function; + +/** + * A {@link ScopeConfigurator} computes configuration for a given {@link InstrumentationScopeInfo}. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +@FunctionalInterface +public interface ScopeConfigurator extends Function { + + /** Create a new builder. */ + static ScopeConfiguratorBuilder builder() { + return new ScopeConfiguratorBuilder<>(unused -> null); + } + + /** + * Convert this {@link ScopeConfigurator} to a builder. Additional added matchers only apply when + * {@link #apply(Object)} returns {@code null}. If this configurator contains {@link + * ScopeConfiguratorBuilder#setDefault(Object)}, additional matchers are never applied. + */ + default ScopeConfiguratorBuilder toBuilder() { + return new ScopeConfiguratorBuilder<>(this); + } +} diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/internal/ScopeConfiguratorBuilder.java b/sdk/common/src/main/java/io/opentelemetry/sdk/internal/ScopeConfiguratorBuilder.java new file mode 100644 index 00000000000..4c32e2f8d9a --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/internal/ScopeConfiguratorBuilder.java @@ -0,0 +1,114 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.internal; + +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; +import javax.annotation.Nullable; + +/** + * Builder for {@link ScopeConfigurator}. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + * + * @param The scope configuration object, e.g. {@code TracerConfig}, {@code LoggerConfig}, + * {@code MeterConfig}. + */ +public final class ScopeConfiguratorBuilder { + + private final ScopeConfigurator baseScopeConfigurator; + @Nullable private T defaultScopeConfig; + private final List> conditions = new ArrayList<>(); + + ScopeConfiguratorBuilder(ScopeConfigurator baseScopeConfigurator) { + this.baseScopeConfigurator = baseScopeConfigurator; + } + + /** + * Set the default scope config, which is returned by {@link ScopeConfigurator#apply(Object)} if a + * {@link InstrumentationScopeInfo} does not match any {@link #addCondition(Predicate, Object) + * conditions}. If a default is not set, an SDK defined default is used. + */ + public ScopeConfiguratorBuilder setDefault(T defaultScopeConfig) { + this.defaultScopeConfig = defaultScopeConfig; + return this; + } + + /** + * Add a condition. Conditions are evaluated in order. The {@code scopeConfig} for the first match + * is returned by {@link ScopeConfigurator#apply(Object)}. + * + * @param scopePredicate predicate that {@link InstrumentationScopeInfo}s are evaluated against + * @param scopeConfig the scope config to use when this condition is the first matching {@code + * scopePredicate} + * @see #nameMatchesGlob(String) + * @see #nameEquals(String) + */ + public ScopeConfiguratorBuilder addCondition( + Predicate scopePredicate, T scopeConfig) { + conditions.add(new Condition<>(scopePredicate, scopeConfig)); + return this; + } + + /** + * Helper function for pattern matching {@link InstrumentationScopeInfo#getName()} against the + * {@code globPattern}. + * + *

{@code globPattern} may contain the wildcard characters {@code *} and {@code ?} with the + * following matching criteria: + * + *

    + *
  • {@code *} matches 0 or more instances of any character + *
  • {@code ?} matches exactly one instance of any character + *
+ * + * @see #addCondition(Predicate, Object) + */ + public static Predicate nameMatchesGlob(String globPattern) { + Predicate globPredicate = GlobUtil.toGlobPatternPredicate(globPattern); + return scopeInfo -> globPredicate.test(scopeInfo.getName()); + } + + /** + * Helper function for exact matching {@link InstrumentationScopeInfo#getName()} against the + * {@code scopeName}. + * + * @see #addCondition(Predicate, Object) + */ + public static Predicate nameEquals(String scopeName) { + return scopeInfo -> scopeInfo.getName().equals(scopeName); + } + + /** Build a {@link ScopeConfigurator} with the configuration of this builder. */ + public ScopeConfigurator build() { + // TODO: return an instance with toString implementation which self describes rules + return scopeInfo -> { + T scopeConfig = baseScopeConfigurator.apply(scopeInfo); + if (scopeConfig != null) { + return scopeConfig; + } + for (Condition condition : conditions) { + if (condition.scopeMatcher.test(scopeInfo)) { + return condition.scopeConfig; + } + } + return defaultScopeConfig; + }; + } + + private static final class Condition { + private final Predicate scopeMatcher; + private final T scopeConfig; + + private Condition(Predicate scopeMatcher, T scopeConfig) { + this.scopeMatcher = scopeMatcher; + this.scopeConfig = scopeConfig; + } + } +} diff --git a/sdk/common/src/test/java/io/opentelemetry/sdk/internal/GlobUtilTest.java b/sdk/common/src/test/java/io/opentelemetry/sdk/internal/GlobUtilTest.java new file mode 100644 index 00000000000..a26928f34bc --- /dev/null +++ b/sdk/common/src/test/java/io/opentelemetry/sdk/internal/GlobUtilTest.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.internal; + +import static io.opentelemetry.sdk.internal.GlobUtil.toGlobPatternPredicate; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class GlobUtilTest { + + @Test + void matchesName() { + assertThat(toGlobPatternPredicate("foo").test("foo")).isTrue(); + assertThat(toGlobPatternPredicate("foo").test("Foo")).isTrue(); + assertThat(toGlobPatternPredicate("foo").test("bar")).isFalse(); + assertThat(toGlobPatternPredicate("fo?").test("foo")).isTrue(); + assertThat(toGlobPatternPredicate("fo??").test("fooo")).isTrue(); + assertThat(toGlobPatternPredicate("fo?").test("fob")).isTrue(); + assertThat(toGlobPatternPredicate("fo?").test("fooo")).isFalse(); + assertThat(toGlobPatternPredicate("*").test("foo")).isTrue(); + assertThat(toGlobPatternPredicate("*").test("bar")).isTrue(); + assertThat(toGlobPatternPredicate("*").test("baz")).isTrue(); + assertThat(toGlobPatternPredicate("*").test("foo.bar.baz")).isTrue(); + assertThat(toGlobPatternPredicate("*").test(null)).isTrue(); + assertThat(toGlobPatternPredicate("*").test("")).isTrue(); + assertThat(toGlobPatternPredicate("fo*").test("fo")).isTrue(); + assertThat(toGlobPatternPredicate("fo*").test("foo")).isTrue(); + assertThat(toGlobPatternPredicate("fo*").test("fooo")).isTrue(); + assertThat(toGlobPatternPredicate("fo*").test("foo.bar.baz")).isTrue(); + assertThat(toGlobPatternPredicate("*bar").test("sandbar")).isTrue(); + assertThat(toGlobPatternPredicate("fo*b*").test("foobar")).isTrue(); + assertThat(toGlobPatternPredicate("fo*b*").test("foob")).isTrue(); + assertThat(toGlobPatternPredicate("fo*b*").test("foo bar")).isTrue(); + assertThat(toGlobPatternPredicate("fo? b??").test("foo bar")).isTrue(); + assertThat(toGlobPatternPredicate("fo? b??").test("fooo bar")).isFalse(); + assertThat(toGlobPatternPredicate("fo* ba?").test("foo is not bar")).isTrue(); + assertThat(toGlobPatternPredicate("fo? b*").test("fox beetles for lunch")).isTrue(); + assertThat(toGlobPatternPredicate("f()[]$^.{}|").test("f()[]$^.{}|")).isTrue(); + assertThat(toGlobPatternPredicate("f()[]$^.{}|?").test("f()[]$^.{}|o")).isTrue(); + assertThat(toGlobPatternPredicate("f()[]$^.{}|*").test("f()[]$^.{}|ooo")).isTrue(); + } +} diff --git a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLogger.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLogger.java index efcb7882ade..74bad693bfa 100644 --- a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLogger.java +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLogger.java @@ -7,23 +7,34 @@ import io.opentelemetry.api.logs.LogRecordBuilder; import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.api.logs.LoggerProvider; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.logs.internal.LoggerConfig; /** SDK implementation of {@link Logger}. */ final class SdkLogger implements Logger { + private static final Logger NOOP_LOGGER = LoggerProvider.noop().get("noop"); + private final LoggerSharedState loggerSharedState; private final InstrumentationScopeInfo instrumentationScopeInfo; + private final LoggerConfig loggerConfig; SdkLogger( - LoggerSharedState loggerSharedState, InstrumentationScopeInfo instrumentationScopeInfo) { + LoggerSharedState loggerSharedState, + InstrumentationScopeInfo instrumentationScopeInfo, + LoggerConfig loggerConfig) { this.loggerSharedState = loggerSharedState; this.instrumentationScopeInfo = instrumentationScopeInfo; + this.loggerConfig = loggerConfig; } @Override public LogRecordBuilder logRecordBuilder() { - return new SdkLogRecordBuilder(loggerSharedState, instrumentationScopeInfo); + if (loggerConfig.isEnabled()) { + return new SdkLogRecordBuilder(loggerSharedState, instrumentationScopeInfo); + } + return NOOP_LOGGER.logRecordBuilder(); } // VisibleForTesting diff --git a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProvider.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProvider.java index 9b9b2e1a97d..2d7b87e6b47 100644 --- a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProvider.java +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProvider.java @@ -11,7 +11,10 @@ import io.opentelemetry.api.logs.LoggerProvider; import io.opentelemetry.sdk.common.Clock; import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.internal.ComponentRegistry; +import io.opentelemetry.sdk.internal.ScopeConfigurator; +import io.opentelemetry.sdk.logs.internal.LoggerConfig; import io.opentelemetry.sdk.resources.Resource; import java.io.Closeable; import java.util.List; @@ -33,6 +36,7 @@ public final class SdkLoggerProvider implements LoggerProvider, Closeable { private final LoggerSharedState sharedState; private final ComponentRegistry loggerComponentRegistry; + private final ScopeConfigurator loggerConfigurator; private final boolean isNoopLogRecordProcessor; /** @@ -48,16 +52,27 @@ public static SdkLoggerProviderBuilder builder() { Resource resource, Supplier logLimitsSupplier, List processors, - Clock clock) { + Clock clock, + ScopeConfigurator loggerConfigurator) { LogRecordProcessor logRecordProcessor = LogRecordProcessor.composite(processors); this.sharedState = new LoggerSharedState(resource, logLimitsSupplier, logRecordProcessor, clock); this.loggerComponentRegistry = new ComponentRegistry<>( - instrumentationScopeInfo -> new SdkLogger(sharedState, instrumentationScopeInfo)); + instrumentationScopeInfo -> + new SdkLogger( + sharedState, + instrumentationScopeInfo, + getLoggerConfig(instrumentationScopeInfo))); + this.loggerConfigurator = loggerConfigurator; this.isNoopLogRecordProcessor = logRecordProcessor instanceof NoopLogRecordProcessor; } + private LoggerConfig getLoggerConfig(InstrumentationScopeInfo instrumentationScopeInfo) { + LoggerConfig loggerConfig = loggerConfigurator.apply(instrumentationScopeInfo); + return loggerConfig == null ? LoggerConfig.defaultConfig() : loggerConfig; + } + @Override public Logger get(String instrumentationScopeName) { return loggerComponentRegistry.get( diff --git a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilder.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilder.java index 8d7004eeabd..87fec0c8d84 100644 --- a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilder.java +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilder.java @@ -11,11 +11,16 @@ import io.opentelemetry.api.logs.Logger; import io.opentelemetry.context.Context; import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.internal.ScopeConfigurator; +import io.opentelemetry.sdk.internal.ScopeConfiguratorBuilder; import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.opentelemetry.sdk.logs.internal.LoggerConfig; import io.opentelemetry.sdk.resources.Resource; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.function.Predicate; import java.util.function.Supplier; /** @@ -29,6 +34,8 @@ public final class SdkLoggerProviderBuilder { private Resource resource = Resource.getDefault(); private Supplier logLimitsSupplier = LogLimits::getDefault; private Clock clock = Clock.getDefault(); + private ScopeConfiguratorBuilder loggerConfiguratorBuilder = + LoggerConfig.configuratorBuilder(); SdkLoggerProviderBuilder() {} @@ -100,12 +107,47 @@ public SdkLoggerProviderBuilder setClock(Clock clock) { return this; } + /** + * Set the logger configurator, which computes {@link LoggerConfig} for each {@link + * InstrumentationScopeInfo}. + * + *

Overrides any matchers added via {@link #addLoggerConfiguratorCondition(Predicate, + * LoggerConfig)}. + * + * @see LoggerConfig#configuratorBuilder() + */ + SdkLoggerProviderBuilder setLoggerConfigurator( + ScopeConfigurator loggerConfigurator) { + this.loggerConfiguratorBuilder = loggerConfigurator.toBuilder(); + return this; + } + + /** + * Adds a condition to the logger configurator, which computes {@link LoggerConfig} for each + * {@link InstrumentationScopeInfo}. + * + *

Applies after any previously added conditions. + * + *

If {@link #setLoggerConfigurator(ScopeConfigurator)} was previously called, this condition + * will only be applied if the {@link ScopeConfigurator#apply(Object)} returns null for the + * matched {@link InstrumentationScopeInfo}(s). + * + * @see ScopeConfiguratorBuilder#nameEquals(String) + * @see ScopeConfiguratorBuilder#nameMatchesGlob(String) + */ + SdkLoggerProviderBuilder addLoggerConfiguratorCondition( + Predicate scopeMatcher, LoggerConfig loggerConfig) { + this.loggerConfiguratorBuilder.addCondition(scopeMatcher, loggerConfig); + return this; + } + /** * Create a {@link SdkLoggerProvider} instance. * * @return an instance configured with the provided options */ public SdkLoggerProvider build() { - return new SdkLoggerProvider(resource, logLimitsSupplier, logRecordProcessors, clock); + return new SdkLoggerProvider( + resource, logLimitsSupplier, logRecordProcessors, clock, loggerConfiguratorBuilder.build()); } } diff --git a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/internal/LoggerConfig.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/internal/LoggerConfig.java new file mode 100644 index 00000000000..39825686c86 --- /dev/null +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/internal/LoggerConfig.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.logs.internal; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.internal.ScopeConfigurator; +import io.opentelemetry.sdk.internal.ScopeConfiguratorBuilder; +import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder; +import java.util.function.Predicate; +import javax.annotation.concurrent.Immutable; + +/** + * A collection of configuration options which define the behavior of a {@link Logger}. + * + * @see SdkLoggerProviderUtil#setLoggerConfigurator(SdkLoggerProviderBuilder, ScopeConfigurator) + * @see SdkLoggerProviderUtil#addLoggerConfiguratorCondition(SdkLoggerProviderBuilder, Predicate, + * LoggerConfig) + */ +@AutoValue +@Immutable +public abstract class LoggerConfig { + + private static final LoggerConfig DEFAULT_CONFIG = + new AutoValue_LoggerConfig(/* enabled= */ true); + private static final LoggerConfig DISABLED_CONFIG = + new AutoValue_LoggerConfig(/* enabled= */ false); + + /** Returns a disabled {@link LoggerConfig}. */ + public static LoggerConfig disabled() { + return DISABLED_CONFIG; + } + + /** Returns an enabled {@link LoggerConfig}. */ + public static LoggerConfig enabled() { + return DEFAULT_CONFIG; + } + + /** + * Returns the default {@link LoggerConfig}, which is used when no configurator is set or when the + * logger configurator returns {@code null} for a {@link InstrumentationScopeInfo}. + */ + public static LoggerConfig defaultConfig() { + return DEFAULT_CONFIG; + } + + /** + * Create a {@link ScopeConfiguratorBuilder} for configuring {@link + * SdkLoggerProviderUtil#setLoggerConfigurator(SdkLoggerProviderBuilder, ScopeConfigurator)}. + */ + public static ScopeConfiguratorBuilder configuratorBuilder() { + return ScopeConfigurator.builder(); + } + + LoggerConfig() {} + + /** Returns {@code true} if this logger is enabled. Defaults to {@code true}. */ + public abstract boolean isEnabled(); +} diff --git a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/internal/SdkLoggerProviderUtil.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/internal/SdkLoggerProviderUtil.java new file mode 100644 index 00000000000..eb4fbb4ec29 --- /dev/null +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/internal/SdkLoggerProviderUtil.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.logs.internal; + +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.internal.ScopeConfigurator; +import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.function.Predicate; + +/** + * A collection of methods that allow use of experimental features prior to availability in public + * APIs. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class SdkLoggerProviderUtil { + + private SdkLoggerProviderUtil() {} + + /** Reflectively set the {@link ScopeConfigurator} to the {@link SdkLoggerProviderBuilder}. */ + public static void setLoggerConfigurator( + SdkLoggerProviderBuilder sdkLoggerProviderBuilder, + ScopeConfigurator loggerConfigurator) { + try { + Method method = + SdkLoggerProviderBuilder.class.getDeclaredMethod( + "setLoggerConfigurator", ScopeConfigurator.class); + method.setAccessible(true); + method.invoke(sdkLoggerProviderBuilder, loggerConfigurator); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException( + "Error calling setLoggerConfigurator on SdkLoggerProviderBuilder", e); + } + } + + /** Reflectively add a logger configurator condition to the {@link SdkLoggerProviderBuilder}. */ + public static void addLoggerConfiguratorCondition( + SdkLoggerProviderBuilder sdkLoggerProviderBuilder, + Predicate scopeMatcher, + LoggerConfig loggerConfig) { + try { + Method method = + SdkLoggerProviderBuilder.class.getDeclaredMethod( + "addLoggerConfiguratorCondition", Predicate.class, LoggerConfig.class); + method.setAccessible(true); + method.invoke(sdkLoggerProviderBuilder, scopeMatcher, loggerConfig); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException( + "Error calling addLoggerConfiguratorCondition on SdkLoggerProviderBuilder", e); + } + } +} diff --git a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/LoggerConfigTest.java b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/LoggerConfigTest.java new file mode 100644 index 00000000000..371098267eb --- /dev/null +++ b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/LoggerConfigTest.java @@ -0,0 +1,127 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.logs; + +import static io.opentelemetry.sdk.internal.ScopeConfiguratorBuilder.nameEquals; +import static io.opentelemetry.sdk.internal.ScopeConfiguratorBuilder.nameMatchesGlob; +import static io.opentelemetry.sdk.logs.internal.LoggerConfig.defaultConfig; +import static io.opentelemetry.sdk.logs.internal.LoggerConfig.enabled; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; + +import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.internal.ScopeConfigurator; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.opentelemetry.sdk.logs.export.SimpleLogRecordProcessor; +import io.opentelemetry.sdk.logs.internal.LoggerConfig; +import io.opentelemetry.sdk.testing.exporter.InMemoryLogRecordExporter; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class LoggerConfigTest { + + @Test + void disableScopes() { + InMemoryLogRecordExporter exporter = InMemoryLogRecordExporter.create(); + SdkLoggerProvider loggerProvider = + SdkLoggerProvider.builder() + // Disable loggerB. Since loggers are enabled by default, loggerA and loggerC are + // enabled. + .addLoggerConfiguratorCondition(nameEquals("loggerB"), LoggerConfig.disabled()) + .addLogRecordProcessor(SimpleLogRecordProcessor.create(exporter)) + .build(); + + Logger loggerA = loggerProvider.get("loggerA"); + Logger loggerB = loggerProvider.get("loggerB"); + Logger loggerC = loggerProvider.get("loggerC"); + + loggerA.logRecordBuilder().setBody("messageA").emit(); + loggerB.logRecordBuilder().setBody("messageB").emit(); + loggerC.logRecordBuilder().setBody("messageC").emit(); + + // Only logs from loggerA and loggerC should be seen + assertThat(exporter.getFinishedLogRecordItems()) + .satisfies( + metrics -> { + Map> logsByScope = + metrics.stream() + .collect(Collectors.groupingBy(LogRecordData::getInstrumentationScopeInfo)); + assertThat(logsByScope.get(InstrumentationScopeInfo.create("loggerA"))).hasSize(1); + assertThat(logsByScope.get(InstrumentationScopeInfo.create("loggerB"))).isNull(); + assertThat(logsByScope.get(InstrumentationScopeInfo.create("loggerC"))).hasSize(1); + }); + } + + @ParameterizedTest + @MethodSource("loggerConfiguratorArgs") + void loggerConfigurator( + ScopeConfigurator loggerConfigurator, + InstrumentationScopeInfo scope, + LoggerConfig expectedLoggerConfig) { + LoggerConfig loggerConfig = loggerConfigurator.apply(scope); + loggerConfig = loggerConfig == null ? defaultConfig() : loggerConfig; + assertThat(loggerConfig).isEqualTo(expectedLoggerConfig); + } + + private static final InstrumentationScopeInfo scopeCat = InstrumentationScopeInfo.create("cat"); + private static final InstrumentationScopeInfo scopeDog = InstrumentationScopeInfo.create("dog"); + private static final InstrumentationScopeInfo scopeDuck = InstrumentationScopeInfo.create("duck"); + + private static Stream loggerConfiguratorArgs() { + ScopeConfigurator defaultConfigurator = + LoggerConfig.configuratorBuilder().build(); + ScopeConfigurator disableCat = + LoggerConfig.configuratorBuilder() + .addCondition(nameEquals("cat"), LoggerConfig.disabled()) + // Second matching rule for cat should be ignored + .addCondition(nameEquals("cat"), enabled()) + .build(); + ScopeConfigurator disableStartsWithD = + LoggerConfig.configuratorBuilder() + .addCondition(nameMatchesGlob("d*"), LoggerConfig.disabled()) + .build(); + ScopeConfigurator enableCat = + LoggerConfig.configuratorBuilder() + .setDefault(LoggerConfig.disabled()) + .addCondition(nameEquals("cat"), enabled()) + // Second matching rule for cat should be ignored + .addCondition(nameEquals("cat"), LoggerConfig.disabled()) + .build(); + ScopeConfigurator enableStartsWithD = + LoggerConfig.configuratorBuilder() + .setDefault(LoggerConfig.disabled()) + .addCondition(nameMatchesGlob("d*"), LoggerConfig.enabled()) + .build(); + + return Stream.of( + // default + Arguments.of(defaultConfigurator, scopeCat, defaultConfig()), + Arguments.of(defaultConfigurator, scopeDog, defaultConfig()), + Arguments.of(defaultConfigurator, scopeDuck, defaultConfig()), + // default enabled, disable cat + Arguments.of(disableCat, scopeCat, LoggerConfig.disabled()), + Arguments.of(disableCat, scopeDog, enabled()), + Arguments.of(disableCat, scopeDuck, enabled()), + // default enabled, disable pattern + Arguments.of(disableStartsWithD, scopeCat, enabled()), + Arguments.of(disableStartsWithD, scopeDog, LoggerConfig.disabled()), + Arguments.of(disableStartsWithD, scopeDuck, LoggerConfig.disabled()), + // default disabled, enable cat + Arguments.of(enableCat, scopeCat, enabled()), + Arguments.of(enableCat, scopeDog, LoggerConfig.disabled()), + Arguments.of(enableCat, scopeDuck, LoggerConfig.disabled()), + // default disabled, enable pattern + Arguments.of(enableStartsWithD, scopeCat, LoggerConfig.disabled()), + Arguments.of(enableStartsWithD, scopeDog, enabled()), + Arguments.of(enableStartsWithD, scopeDuck, enabled())); + } +} diff --git a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerTest.java b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerTest.java index f8246dfc0a9..2ea1ee19291 100644 --- a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerTest.java +++ b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerTest.java @@ -24,6 +24,7 @@ import io.opentelemetry.sdk.common.Clock; import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.logs.internal.LoggerConfig; import io.opentelemetry.sdk.resources.Resource; import java.util.Arrays; import java.util.concurrent.TimeUnit; @@ -44,7 +45,7 @@ void logRecordBuilder() { when(state.getLogRecordProcessor()).thenReturn(logRecordProcessor); when(state.getClock()).thenReturn(clock); - SdkLogger logger = new SdkLogger(state, info); + SdkLogger logger = new SdkLogger(state, info, LoggerConfig.defaultConfig()); LogRecordBuilder logRecordBuilder = logger.logRecordBuilder(); logRecordBuilder.setBody("foo"); diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeter.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeter.java index bd356b1e8b9..bb76fe6701a 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeter.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeter.java @@ -15,6 +15,7 @@ import io.opentelemetry.api.metrics.ObservableMeasurement; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.internal.MeterConfig; import io.opentelemetry.sdk.metrics.internal.export.RegisteredReader; import io.opentelemetry.sdk.metrics.internal.state.CallbackRegistration; import io.opentelemetry.sdk.metrics.internal.state.MeterProviderSharedState; @@ -55,14 +56,17 @@ final class SdkMeter implements Meter { private final InstrumentationScopeInfo instrumentationScopeInfo; private final MeterProviderSharedState meterProviderSharedState; private final MeterSharedState meterSharedState; + private final MeterConfig meterConfig; SdkMeter( MeterProviderSharedState meterProviderSharedState, InstrumentationScopeInfo instrumentationScopeInfo, - List registeredReaders) { + List registeredReaders, + MeterConfig meterConfig) { this.instrumentationScopeInfo = instrumentationScopeInfo; this.meterProviderSharedState = meterProviderSharedState; this.meterSharedState = MeterSharedState.create(instrumentationScopeInfo, registeredReaders); + this.meterConfig = meterConfig; } // Visible for testing @@ -82,34 +86,32 @@ void resetForTest() { @Override public LongCounterBuilder counterBuilder(String name) { - return !checkValidInstrumentName(name) - ? NOOP_METER.counterBuilder(NOOP_INSTRUMENT_NAME) - : new SdkLongCounter.SdkLongCounterBuilder( - meterProviderSharedState, meterSharedState, name); + return meterConfig.isEnabled() && checkValidInstrumentName(name) + ? new SdkLongCounter.SdkLongCounterBuilder(meterProviderSharedState, meterSharedState, name) + : NOOP_METER.counterBuilder(NOOP_INSTRUMENT_NAME); } @Override public LongUpDownCounterBuilder upDownCounterBuilder(String name) { - return !checkValidInstrumentName(name) - ? NOOP_METER.upDownCounterBuilder(NOOP_INSTRUMENT_NAME) - : new SdkLongUpDownCounter.SdkLongUpDownCounterBuilder( - meterProviderSharedState, meterSharedState, name); + return meterConfig.isEnabled() && checkValidInstrumentName(name) + ? new SdkLongUpDownCounter.SdkLongUpDownCounterBuilder( + meterProviderSharedState, meterSharedState, name) + : NOOP_METER.upDownCounterBuilder(NOOP_INSTRUMENT_NAME); } @Override public DoubleHistogramBuilder histogramBuilder(String name) { - return !checkValidInstrumentName(name) - ? NOOP_METER.histogramBuilder(NOOP_INSTRUMENT_NAME) - : new SdkDoubleHistogram.SdkDoubleHistogramBuilder( - meterProviderSharedState, meterSharedState, name); + return meterConfig.isEnabled() && checkValidInstrumentName(name) + ? new SdkDoubleHistogram.SdkDoubleHistogramBuilder( + meterProviderSharedState, meterSharedState, name) + : NOOP_METER.histogramBuilder(NOOP_INSTRUMENT_NAME); } @Override public DoubleGaugeBuilder gaugeBuilder(String name) { - return !checkValidInstrumentName(name) - ? NOOP_METER.gaugeBuilder(NOOP_INSTRUMENT_NAME) - : new SdkDoubleGauge.SdkDoubleGaugeBuilder( - meterProviderSharedState, meterSharedState, name); + return meterConfig.isEnabled() && checkValidInstrumentName(name) + ? new SdkDoubleGauge.SdkDoubleGaugeBuilder(meterProviderSharedState, meterSharedState, name) + : NOOP_METER.gaugeBuilder(NOOP_INSTRUMENT_NAME); } @Override @@ -117,6 +119,9 @@ public BatchCallback batchCallback( Runnable callback, ObservableMeasurement observableMeasurement, ObservableMeasurement... additionalMeasurements) { + if (!meterConfig.isEnabled()) { + return NOOP_METER.batchCallback(callback, observableMeasurement, additionalMeasurements); + } Set measurements = new HashSet<>(); measurements.add(observableMeasurement); Collections.addAll(measurements, additionalMeasurements); 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 63b241bc5b1..6990ea30cac 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 @@ -11,11 +11,14 @@ import io.opentelemetry.api.metrics.MeterProvider; import io.opentelemetry.sdk.common.Clock; import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.internal.ComponentRegistry; +import io.opentelemetry.sdk.internal.ScopeConfigurator; import io.opentelemetry.sdk.metrics.data.MetricData; import io.opentelemetry.sdk.metrics.export.CollectionRegistration; import io.opentelemetry.sdk.metrics.export.MetricProducer; import io.opentelemetry.sdk.metrics.export.MetricReader; +import io.opentelemetry.sdk.metrics.internal.MeterConfig; import io.opentelemetry.sdk.metrics.internal.SdkMeterProviderUtil; import io.opentelemetry.sdk.metrics.internal.exemplar.ExemplarFilter; import io.opentelemetry.sdk.metrics.internal.export.CardinalityLimitSelector; @@ -49,6 +52,7 @@ public final class SdkMeterProvider implements MeterProvider, Closeable { private final List metricProducers; private final MeterProviderSharedState sharedState; private final ComponentRegistry registry; + private final ScopeConfigurator meterConfigurator; private final AtomicBoolean isClosed = new AtomicBoolean(false); /** Returns a new {@link SdkMeterProviderBuilder} for {@link SdkMeterProvider}. */ @@ -62,7 +66,8 @@ public static SdkMeterProviderBuilder builder() { List metricProducers, Clock clock, Resource resource, - ExemplarFilter exemplarFilter) { + ExemplarFilter exemplarFilter, + ScopeConfigurator meterConfigurator) { long startEpochNanos = clock.now(); this.registeredViews = registeredViews; this.registeredReaders = @@ -79,7 +84,12 @@ public static SdkMeterProviderBuilder builder() { this.registry = new ComponentRegistry<>( instrumentationLibraryInfo -> - new SdkMeter(sharedState, instrumentationLibraryInfo, registeredReaders)); + new SdkMeter( + sharedState, + instrumentationLibraryInfo, + registeredReaders, + getMeterConfig(instrumentationLibraryInfo))); + this.meterConfigurator = meterConfigurator; for (RegisteredReader registeredReader : registeredReaders) { List readerMetricProducers = new ArrayList<>(metricProducers); readerMetricProducers.add(new LeasedMetricProducer(registry, sharedState, registeredReader)); @@ -90,6 +100,11 @@ public static SdkMeterProviderBuilder builder() { } } + private MeterConfig getMeterConfig(InstrumentationScopeInfo instrumentationScopeInfo) { + MeterConfig meterConfig = meterConfigurator.apply(instrumentationScopeInfo); + return meterConfig == null ? MeterConfig.defaultConfig() : meterConfig; + } + @Override public MeterBuilder meterBuilder(String instrumentationScopeName) { if (registeredReaders.isEmpty()) { 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 3e444f10f93..9317c26e6c9 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 @@ -6,8 +6,12 @@ package io.opentelemetry.sdk.metrics; import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.internal.ScopeConfigurator; +import io.opentelemetry.sdk.internal.ScopeConfiguratorBuilder; import io.opentelemetry.sdk.metrics.export.MetricProducer; import io.opentelemetry.sdk.metrics.export.MetricReader; +import io.opentelemetry.sdk.metrics.internal.MeterConfig; import io.opentelemetry.sdk.metrics.internal.SdkMeterProviderUtil; import io.opentelemetry.sdk.metrics.internal.debug.SourceInfo; import io.opentelemetry.sdk.metrics.internal.exemplar.ExemplarFilter; @@ -18,6 +22,7 @@ import java.util.IdentityHashMap; import java.util.List; import java.util.Objects; +import java.util.function.Predicate; /** * Builder class for the {@link SdkMeterProvider}. @@ -40,6 +45,8 @@ public final class SdkMeterProviderBuilder { private final List metricProducers = new ArrayList<>(); private final List registeredViews = new ArrayList<>(); private ExemplarFilter exemplarFilter = DEFAULT_EXEMPLAR_FILTER; + private ScopeConfiguratorBuilder meterConfiguratorBuilder = + MeterConfig.configuratorBuilder(); SdkMeterProviderBuilder() {} @@ -150,9 +157,48 @@ public SdkMeterProviderBuilder registerMetricProducer(MetricProducer metricProdu return this; } + /** + * Set the meter configurator, which computes {@link MeterConfig} for each {@link + * InstrumentationScopeInfo}. + * + *

Overrides any matchers added via {@link #addMeterConfiguratorCondition(Predicate, + * MeterConfig)}. + * + * @see MeterConfig#configuratorBuilder() + */ + SdkMeterProviderBuilder setMeterConfigurator(ScopeConfigurator meterConfigurator) { + this.meterConfiguratorBuilder = meterConfigurator.toBuilder(); + return this; + } + + /** + * Adds a condition to the meter configurator, which computes {@link MeterConfig} for each {@link + * InstrumentationScopeInfo}. + * + *

Applies after any previously added conditions. + * + *

If {@link #setMeterConfigurator(ScopeConfigurator)} was previously called, this condition + * will only be applied if the {@link ScopeConfigurator#apply(Object)} returns null for the + * matched {@link InstrumentationScopeInfo}(s). + * + * @see ScopeConfiguratorBuilder#nameEquals(String) + * @see ScopeConfiguratorBuilder#nameMatchesGlob(String) + */ + SdkMeterProviderBuilder addMeterConfiguratorCondition( + Predicate scopeMatcher, MeterConfig meterConfig) { + this.meterConfiguratorBuilder.addCondition(scopeMatcher, meterConfig); + return this; + } + /** Returns an {@link SdkMeterProvider} built with the configuration of this builder. */ public SdkMeterProvider build() { return new SdkMeterProvider( - registeredViews, metricReaders, metricProducers, clock, resource, exemplarFilter); + registeredViews, + metricReaders, + metricProducers, + clock, + resource, + exemplarFilter, + meterConfiguratorBuilder.build()); } } diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/MeterConfig.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/MeterConfig.java new file mode 100644 index 00000000000..12d2c1df1b1 --- /dev/null +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/MeterConfig.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.internal; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.internal.ScopeConfigurator; +import io.opentelemetry.sdk.internal.ScopeConfiguratorBuilder; +import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; +import java.util.function.Predicate; +import javax.annotation.concurrent.Immutable; + +/** + * A collection of configuration options which define the behavior of a {@link Meter}. + * + * @see SdkMeterProviderUtil#setMeterConfigurator(SdkMeterProviderBuilder, ScopeConfigurator) + * @see SdkMeterProviderUtil#addMeterConfiguratorCondition(SdkMeterProviderBuilder, Predicate, + * MeterConfig) + */ +@AutoValue +@Immutable +public abstract class MeterConfig { + + private static final MeterConfig DEFAULT_CONFIG = new AutoValue_MeterConfig(/* enabled= */ true); + private static final MeterConfig DISABLED_CONFIG = + new AutoValue_MeterConfig(/* enabled= */ false); + + /** Returns a disabled {@link MeterConfig}. */ + public static MeterConfig disabled() { + return DISABLED_CONFIG; + } + + /** Returns an enabled {@link MeterConfig}. */ + public static MeterConfig enabled() { + return DEFAULT_CONFIG; + } + + /** + * Returns the default {@link MeterConfig}, which is used when no configurator is set or when the + * meter configurator returns {@code null} for a {@link InstrumentationScopeInfo}. + */ + public static MeterConfig defaultConfig() { + return DEFAULT_CONFIG; + } + + /** + * Create a {@link ScopeConfiguratorBuilder} for configuring {@link + * SdkMeterProviderUtil#setMeterConfigurator(SdkMeterProviderBuilder, ScopeConfigurator)}. + */ + public static ScopeConfiguratorBuilder configuratorBuilder() { + return ScopeConfigurator.builder(); + } + + MeterConfig() {} + + /** Returns {@code true} if this meter is enabled. Defaults to {@code true}. */ + public abstract boolean isEnabled(); +} 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 ecaf5e388ca..e27856881e1 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 @@ -5,6 +5,8 @@ package io.opentelemetry.sdk.metrics.internal; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.internal.ScopeConfigurator; import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; import io.opentelemetry.sdk.metrics.ViewBuilder; @@ -18,8 +20,11 @@ import java.util.function.Predicate; /** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. + * A collection of methods that allow use of experimental features prior to availability in public + * APIs. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. */ public final class SdkMeterProviderUtil { @@ -66,6 +71,39 @@ public static void registerMetricReaderWithCardinalitySelector( } } + /** Reflectively set the {@link ScopeConfigurator} to the {@link SdkMeterProviderBuilder}. */ + public static void setMeterConfigurator( + SdkMeterProviderBuilder sdkMeterProviderBuilder, + ScopeConfigurator meterConfigurator) { + try { + Method method = + SdkMeterProviderBuilder.class.getDeclaredMethod( + "setMeterConfigurator", ScopeConfigurator.class); + method.setAccessible(true); + method.invoke(sdkMeterProviderBuilder, meterConfigurator); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException( + "Error calling setMeterConfigurator on SdkMeterProviderBuilder", e); + } + } + + /** Reflectively add a tracer configurator condition to the {@link SdkMeterProviderBuilder}. */ + public static void addMeterConfiguratorCondition( + SdkMeterProviderBuilder sdkMeterProviderBuilder, + Predicate scopeMatcher, + MeterConfig meterConfig) { + try { + Method method = + SdkMeterProviderBuilder.class.getDeclaredMethod( + "addMeterConfiguratorCondition", Predicate.class, MeterConfig.class); + method.setAccessible(true); + method.invoke(sdkMeterProviderBuilder, scopeMatcher, meterConfig); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException( + "Error calling addMeterConfiguratorCondition on SdkMeterProviderBuilder", e); + } + } + /** * Reflectively add an {@link AttributesProcessor} to the {@link ViewBuilder} which appends * key-values from baggage to all measurements. diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/MeterProviderSharedState.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/MeterProviderSharedState.java index 9b185b36a84..8f3647c6d46 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/MeterProviderSharedState.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/MeterProviderSharedState.java @@ -21,9 +21,12 @@ @AutoValue @Immutable public abstract class MeterProviderSharedState { + public static MeterProviderSharedState create( Clock clock, Resource resource, ExemplarFilter exemplarFilter, long startEpochNanos) { - return new AutoValue_MeterProviderSharedState(clock, resource, startEpochNanos, exemplarFilter); + MeterProviderSharedState sharedState = + new AutoValue_MeterProviderSharedState(clock, resource, startEpochNanos, exemplarFilter); + return sharedState; } MeterProviderSharedState() {} 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 9e5bba37233..4df9f1a35ec 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 @@ -9,6 +9,7 @@ import static java.util.Objects.requireNonNull; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.internal.GlobUtil; import io.opentelemetry.sdk.metrics.Aggregation; import io.opentelemetry.sdk.metrics.InstrumentSelector; import io.opentelemetry.sdk.metrics.InstrumentType; @@ -27,10 +28,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.function.Predicate; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.regex.Pattern; import javax.annotation.concurrent.Immutable; /** @@ -170,7 +169,8 @@ private static boolean matchesSelector( return false; } if (selector.getInstrumentName() != null - && !toGlobPatternPredicate(selector.getInstrumentName()).test(descriptor.getName())) { + && !GlobUtil.toGlobPatternPredicate(selector.getInstrumentName()) + .test(descriptor.getName())) { return false; } return matchesMeter(selector, meterScope); @@ -190,69 +190,6 @@ private static boolean matchesMeter( || selector.getMeterSchemaUrl().equals(meterScope.getSchemaUrl()); } - /** - * Return a predicate that returns {@code true} if a string matches the {@code globPattern}. - * - *

{@code globPattern} may contain the wildcard characters {@code *} and {@code ?} with the - * following matching criteria: - * - *

    - *
  • {@code *} matches 0 or more instances of any character - *
  • {@code ?} matches exactly one instance of any character - *
- */ - // Visible for testing - static Predicate toGlobPatternPredicate(String globPattern) { - // Match all - if (globPattern.equals("*")) { - return unused -> true; - } - - // If globPattern contains '*' or '?', convert it to a regex and return corresponding predicate - for (int i = 0; i < globPattern.length(); i++) { - char c = globPattern.charAt(i); - if (c == '*' || c == '?') { - Pattern pattern = toRegexPattern(globPattern); - return string -> pattern.matcher(string).matches(); - } - } - - // Exact match, ignoring case - return globPattern::equalsIgnoreCase; - } - - /** - * Transform the {@code globPattern} to a regex by converting {@code *} to {@code .*}, {@code ?} - * to {@code .}, and escaping other regex special characters. - */ - private static Pattern toRegexPattern(String globPattern) { - int tokenStart = -1; - StringBuilder patternBuilder = new StringBuilder(); - for (int i = 0; i < globPattern.length(); i++) { - char c = globPattern.charAt(i); - if (c == '*' || c == '?') { - if (tokenStart != -1) { - patternBuilder.append(Pattern.quote(globPattern.substring(tokenStart, i))); - tokenStart = -1; - } - if (c == '*') { - patternBuilder.append(".*"); - } else { - // c == '?' - patternBuilder.append("."); - } - } else { - if (tokenStart == -1) { - tokenStart = i; - } - } - } - if (tokenStart != -1) { - patternBuilder.append(Pattern.quote(globPattern.substring(tokenStart))); - } - return Pattern.compile(patternBuilder.toString()); - } - private static RegisteredView applyAdviceToDefaultView( RegisteredView instrumentDefaultView, Advice advice) { return RegisteredView.create( diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/MeterConfigTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/MeterConfigTest.java new file mode 100644 index 00000000000..3b78f2abd0d --- /dev/null +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/MeterConfigTest.java @@ -0,0 +1,148 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import static io.opentelemetry.sdk.internal.ScopeConfiguratorBuilder.nameEquals; +import static io.opentelemetry.sdk.internal.ScopeConfiguratorBuilder.nameMatchesGlob; +import static io.opentelemetry.sdk.metrics.internal.MeterConfig.defaultConfig; +import static io.opentelemetry.sdk.metrics.internal.MeterConfig.disabled; +import static io.opentelemetry.sdk.metrics.internal.MeterConfig.enabled; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; + +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.internal.ScopeConfigurator; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.internal.MeterConfig; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class MeterConfigTest { + + @Test + void disableScopes() { + InMemoryMetricReader reader = InMemoryMetricReader.create(); + SdkMeterProvider meterProvider = + SdkMeterProvider.builder() + // Disable meterB. Since meters are enabled by default, meterA and meterC are enabled. + .addMeterConfiguratorCondition(nameEquals("meterB"), disabled()) + .registerMetricReader(reader) + .build(); + + Meter meterA = meterProvider.get("meterA"); + Meter meterB = meterProvider.get("meterB"); + Meter meterC = meterProvider.get("meterC"); + + meterA.counterBuilder("counterA").build().add(1); + meterA.counterBuilder("asyncCounterA").buildWithCallback(observable -> observable.record(1)); + meterA.upDownCounterBuilder("upDownCounterA").build().add(1); + meterA + .upDownCounterBuilder("asyncUpDownCounterA") + .buildWithCallback(observable -> observable.record(1)); + meterA.histogramBuilder("histogramA").build().record(1.0); + meterA.gaugeBuilder("gaugeA").buildWithCallback(observable -> observable.record(1.0)); + + meterB.counterBuilder("counterB").build().add(1); + meterB.counterBuilder("asyncCounterB").buildWithCallback(observable -> observable.record(1)); + meterB.upDownCounterBuilder("upDownCounterB").build().add(1); + meterB + .upDownCounterBuilder("asyncUpDownCounterB") + .buildWithCallback(observable -> observable.record(1)); + meterB.histogramBuilder("histogramB").build().record(1.0); + meterB.gaugeBuilder("gaugeB").buildWithCallback(observable -> observable.record(1.0)); + + meterC.counterBuilder("counterC").build().add(1); + meterC.counterBuilder("asyncCounterC").buildWithCallback(observable -> observable.record(1)); + meterC.upDownCounterBuilder("upDownCounterC").build().add(1); + meterC + .upDownCounterBuilder("asyncUpDownCounterC") + .buildWithCallback(observable -> observable.record(1)); + meterC.histogramBuilder("histogramC").build().record(1.0); + meterC.gaugeBuilder("gaugeC").buildWithCallback(observable -> observable.record(1.0)); + + // Only metrics from meterA and meterC should be seen + assertThat(reader.collectAllMetrics()) + .satisfies( + metrics -> { + Map> metricsByScope = + metrics.stream() + .collect(Collectors.groupingBy(MetricData::getInstrumentationScopeInfo)); + assertThat(metricsByScope.get(InstrumentationScopeInfo.create("meterA"))).hasSize(6); + assertThat(metricsByScope.get(InstrumentationScopeInfo.create("meterB"))).isNull(); + assertThat(metricsByScope.get(InstrumentationScopeInfo.create("meterC"))).hasSize(6); + }); + } + + @ParameterizedTest + @MethodSource("meterConfiguratorArgs") + void meterConfigurator( + ScopeConfigurator meterConfigurator, + InstrumentationScopeInfo scope, + MeterConfig expectedMeterConfig) { + MeterConfig meterConfig = meterConfigurator.apply(scope); + meterConfig = meterConfig == null ? defaultConfig() : meterConfig; + assertThat(meterConfig).isEqualTo(expectedMeterConfig); + } + + private static final InstrumentationScopeInfo scopeCat = InstrumentationScopeInfo.create("cat"); + private static final InstrumentationScopeInfo scopeDog = InstrumentationScopeInfo.create("dog"); + private static final InstrumentationScopeInfo scopeDuck = InstrumentationScopeInfo.create("duck"); + + private static Stream meterConfiguratorArgs() { + ScopeConfigurator defaultConfigurator = MeterConfig.configuratorBuilder().build(); + ScopeConfigurator disableCat = + MeterConfig.configuratorBuilder() + .addCondition(nameEquals("cat"), MeterConfig.disabled()) + // Second matching rule for cat should be ignored + .addCondition(nameEquals("cat"), enabled()) + .build(); + ScopeConfigurator disableStartsWithD = + MeterConfig.configuratorBuilder() + .addCondition(nameMatchesGlob("d*"), MeterConfig.disabled()) + .build(); + ScopeConfigurator enableCat = + MeterConfig.configuratorBuilder() + .setDefault(MeterConfig.disabled()) + .addCondition(nameEquals("cat"), enabled()) + // Second matching rule for cat should be ignored + .addCondition(nameEquals("cat"), MeterConfig.disabled()) + .build(); + ScopeConfigurator enableStartsWithD = + MeterConfig.configuratorBuilder() + .setDefault(MeterConfig.disabled()) + .addCondition(nameMatchesGlob("d*"), MeterConfig.enabled()) + .build(); + + return Stream.of( + // default + Arguments.of(defaultConfigurator, scopeCat, defaultConfig()), + Arguments.of(defaultConfigurator, scopeDog, defaultConfig()), + Arguments.of(defaultConfigurator, scopeDuck, defaultConfig()), + // default enabled, disable cat + Arguments.of(disableCat, scopeCat, MeterConfig.disabled()), + Arguments.of(disableCat, scopeDog, enabled()), + Arguments.of(disableCat, scopeDuck, enabled()), + // default enabled, disable pattern + Arguments.of(disableStartsWithD, scopeCat, enabled()), + Arguments.of(disableStartsWithD, scopeDog, MeterConfig.disabled()), + Arguments.of(disableStartsWithD, scopeDuck, MeterConfig.disabled()), + // default disabled, enable cat + Arguments.of(enableCat, scopeCat, enabled()), + Arguments.of(enableCat, scopeDog, MeterConfig.disabled()), + Arguments.of(enableCat, scopeDuck, MeterConfig.disabled()), + // default disabled, enable pattern + Arguments.of(enableStartsWithD, scopeCat, MeterConfig.disabled()), + Arguments.of(enableStartsWithD, scopeDog, enabled()), + Arguments.of(enableStartsWithD, scopeDuck, enabled())); + } +} 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 abba1c32133..d2f5925873c 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,7 +7,6 @@ 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.toGlobPatternPredicate; import static org.assertj.core.api.Assertions.assertThat; import io.github.netmikey.logunit.api.LogCapturer; @@ -544,26 +543,4 @@ void findViews_ApplyAdvice() { INSTRUMENTATION_SCOPE_INFO)) .isEqualTo(Collections.singletonList(DEFAULT_REGISTERED_VIEW)); } - - @Test - void matchesName() { - assertThat(toGlobPatternPredicate("foo").test("foo")).isTrue(); - assertThat(toGlobPatternPredicate("foo").test("Foo")).isTrue(); - assertThat(toGlobPatternPredicate("foo").test("bar")).isFalse(); - assertThat(toGlobPatternPredicate("fo?").test("foo")).isTrue(); - assertThat(toGlobPatternPredicate("fo??").test("fooo")).isTrue(); - assertThat(toGlobPatternPredicate("fo?").test("fob")).isTrue(); - assertThat(toGlobPatternPredicate("fo?").test("fooo")).isFalse(); - assertThat(toGlobPatternPredicate("*").test("foo")).isTrue(); - assertThat(toGlobPatternPredicate("*").test("bar")).isTrue(); - assertThat(toGlobPatternPredicate("*").test("baz")).isTrue(); - assertThat(toGlobPatternPredicate("*").test("foo.bar.baz")).isTrue(); - assertThat(toGlobPatternPredicate("fo*").test("fo")).isTrue(); - assertThat(toGlobPatternPredicate("fo*").test("foo")).isTrue(); - assertThat(toGlobPatternPredicate("fo*").test("fooo")).isTrue(); - assertThat(toGlobPatternPredicate("fo*").test("foo.bar.baz")).isTrue(); - assertThat(toGlobPatternPredicate("f()[]$^.{}|").test("f()[]$^.{}|")).isTrue(); - assertThat(toGlobPatternPredicate("f()[]$^.{}|?").test("f()[]$^.{}|o")).isTrue(); - assertThat(toGlobPatternPredicate("f()[]$^.{}|*").test("f()[]$^.{}|ooo")).isTrue(); - } } diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracer.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracer.java index 81282097cd1..a0b2704fc72 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracer.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracer.java @@ -9,21 +9,31 @@ import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.api.trace.TracerProvider; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.trace.internal.TracerConfig; /** {@link SdkTracer} is SDK implementation of {@link Tracer}. */ final class SdkTracer implements Tracer { static final String FALLBACK_SPAN_NAME = ""; + private static final Tracer NOOP_TRACER = TracerProvider.noop().get("noop"); private final TracerSharedState sharedState; private final InstrumentationScopeInfo instrumentationScopeInfo; + private final TracerConfig tracerConfig; - SdkTracer(TracerSharedState sharedState, InstrumentationScopeInfo instrumentationScopeInfo) { + SdkTracer( + TracerSharedState sharedState, + InstrumentationScopeInfo instrumentationScopeInfo, + TracerConfig tracerConfig) { this.sharedState = sharedState; this.instrumentationScopeInfo = instrumentationScopeInfo; + this.tracerConfig = tracerConfig; } @Override public SpanBuilder spanBuilder(String spanName) { + if (!tracerConfig.isEnabled()) { + return NOOP_TRACER.spanBuilder(spanName); + } if (spanName == null || spanName.trim().isEmpty()) { spanName = FALLBACK_SPAN_NAME; } diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProvider.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProvider.java index 2e07af579a9..036e812c5ab 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProvider.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProvider.java @@ -10,8 +10,11 @@ import io.opentelemetry.api.trace.TracerProvider; import io.opentelemetry.sdk.common.Clock; import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.internal.ComponentRegistry; +import io.opentelemetry.sdk.internal.ScopeConfigurator; import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.internal.TracerConfig; import io.opentelemetry.sdk.trace.samplers.Sampler; import java.io.Closeable; import java.util.List; @@ -27,6 +30,7 @@ public final class SdkTracerProvider implements TracerProvider, Closeable { static final String DEFAULT_TRACER_NAME = ""; private final TracerSharedState sharedState; private final ComponentRegistry tracerSdkComponentRegistry; + private final ScopeConfigurator tracerConfigurator; /** * Returns a new {@link SdkTracerProviderBuilder} for {@link SdkTracerProvider}. @@ -37,19 +41,31 @@ public static SdkTracerProviderBuilder builder() { return new SdkTracerProviderBuilder(); } + @SuppressWarnings("NonApiType") SdkTracerProvider( Clock clock, IdGenerator idsGenerator, Resource resource, Supplier spanLimitsSupplier, Sampler sampler, - List spanProcessors) { + List spanProcessors, + ScopeConfigurator tracerConfigurator) { this.sharedState = new TracerSharedState( clock, idsGenerator, resource, spanLimitsSupplier, sampler, spanProcessors); this.tracerSdkComponentRegistry = new ComponentRegistry<>( - instrumentationScopeInfo -> new SdkTracer(sharedState, instrumentationScopeInfo)); + instrumentationScopeInfo -> + new SdkTracer( + sharedState, + instrumentationScopeInfo, + getTracerConfig(instrumentationScopeInfo))); + this.tracerConfigurator = tracerConfigurator; + } + + private TracerConfig getTracerConfig(InstrumentationScopeInfo instrumentationScopeInfo) { + TracerConfig tracerConfig = tracerConfigurator.apply(instrumentationScopeInfo); + return tracerConfig == null ? TracerConfig.defaultConfig() : tracerConfig; } @Override diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilder.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilder.java index 20dd76536d5..f84c7f79855 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilder.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilder.java @@ -8,11 +8,16 @@ import static java.util.Objects.requireNonNull; import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.internal.ScopeConfigurator; +import io.opentelemetry.sdk.internal.ScopeConfiguratorBuilder; import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.internal.TracerConfig; import io.opentelemetry.sdk.trace.samplers.Sampler; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.function.Predicate; import java.util.function.Supplier; /** Builder of {@link SdkTracerProvider}. */ @@ -26,6 +31,8 @@ public final class SdkTracerProviderBuilder { private Resource resource = Resource.getDefault(); private Supplier spanLimitsSupplier = SpanLimits::getDefault; private Sampler sampler = DEFAULT_SAMPLER; + private ScopeConfiguratorBuilder tracerConfiguratorBuilder = + TracerConfig.configuratorBuilder(); /** * Assign a {@link Clock}. {@link Clock} will be used each time a {@link @@ -147,6 +154,40 @@ public SdkTracerProviderBuilder addSpanProcessor(SpanProcessor spanProcessor) { return this; } + /** + * Set the tracer configurator, which computes {@link TracerConfig} for each {@link + * InstrumentationScopeInfo}. + * + *

Overrides any matchers added via {@link #addTracerConfiguratorCondition(Predicate, + * TracerConfig)}. + * + * @see TracerConfig#configuratorBuilder() + */ + SdkTracerProviderBuilder setTracerConfigurator( + ScopeConfigurator tracerConfigurator) { + this.tracerConfiguratorBuilder = tracerConfigurator.toBuilder(); + return this; + } + + /** + * Adds a condition to the tracer configurator, which computes {@link TracerConfig} for each + * {@link InstrumentationScopeInfo}. + * + *

Applies after any previously added conditions. + * + *

If {@link #setTracerConfigurator(ScopeConfigurator)} was previously called, this condition + * will only be applied if the {@link ScopeConfigurator#apply(Object)} returns null for the + * matched {@link InstrumentationScopeInfo}(s). + * + * @see ScopeConfiguratorBuilder#nameEquals(String) + * @see ScopeConfiguratorBuilder#nameMatchesGlob(String) + */ + SdkTracerProviderBuilder addTracerConfiguratorCondition( + Predicate scopeMatcher, TracerConfig tracerConfig) { + this.tracerConfiguratorBuilder.addCondition(scopeMatcher, tracerConfig); + return this; + } + /** * Create a new {@link SdkTracerProvider} instance with the configuration. * @@ -154,7 +195,13 @@ public SdkTracerProviderBuilder addSpanProcessor(SpanProcessor spanProcessor) { */ public SdkTracerProvider build() { return new SdkTracerProvider( - clock, idsGenerator, resource, spanLimitsSupplier, sampler, spanProcessors); + clock, + idsGenerator, + resource, + spanLimitsSupplier, + sampler, + spanProcessors, + tracerConfiguratorBuilder.build()); } SdkTracerProviderBuilder() {} diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/TracerSharedState.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/TracerSharedState.java index 99cd1ea26c0..3d07a2853f8 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/TracerSharedState.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/TracerSharedState.java @@ -15,6 +15,7 @@ // Represents the shared state/config between all Tracers created by the same TracerProvider. final class TracerSharedState { + private final Object lock = new Object(); private final Clock clock; private final IdGenerator idGenerator; diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/SdkTracerProviderUtil.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/SdkTracerProviderUtil.java new file mode 100644 index 00000000000..7d00f230dea --- /dev/null +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/SdkTracerProviderUtil.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.internal; + +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.internal.ScopeConfigurator; +import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.function.Predicate; + +/** + * A collection of methods that allow use of experimental features prior to availability in public + * APIs. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class SdkTracerProviderUtil { + + private SdkTracerProviderUtil() {} + + /** Reflectively set the {@link ScopeConfigurator} to the {@link SdkTracerProviderBuilder}. */ + public static void setTracerConfigurator( + SdkTracerProviderBuilder sdkTracerProviderBuilder, + ScopeConfigurator tracerConfigurator) { + try { + Method method = + SdkTracerProviderBuilder.class.getDeclaredMethod( + "setTracerConfigurator", ScopeConfigurator.class); + method.setAccessible(true); + method.invoke(sdkTracerProviderBuilder, tracerConfigurator); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException( + "Error calling setTracerConfigurator on SdkTracerProviderBuilder", e); + } + } + + /** Reflectively add a tracer configurator condition to the {@link SdkTracerProviderBuilder}. */ + public static void addTracerConfiguratorCondition( + SdkTracerProviderBuilder sdkTracerProviderBuilder, + Predicate scopeMatcher, + TracerConfig tracerConfig) { + try { + Method method = + SdkTracerProviderBuilder.class.getDeclaredMethod( + "addTracerConfiguratorCondition", Predicate.class, TracerConfig.class); + method.setAccessible(true); + method.invoke(sdkTracerProviderBuilder, scopeMatcher, tracerConfig); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException( + "Error calling addTracerConfiguratorCondition on SdkTracerProviderBuilder", e); + } + } +} diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/TracerConfig.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/TracerConfig.java new file mode 100644 index 00000000000..d019055b36a --- /dev/null +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/TracerConfig.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.internal; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.internal.ScopeConfigurator; +import io.opentelemetry.sdk.internal.ScopeConfiguratorBuilder; +import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; +import java.util.function.Predicate; +import javax.annotation.concurrent.Immutable; + +/** + * A collection of configuration options which define the behavior of a {@link Tracer}. + * + * @see SdkTracerProviderUtil#setTracerConfigurator(SdkTracerProviderBuilder, ScopeConfigurator) + * @see SdkTracerProviderUtil#addTracerConfiguratorCondition(SdkTracerProviderBuilder, Predicate, + * TracerConfig) + */ +@AutoValue +@Immutable +public abstract class TracerConfig { + + private static final TracerConfig DEFAULT_CONFIG = + new AutoValue_TracerConfig(/* enabled= */ true); + private static final TracerConfig DISABLED_CONFIG = + new AutoValue_TracerConfig(/* enabled= */ false); + + /** Returns a disabled {@link TracerConfig}. */ + public static TracerConfig disabled() { + return DISABLED_CONFIG; + } + + /** Returns an enabled {@link TracerConfig}. */ + public static TracerConfig enabled() { + return DEFAULT_CONFIG; + } + + /** + * Returns the default {@link TracerConfig}, which is used when no configurator is set or when the + * tracer configurator returns {@code null} for a {@link InstrumentationScopeInfo}. + */ + public static TracerConfig defaultConfig() { + return DEFAULT_CONFIG; + } + + /** + * Create a {@link ScopeConfiguratorBuilder} for configuring {@link + * SdkTracerProviderUtil#setTracerConfigurator(SdkTracerProviderBuilder, ScopeConfigurator)}. + */ + public static ScopeConfiguratorBuilder configuratorBuilder() { + return ScopeConfigurator.builder(); + } + + TracerConfig() {} + + /** Returns {@code true} if this tracer is enabled. Defaults to {@code true}. */ + public abstract boolean isEnabled(); +} diff --git a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/TracerConfigTest.java b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/TracerConfigTest.java new file mode 100644 index 00000000000..4df99b4c999 --- /dev/null +++ b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/TracerConfigTest.java @@ -0,0 +1,156 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import static io.opentelemetry.sdk.internal.ScopeConfiguratorBuilder.nameEquals; +import static io.opentelemetry.sdk.internal.ScopeConfiguratorBuilder.nameMatchesGlob; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.trace.internal.TracerConfig.defaultConfig; +import static io.opentelemetry.sdk.trace.internal.TracerConfig.disabled; +import static io.opentelemetry.sdk.trace.internal.TracerConfig.enabled; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.internal.ScopeConfigurator; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import io.opentelemetry.sdk.trace.internal.TracerConfig; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class TracerConfigTest { + + @Test + void disableScopes() throws InterruptedException { + InMemorySpanExporter exporter = InMemorySpanExporter.create(); + SdkTracerProvider tracerProvider = + SdkTracerProvider.builder() + // Disable tracerB. Since tracers are enabled by default, tracerA and tracerC are + // enabled. + .addTracerConfiguratorCondition(nameEquals("tracerB"), disabled()) + .addSpanProcessor(SimpleSpanProcessor.create(exporter)) + .build(); + + Tracer tracerA = tracerProvider.get("tracerA"); + Tracer tracerB = tracerProvider.get("tracerB"); + Tracer tracerC = tracerProvider.get("tracerC"); + + Span parent; + Span child; + Span grandchild; + + parent = tracerA.spanBuilder("parent").startSpan(); + try (Scope parentScope = parent.makeCurrent()) { + parent.setAttribute("a", "1"); + child = tracerB.spanBuilder("child").startSpan(); + // tracerB is disabled and should behave the same as noop tracer + assertThat(child.getSpanContext()).isEqualTo(parent.getSpanContext()); + assertThat(child.isRecording()).isFalse(); + try (Scope childScope = child.makeCurrent()) { + child.setAttribute("b", "1"); + grandchild = tracerC.spanBuilder("grandchild").startSpan(); + try (Scope grandchildScope = grandchild.makeCurrent()) { + grandchild.setAttribute("c", "1"); + Thread.sleep(100); + } finally { + grandchild.end(); + } + } finally { + child.end(); + } + } finally { + parent.end(); + } + + // Only contain tracerA:parent and tracerC:child should be seen + // tracerC:grandchild should list tracerA:parent as its parent + assertThat(exporter.getFinishedSpanItems()) + .satisfiesExactlyInAnyOrder( + spanData -> + assertThat(spanData) + .hasInstrumentationScopeInfo(InstrumentationScopeInfo.create("tracerA")) + .hasName("parent") + .hasSpanId(parent.getSpanContext().getSpanId()) + .hasParentSpanId(SpanId.getInvalid()) + .hasAttributes(Attributes.builder().put("a", "1").build()), + spanData -> + assertThat(spanData) + .hasInstrumentationScopeInfo(InstrumentationScopeInfo.create("tracerC")) + .hasName("grandchild") + .hasSpanId(grandchild.getSpanContext().getSpanId()) + .hasParentSpanId(parent.getSpanContext().getSpanId()) + .hasAttributes(Attributes.builder().put("c", "1").build())); + } + + @ParameterizedTest + @MethodSource("tracerConfiguratorArgs") + void tracerConfigurator( + ScopeConfigurator tracerConfigurator, + InstrumentationScopeInfo scope, + TracerConfig expectedTracerConfig) { + TracerConfig tracerConfig = tracerConfigurator.apply(scope); + tracerConfig = tracerConfig == null ? defaultConfig() : tracerConfig; + assertThat(tracerConfig).isEqualTo(expectedTracerConfig); + } + + private static final InstrumentationScopeInfo scopeCat = InstrumentationScopeInfo.create("cat"); + private static final InstrumentationScopeInfo scopeDog = InstrumentationScopeInfo.create("dog"); + private static final InstrumentationScopeInfo scopeDuck = InstrumentationScopeInfo.create("duck"); + + private static Stream tracerConfiguratorArgs() { + ScopeConfigurator defaultConfigurator = + TracerConfig.configuratorBuilder().build(); + ScopeConfigurator disableCat = + TracerConfig.configuratorBuilder() + .addCondition(nameEquals("cat"), disabled()) + // Second matching rule for cat should be ignored + .addCondition(nameEquals("cat"), enabled()) + .build(); + ScopeConfigurator disableStartsWithD = + TracerConfig.configuratorBuilder().addCondition(nameMatchesGlob("d*"), disabled()).build(); + ScopeConfigurator enableCat = + TracerConfig.configuratorBuilder() + .setDefault(disabled()) + .addCondition(nameEquals("cat"), enabled()) + // Second matching rule for cat should be ignored + .addCondition(nameEquals("cat"), disabled()) + .build(); + ScopeConfigurator enableStartsWithD = + TracerConfig.configuratorBuilder() + .setDefault(disabled()) + .addCondition(nameMatchesGlob("d*"), TracerConfig.enabled()) + .build(); + + return Stream.of( + // default + Arguments.of(defaultConfigurator, scopeCat, defaultConfig()), + Arguments.of(defaultConfigurator, scopeDog, defaultConfig()), + Arguments.of(defaultConfigurator, scopeDuck, defaultConfig()), + // default enabled, disable cat + Arguments.of(disableCat, scopeCat, disabled()), + Arguments.of(disableCat, scopeDog, enabled()), + Arguments.of(disableCat, scopeDuck, enabled()), + // default enabled, disable pattern + Arguments.of(disableStartsWithD, scopeCat, enabled()), + Arguments.of(disableStartsWithD, scopeDog, disabled()), + Arguments.of(disableStartsWithD, scopeDuck, disabled()), + // default disabled, enable cat + Arguments.of(enableCat, scopeCat, enabled()), + Arguments.of(enableCat, scopeDog, disabled()), + Arguments.of(enableCat, scopeDuck, disabled()), + // default disabled, enable pattern + Arguments.of(enableStartsWithD, scopeCat, disabled()), + Arguments.of(enableStartsWithD, scopeDog, enabled()), + Arguments.of(enableStartsWithD, scopeDuck, enabled())); + } +}