Client span keys: suppressing same instrumentation
ADD
instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/CompositeSuppressionStrategy.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.instrumenter;
+
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.context.Context;
+
+final class CompositeSuppressionStrategy extends SpanSuppressionStrategy {
+ private final SpanSuppressionStrategy clientStrategy;
+ private final SpanSuppressionStrategy producerStrategy;
+ private final SpanSuppressionStrategy serverStrategy;
+ private final SpanSuppressionStrategy consumerStrategy;
+
+ CompositeSuppressionStrategy(
+ SpanSuppressionStrategy client,
+ SpanSuppressionStrategy producer,
+ SpanSuppressionStrategy server,
+ SpanSuppressionStrategy consumer) {
+ this.clientStrategy = client;
+ this.producerStrategy = producer;
+ this.serverStrategy = server;
+ this.consumerStrategy = consumer;
+ }
+
+ @Override
+ Context storeInContext(Context context, SpanKind spanKind, Span span) {
+ switch (spanKind) {
+ case CLIENT:
+ return clientStrategy.storeInContext(context, spanKind, span);
+ case PRODUCER:
+ return producerStrategy.storeInContext(context, spanKind, span);
+ case SERVER:
+ return serverStrategy.storeInContext(context, spanKind, span);
+ case CONSUMER:
+ return consumerStrategy.storeInContext(context, spanKind, span);
+ case INTERNAL:
+ return context;
+ }
+ return context;
+ }
+
+ @Override
+ boolean shouldSuppress(Context parentContext, SpanKind spanKind) {
+ switch (spanKind) {
+ case CLIENT:
+ return clientStrategy.shouldSuppress(parentContext, spanKind);
+ case PRODUCER:
+ return producerStrategy.shouldSuppress(parentContext, spanKind);
+ case SERVER:
+ return serverStrategy.shouldSuppress(parentContext, spanKind);
+ case CONSUMER:
+ return consumerStrategy.shouldSuppress(parentContext, spanKind);
+ case INTERNAL:
+ return false;
+ }
+ return false;
+ }
+}

MODIFY
instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/Instrumenter.java
instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/Instrumenter.java
@@ -15,9 +15,6 @@ import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.InstrumentationVersion;
import io.opentelemetry.instrumentation.api.internal.SupportabilityMetrics;
-import io.opentelemetry.instrumentation.api.tracer.ClientSpan;
-import io.opentelemetry.instrumentation.api.tracer.ConsumerSpan;
-import io.opentelemetry.instrumentation.api.tracer.ServerSpan;
import java.util.ArrayList;
import java.util.List;
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -76,6 +73,7 @@ public class Instrumenter<REQUEST, RESPONSE> {
private final ErrorCauseExtractor errorCauseExtractor;
@Nullable private final StartTimeExtractor<REQUEST> startTimeExtractor;
@Nullable private final EndTimeExtractor<RESPONSE> endTimeExtractor;
+ private final SpanSuppressionStrategy spanSuppressionStrategy;
Instrumenter(InstrumenterBuilder<REQUEST, RESPONSE> builder) {
this.instrumentationName = builder.instrumentationName;
@@ -90,6 +88,7 @@ public class Instrumenter<REQUEST, RESPONSE> {
this.errorCauseExtractor = builder.errorCauseExtractor;
this.startTimeExtractor = builder.startTimeExtractor;
this.endTimeExtractor = builder.endTimeExtractor;
+ this.spanSuppressionStrategy = builder.getSpanSuppressionStrategy();
}
/**
@@ -99,21 +98,9 @@ public class Instrumenter<REQUEST, RESPONSE> {
* without calling those methods.
*/
public boolean shouldStart(Context parentContext, REQUEST request) {
- boolean suppressed = false;
SpanKind spanKind = spanKindExtractor.extract(request);
- switch (spanKind) {
- case SERVER:
- suppressed = ServerSpan.exists(parentContext);
- break;
- case CONSUMER:
- suppressed = ConsumerSpan.exists(parentContext);
- break;
- case CLIENT:
- suppressed = ClientSpan.exists(parentContext);
- break;
- default:
- break;
- }
+ boolean suppressed = spanSuppressionStrategy.shouldSuppress(parentContext, spanKind);
+
if (suppressed) {
supportability.recordSuppressedSpan(spanKind, instrumentationName);
}
@@ -158,16 +145,8 @@ public class Instrumenter<REQUEST, RESPONSE> {
spanBuilder.setAllAttributes(attributes);
Span span = spanBuilder.startSpan();
context = context.with(span);
- switch (spanKind) {
- case SERVER:
- return ServerSpan.with(context, span);
- case CLIENT:
- return ClientSpan.with(context, span);
- case CONSUMER:
- return ConsumerSpan.with(context, span);
- default:
- return context;
- }
+
+ return spanSuppressionStrategy.storeInContext(context, spanKind, span);
}
/**

MODIFY
instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java
instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java
@@ -10,13 +10,21 @@ import static java.util.Objects.requireNonNull;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.metrics.GlobalMeterProvider;
import io.opentelemetry.api.metrics.Meter;
+import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.context.propagation.TextMapGetter;
import io.opentelemetry.context.propagation.TextMapSetter;
import io.opentelemetry.instrumentation.api.annotations.UnstableApi;
+import io.opentelemetry.instrumentation.api.config.Config;
+import io.opentelemetry.instrumentation.api.instrumenter.db.DbAttributesExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.http.HttpAttributesExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcAttributesExtractor;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
@@ -26,6 +34,12 @@ import org.checkerframework.checker.nullness.qual.Nullable;
*/
public final class InstrumenterBuilder<REQUEST, RESPONSE> {
+ /** Instrumentation type suppression configuration property key. */
+ private static final boolean ENABLE_SPAN_SUPPRESSION_BY_TYPE =
+ Config.get()
+ .getBooleanProperty(
+ "otel.instrumentation.experimental.outgoing-span-suppression-by-type", false);
+
final OpenTelemetry openTelemetry;
final Meter meter;
final String instrumentationName;
@@ -43,6 +57,8 @@ public final class InstrumenterBuilder<REQUEST, RESPONSE> {
@Nullable StartTimeExtractor<REQUEST> startTimeExtractor = null;
@Nullable EndTimeExtractor<RESPONSE> endTimeExtractor = null;
+ private boolean enableSpanSuppressionByType = ENABLE_SPAN_SUPPRESSION_BY_TYPE;
+
InstrumenterBuilder(
OpenTelemetry openTelemetry,
String instrumentationName,
@@ -120,6 +136,38 @@ public final class InstrumenterBuilder<REQUEST, RESPONSE> {
return this;
}
+ // visible for tests
+ /**
+ * Enables suppression based on client instrumentation type.
+ *
+ * <p><strong>When enabled, suppresses nested spans depending on their {@link SpanKind} and
+ * type</strong>.
+ *
+ * <ul>
+ * <li>CLIENT and PRODUCER nested spans are suppressed based on their type (HTTP, RPC, DB,
+ * MESSAGING) i.e. if span with the same type is on the context, new span of this type will
+ * not be started.
+ * </ul>
+ *
+ * <p><strong>When disabled:</strong>
+ *
+ * <ul>
+ * <li>CLIENT and PRODUCER nested spans are always suppressed
+ * </ul>
+ *
+ * <p><strong>In both cases:</strong>
+ *
+ * <ul>
+ * <li>SERVER and CONSUMER nested spans are always suppressed
+ * <li>INTERNAL spans are never suppressed
+ * </ul>
+ */
+ InstrumenterBuilder<REQUEST, RESPONSE> enableInstrumentationTypeSuppression(
+ boolean enableInstrumentationType) {
+ this.enableSpanSuppressionByType = enableInstrumentationType;
+ return this;
+ }
+
/**
* Returns a new {@link Instrumenter} which will create client spans and inject context into
* requests.
@@ -189,6 +237,34 @@ public final class InstrumenterBuilder<REQUEST, RESPONSE> {
return constructor.create(this);
}
+ SpanSuppressionStrategy getSpanSuppressionStrategy() {
+ if (!enableSpanSuppressionByType) {
+ // if not enabled, preserve current behavior, not distinguishing types
+ return SpanSuppressionStrategy.SUPPRESS_ALL_NESTED_OUTGOING_STRATEGY;
+ }
+
+ Set<SpanKey> spanKeys = spanKeysFromAttributeExtractor(this.attributesExtractors);
+ return SpanSuppressionStrategy.from(spanKeys);
+ }
+
+ private static Set<SpanKey> spanKeysFromAttributeExtractor(
+ List<? extends AttributesExtractor<?, ?>> attributesExtractors) {
+
+ Set<SpanKey> spanKeys = new HashSet<>();
+ for (AttributesExtractor<?, ?> attributeExtractor : attributesExtractors) {
+ if (attributeExtractor instanceof HttpAttributesExtractor) {
+ spanKeys.add(SpanKey.HTTP_CLIENT);
+ } else if (attributeExtractor instanceof RpcAttributesExtractor) {
+ spanKeys.add(SpanKey.RPC_CLIENT);
+ } else if (attributeExtractor instanceof DbAttributesExtractor) {
+ spanKeys.add(SpanKey.DB_CLIENT);
+ } else if (attributeExtractor instanceof MessagingAttributesExtractor) {
+ spanKeys.add(SpanKey.MESSAGING_PRODUCER);
+ }
+ }
+ return spanKeys;
+ }
+
private interface InstrumenterConstructor<RQ, RS> {
Instrumenter<RQ, RS> create(InstrumenterBuilder<RQ, RS> builder);

ADD
instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/NoopSuppressionStrategy.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.instrumenter;
+
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.context.Context;
+
+final class NoopSuppressionStrategy extends SpanSuppressionStrategy {
+
+ static final SpanSuppressionStrategy INSTANCE = new NoopSuppressionStrategy();
+
+ @Override
+ Context storeInContext(Context context, SpanKind spanKind, Span span) {
+ return context;
+ }
+
+ @Override
+ boolean shouldSuppress(Context parentContext, SpanKind spanKind) {
+ return false;
+ }
+}

ADD
instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/SpanKey.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.instrumenter;
+
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.ContextKey;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/** Makes span keys for specific instrumentation accessible to enrich and suppress spans. */
+public final class SpanKey {
+
+ private static final ContextKey<Span> SERVER_KEY =
+ ContextKey.named("opentelemetry-traces-span-key-server");
+ private static final ContextKey<Span> CONSUMER_KEY =
+ ContextKey.named("opentelemetry-traces-span-key-consumer");
+
+ // TODO bridge these constants in AgentContextStorage
+ private static final ContextKey<Span> HTTP_KEY =
+ ContextKey.named("opentelemetry-traces-span-key-http");
+ private static final ContextKey<Span> RPC_KEY =
+ ContextKey.named("opentelemetry-traces-span-key-rpc");
+ private static final ContextKey<Span> DB_KEY =
+ ContextKey.named("opentelemetry-traces-span-key-db");
+ private static final ContextKey<Span> MESSAGING_KEY =
+ ContextKey.named("opentelemetry-traces-span-key-messaging");
+
+ // this is used instead of above, depending on the configuration value for
+ // otel.instrumentation.experimental.outgoing-span-suppression-by-type
+ private static final ContextKey<Span> CLIENT_KEY =
+ ContextKey.named("opentelemetry-traces-span-key-client");
+
+ private static final ContextKey<Span> PRODUCER_KEY =
+ ContextKey.named("opentelemetry-traces-span-key-producer");
+
+ public static final SpanKey SERVER = new SpanKey(SERVER_KEY);
+ public static final SpanKey CONSUMER = new SpanKey(CONSUMER_KEY);
+
+ static final SpanKey HTTP_CLIENT = new SpanKey(HTTP_KEY);
+ static final SpanKey RPC_CLIENT = new SpanKey(RPC_KEY);
+ static final SpanKey DB_CLIENT = new SpanKey(DB_KEY);
+ static final SpanKey MESSAGING_PRODUCER = new SpanKey(MESSAGING_KEY);
+
+ // this is used instead of above, depending on the configuration value for
+ // otel.instrumentation.experimental.outgoing-span-suppression-by-type
+ public static final SpanKey ALL_CLIENTS = new SpanKey(CLIENT_KEY);
+ public static final SpanKey ALL_PRODUCERS = new SpanKey(PRODUCER_KEY);
+
+ private final ContextKey<Span> key;
+
+ SpanKey(ContextKey<Span> key) {
+ this.key = key;
+ }
+
+ public Context storeInContext(Context context, Span span) {
+ return context.with(key, span);
+ }
+
+ @Nullable
+ public Span fromContextOrNull(Context context) {
+ return context.get(key);
+ }
+}

ADD
instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/SpanSuppressionStrategy.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.instrumenter;
+
+import static java.util.Collections.singleton;
+
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.context.Context;
+import java.util.Set;
+
+abstract class SpanSuppressionStrategy {
+ private static final SpanSuppressionStrategy SERVER_STRATEGY =
+ new SuppressIfSameSpanKeyStrategy(singleton(SpanKey.SERVER));
+ private static final SpanSuppressionStrategy CONSUMER_STRATEGY =
+ new StoreOnlyStrategy(singleton(SpanKey.CONSUMER));
+ private static final SpanSuppressionStrategy ALL_CLIENTS_STRATEGY =
+ new SuppressIfSameSpanKeyStrategy(singleton(SpanKey.ALL_CLIENTS));
+ private static final SpanSuppressionStrategy ALL_PRODUCERS_STRATEGY =
+ new SuppressIfSameSpanKeyStrategy(singleton(SpanKey.ALL_PRODUCERS));
+
+ public static final SpanSuppressionStrategy SUPPRESS_ALL_NESTED_OUTGOING_STRATEGY =
+ new CompositeSuppressionStrategy(
+ ALL_CLIENTS_STRATEGY, ALL_PRODUCERS_STRATEGY, SERVER_STRATEGY, CONSUMER_STRATEGY);
+
+ private static final SpanSuppressionStrategy NO_CLIENT_SUPPRESSION_STRATEGY =
+ new CompositeSuppressionStrategy(
+ NoopSuppressionStrategy.INSTANCE,
+ NoopSuppressionStrategy.INSTANCE,
+ SERVER_STRATEGY,
+ CONSUMER_STRATEGY);
+
+ static SpanSuppressionStrategy from(Set<SpanKey> clientSpanKeys) {
+ if (clientSpanKeys.isEmpty()) {
+ return NO_CLIENT_SUPPRESSION_STRATEGY;
+ }
+
+ SpanSuppressionStrategy clientOrProducerStrategy =
+ new SuppressIfSameSpanKeyStrategy(clientSpanKeys);
+ return new CompositeSuppressionStrategy(
+ clientOrProducerStrategy, clientOrProducerStrategy, SERVER_STRATEGY, CONSUMER_STRATEGY);
+ }
+
+ abstract Context storeInContext(Context context, SpanKind spanKind, Span span);
+
+ abstract boolean shouldSuppress(Context parentContext, SpanKind spanKind);
+}

ADD
instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/StoreOnlyStrategy.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.instrumenter;
+
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.context.Context;
+import java.util.Set;
+
+final class StoreOnlyStrategy extends SpanSuppressionStrategy {
+
+ private final Set<SpanKey> outgoingSpanKeys;
+
+ StoreOnlyStrategy(Set<SpanKey> outgoingSpanKeys) {
+ this.outgoingSpanKeys = outgoingSpanKeys;
+ }
+
+ @Override
+ Context storeInContext(Context context, SpanKind spanKind, Span span) {
+ for (SpanKey outgoingSpanKey : outgoingSpanKeys) {
+ context = outgoingSpanKey.storeInContext(context, span);
+ }
+ return context;
+ }
+
+ @Override
+ boolean shouldSuppress(Context parentContext, SpanKind spanKind) {
+ return false;
+ }
+}

ADD
instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/SuppressIfSameSpanKeyStrategy.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.instrumenter;
+
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.context.Context;
+import java.util.Set;
+
+final class SuppressIfSameSpanKeyStrategy extends SpanSuppressionStrategy {
+
+ private final Set<SpanKey> outgoingSpanKeys;
+
+ SuppressIfSameSpanKeyStrategy(Set<SpanKey> outgoingSpanKeys) {
+ this.outgoingSpanKeys = outgoingSpanKeys;
+ }
+
+ @Override
+ Context storeInContext(Context context, SpanKind spanKind, Span span) {
+ for (SpanKey outgoingSpanKey : outgoingSpanKeys) {
+ context = outgoingSpanKey.storeInContext(context, span);
+ }
+ return context;
+ }
+
+ @Override
+ boolean shouldSuppress(Context parentContext, SpanKind spanKind) {
+ for (SpanKey outgoingSpanKey : outgoingSpanKeys) {
+ if (outgoingSpanKey.fromContextOrNull(parentContext) == null) {
+ return false;
+ }
+ }
+ return true;
+ }
+}

MODIFY
instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/ClientSpan.java
instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/ClientSpan.java
@@ -8,7 +8,7 @@ package io.opentelemetry.instrumentation.api.tracer;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.context.Context;
-import io.opentelemetry.context.ContextKey;
+import io.opentelemetry.instrumentation.api.instrumenter.SpanKey;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
@@ -16,9 +16,6 @@ import org.checkerframework.checker.nullness.qual.Nullable;
* the {@link Context}.
*/
public final class ClientSpan {
- // Keeps track of the client span in a subtree corresponding to a client request.
- private static final ContextKey<Span> KEY =
- ContextKey.named("opentelemetry-traces-client-span-key");
/** Returns true when a {@link SpanKind#CLIENT} span is present in the passed {@code context}. */
public static boolean exists(Context context) {
@@ -31,11 +28,11 @@ public final class ClientSpan {
*/
@Nullable
public static Span fromContextOrNull(Context context) {
- return context.get(KEY);
+ return SpanKey.ALL_CLIENTS.fromContextOrNull(context);
}
public static Context with(Context context, Span clientSpan) {
- return context.with(KEY, clientSpan);
+ return SpanKey.ALL_CLIENTS.storeInContext(context, clientSpan);
}
private ClientSpan() {}

MODIFY
instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/ConsumerSpan.java
instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/ConsumerSpan.java
@@ -8,7 +8,7 @@ package io.opentelemetry.instrumentation.api.tracer;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.context.Context;
-import io.opentelemetry.context.ContextKey;
+import io.opentelemetry.instrumentation.api.instrumenter.SpanKey;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
@@ -16,9 +16,6 @@ import org.checkerframework.checker.nullness.qual.Nullable;
* the {@link Context}.
*/
public final class ConsumerSpan {
- // Keeps track of the consumer span for the current trace.
- private static final ContextKey<Span> KEY =
- ContextKey.named("opentelemetry-traces-consumer-span-key");
/**
* Returns true when a {@link SpanKind#CONSUMER} span is present in the passed {@code context}.
@@ -33,11 +30,11 @@ public final class ConsumerSpan {
*/
@Nullable
public static Span fromContextOrNull(Context context) {
- return context.get(KEY);
+ return SpanKey.CONSUMER.fromContextOrNull(context);
}
public static Context with(Context context, Span consumerSpan) {
- return context.with(KEY, consumerSpan);
+ return SpanKey.CONSUMER.storeInContext(context, consumerSpan);
}
private ConsumerSpan() {}

MODIFY
instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/ServerSpan.java
instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/ServerSpan.java
@@ -8,7 +8,7 @@ package io.opentelemetry.instrumentation.api.tracer;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.context.Context;
-import io.opentelemetry.context.ContextKey;
+import io.opentelemetry.instrumentation.api.instrumenter.SpanKey;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
@@ -16,9 +16,6 @@ import org.checkerframework.checker.nullness.qual.Nullable;
* the {@link Context}.
*/
public final class ServerSpan {
- // Keeps track of the server span for the current trace.
- private static final ContextKey<Span> KEY =
- ContextKey.named("opentelemetry-traces-server-span-key");
/** Returns true when a {@link SpanKind#SERVER} span is present in the passed {@code context}. */
public static boolean exists(Context context) {
@@ -31,11 +28,11 @@ public final class ServerSpan {
*/
@Nullable
public static Span fromContextOrNull(Context context) {
- return context.get(KEY);
+ return SpanKey.SERVER.fromContextOrNull(context);
}
public static Context with(Context context, Span serverSpan) {
- return context.with(KEY, serverSpan);
+ return SpanKey.SERVER.storeInContext(context, serverSpan);
}
private ServerSpan() {}

MODIFY
instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterTest.java
instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterTest.java
@@ -21,9 +21,11 @@ import io.opentelemetry.api.trace.TraceState;
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.TextMapGetter;
+import io.opentelemetry.instrumentation.api.instrumenter.db.DbAttributesExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpAttributesExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.net.NetAttributesExtractor;
-import io.opentelemetry.instrumentation.api.tracer.ServerSpan;
+import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcAttributesExtractor;
import io.opentelemetry.sdk.common.InstrumentationLibraryInfo;
import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension;
import io.opentelemetry.sdk.trace.data.LinkData;
@@ -129,7 +131,12 @@ class InstrumenterTest {
static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create();
@Mock HttpAttributesExtractor<Map<String, String>, Map<String, String>> mockHttpAttributes;
+ @Mock DbAttributesExtractor<Map<String, String>, Map<String, String>> mockDbAttributes;
+ @Mock
+ MessagingAttributesExtractor<Map<String, String>, Map<String, String>> mockMessagingAttributes;
+
+ @Mock RpcAttributesExtractor<Map<String, String>, Map<String, String>> mockRpcAttributes;
@Mock NetAttributesExtractor<Map<String, String>, Map<String, String>> mockNetAttributes;
@Test
@@ -145,7 +152,7 @@ class InstrumenterTest {
SpanContext spanContext = Span.fromContext(context).getSpanContext();
assertThat(spanContext.isValid()).isTrue();
- assertThat(ServerSpan.fromContextOrNull(context).getSpanContext()).isEqualTo(spanContext);
+ assertThat(SpanKey.SERVER.fromContextOrNull(context).getSpanContext()).isEqualTo(spanContext);
instrumenter.end(context, REQUEST, RESPONSE, null);
@@ -188,7 +195,7 @@ class InstrumenterTest {
SpanContext spanContext = Span.fromContext(context).getSpanContext();
assertThat(spanContext.isValid()).isTrue();
- assertThat(ServerSpan.fromContextOrNull(context).getSpanContext()).isEqualTo(spanContext);
+ assertThat(SpanKey.SERVER.fromContextOrNull(context).getSpanContext()).isEqualTo(spanContext);
instrumenter.end(context, REQUEST, RESPONSE, new IllegalStateException("test"));
@@ -225,7 +232,7 @@ class InstrumenterTest {
SpanContext spanContext = Span.fromContext(context).getSpanContext();
assertThat(spanContext.isValid()).isTrue();
- assertThat(ServerSpan.fromContextOrNull(context).getSpanContext()).isEqualTo(spanContext);
+ assertThat(SpanKey.SERVER.fromContextOrNull(context).getSpanContext()).isEqualTo(spanContext);
instrumenter.end(context, request, RESPONSE, null);
@@ -261,7 +268,7 @@ class InstrumenterTest {
SpanContext spanContext = Span.fromContext(context).getSpanContext();
assertThat(spanContext.isValid()).isTrue();
- assertThat(ServerSpan.fromContextOrNull(context).getSpanContext()).isEqualTo(spanContext);
+ assertThat(SpanKey.SERVER.fromContextOrNull(context).getSpanContext()).isEqualTo(spanContext);
instrumenter.end(context, REQUEST, RESPONSE, null);
@@ -304,7 +311,7 @@ class InstrumenterTest {
SpanContext spanContext = Span.fromContext(context).getSpanContext();
assertThat(spanContext.isValid()).isTrue();
- assertThat(ServerSpan.fromContextOrNull(context).getSpanContext()).isEqualTo(spanContext);
+ assertThat(SpanKey.SERVER.fromContextOrNull(context).getSpanContext()).isEqualTo(spanContext);
instrumenter.end(context, request, RESPONSE, null);
@@ -346,7 +353,7 @@ class InstrumenterTest {
SpanContext spanContext = Span.fromContext(context).getSpanContext();
assertThat(spanContext.isValid()).isTrue();
- assertThat(ServerSpan.fromContextOrNull(context).getSpanContext()).isEqualTo(spanContext);
+ assertThat(SpanKey.SERVER.fromContextOrNull(context).getSpanContext()).isEqualTo(spanContext);
instrumenter.end(context, request, RESPONSE, null);
@@ -735,6 +742,186 @@ class InstrumenterTest {
.isEqualTo("1111:1111:1111:1111:1111:1111:1111:1111");
}
+ @Test
+ void clientNestedSpansSuppressed_whenInstrumentationTypeDisabled() {
+ // this test depends on default config option for InstrumentationType
+
+ Instrumenter<Map<String, String>, Map<String, String>> instrumenterOuter =
+ getInstrumenterWithType(false);
+ Instrumenter<Map<String, String>, Map<String, String>> instrumenterInner =
+ getInstrumenterWithType(false);
+
+ Map<String, String> request = new HashMap<>(REQUEST);
+
+ Context context = instrumenterOuter.start(Context.root(), request);
+ assertThat(instrumenterInner.shouldStart(context, request)).isFalse();
+ }
+
+ @Test
+ void clientNestedSpansSuppressed_whenInstrumentationTypeDisabled2() {
+ Instrumenter<Map<String, String>, Map<String, String>> instrumenterOuter =
+ getInstrumenterWithType(false, mockDbAttributes);
+ Instrumenter<Map<String, String>, Map<String, String>> instrumenterInner =
+ getInstrumenterWithType(false, mockHttpAttributes);
+
+ Map<String, String> request = new HashMap<>(REQUEST);
+
+ Context context = instrumenterOuter.start(Context.root(), request);
+ assertThat(instrumenterInner.shouldStart(context, request)).isFalse();
+ }
+
+ @Test
+ void clientNestedSuppressed_whenSameInstrumentationType() {
+ Instrumenter<Map<String, String>, Map<String, String>> instrumenterOuter =
+ getInstrumenterWithType(true, mockDbAttributes);
+ Instrumenter<Map<String, String>, Map<String, String>> instrumenterInner =
+ getInstrumenterWithType(true, mockDbAttributes);
+
+ Map<String, String> request = new HashMap<>(REQUEST);
+
+ assertThat(instrumenterOuter.shouldStart(Context.root(), request)).isTrue();
+ assertThat(instrumenterInner.shouldStart(Context.root(), request)).isTrue();
+
+ Context context = instrumenterOuter.start(Context.root(), request);
+ assertThat(instrumenterInner.shouldStart(context, request)).isFalse();
+ }
+
+ @Test
+ void clientNestedNotSuppressed_wehnDifferentInstrumentationCategories() {
+ Instrumenter<Map<String, String>, Map<String, String>> instrumenterOuter =
+ getInstrumenterWithType(true, mockDbAttributes);
+ Instrumenter<Map<String, String>, Map<String, String>> instrumenterInner =
+ getInstrumenterWithType(true, mockHttpAttributes);
+
+ Map<String, String> request = new HashMap<>(REQUEST);
+
+ Context context = instrumenterOuter.start(Context.root(), request);
+ assertThat(instrumenterInner.shouldStart(context, request)).isTrue();
+ }
+
+ @Test
+ void clientNestedGenericNotSuppressed() {
+ Instrumenter<Map<String, String>, Map<String, String>> instrumenterOuter =
+ getInstrumenterWithType(true, new AttributesExtractor1());
+ Instrumenter<Map<String, String>, Map<String, String>> instrumenterInner =
+ getInstrumenterWithType(true, new AttributesExtractor1());
+
+ Map<String, String> request = new HashMap<>(REQUEST);
+ Context context = instrumenterOuter.start(Context.root(), request);
+ assertThat(instrumenterInner.shouldStart(context, request)).isTrue();
+ }
+
+ @Test
+ void clientNestedGenericSpansNotSuppressed_whenNoExtractors() {
+ // this test depends on default config option for InstrumentationType
+
+ Instrumenter<Map<String, String>, Map<String, String>> instrumenterOuter =
+ getInstrumenterWithType(true);
+ Instrumenter<Map<String, String>, Map<String, String>> instrumenterInner =
+ getInstrumenterWithType(true, new AttributesExtractor[] {null});
+
+ Map<String, String> request = new HashMap<>(REQUEST);
+
+ Context context = instrumenterOuter.start(Context.root(), request);
+ assertThat(instrumenterInner.shouldStart(context, request)).isTrue();
+ }
+
+ @Test
+ void instrumentationTypeDetected_http() {
+ Instrumenter<Map<String, String>, Map<String, String>> instrumenter =
+ getInstrumenterWithType(true, mockHttpAttributes, new AttributesExtractor1());
+
+ Map<String, String> request = new HashMap<>(REQUEST);
+
+ Context context = instrumenter.start(Context.root(), request);
+ validateInstrumentationTypeSpanPresent(SpanKey.HTTP_CLIENT, context);
+ }
+
+ @Test
+ void instrumentationTypeDetected_db() {
+ Instrumenter<Map<String, String>, Map<String, String>> instrumenter =
+ getInstrumenterWithType(true, mockDbAttributes, new AttributesExtractor2());
+
+ Map<String, String> request = new HashMap<>(REQUEST);
+
+ Context context = instrumenter.start(Context.root(), request);
+ validateInstrumentationTypeSpanPresent(SpanKey.DB_CLIENT, context);
+ }
+
+ @Test
+ void instrumentationTypeDetected_rpc() {
+ Instrumenter<Map<String, String>, Map<String, String>> instrumenter =
+ getInstrumenterWithType(true, mockRpcAttributes);
+
+ Map<String, String> request = new HashMap<>(REQUEST);
+
+ Context context = instrumenter.start(Context.root(), request);
+ validateInstrumentationTypeSpanPresent(SpanKey.RPC_CLIENT, context);
+ }
+
+ @Test
+ void instrumentationTypeDetected_messaging() {
+ Instrumenter<Map<String, String>, Map<String, String>> instrumenter =
+ getInstrumenterWithType(true, mockMessagingAttributes);
+
+ Map<String, String> request = new HashMap<>(REQUEST);
+
+ Context context = instrumenter.start(Context.root(), request);
+ validateInstrumentationTypeSpanPresent(SpanKey.MESSAGING_PRODUCER, context);
+ }
+
+ @Test
+ void instrumentationTypeDetected_mix() {
+ Instrumenter<Map<String, String>, Map<String, String>> instrumenter =
+ getInstrumenterWithType(
+ true,
+ new AttributesExtractor2(),
+ mockMessagingAttributes,
+ mockNetAttributes,
+ mockDbAttributes);
+
+ Map<String, String> request = new HashMap<>(REQUEST);
+
+ Context context = instrumenter.start(Context.root(), request);
+ validateInstrumentationTypeSpanPresent(SpanKey.MESSAGING_PRODUCER, context);
+ }
+
+ @Test
+ void instrumentationTypeDetected_generic() {
+ Instrumenter<Map<String, String>, Map<String, String>> instrumenter =
+ getInstrumenterWithType(true, new AttributesExtractor2(), mockNetAttributes);
+
+ Map<String, String> request = new HashMap<>(REQUEST);
+
+ Context context = instrumenter.start(Context.root(), request);
+ Span span = Span.fromContext(context);
+
+ assertThat(span).isNotNull();
+
+ assertThat(SpanKey.HTTP_CLIENT.fromContextOrNull(context)).isNull();
+ assertThat(SpanKey.DB_CLIENT.fromContextOrNull(context)).isNull();
+ assertThat(SpanKey.RPC_CLIENT.fromContextOrNull(context)).isNull();
+ assertThat(SpanKey.MESSAGING_PRODUCER.fromContextOrNull(context)).isNull();
+ }
+
+ private static void validateInstrumentationTypeSpanPresent(SpanKey spanKey, Context context) {
+ Span span = Span.fromContext(context);
+
+ assertThat(span).isNotNull();
+ assertThat(spanKey.fromContextOrNull(context)).isSameAs(span);
+ }
+
+ private static Instrumenter<Map<String, String>, Map<String, String>> getInstrumenterWithType(
+ boolean enableInstrumenation, AttributesExtractor... attributeExtractors) {
+ InstrumenterBuilder<Map<String, String>, Map<String, String>> builder =
+ Instrumenter.<Map<String, String>, Map<String, String>>newBuilder(
+ otelTesting.getOpenTelemetry(), "test", unused -> "span")
+ .addAttributesExtractors(attributeExtractors)
+ .enableInstrumentationTypeSuppression(enableInstrumenation);
+
+ return builder.newClientInstrumenter(Map::put);
+ }
+
private static LinkData expectedSpanLink() {
return LinkData.create(
SpanContext.create(

ADD
instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/SpanSuppressionStrategyTest.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.instrumenter;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.context.Context;
+import java.util.Collections;
+import java.util.HashSet;
+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.MethodSource;
+
+public class SpanSuppressionStrategyTest {
+
+ private static final Span SPAN = Span.getInvalid();
+
+ @Test
+ public void serverSpan_getSet() {
+ assertThat(SpanKey.SERVER.fromContextOrNull(Context.root())).isNull();
+
+ Context context = SpanKey.SERVER.storeInContext(Context.root(), SPAN);
+
+ SpanSuppressionStrategy strategy =
+ SpanSuppressionStrategy.from(Collections.singleton(SpanKey.SERVER));
+ assertThat(strategy.shouldSuppress(context, SpanKind.SERVER)).isTrue();
+
+ assertThat(SpanKey.SERVER.fromContextOrNull(context)).isSameAs(SPAN);
+ allClientSpanKeys().forEach(spanKey -> assertThat(spanKey.fromContextOrNull(context)).isNull());
+
+ assertThat(SpanKey.CONSUMER.fromContextOrNull(context)).isNull();
+ }
+
+ @Test
+ public void serverSpan_getSetWithStrategy() {
+ SpanSuppressionStrategy strategy =
+ SpanSuppressionStrategy.from(Collections.singleton(SpanKey.SERVER));
+ assertThat(strategy.shouldSuppress(Context.root(), SpanKind.SERVER)).isFalse();
+ assertThat(SpanKey.SERVER.fromContextOrNull(Context.root())).isNull();
+
+ Context context = strategy.storeInContext(Context.root(), SpanKind.SERVER, SPAN);
+
+ assertThat(strategy.shouldSuppress(context, SpanKind.SERVER)).isTrue();
+ assertThat(SpanKey.SERVER.fromContextOrNull(context)).isSameAs(SPAN);
+
+ allClientSpanKeys().forEach(spanKey -> assertThat(spanKey.fromContextOrNull(context)).isNull());
+
+ assertThat(SpanKey.CONSUMER.fromContextOrNull(context)).isNull();
+ }
+
+ @Test
+ public void consumerSpan_getSet() {
+ SpanSuppressionStrategy strategy =
+ SpanSuppressionStrategy.from(Collections.singleton(SpanKey.CONSUMER));
+
+ assertThat(strategy.shouldSuppress(Context.root(), SpanKind.CONSUMER)).isFalse();
+ assertThat(SpanKey.CONSUMER.fromContextOrNull(Context.root())).isNull();
+
+ Context context = SpanKey.CONSUMER.storeInContext(Context.root(), SPAN);
+
+ // never suppress CONSUMER
+ assertThat(strategy.shouldSuppress(context, SpanKind.CONSUMER)).isFalse();
+
+ assertThat(SpanKey.CONSUMER.fromContextOrNull(context)).isSameAs(SPAN);
+ allClientSpanKeys().forEach(spanKey -> assertThat(spanKey.fromContextOrNull(context)).isNull());
+
+ assertThat(SpanKey.SERVER.fromContextOrNull(context)).isNull();
+ }
+
+ @ParameterizedTest
+ @MethodSource("allClientSpanKeys")
+ public void clientSpan_differentForAllTypes(SpanKey spanKey) {
+ SpanSuppressionStrategy strategy = SpanSuppressionStrategy.from(Collections.singleton(spanKey));
+ assertThat(strategy.shouldSuppress(Context.root(), SpanKind.CLIENT)).isFalse();
+
+ Context context = spanKey.storeInContext(Context.root(), SPAN);
+
+ assertThat(strategy.shouldSuppress(context, SpanKind.CLIENT)).isTrue();
+ assertThat(strategy.shouldSuppress(context, SpanKind.SERVER)).isFalse();
+ assertThat(strategy.shouldSuppress(context, SpanKind.CONSUMER)).isFalse();
+
+ assertThat(spanKey.fromContextOrNull(context)).isSameAs(SPAN);
+
+ allClientSpanKeys()
+ .filter(key -> key != spanKey)
+ .forEach(key -> assertThat(key.fromContextOrNull(context)).isNull());
+ }
+
+ @ParameterizedTest
+ @MethodSource("allClientSpanKeys")
+ public void client_sameAsProducer(SpanKey spanKey) {
+ SpanSuppressionStrategy strategy = SpanSuppressionStrategy.from(Collections.singleton(spanKey));
+
+ Context context = spanKey.storeInContext(Context.root(), SPAN);
+
+ assertThat(strategy.shouldSuppress(context, SpanKind.CLIENT)).isTrue();
+ assertThat(strategy.shouldSuppress(context, SpanKind.PRODUCER)).isTrue();
+
+ allClientSpanKeys()
+ .forEach(
+ anotherKey -> {
+ if (spanKey != anotherKey) {
+ SpanSuppressionStrategy anotherStrategy =
+ SpanSuppressionStrategy.from(Collections.singleton(anotherKey));
+ assertThat(anotherStrategy.shouldSuppress(context, SpanKind.CLIENT)).isFalse();
+ assertThat(anotherStrategy.shouldSuppress(context, SpanKind.PRODUCER)).isFalse();
+ }
+ });
+ }
+
+ @Test
+ public void allNestedOutgoing_producerDoesNotSuppressClient() {
+ SpanSuppressionStrategy strategy =
+ SpanSuppressionStrategy.SUPPRESS_ALL_NESTED_OUTGOING_STRATEGY;
+
+ Context contextClient = SpanKey.ALL_CLIENTS.storeInContext(Context.root(), SPAN);
+ Context contextProducer = SpanKey.ALL_PRODUCERS.storeInContext(Context.root(), SPAN);
+
+ assertThat(strategy.shouldSuppress(contextClient, SpanKind.CLIENT)).isTrue();
+ assertThat(strategy.shouldSuppress(contextClient, SpanKind.PRODUCER)).isFalse();
+
+ assertThat(strategy.shouldSuppress(contextProducer, SpanKind.CLIENT)).isFalse();
+ assertThat(strategy.shouldSuppress(contextProducer, SpanKind.PRODUCER)).isTrue();
+ }
+
+ @Test
+ public void multipleKeys() {
+
+ SpanSuppressionStrategy strategy =
+ SpanSuppressionStrategy.from(allClientSpanKeys().collect(Collectors.toSet()));
+
+ Context context = strategy.storeInContext(Context.root(), SpanKind.CLIENT, SPAN);
+
+ assertThat(strategy.shouldSuppress(context, SpanKind.CLIENT)).isTrue();
+ assertThat(strategy.shouldSuppress(context, SpanKind.PRODUCER)).isTrue();
+ assertThat(strategy.shouldSuppress(context, SpanKind.SERVER)).isFalse();
+ assertThat(strategy.shouldSuppress(context, SpanKind.CONSUMER)).isFalse();
+
+ allClientSpanKeys()
+ .forEach(
+ key -> {
+ assertThat(key.fromContextOrNull(context)).isSameAs(SPAN);
+ });
+ }
+
+ @Test
+ public void noKeys_clientIsNeverSuppressed() {
+
+ SpanSuppressionStrategy strategy = SpanSuppressionStrategy.from(new HashSet<>());
+
+ Context context = strategy.storeInContext(Context.root(), SpanKind.CLIENT, SPAN);
+ assertThat(context).isSameAs(Context.root());
+
+ assertThat(strategy.shouldSuppress(context, SpanKind.CLIENT)).isFalse();
+ assertThat(strategy.shouldSuppress(context, SpanKind.PRODUCER)).isFalse();
+ assertThat(strategy.shouldSuppress(context, SpanKind.SERVER)).isFalse();
+ assertThat(strategy.shouldSuppress(context, SpanKind.CONSUMER)).isFalse();
+
+ allClientSpanKeys()
+ .forEach(
+ key -> {
+ assertThat(key.fromContextOrNull(context)).isNull();
+ });
+ }
+
+ @Test
+ public void noKeys_serverIsSuppressed() {
+
+ SpanSuppressionStrategy strategy = SpanSuppressionStrategy.from(new HashSet<>());
+
+ Context context = strategy.storeInContext(Context.root(), SpanKind.SERVER, SPAN);
+
+ assertThat(strategy.shouldSuppress(context, SpanKind.SERVER)).isTrue();
+ assertThat(SpanKey.SERVER.fromContextOrNull(context)).isSameAs(SPAN);
+
+ allClientSpanKeys()
+ .forEach(
+ key -> {
+ assertThat(key.fromContextOrNull(context)).isNull();
+ });
+ }
+
+ @Test
+ public void noKeys_consumerIsNeverSuppressed() {
+
+ SpanSuppressionStrategy strategy = SpanSuppressionStrategy.from(new HashSet<>());
+
+ Context context = strategy.storeInContext(Context.root(), SpanKind.CONSUMER, SPAN);
+
+ assertThat(strategy.shouldSuppress(context, SpanKind.CONSUMER)).isFalse();
+ assertThat(SpanKey.CONSUMER.fromContextOrNull(context)).isSameAs(SPAN);
+
+ allClientSpanKeys()
+ .forEach(
+ key -> {
+ assertThat(key.fromContextOrNull(context)).isNull();
+ });
+ }
+
+ private static Stream<SpanKey> allClientSpanKeys() {
+ return Stream.of(
+ SpanKey.ALL_CLIENTS,
+ SpanKey.ALL_PRODUCERS,
+ SpanKey.HTTP_CLIENT,
+ SpanKey.DB_CLIENT,
+ SpanKey.RPC_CLIENT,
+ SpanKey.MESSAGING_PRODUCER);
+ }
+}

Client span keys: suppressing same instrumentation (#3691)