diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-exporter-zipkin.txt b/docs/apidiffs/current_vs_latest/opentelemetry-exporter-zipkin.txt index b941adbcdd2..df26146497b 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-exporter-zipkin.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-exporter-zipkin.txt @@ -1,9 +1,2 @@ Comparing source compatibility of against -*** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.exporter.zipkin.ZipkinSpanExporterBuilder (not serializable) - === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 - === UNCHANGED METHOD: PUBLIC io.opentelemetry.exporter.zipkin.ZipkinSpanExporterBuilder setEncoder(zipkin2.codec.BytesEncoder) - +++ NEW ANNOTATION: java.lang.Deprecated - +++ NEW METHOD: PUBLIC(+) io.opentelemetry.exporter.zipkin.ZipkinSpanExporterBuilder setEncoder(zipkin2.reporter.BytesEncoder) - === UNCHANGED METHOD: PUBLIC io.opentelemetry.exporter.zipkin.ZipkinSpanExporterBuilder setSender(zipkin2.reporter.Sender) - +++ NEW ANNOTATION: java.lang.Deprecated - +++ NEW METHOD: PUBLIC(+) io.opentelemetry.exporter.zipkin.ZipkinSpanExporterBuilder setSender(zipkin2.reporter.BytesMessageSender) +No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt index df26146497b..e659ed1f6cd 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt @@ -1,2 +1,10 @@ Comparing source compatibility of against -No changes. \ No newline at end of file ++++* NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.sdk.common.ScopeConfig (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++* NEW METHOD: PUBLIC(+) STATIC(+) java.util.function.Function applyToMatching(java.util.function.Predicate, java.lang.Object) + GENERIC TEMPLATES: +++ T:java.lang.Object + +++* NEW METHOD: PUBLIC(+) STATIC(+) java.util.function.Function applyToMatching(java.util.function.Predicate, java.lang.Object, java.lang.Object) + GENERIC TEMPLATES: +++ T:java.lang.Object + +++ NEW METHOD: PUBLIC(+) STATIC(+) java.util.function.Predicate scopeNameEquals(java.lang.String) + +++ NEW METHOD: PUBLIC(+) STATIC(+) java.util.function.Predicate scopeNameMatches(java.lang.String) diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-logs.txt b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-logs.txt index df26146497b..c07df020398 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-logs.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-logs.txt @@ -1,2 +1,10 @@ Comparing source compatibility of against -No changes. \ No newline at end of file ++++ NEW CLASS: PUBLIC(+) ABSTRACT(+) io.opentelemetry.sdk.logs.LoggerConfig (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.sdk.logs.LoggerConfig defaultConfig() + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.sdk.logs.LoggerConfig disabled() + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) boolean isEnabled() +*** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder addScopeConfig(io.opentelemetry.sdk.common.ScopeSelector, io.opentelemetry.sdk.logs.LoggerConfig) diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-metrics.txt b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-metrics.txt index df26146497b..2479717dc75 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-metrics.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-metrics.txt @@ -1,2 +1,10 @@ Comparing source compatibility of against -No changes. \ No newline at end of file ++++ NEW CLASS: PUBLIC(+) ABSTRACT(+) io.opentelemetry.sdk.metrics.MeterConfig (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.sdk.metrics.MeterConfig defaultConfig() + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.sdk.metrics.MeterConfig disabled() + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) boolean isEnabled() +*** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder addScopeConfig(io.opentelemetry.sdk.common.ScopeSelector, io.opentelemetry.sdk.metrics.MeterConfig) diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-trace.txt b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-trace.txt index df26146497b..9a9b3e8f4a6 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-trace.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-trace.txt @@ -1,2 +1,10 @@ Comparing source compatibility of against -No changes. \ No newline at end of file +*** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.sdk.trace.SdkTracerProviderBuilder (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.trace.SdkTracerProviderBuilder addScopeConfig(io.opentelemetry.sdk.common.ScopeSelector, io.opentelemetry.sdk.trace.TracerConfig) ++++ NEW CLASS: PUBLIC(+) ABSTRACT(+) io.opentelemetry.sdk.trace.TracerConfig (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.sdk.trace.TracerConfig defaultConfig() + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.sdk.trace.TracerConfig disabled() + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) boolean isEnabled() diff --git a/sdk/all/src/test/java/io/opentelemetry/sdk/ScopeConfigTest.java b/sdk/all/src/test/java/io/opentelemetry/sdk/ScopeConfigTest.java new file mode 100644 index 00000000000..24fe05f8a2d --- /dev/null +++ b/sdk/all/src/test/java/io/opentelemetry/sdk/ScopeConfigTest.java @@ -0,0 +1,216 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk; + +import static io.opentelemetry.sdk.common.ScopeConfig.applyToMatching; +import static io.opentelemetry.sdk.common.ScopeConfig.scopeNameEquals; +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.LoggerConfig; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.opentelemetry.sdk.logs.export.SimpleLogRecordProcessor; +import io.opentelemetry.sdk.metrics.MeterConfig; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.data.MetricData; +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.TracerConfig; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +class ScopeConfigTest { + + private final InMemoryLogRecordExporter logRecordExporter = InMemoryLogRecordExporter.create(); + private final InMemoryMetricReader metricReader = InMemoryMetricReader.create(); + private final InMemorySpanExporter spanExporter = InMemorySpanExporter.create(); + + /** + * Disable "scopeB". All other scopes are enabled by default. + */ + @Test + void disableScopeB() { + OpenTelemetrySdk sdk = + OpenTelemetrySdk.builder() + .setTracerProvider( + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) + .setTracerConfigProvider( + applyToMatching(scopeNameEquals("scopeB"), TracerConfig.disabled())) + .build()) + .setMeterProvider( + SdkMeterProvider.builder() + .registerMetricReader(metricReader) + .setMeterConfigProvider( + applyToMatching(scopeNameEquals("scopeB"), MeterConfig.disabled())) + .build()) + .setLoggerProvider( + SdkLoggerProvider.builder() + .addLogRecordProcessor(SimpleLogRecordProcessor.create(logRecordExporter)) + .setLoggerConfigProvider( + applyToMatching(scopeNameEquals("scopeB"), LoggerConfig.disabled())) + .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(InstrumentationScopeInfo.create("scopeA"))).hasSize(1); + assertThat(spansByScope.get(InstrumentationScopeInfo.create("scopeB"))).isNull(); + assertThat(spansByScope.get(InstrumentationScopeInfo.create("scopeC"))).hasSize(1); + }); + assertThat(metricReader.collectAllMetrics()) + .satisfies( + metrics -> { + Map> metricsByScope = + metrics.stream() + .collect(Collectors.groupingBy(MetricData::getInstrumentationScopeInfo)); + assertThat(metricsByScope.get(InstrumentationScopeInfo.create("scopeA"))).hasSize(1); + assertThat(metricsByScope.get(InstrumentationScopeInfo.create("scopeB"))).isNull(); + assertThat(metricsByScope.get(InstrumentationScopeInfo.create("scopeC"))).hasSize(1); + }); + assertThat(logRecordExporter.getFinishedLogRecordItems()) + .satisfies( + logs -> { + Map> logsByScope = + logs.stream() + .collect(Collectors.groupingBy(LogRecordData::getInstrumentationScopeInfo)); + assertThat(logsByScope.get(InstrumentationScopeInfo.create("scopeA"))).hasSize(1); + assertThat(logsByScope.get(InstrumentationScopeInfo.create("scopeB"))).isNull(); + assertThat(logsByScope.get(InstrumentationScopeInfo.create("scopeC"))).hasSize(1); + }); + } + + /** + * Disable all scopes by default and enable a single scope. + */ + @Test + void disableAllScopesExceptB() { + OpenTelemetrySdk sdk = + OpenTelemetrySdk.builder() + .setTracerProvider( + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) + .setTracerConfigProvider( + applyToMatching(scopeNameEquals("scopeB"), TracerConfig.enabled(), TracerConfig.disabled())) + .build()) + .setMeterProvider( + SdkMeterProvider.builder() + .registerMetricReader(metricReader) + .setMeterConfigProvider( + applyToMatching(scopeNameEquals("scopeB"), MeterConfig.enabled(), MeterConfig.disabled())) + .build()) + .setLoggerProvider( + SdkLoggerProvider.builder() + .addLogRecordProcessor(SimpleLogRecordProcessor.create(logRecordExporter)) + .setLoggerConfigProvider( + applyToMatching(scopeNameEquals("scopeB"), LoggerConfig.enabled(), LoggerConfig.disabled())) + .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(InstrumentationScopeInfo.create("scopeA"))).isNull(); + assertThat(spansByScope.get(InstrumentationScopeInfo.create("scopeB"))).hasSize(1); + assertThat(spansByScope.get(InstrumentationScopeInfo.create("scopeC"))).isNull(); + }); + assertThat(metricReader.collectAllMetrics()) + .satisfies( + metrics -> { + Map> metricsByScope = + metrics.stream() + .collect(Collectors.groupingBy(MetricData::getInstrumentationScopeInfo)); + assertThat(metricsByScope.get(InstrumentationScopeInfo.create("scopeA"))).isNull(); + assertThat(metricsByScope.get(InstrumentationScopeInfo.create("scopeB"))).hasSize(1); + assertThat(metricsByScope.get(InstrumentationScopeInfo.create("scopeC"))).isNull(); + }); + assertThat(logRecordExporter.getFinishedLogRecordItems()) + .satisfies( + logs -> { + Map> logsByScope = + logs.stream() + .collect(Collectors.groupingBy(LogRecordData::getInstrumentationScopeInfo)); + assertThat(logsByScope.get(InstrumentationScopeInfo.create("scopeA"))).isNull(); + assertThat(logsByScope.get(InstrumentationScopeInfo.create("scopeB"))).hasSize(1); + assertThat(logsByScope.get(InstrumentationScopeInfo.create("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"); + Meter scopeAMeter = openTelemetry.getMeter("scopeA"); + Logger scopeALogger = openTelemetry.getLogsBridge().get("scopeA"); + 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"); + Meter scopeBMeter = openTelemetry.getMeter("scopeB"); + Logger scopeBLogger = openTelemetry.getLogsBridge().get("scopeB"); + 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"); + Meter scopeCMeter = openTelemetry.getMeter("scopeC"); + Logger scopeCLogger = openTelemetry.getLogsBridge().get("scopeC"); + 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/common/ScopeConfig.java b/sdk/common/src/main/java/io/opentelemetry/sdk/common/ScopeConfig.java new file mode 100644 index 00000000000..2e3308c490c --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/common/ScopeConfig.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.common; + +import io.opentelemetry.sdk.internal.GlobUtil; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * Utilities for configuring scopes. + */ +public final class ScopeConfig { + + /** + * Returns a function which returns {@code matchingConfig} to scopes which match the {@code scopeMatcher}. If a scope does match, returns null, which triggers the default behavior. + * + *

See {@link #scopeNameEquals(String)}, {@link #scopeNameMatches(String)} for helper functions for {@code scopeMatcher}. + */ + public static Function applyToMatching( + Predicate scopeMatcher, T matchingConfig) { + return scopeInfo -> scopeMatcher.test(scopeInfo) ? matchingConfig : null; + } + + /** + * Returns a function which returns {@code matchingConfig} to scopes which match the {@code scopeMatcher}, else returns {@code defaultConfig}. This is useful for overriding the default behavior. For example, you can disable by default and selectively enable select scopes. + * + *

See {@link #scopeNameEquals(String)}, {@link #scopeNameMatches(String)} for helper functions for {@code scopeMatcher}. + */ + public static Function applyToMatching( + Predicate scopeMatcher, T matchingConfig, T defaultConfig) { + return scopeInfo -> scopeMatcher.test(scopeInfo) ? matchingConfig : defaultConfig; + } + + /** + * Returns a predicate which returns {@code true} if the {@link InstrumentationScopeInfo#getName()} is an exact match of {@code targetScopeName}. + */ + public static Predicate scopeNameEquals(String scopeName) { + return scopeInfo -> scopeInfo.getName().equals(scopeName); + } + + /** + * Returns a predicate which returns {@code true} if the {@link InstrumentationScopeInfo#getName()} is a wildcard match of the {@code scopeNameGlobPattern}. + * + *

{@code scopeNameGlobPattern} name 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 scopeNameMatches( + String scopeNameGlobPattern) { + Predicate globPredicate = GlobUtil.toGlobPatternPredicate(scopeNameGlobPattern); + return scopeInfo -> globPredicate.test(scopeInfo.getName()); + } + + private ScopeConfig() {} +} 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/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..dca7860921c --- /dev/null +++ b/sdk/common/src/test/java/io/opentelemetry/sdk/internal/GlobUtilTest.java @@ -0,0 +1,36 @@ +/* + * 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("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/logs/src/main/java/io/opentelemetry/sdk/logs/LoggerConfig.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/LoggerConfig.java new file mode 100644 index 00000000000..b3c4b8103a5 --- /dev/null +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/LoggerConfig.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.logs; + +import com.google.auto.value.AutoValue; +import javax.annotation.concurrent.Immutable; + +@AutoValue +@Immutable +public abstract class LoggerConfig { + + private static final LoggerConfig DEFAULT_CONFIG = + new AutoValue_LoggerConfig(/* enabled= */ true); + + public static LoggerConfig disabled() { + return new AutoValue_LoggerConfig(/* enabled= */ false); + } + + public static LoggerConfig enabled() { + return DEFAULT_CONFIG; + } + + public static LoggerConfig defaultConfig() { + return DEFAULT_CONFIG; + } + + 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/LoggerSharedState.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/LoggerSharedState.java index 768871e1e57..32d6539e8da 100644 --- a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/LoggerSharedState.java +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/LoggerSharedState.java @@ -7,7 +7,9 @@ import io.opentelemetry.sdk.common.Clock; import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.resources.Resource; +import java.util.function.Function; import java.util.function.Supplier; import javax.annotation.Nullable; @@ -21,17 +23,20 @@ final class LoggerSharedState { private final Supplier logLimitsSupplier; private final LogRecordProcessor logRecordProcessor; private final Clock clock; + private final Function loggerConfigProvider; @Nullable private volatile CompletableResultCode shutdownResult = null; LoggerSharedState( Resource resource, Supplier logLimitsSupplier, LogRecordProcessor logRecordProcessor, - Clock clock) { + Clock clock, + Function loggerConfigProvider) { this.resource = resource; this.logLimitsSupplier = logLimitsSupplier; this.logRecordProcessor = logRecordProcessor; this.clock = clock; + this.loggerConfigProvider = loggerConfigProvider; } Resource getResource() { @@ -50,6 +55,11 @@ Clock getClock() { return clock; } + LoggerConfig getLoggerConfig(InstrumentationScopeInfo instrumentationScopeInfo) { + LoggerConfig loggerConfig = loggerConfigProvider.apply(instrumentationScopeInfo); + return loggerConfig == null ? LoggerConfig.defaultConfig() : loggerConfig; + } + boolean hasBeenShutdown() { return shutdownResult != null; } 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..2aa9c35feb4 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,22 +7,30 @@ import io.opentelemetry.api.logs.LogRecordBuilder; import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.api.logs.LoggerProvider; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; /** 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) { this.loggerSharedState = loggerSharedState; this.instrumentationScopeInfo = instrumentationScopeInfo; + this.loggerConfig = loggerSharedState.getLoggerConfig(instrumentationScopeInfo); } @Override public LogRecordBuilder logRecordBuilder() { + if (!loggerConfig.isEnabled()) { + return NOOP_LOGGER.logRecordBuilder(); + } return new SdkLogRecordBuilder(loggerSharedState, instrumentationScopeInfo); } 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..ddf95c4469a 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,11 +11,13 @@ 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.resources.Resource; import java.io.Closeable; import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.function.Function; import java.util.function.Supplier; import java.util.logging.Level; import javax.annotation.Nullable; @@ -48,10 +50,12 @@ public static SdkLoggerProviderBuilder builder() { Resource resource, Supplier logLimitsSupplier, List processors, - Clock clock) { + Clock clock, + Function loggerConfigProvider) { LogRecordProcessor logRecordProcessor = LogRecordProcessor.composite(processors); this.sharedState = - new LoggerSharedState(resource, logLimitsSupplier, logRecordProcessor, clock); + new LoggerSharedState( + resource, logLimitsSupplier, logRecordProcessor, clock, loggerConfigProvider); this.loggerComponentRegistry = new ComponentRegistry<>( instrumentationScopeInfo -> new SdkLogger(sharedState, instrumentationScopeInfo)); 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..d266073fd66 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,13 @@ 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.logs.data.LogRecordData; import io.opentelemetry.sdk.resources.Resource; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.function.Function; import java.util.function.Supplier; /** @@ -29,6 +31,8 @@ public final class SdkLoggerProviderBuilder { private Resource resource = Resource.getDefault(); private Supplier logLimitsSupplier = LogLimits::getDefault; private Clock clock = Clock.getDefault(); + private Function loggerConfigProvider = + unused -> LoggerConfig.defaultConfig(); SdkLoggerProviderBuilder() {} @@ -100,12 +104,19 @@ public SdkLoggerProviderBuilder setClock(Clock clock) { return this; } + public SdkLoggerProviderBuilder setLoggerConfigProvider( + Function loggerConfigProvider) { + this.loggerConfigProvider = loggerConfigProvider; + 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, loggerConfigProvider); } } 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..139f0efbbe0 --- /dev/null +++ b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/LoggerConfigTest.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.logs; + +import static io.opentelemetry.sdk.common.ScopeConfig.applyToMatching; +import static io.opentelemetry.sdk.common.ScopeConfig.scopeNameEquals; +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.logs.data.LogRecordData; +import io.opentelemetry.sdk.logs.export.SimpleLogRecordProcessor; +import io.opentelemetry.sdk.testing.exporter.InMemoryLogRecordExporter; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +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. + .setLoggerConfigProvider( + applyToMatching(scopeNameEquals("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); + }); + } +} diff --git a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/LoggerSharedStateTest.java b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/LoggerSharedStateTest.java index 29a3a846a56..b9b4d5e4f74 100644 --- a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/LoggerSharedStateTest.java +++ b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/LoggerSharedStateTest.java @@ -24,7 +24,11 @@ void shutdown() { when(logRecordProcessor.shutdown()).thenReturn(code); LoggerSharedState state = new LoggerSharedState( - Resource.empty(), LogLimits::getDefault, logRecordProcessor, Clock.getDefault()); + Resource.empty(), + LogLimits::getDefault, + logRecordProcessor, + Clock.getDefault(), + unused -> LoggerConfig.defaultConfig()); state.shutdown(); state.shutdown(); verify(logRecordProcessor, times(1)).shutdown(); 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..b290de37c54 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 @@ -43,6 +43,7 @@ void logRecordBuilder() { when(state.getResource()).thenReturn(Resource.getDefault()); when(state.getLogRecordProcessor()).thenReturn(logRecordProcessor); when(state.getClock()).thenReturn(clock); + when(state.getLoggerConfig(any())).thenReturn(LoggerConfig.defaultConfig()); SdkLogger logger = new SdkLogger(state, info); LogRecordBuilder logRecordBuilder = logger.logRecordBuilder(); diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/MeterConfig.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/MeterConfig.java new file mode 100644 index 00000000000..d5a1a7a7990 --- /dev/null +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/MeterConfig.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import com.google.auto.value.AutoValue; +import javax.annotation.concurrent.Immutable; + +@AutoValue +@Immutable +public abstract class MeterConfig { + + private static final MeterConfig DEFAULT_CONFIG = new AutoValue_MeterConfig(/* enabled= */ true); + + public static MeterConfig disabled() { + return new AutoValue_MeterConfig(/* enabled= */ false); + } + + public static MeterConfig enabled() { + return DEFAULT_CONFIG; + } + + public static MeterConfig defaultConfig() { + return DEFAULT_CONFIG; + } + + 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/SdkMeter.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeter.java index 02eb45a26ff..d88139a032d 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 @@ -55,6 +55,7 @@ final class SdkMeter implements Meter { private final InstrumentationScopeInfo instrumentationScopeInfo; private final MeterProviderSharedState meterProviderSharedState; private final MeterSharedState meterSharedState; + private final MeterConfig meterConfig; SdkMeter( MeterProviderSharedState meterProviderSharedState, @@ -63,6 +64,7 @@ final class SdkMeter implements Meter { this.instrumentationScopeInfo = instrumentationScopeInfo; this.meterProviderSharedState = meterProviderSharedState; this.meterSharedState = MeterSharedState.create(instrumentationScopeInfo, registeredReaders); + this.meterConfig = meterProviderSharedState.getMeterConfig(instrumentationScopeInfo); } // Visible for testing @@ -82,7 +84,7 @@ void resetForTest() { @Override public LongCounterBuilder counterBuilder(String name) { - return !checkValidInstrumentName(name) + return !meterConfig.isEnabled() || !checkValidInstrumentName(name) ? NOOP_METER.counterBuilder(NOOP_INSTRUMENT_NAME) : new SdkLongCounter.SdkLongCounterBuilder( meterProviderSharedState, meterSharedState, name); @@ -90,7 +92,7 @@ public LongCounterBuilder counterBuilder(String name) { @Override public LongUpDownCounterBuilder upDownCounterBuilder(String name) { - return !checkValidInstrumentName(name) + return !meterConfig.isEnabled() || !checkValidInstrumentName(name) ? NOOP_METER.upDownCounterBuilder(NOOP_INSTRUMENT_NAME) : new SdkLongUpDownCounter.SdkLongUpDownCounterBuilder( meterProviderSharedState, meterSharedState, name); @@ -98,7 +100,7 @@ public LongUpDownCounterBuilder upDownCounterBuilder(String name) { @Override public DoubleHistogramBuilder histogramBuilder(String name) { - return !checkValidInstrumentName(name) + return !meterConfig.isEnabled() || !checkValidInstrumentName(name) ? NOOP_METER.histogramBuilder(NOOP_INSTRUMENT_NAME) : new SdkDoubleHistogram.SdkDoubleHistogramBuilder( meterProviderSharedState, meterSharedState, name); @@ -106,7 +108,7 @@ public DoubleHistogramBuilder histogramBuilder(String name) { @Override public DoubleGaugeBuilder gaugeBuilder(String name) { - return !checkValidInstrumentName(name) + return !meterConfig.isEnabled() || !checkValidInstrumentName(name) ? NOOP_METER.gaugeBuilder(NOOP_INSTRUMENT_NAME) : new SdkDoubleGauge.SdkDoubleGaugeBuilder( meterProviderSharedState, meterSharedState, name); 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..ef68e37fd9c 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,6 +11,7 @@ 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.metrics.data.MetricData; import io.opentelemetry.sdk.metrics.export.CollectionRegistration; @@ -32,6 +33,7 @@ import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; import java.util.logging.Logger; /** @@ -62,7 +64,8 @@ public static SdkMeterProviderBuilder builder() { List metricProducers, Clock clock, Resource resource, - ExemplarFilter exemplarFilter) { + ExemplarFilter exemplarFilter, + Function meterConfigProvider) { long startEpochNanos = clock.now(); this.registeredViews = registeredViews; this.registeredReaders = @@ -75,7 +78,8 @@ public static SdkMeterProviderBuilder builder() { .collect(toList()); this.metricProducers = metricProducers; this.sharedState = - MeterProviderSharedState.create(clock, resource, exemplarFilter, startEpochNanos); + MeterProviderSharedState.create( + clock, resource, exemplarFilter, startEpochNanos, meterConfigProvider); this.registry = new ComponentRegistry<>( instrumentationLibraryInfo -> 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..0fb9a9bebb8 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,6 +6,7 @@ package io.opentelemetry.sdk.metrics; import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.metrics.export.MetricProducer; import io.opentelemetry.sdk.metrics.export.MetricReader; import io.opentelemetry.sdk.metrics.internal.SdkMeterProviderUtil; @@ -18,6 +19,7 @@ import java.util.IdentityHashMap; import java.util.List; import java.util.Objects; +import java.util.function.Function; /** * Builder class for the {@link SdkMeterProvider}. @@ -40,6 +42,8 @@ public final class SdkMeterProviderBuilder { private final List metricProducers = new ArrayList<>(); private final List registeredViews = new ArrayList<>(); private ExemplarFilter exemplarFilter = DEFAULT_EXEMPLAR_FILTER; + private Function meterConfigProvider = + unused -> MeterConfig.defaultConfig(); SdkMeterProviderBuilder() {} @@ -150,9 +154,21 @@ public SdkMeterProviderBuilder registerMetricProducer(MetricProducer metricProdu return this; } + public SdkMeterProviderBuilder setMeterConfigProvider( + Function meterConfigProvider) { + this.meterConfigProvider = meterConfigProvider; + 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, + meterConfigProvider); } } 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..ef52fbfaae4 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 @@ -7,9 +7,14 @@ import com.google.auto.value.AutoValue; import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.metrics.MeterConfig; import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.metrics.internal.exemplar.ExemplarFilter; import io.opentelemetry.sdk.resources.Resource; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; import javax.annotation.concurrent.Immutable; /** @@ -21,9 +26,20 @@ @AutoValue @Immutable public abstract class MeterProviderSharedState { + + private final AtomicReference> + meterConfigProviderRef = new AtomicReference<>(); + public static MeterProviderSharedState create( - Clock clock, Resource resource, ExemplarFilter exemplarFilter, long startEpochNanos) { - return new AutoValue_MeterProviderSharedState(clock, resource, startEpochNanos, exemplarFilter); + Clock clock, + Resource resource, + ExemplarFilter exemplarFilter, + long startEpochNanos, + Function meterConfigProvider) { + MeterProviderSharedState sharedState = + new AutoValue_MeterProviderSharedState(clock, resource, startEpochNanos, exemplarFilter); + sharedState.meterConfigProviderRef.set(meterConfigProvider); + return sharedState; } MeterProviderSharedState() {} @@ -39,4 +55,11 @@ public static MeterProviderSharedState create( /** Returns the {@link ExemplarFilter} for remembering synchronous measurements. */ abstract ExemplarFilter getExemplarFilter(); + + public MeterConfig getMeterConfig(InstrumentationScopeInfo instrumentationScopeInfo) { + Function meterConfigProvider = + Objects.requireNonNull(meterConfigProviderRef.get()); + MeterConfig meterConfig = meterConfigProvider.apply(instrumentationScopeInfo); + return meterConfig == null ? MeterConfig.defaultConfig() : meterConfig; + } } 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/InstrumentBuilderTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/InstrumentBuilderTest.java index a351c95d0e6..75d9486f852 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/InstrumentBuilderTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/InstrumentBuilderTest.java @@ -15,13 +15,18 @@ import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.testing.time.TestClock; import java.util.Collections; +import java.util.LinkedHashMap; import org.junit.jupiter.api.Test; class InstrumentBuilderTest { public static final MeterProviderSharedState PROVIDER_SHARED_STATE = MeterProviderSharedState.create( - TestClock.create(), Resource.getDefault(), ExemplarFilter.alwaysOff(), 0); + TestClock.create(), + Resource.getDefault(), + ExemplarFilter.alwaysOff(), + 0, + new LinkedHashMap<>()); static final InstrumentationScopeInfo SCOPE = InstrumentationScopeInfo.create("scope-name"); public static final MeterSharedState METER_SHARED_STATE = MeterSharedState.create(SCOPE, Collections.emptyList()); 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..da4dfb4bb74 --- /dev/null +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/MeterConfigTest.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import static io.opentelemetry.sdk.common.ScopeSelector.named; +import static io.opentelemetry.sdk.metrics.MeterConfig.disabled; +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.metrics.data.MetricData; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +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. + .addScopeConfig(named("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); + }); + } +} 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..c19350a88d1 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 @@ -13,17 +13,23 @@ /** {@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) { this.sharedState = sharedState; this.instrumentationScopeInfo = instrumentationScopeInfo; + this.tracerConfig = sharedState.getTracerConfig(instrumentationScopeInfo); } @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..6b1a955bfcf 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,12 +10,14 @@ 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.resources.Resource; import io.opentelemetry.sdk.trace.samplers.Sampler; import java.io.Closeable; import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.function.Function; import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; @@ -37,16 +39,24 @@ public static SdkTracerProviderBuilder builder() { return new SdkTracerProviderBuilder(); } + @SuppressWarnings("NonApiType") SdkTracerProvider( Clock clock, IdGenerator idsGenerator, Resource resource, Supplier spanLimitsSupplier, Sampler sampler, - List spanProcessors) { + List spanProcessors, + Function tracerConfigProvider) { this.sharedState = new TracerSharedState( - clock, idsGenerator, resource, spanLimitsSupplier, sampler, spanProcessors); + clock, + idsGenerator, + resource, + spanLimitsSupplier, + sampler, + spanProcessors, + tracerConfigProvider); this.tracerSdkComponentRegistry = new ComponentRegistry<>( instrumentationScopeInfo -> new SdkTracer(sharedState, instrumentationScopeInfo)); 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..6464709ad3a 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,13 @@ import static java.util.Objects.requireNonNull; import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.samplers.Sampler; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.function.Function; import java.util.function.Supplier; /** Builder of {@link SdkTracerProvider}. */ @@ -26,6 +28,8 @@ public final class SdkTracerProviderBuilder { private Resource resource = Resource.getDefault(); private Supplier spanLimitsSupplier = SpanLimits::getDefault; private Sampler sampler = DEFAULT_SAMPLER; + private Function tracerConfigProvider = + unused -> TracerConfig.defaultConfig(); /** * Assign a {@link Clock}. {@link Clock} will be used each time a {@link @@ -147,6 +151,12 @@ public SdkTracerProviderBuilder addSpanProcessor(SpanProcessor spanProcessor) { return this; } + public SdkTracerProviderBuilder setTracerConfigProvider( + Function tracerConfigProvider) { + this.tracerConfigProvider = tracerConfigProvider; + return this; + } + /** * Create a new {@link SdkTracerProvider} instance with the configuration. * @@ -154,7 +164,13 @@ public SdkTracerProviderBuilder addSpanProcessor(SpanProcessor spanProcessor) { */ public SdkTracerProvider build() { return new SdkTracerProvider( - clock, idsGenerator, resource, spanLimitsSupplier, sampler, spanProcessors); + clock, + idsGenerator, + resource, + spanLimitsSupplier, + sampler, + spanProcessors, + tracerConfigProvider); } SdkTracerProviderBuilder() {} diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/TracerConfig.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/TracerConfig.java new file mode 100644 index 00000000000..a7ffd25d915 --- /dev/null +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/TracerConfig.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import com.google.auto.value.AutoValue; +import javax.annotation.concurrent.Immutable; + +@AutoValue +@Immutable +public abstract class TracerConfig { + + private static final TracerConfig DEFAULT_CONFIG = + new AutoValue_TracerConfig(/* enabled= */ true); + + public static TracerConfig disabled() { + return new AutoValue_TracerConfig(/* enabled= */ false); + } + + public static TracerConfig enabled() { + return DEFAULT_CONFIG; + } + + public static TracerConfig defaultConfig() { + return DEFAULT_CONFIG; + } + + TracerConfig() {} + + /** Returns {@code true} if this tracer is enabled. Defaults to {@code true}. */ + public abstract boolean isEnabled(); +} 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..1681affb9d0 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 @@ -7,14 +7,17 @@ import io.opentelemetry.sdk.common.Clock; import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.samplers.Sampler; import java.util.List; +import java.util.function.Function; import java.util.function.Supplier; import javax.annotation.Nullable; // 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; @@ -25,16 +28,19 @@ final class TracerSharedState { private final Supplier spanLimitsSupplier; private final Sampler sampler; private final SpanProcessor activeSpanProcessor; + private final Function tracerConfigProvider; @Nullable private volatile CompletableResultCode shutdownResult = null; + @SuppressWarnings("NonApiType") TracerSharedState( Clock clock, IdGenerator idGenerator, Resource resource, Supplier spanLimitsSupplier, Sampler sampler, - List spanProcessors) { + List spanProcessors, + Function tracerConfigProvider) { this.clock = clock; this.idGenerator = idGenerator; this.idGeneratorSafeToSkipIdValidation = idGenerator instanceof RandomIdGenerator; @@ -42,6 +48,7 @@ final class TracerSharedState { this.spanLimitsSupplier = spanLimitsSupplier; this.sampler = sampler; activeSpanProcessor = SpanProcessor.composite(spanProcessors); + this.tracerConfigProvider = tracerConfigProvider; } Clock getClock() { @@ -79,6 +86,11 @@ SpanProcessor getActiveSpanProcessor() { return activeSpanProcessor; } + TracerConfig getTracerConfig(InstrumentationScopeInfo instrumentationScopeInfo) { + TracerConfig tracerConfig = tracerConfigProvider.apply(instrumentationScopeInfo); + return tracerConfig == null ? TracerConfig.defaultConfig() : tracerConfig; + } + /** * Returns {@code true} if tracing has been shut down. * 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..abacbab6f29 --- /dev/null +++ b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/TracerConfigTest.java @@ -0,0 +1,86 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import static io.opentelemetry.sdk.common.ScopeConfig.applyToMatching; +import static io.opentelemetry.sdk.common.ScopeConfig.scopeNameEquals; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; + +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.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import org.junit.jupiter.api.Test; + +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. + .setTracerConfigProvider( + applyToMatching(scopeNameEquals("scopeB"), TracerConfig.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())); + } +}