From 29883b5bfb2fec60765ae1497edc63af14e516a1 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Mon, 3 Nov 2025 08:43:12 +0100 Subject: [PATCH] add thread details for declarative config --- instrumentation-api/build.gradle.kts | 1 + .../api/instrumenter/InstrumenterBuilder.java | 64 +++++++++++++ .../instrumenter/AddThreadDetailsTest.java | 90 +++++++++++++++++++ 3 files changed, 155 insertions(+) create mode 100644 instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/AddThreadDetailsTest.java diff --git a/instrumentation-api/build.gradle.kts b/instrumentation-api/build.gradle.kts index 3b798b826de2..38dfcbf8a6a5 100644 --- a/instrumentation-api/build.gradle.kts +++ b/instrumentation-api/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { testImplementation("io.opentelemetry.javaagent:opentelemetry-testing-common") testImplementation("io.opentelemetry:opentelemetry-sdk-testing") testImplementation("io.opentelemetry:opentelemetry-exporter-common") + testImplementation("io.opentelemetry:opentelemetry-sdk-extension-incubator") testImplementation("org.junit-pioneer:junit-pioneer") jmhImplementation(project(":instrumentation-api-incubator")) diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java index 0d476656b9e5..365ccf4f2ee2 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java @@ -5,11 +5,18 @@ package io.opentelemetry.instrumentation.api.instrumenter; +import static io.opentelemetry.api.common.AttributeKey.longKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.api.incubator.config.DeclarativeConfigProperties.empty; import static java.util.Objects.requireNonNull; import static java.util.logging.Level.WARNING; import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.incubator.ExtendedOpenTelemetry; +import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.api.metrics.MeterBuilder; import io.opentelemetry.api.trace.SpanKind; @@ -54,6 +61,8 @@ public final class InstrumenterBuilder { ConfigPropertiesUtil.getString( "otel.instrumentation.experimental.span-suppression-strategy")); + static boolean isIncubator = isIncubator(); + final OpenTelemetry openTelemetry; final String instrumentationName; SpanNameExtractor spanNameExtractor; @@ -84,6 +93,16 @@ public final class InstrumenterBuilder { operationListenerAttributesExtractor, "operationListenerAttributesExtractor"))); } + private static boolean isIncubator() { + try { + Class.forName("io.opentelemetry.api.incubator.ExtendedOpenTelemetry"); + return true; + } catch (ClassNotFoundException e) { + // incubator module is not available + return false; + } + } + InstrumenterBuilder( OpenTelemetry openTelemetry, String instrumentationName, @@ -297,6 +316,7 @@ private Instrumenter buildInstrumenter( InstrumenterConstructor constructor, SpanKindExtractor spanKindExtractor) { + addThreadDetailsAttributeExtractor(this); applyCustomizers(this); this.spanKindExtractor = spanKindExtractor; @@ -446,6 +466,28 @@ public void setSpanNameExtractor( } } + private static void addThreadDetailsAttributeExtractor( + InstrumenterBuilder builder) { + if (isIncubator && builder.openTelemetry instanceof ExtendedOpenTelemetry) { + // Declarative config is used. + // Otherwise, thread details are configured in + // io.opentelemetry.javaagent.tooling.AgentTracerProviderConfigurer. + + DeclarativeConfigProperties instrumentationConfig = + ((ExtendedOpenTelemetry) builder.openTelemetry) + .getConfigProvider() + .getInstrumentationConfig(); + + if (instrumentationConfig != null + && instrumentationConfig + .getStructured("java", empty()) + .getStructured("thread_details", empty()) + .getBoolean("enabled", false)) { + builder.addAttributesExtractor(new ThreadDetailsAttributesExtractor<>()); + } + } + } + private interface InstrumenterConstructor { Instrumenter create(InstrumenterBuilder builder); @@ -490,4 +532,26 @@ public void propagateOperationListenersToOnEnd( } }); } + + private static class ThreadDetailsAttributesExtractor + implements AttributesExtractor { + // attributes are not stable yet + private static final AttributeKey THREAD_ID = longKey("thread.id"); + private static final AttributeKey THREAD_NAME = stringKey("thread.name"); + + @Override + public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) { + Thread currentThread = Thread.currentThread(); + attributes.put(THREAD_ID, currentThread.getId()); + attributes.put(THREAD_NAME, currentThread.getName()); + } + + @Override + public void onEnd( + AttributesBuilder attributes, + Context context, + REQUEST request, + @Nullable RESPONSE response, + @Nullable Throwable error) {} + } } diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/AddThreadDetailsTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/AddThreadDetailsTest.java new file mode 100644 index 000000000000..606c75296b66 --- /dev/null +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/AddThreadDetailsTest.java @@ -0,0 +1,90 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonMap; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfiguration; +import io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfigurationBuilder; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.ExperimentalLanguageSpecificInstrumentationModel; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.InstrumentationModel; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfigurationModel; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.semconv.incubating.ThreadIncubatingAttributes; +import java.lang.reflect.Field; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Stream; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class AddThreadDetailsTest { + + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + public static Stream allEnabledAndDisabledValues() { + return Stream.of( + Arguments.of( + true, + (Consumer) + span -> + span.hasAttributesSatisfying( + satisfies(ThreadIncubatingAttributes.THREAD_ID, n -> n.isNotNull()), + satisfies(ThreadIncubatingAttributes.THREAD_NAME, n -> n.isNotBlank()))), + Arguments.of( + false, + (Consumer) + span -> + span.hasAttributesSatisfying( + satisfies(ThreadIncubatingAttributes.THREAD_ID, n -> n.isNull()), + satisfies(ThreadIncubatingAttributes.THREAD_NAME, n -> n.isNull())))); + } + + @ParameterizedTest(name = "enabled={0}") + @MethodSource("allEnabledAndDisabledValues") + void enabled(boolean enabled, Consumer spanAttributesConsumer) + throws NoSuchFieldException, IllegalAccessException { + OpenTelemetry openTelemetry = DeclarativeConfiguration.create(model(enabled)); + Instrumenter, Map> instrumenter = + Instrumenter., Map>builder( + openTelemetry, "test", name -> "span") + .buildInstrumenter(); + + // OpenTelemetryExtension doesn't allow passing a custom OpenTelemetry instance + Field field = Instrumenter.class.getDeclaredField("tracer"); + field.setAccessible(true); + field.set(instrumenter, otelTesting.getOpenTelemetry().getTracer("test")); + + Context context = instrumenter.start(Context.root(), emptyMap()); + instrumenter.end(context, emptyMap(), emptyMap(), null); + + otelTesting + .assertTraces() + .hasTracesSatisfyingExactly( + trace -> trace.hasSpansSatisfyingExactly(spanAttributesConsumer)); + } + + private static OpenTelemetryConfigurationModel model(boolean enabled) { + return new DeclarativeConfigurationBuilder() + .customizeModel( + new OpenTelemetryConfigurationModel() + .withFileFormat("1.0-rc.1") + .withInstrumentationDevelopment( + new InstrumentationModel() + .withJava( + new ExperimentalLanguageSpecificInstrumentationModel() + .withAdditionalProperty( + "thread_details", singletonMap("enabled", enabled))))); + } +}