Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions instrumentation-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -54,6 +61,8 @@ public final class InstrumenterBuilder<REQUEST, RESPONSE> {
ConfigPropertiesUtil.getString(
"otel.instrumentation.experimental.span-suppression-strategy"));

static boolean isIncubator = isIncubator();

final OpenTelemetry openTelemetry;
final String instrumentationName;
SpanNameExtractor<? super REQUEST> spanNameExtractor;
Expand Down Expand Up @@ -84,6 +93,20 @@ public final class InstrumenterBuilder<REQUEST, RESPONSE> {
operationListenerAttributesExtractor, "operationListenerAttributesExtractor")));
}

private static boolean isIncubator() {
try {
Class.forName("io.opentelemetry.api.incubator.ExtendedOpenTelemetry");
return true;
} catch (ClassNotFoundException e) {
// The incubator module is not available.
// This only happens in OpenTelemetry API instrumentation tests, where an older version of
// OpenTelemetry API is used that does not have ExtendedOpenTelemetry.
// Having the incubator module without ExtendedOpenTelemetry class should still return false
// for those tests to avoid a ClassNotFoundException.
return false;
}
}

InstrumenterBuilder(
OpenTelemetry openTelemetry,
String instrumentationName,
Expand Down Expand Up @@ -297,6 +320,7 @@ private Instrumenter<REQUEST, RESPONSE> buildInstrumenter(
InstrumenterConstructor<REQUEST, RESPONSE> constructor,
SpanKindExtractor<? super REQUEST> spanKindExtractor) {

addThreadDetailsAttributeExtractor(this);
applyCustomizers(this);

this.spanKindExtractor = spanKindExtractor;
Expand Down Expand Up @@ -446,6 +470,28 @@ public void setSpanNameExtractor(
}
}

private static <RESPONSE, REQUEST> void addThreadDetailsAttributeExtractor(
InstrumenterBuilder<REQUEST, RESPONSE> builder) {
if (isIncubator && builder.openTelemetry instanceof ExtendedOpenTelemetry) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about this. Currently we attach AddThreadDetailsSpanProcessor only in agent/spring boot and offer a non-declarative config option to disable it. Are you sure this needs to be baked into the instrumentation-api? Perhaps we should maintain the current behavior and only add it for agent/spring starter? I believe we could use the instrumenter cusomizers for this. If we are going to bake it in I suspect we'll have to provide a different way for disabling it since OpenTelemetry instance could be produced without the declarative config.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we'll have to provide a different way for disabling it

ExtendedOpenTelemetry is only returned for declarative config and thread details are disabled by default

Are you sure this needs to be baked into the instrumentation-api

I guess we have the opportunity to decide how we image configuration going forward.

The property name doesn't include agent or spring, so it should always apply - that's the design we currently have.

// 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<RQ, RS> {
Instrumenter<RQ, RS> create(InstrumenterBuilder<RQ, RS> builder);

Expand Down Expand Up @@ -490,4 +536,26 @@ public <RQ, RS> void propagateOperationListenersToOnEnd(
}
});
}

private static class ThreadDetailsAttributesExtractor<RESPONSE, REQUEST>
implements AttributesExtractor<REQUEST, RESPONSE> {
// attributes are not stable yet
private static final AttributeKey<Long> THREAD_ID = longKey("thread.id");
private static final AttributeKey<String> 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) {}
}
}
Original file line number Diff line number Diff line change
@@ -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<Arguments> allEnabledAndDisabledValues() {
return Stream.of(
Arguments.of(
true,
(Consumer<SpanDataAssert>)
span ->
span.hasAttributesSatisfying(
satisfies(ThreadIncubatingAttributes.THREAD_ID, n -> n.isNotNull()),
satisfies(ThreadIncubatingAttributes.THREAD_NAME, n -> n.isNotBlank()))),
Arguments.of(
false,
(Consumer<SpanDataAssert>)
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<SpanDataAssert> spanAttributesConsumer)
throws NoSuchFieldException, IllegalAccessException {
OpenTelemetry openTelemetry = DeclarativeConfiguration.create(model(enabled));
Instrumenter<Map<String, String>, Map<String, String>> instrumenter =
Instrumenter.<Map<String, String>, Map<String, String>>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)))));
}
}
Loading