diff --git a/instrumentation-api-incubator/build.gradle.kts b/instrumentation-api-incubator/build.gradle.kts index 1a59ad1364a8..5d87535b7a2b 100644 --- a/instrumentation-api-incubator/build.gradle.kts +++ b/instrumentation-api-incubator/build.gradle.kts @@ -92,6 +92,7 @@ tasks { testClassesDirs = sourceSets.test.get().output.classesDirs classpath = sourceSets.test.get().runtimeClasspath jvmArgs("-Dotel.semconv-stability.opt-in=database,code,service.peer,rpc") + jvmArgs("-Dotel.semconv-stability.preview=messaging") inputs.dir(jflexOutputDir) } @@ -99,6 +100,7 @@ tasks { testClassesDirs = sourceSets.test.get().output.classesDirs classpath = sourceSets.test.get().runtimeClasspath jvmArgs("-Dotel.semconv-stability.opt-in=database/dup,code/dup,service.peer/dup,rpc/dup") + jvmArgs("-Dotel.semconv-stability.preview=messaging/dup") inputs.dir(jflexOutputDir) } @@ -117,6 +119,11 @@ tasks { } check { - dependsOn(testStableSemconv, testBothSemconv, testExceptionSignalLogs, testExceptionSignalLogsDup) + dependsOn( + testStableSemconv, + testBothSemconv, + testExceptionSignalLogs, + testExceptionSignalLogsDup, + ) } } diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessageOperation.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessageOperation.java index e7ef5c479ed0..c1062bd0d056 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessageOperation.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessageOperation.java @@ -5,24 +5,19 @@ package io.opentelemetry.instrumentation.api.incubator.semconv.messaging; -import java.util.Locale; - -/** - * Represents type of operations - * that may be used in a messaging system. - */ +/** Represents an operation that may be used in a messaging system. */ public enum MessageOperation { - PUBLISH, - RECEIVE, - PROCESS; + PUBLISH(MessagingOperationType.SEND), + RECEIVE(MessagingOperationType.RECEIVE), + PROCESS(MessagingOperationType.PROCESS); + + private final MessagingOperationType operationType; + + MessageOperation(MessagingOperationType operationType) { + this.operationType = operationType; + } - /** - * Returns the operation name as defined in the - * specification. - */ - String operationName() { - return name().toLowerCase(Locale.ROOT); + MessagingOperationType type() { + return operationType; } } diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingAttributesExtractor.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingAttributesExtractor.java index 3ad89017a047..3a680fa8f625 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingAttributesExtractor.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingAttributesExtractor.java @@ -5,6 +5,10 @@ package io.opentelemetry.instrumentation.api.incubator.semconv.messaging; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitOldMessagingSemconv; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; +import static io.opentelemetry.semconv.ErrorAttributes.ERROR_TYPE; + import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; @@ -17,7 +21,7 @@ /** * Extractor of messaging + * href="https://github.com/open-telemetry/semantic-conventions/blob/v1.43.0/docs/messaging/messaging-spans.md">messaging * attributes. * *

This class delegates to a type-specific {@link MessagingAttributesGetter} for individual @@ -29,8 +33,10 @@ public final class MessagingAttributesExtractor // copied from MessagingIncubatingAttributes private static final AttributeKey MESSAGING_BATCH_MESSAGE_COUNT = AttributeKey.longKey("messaging.batch.message_count"); - private static final AttributeKey MESSAGING_CLIENT_ID = + private static final AttributeKey MESSAGING_CLIENT_ID_OLD = AttributeKey.stringKey("messaging.client_id"); + private static final AttributeKey MESSAGING_CLIENT_ID = + AttributeKey.stringKey("messaging.client.id"); private static final AttributeKey MESSAGING_DESTINATION_ANONYMOUS = AttributeKey.booleanKey("messaging.destination.anonymous"); private static final AttributeKey MESSAGING_DESTINATION_NAME = @@ -51,49 +57,78 @@ public final class MessagingAttributesExtractor AttributeKey.stringKey("messaging.message.id"); private static final AttributeKey MESSAGING_OPERATION = AttributeKey.stringKey("messaging.operation"); + private static final AttributeKey MESSAGING_OPERATION_NAME = + AttributeKey.stringKey("messaging.operation.name"); + private static final AttributeKey MESSAGING_OPERATION_TYPE = + AttributeKey.stringKey("messaging.operation.type"); private static final AttributeKey MESSAGING_SYSTEM = AttributeKey.stringKey("messaging.system"); static final String TEMP_DESTINATION_NAME = "(temporary)"; - /** - * Creates the messaging attributes extractor for the given {@link MessageOperation operation} - * with default configuration. - */ + /** Creates the messaging attributes extractor for the given operation type. */ + public static AttributesExtractor createForOperationType( + MessagingAttributesGetter getter, MessagingOperationType operationType) { + return builderForOperationType(getter, operationType).build(); + } + + /** Creates the messaging attributes extractor for the given operation. */ public static AttributesExtractor create( - MessagingAttributesGetter getter, MessageOperation operation) { + MessagingAttributesGetter getter, @Nullable MessageOperation operation) { return builder(getter, operation).build(); } /** - * Returns a new {@link MessagingAttributesExtractorBuilder} for the given {@link MessageOperation - * operation} that can be used to configure the messaging attributes extractor. + * Returns a new {@link MessagingAttributesExtractorBuilder} configured for the given operation + * type. */ + public static + MessagingAttributesExtractorBuilder builderForOperationType( + MessagingAttributesGetter getter, + @Nullable MessagingOperationType operationType) { + return new MessagingAttributesExtractorBuilder<>(getter, operationType, true); + } + + /** Returns a new messaging attributes extractor builder for the given operation. */ public static MessagingAttributesExtractorBuilder builder( - MessagingAttributesGetter getter, MessageOperation operation) { - return new MessagingAttributesExtractorBuilder<>(getter, operation); + MessagingAttributesGetter getter, @Nullable MessageOperation operation) { + return new MessagingAttributesExtractorBuilder<>( + getter, operation == null ? null : operation.type(), false); } private final MessagingAttributesGetter getter; - private final MessageOperation operation; + @Nullable private final MessagingOperationType operationType; + @Nullable private final String operationName; + private final boolean supportsStableSemconv; private final List capturedHeaders; MessagingAttributesExtractor( MessagingAttributesGetter getter, - MessageOperation operation, + @Nullable MessagingOperationType operationType, + @Nullable String operationName, + boolean supportsStableSemconv, List capturedHeaders) { this.getter = getter; - this.operation = operation; + this.operationType = operationType; + this.operationName = operationName; + this.supportsStableSemconv = supportsStableSemconv; this.capturedHeaders = new ArrayList<>(capturedHeaders); } @Override public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) { + boolean emitOldSemconv = !supportsStableSemconv || emitOldMessagingSemconv(); + boolean emitStableSemconv = supportsStableSemconv && emitStableMessagingSemconv(); attributes.put(MESSAGING_SYSTEM, getter.getSystem(request)); boolean isTemporaryDestination = getter.isTemporaryDestination(request); if (isTemporaryDestination) { attributes.put(MESSAGING_DESTINATION_TEMPORARY, true); - attributes.put(MESSAGING_DESTINATION_NAME, TEMP_DESTINATION_NAME); + if (emitStableSemconv) { + attributes.put(MESSAGING_DESTINATION_NAME, getter.getDestination(request)); + attributes.put(MESSAGING_DESTINATION_TEMPLATE, getter.getDestinationTemplate(request)); + } else { + attributes.put(MESSAGING_DESTINATION_NAME, TEMP_DESTINATION_NAME); + } } else { attributes.put(MESSAGING_DESTINATION_NAME, getter.getDestination(request)); attributes.put(MESSAGING_DESTINATION_TEMPLATE, getter.getDestinationTemplate(request)); @@ -106,9 +141,20 @@ public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST attributes.put(MESSAGING_MESSAGE_CONVERSATION_ID, getter.getConversationId(request)); attributes.put(MESSAGING_MESSAGE_BODY_SIZE, getter.getMessageBodySize(request)); attributes.put(MESSAGING_MESSAGE_ENVELOPE_SIZE, getter.getMessageEnvelopeSize(request)); - attributes.put(MESSAGING_CLIENT_ID, getter.getClientId(request)); - if (operation != null) { - attributes.put(MESSAGING_OPERATION, operation.operationName()); + if (emitOldSemconv) { + attributes.put(MESSAGING_CLIENT_ID_OLD, getter.getClientId(request)); + } + if (emitStableSemconv) { + attributes.put(MESSAGING_CLIENT_ID, getter.getClientId(request)); + } + if (emitOldSemconv && operationType != null) { + attributes.put(MESSAGING_OPERATION, operationType.defaultOperationName()); + } + if (emitStableSemconv) { + attributes.put(MESSAGING_OPERATION_NAME, operationName); + if (operationType != null) { + attributes.put(MESSAGING_OPERATION_TYPE, operationType.value()); + } } } @@ -121,6 +167,13 @@ public void onEnd( @Nullable Throwable error) { attributes.put(MESSAGING_MESSAGE_ID, getter.getMessageId(request, response)); attributes.put(MESSAGING_BATCH_MESSAGE_COUNT, getter.getBatchMessageCount(request, response)); + if (supportsStableSemconv && emitStableMessagingSemconv()) { + String errorType = getter.getErrorType(request, response, error); + if (errorType == null && error != null) { + errorType = error.getClass().getName(); + } + attributes.put(ERROR_TYPE, errorType); + } for (String name : capturedHeaders) { List values = getter.getMessageHeader(request, name); @@ -137,17 +190,21 @@ public void onEnd( @Override @Nullable public SpanKey internalGetSpanKey() { - if (operation == null) { + if (operationType == null) { return null; } - switch (operation) { - case PUBLISH: + switch (operationType) { + case CREATE: + return SpanKey.PRODUCER_CREATE; + case SEND: return SpanKey.PRODUCER; case RECEIVE: return SpanKey.CONSUMER_RECEIVE; case PROCESS: return SpanKey.CONSUMER_PROCESS; + case SETTLE: + return SpanKey.CONSUMER_SETTLE; } throw new IllegalStateException("Can't possibly happen"); } diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingAttributesExtractorBuilder.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingAttributesExtractorBuilder.java index 61f6c6916549..925047dabecf 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingAttributesExtractorBuilder.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingAttributesExtractorBuilder.java @@ -6,24 +6,43 @@ package io.opentelemetry.instrumentation.api.incubator.semconv.messaging; import static java.util.Collections.emptyList; +import static java.util.Objects.requireNonNull; import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import javax.annotation.Nullable; /** A builder of {@link MessagingAttributesExtractor}. */ public final class MessagingAttributesExtractorBuilder { final MessagingAttributesGetter getter; - final MessageOperation operation; + @Nullable private final MessagingOperationType operationType; + @Nullable private String operationName; + private final boolean supportsStableSemconv; List capturedHeaders = emptyList(); MessagingAttributesExtractorBuilder( - MessagingAttributesGetter getter, MessageOperation operation) { + MessagingAttributesGetter getter, + @Nullable MessagingOperationType operationType, + boolean supportsStableSemconv) { this.getter = getter; - this.operation = operation; + this.operationType = operationType; + this.operationName = operationType == null ? null : operationType.defaultOperationName(); + this.supportsStableSemconv = supportsStableSemconv; + } + + /** Configures the system-specific operation name emitted as {@code messaging.operation.name}. */ + @CanIgnoreReturnValue + public MessagingAttributesExtractorBuilder setOperationName( + String operationName) { + if (!supportsStableSemconv) { + throw new IllegalStateException("Operation name is not configurable for legacy builders"); + } + this.operationName = requireNonNull(operationName, "operationName"); + return this; } /** @@ -47,6 +66,10 @@ public MessagingAttributesExtractorBuilder setCapturedHeaders * MessagingAttributesExtractorBuilder}. */ public AttributesExtractor build() { - return new MessagingAttributesExtractor<>(getter, operation, capturedHeaders); + if (supportsStableSemconv) { + requireNonNull(operationName, "operationName"); + } + return new MessagingAttributesExtractor<>( + getter, operationType, operationName, supportsStableSemconv, capturedHeaders); } } diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingAttributesGetter.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingAttributesGetter.java index ae0b29471cf0..eaea16aaf74f 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingAttributesGetter.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingAttributesGetter.java @@ -55,6 +55,21 @@ default String getDestinationPartitionId(REQUEST request) { return null; } + /** + * Returns a description of a class of error the operation ended with. + * + *

If this method returns {@code null}, the exception class name (if any) will be used as error + * type. + * + *

The cardinality of the error type should be low. The instrumentations implementing this + * method are recommended to document the custom values they support. + */ + @Nullable + default String getErrorType( + REQUEST request, @Nullable RESPONSE response, @Nullable Throwable error) { + return null; + } + /** * Extracts all values of header named {@code name} from the request, or an empty list if there * were none. diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingConsumerMetrics.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingConsumerMetrics.java index 87a98b106d74..76a62a828e84 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingConsumerMetrics.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingConsumerMetrics.java @@ -5,6 +5,9 @@ package io.opentelemetry.instrumentation.api.incubator.semconv.messaging; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitOldMessagingSemconv; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; +import static io.opentelemetry.semconv.ErrorAttributes.ERROR_TYPE; import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.logging.Level.FINE; @@ -23,10 +26,11 @@ import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; import io.opentelemetry.instrumentation.api.internal.OperationMetricsUtil; import java.util.logging.Logger; +import javax.annotation.Nullable; /** * {@link OperationListener} which keeps track of Consumer + * href="https://github.com/open-telemetry/semantic-conventions/blob/v1.43.0/docs/messaging/messaging-metrics.md#consumer-metrics">consumer * metrics. */ public final class MessagingConsumerMetrics implements OperationListener { @@ -35,39 +39,70 @@ public final class MessagingConsumerMetrics implements OperationListener { // copied from MessagingIncubatingAttributes private static final AttributeKey MESSAGING_BATCH_MESSAGE_COUNT = AttributeKey.longKey("messaging.batch.message_count"); + private static final AttributeKey MESSAGING_OPERATION = + AttributeKey.stringKey("messaging.operation"); + private static final AttributeKey MESSAGING_OPERATION_TYPE = + AttributeKey.stringKey("messaging.operation.type"); private static final ContextKey MESSAGING_CONSUMER_METRICS_STATE = ContextKey.named("messaging-consumer-metrics-state"); private static final Logger logger = Logger.getLogger(MessagingConsumerMetrics.class.getName()); - private final DoubleHistogram receiveDurationHistogram; - private final LongCounter receiveMessageCount; + private final boolean supportsStableSemconv; + private final boolean consumedMessagesOnly; + private final boolean enabled; + @Nullable private final DoubleHistogram receiveDurationHistogram; + @Nullable private final LongCounter receiveMessageCount; + @Nullable private final DoubleHistogram clientOperationDurationHistogram; + @Nullable private final LongCounter consumedMessagesCounter; - private MessagingConsumerMetrics(Meter meter) { - DoubleHistogramBuilder durationBuilder = - meter - .histogramBuilder("messaging.receive.duration") - .setDescription("Measures the duration of receive operation.") - .setExplicitBucketBoundariesAdvice(MessagingMetricsAdvice.DURATION_SECONDS_BUCKETS) - .setUnit("s"); - MessagingMetricsAdvice.applyReceiveDurationAdvice(durationBuilder); - receiveDurationHistogram = durationBuilder.build(); + private MessagingConsumerMetrics(Meter meter, boolean supportsStableSemconv) { + this(meter, supportsStableSemconv, false); + } - LongCounterBuilder longCounterBuilder = - meter - .counterBuilder("messaging.receive.messages") - .setDescription("Measures the number of received messages.") - .setUnit("{message}"); - MessagingMetricsAdvice.applyReceiveMessagesAdvice(longCounterBuilder); - receiveMessageCount = longCounterBuilder.build(); + private MessagingConsumerMetrics( + Meter meter, boolean supportsStableSemconv, boolean consumedMessagesOnly) { + this.supportsStableSemconv = supportsStableSemconv; + this.consumedMessagesOnly = consumedMessagesOnly; + boolean emitOldSemconv = !supportsStableSemconv || emitOldMessagingSemconv(); + boolean emitStableSemconv = supportsStableSemconv && emitStableMessagingSemconv(); + receiveDurationHistogram = + !consumedMessagesOnly && emitOldSemconv ? buildReceiveDuration(meter) : null; + receiveMessageCount = + !consumedMessagesOnly && emitOldSemconv ? buildReceiveMessages(meter) : null; + clientOperationDurationHistogram = + !consumedMessagesOnly && emitStableSemconv ? buildClientOperationDuration(meter) : null; + consumedMessagesCounter = emitStableSemconv ? buildConsumedMessages(meter) : null; + enabled = + receiveDurationHistogram != null + || receiveMessageCount != null + || clientOperationDurationHistogram != null + || consumedMessagesCounter != null; } + /** Returns metrics for extractors configured with {@link MessageOperation}. */ public static OperationMetrics get() { - return OperationMetricsUtil.create("messaging consumer", MessagingConsumerMetrics::new); + return OperationMetricsUtil.create( + "messaging consumer", meter -> new MessagingConsumerMetrics(meter, false)); + } + + /** Returns metrics for extractors configured with {@link MessagingOperationType}. */ + public static OperationMetrics getForOperationType() { + return OperationMetricsUtil.create( + "messaging consumer", meter -> new MessagingConsumerMetrics(meter, true)); + } + + /** Returns only the stable consumed-messages metric for a delivered message. */ + public static OperationMetrics getConsumedMessages() { + return OperationMetricsUtil.create( + "messaging consumed messages", meter -> new MessagingConsumerMetrics(meter, true, true)); } @Override @CanIgnoreReturnValue public Context onStart(Context context, Attributes startAttributes, long startNanos) { + if (!enabled) { + return context; + } return context.with( MESSAGING_CONSUMER_METRICS_STATE, new AutoValue_MessagingConsumerMetrics_State(startAttributes, startNanos)); @@ -75,6 +110,9 @@ public Context onStart(Context context, Attributes startAttributes, long startNa @Override public void onEnd(Context context, Attributes endAttributes, long endNanos) { + if (!enabled) { + return; + } MessagingConsumerMetrics.State state = context.get(MESSAGING_CONSUMER_METRICS_STATE); if (state == null) { logger.log( @@ -85,21 +123,104 @@ public void onEnd(Context context, Attributes endAttributes, long endNanos) { } Attributes attributes = state.startAttributes().toBuilder().putAll(endAttributes).build(); - receiveDurationHistogram.record( - (endNanos - state.startTimeNanos()) / NANOS_PER_S, attributes, context); + double duration = (endNanos - state.startTimeNanos()) / NANOS_PER_S; + String operationType = attributes.get(MESSAGING_OPERATION_TYPE); + boolean recordsLegacyReceive = + !supportsStableSemconv + || "receive".equals(attributes.get(MESSAGING_OPERATION)) + || MessagingOperationType.RECEIVE.value().equals(operationType); + if (receiveDurationHistogram != null && recordsLegacyReceive) { + receiveDurationHistogram.record(duration, attributes, context); + } + // Metric view attribute advice can only select keys statically. The concrete destination name + // must be omitted when a template is available or the destination is temporary or anonymous, + // so this conditional requirement must be enforced before recording. + Attributes filteredAttributes = + clientOperationDurationHistogram != null || consumedMessagesCounter != null + ? MessagingMetricsAdvice.filterAttributes(attributes) + : attributes; + if (clientOperationDurationHistogram != null + && !MessagingOperationType.PROCESS.value().equals(operationType)) { + clientOperationDurationHistogram.record(duration, filteredAttributes, context); + } - long receiveMessagesCount = getReceiveMessagesCount(state.startAttributes(), endAttributes); - receiveMessageCount.add(receiveMessagesCount, attributes, context); + Long batchMessageCount = getBatchMessageCount(state.startAttributes(), endAttributes); + if (receiveMessageCount != null && recordsLegacyReceive) { + long receiveMessagesCount = batchMessageCount == null ? 1 : batchMessageCount; + if (!supportsStableSemconv || receiveMessagesCount > 0) { + receiveMessageCount.add(receiveMessagesCount, attributes, context); + } + } + if (consumedMessagesCounter != null + && (consumedMessagesOnly || MessagingOperationType.RECEIVE.value().equals(operationType))) { + long consumedMessagesCount = + getConsumedMessagesCount(attributes, batchMessageCount, consumedMessagesOnly); + if (consumedMessagesCount > 0) { + consumedMessagesCounter.add(consumedMessagesCount, filteredAttributes, context); + } + } } - private static long getReceiveMessagesCount(Attributes... attributesList) { + @Nullable + private static Long getBatchMessageCount(Attributes... attributesList) { for (Attributes attributes : attributesList) { Long value = attributes.get(MESSAGING_BATCH_MESSAGE_COUNT); if (value != null) { return value; } } - return 1; + return null; + } + + private static long getConsumedMessagesCount( + Attributes attributes, @Nullable Long batchMessageCount, boolean consumedMessagesOnly) { + if (batchMessageCount != null) { + return batchMessageCount; + } + return consumedMessagesOnly || attributes.get(ERROR_TYPE) == null ? 1 : 0; + } + + private static DoubleHistogram buildReceiveDuration(Meter meter) { + DoubleHistogramBuilder builder = + meter + .histogramBuilder("messaging.receive.duration") + .setDescription("Measures the duration of receive operation.") + .setExplicitBucketBoundariesAdvice(MessagingMetricsAdvice.DURATION_SECONDS_BUCKETS) + .setUnit("s"); + MessagingMetricsAdvice.applyOldDurationAdvice(builder); + return builder.build(); + } + + private static LongCounter buildReceiveMessages(Meter meter) { + LongCounterBuilder builder = + meter + .counterBuilder("messaging.receive.messages") + .setDescription("Measures the number of received messages.") + .setUnit("{message}"); + MessagingMetricsAdvice.applyOldMessagesAdvice(builder); + return builder.build(); + } + + private static DoubleHistogram buildClientOperationDuration(Meter meter) { + DoubleHistogramBuilder builder = + meter + .histogramBuilder("messaging.client.operation.duration") + .setDescription( + "Duration of messaging operation initiated by a producer or consumer client.") + .setExplicitBucketBoundariesAdvice(MessagingMetricsAdvice.DURATION_SECONDS_BUCKETS) + .setUnit("s"); + MessagingMetricsAdvice.applyClientOperationDurationAdvice(builder); + return builder.build(); + } + + private static LongCounter buildConsumedMessages(Meter meter) { + LongCounterBuilder builder = + meter + .counterBuilder("messaging.client.consumed.messages") + .setDescription("Number of messages that were delivered to the application.") + .setUnit("{message}"); + MessagingMetricsAdvice.applyConsumedMessagesAdvice(builder); + return builder.build(); } @AutoValue diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingMetricsAdvice.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingMetricsAdvice.java index ddc4115ea7f4..defc418fc406 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingMetricsAdvice.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingMetricsAdvice.java @@ -12,10 +12,12 @@ import static java.util.Collections.unmodifiableList; import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.incubator.metrics.ExtendedDoubleHistogramBuilder; import io.opentelemetry.api.incubator.metrics.ExtendedLongCounterBuilder; import io.opentelemetry.api.metrics.DoubleHistogramBuilder; import io.opentelemetry.api.metrics.LongCounterBuilder; +import java.util.ArrayList; import java.util.List; final class MessagingMetricsAdvice { @@ -28,14 +30,26 @@ final class MessagingMetricsAdvice { AttributeKey.stringKey("messaging.system"); private static final AttributeKey MESSAGING_DESTINATION_NAME = AttributeKey.stringKey("messaging.destination.name"); + private static final AttributeKey MESSAGING_DESTINATION_ANONYMOUS = + AttributeKey.booleanKey("messaging.destination.anonymous"); + private static final AttributeKey MESSAGING_DESTINATION_TEMPORARY = + AttributeKey.booleanKey("messaging.destination.temporary"); private static final AttributeKey MESSAGING_OPERATION = AttributeKey.stringKey("messaging.operation"); + private static final AttributeKey MESSAGING_OPERATION_NAME = + AttributeKey.stringKey("messaging.operation.name"); + private static final AttributeKey MESSAGING_OPERATION_TYPE = + AttributeKey.stringKey("messaging.operation.type"); + private static final AttributeKey MESSAGING_CONSUMER_GROUP_NAME = + AttributeKey.stringKey("messaging.consumer.group.name"); + private static final AttributeKey MESSAGING_DESTINATION_SUBSCRIPTION_NAME = + AttributeKey.stringKey("messaging.destination.subscription.name"); private static final AttributeKey MESSAGING_DESTINATION_PARTITION_ID = AttributeKey.stringKey("messaging.destination.partition.id"); private static final AttributeKey MESSAGING_DESTINATION_TEMPLATE = AttributeKey.stringKey("messaging.destination.template"); - private static final List> MESSAGING_ATTRIBUTES = + private static final List> OLD_ATTRIBUTES = asList( MESSAGING_SYSTEM, MESSAGING_DESTINATION_NAME, @@ -46,25 +60,88 @@ final class MessagingMetricsAdvice { SERVER_PORT, SERVER_ADDRESS); - static void applyPublishDurationAdvice(DoubleHistogramBuilder builder) { + private static final List> CLIENT_OPERATION_DURATION_ATTRIBUTES = + buildAttributes(true, true, true); + private static final List> SENT_MESSAGES_ATTRIBUTES = + buildAttributes(false, false, true); + private static final List> CONSUMED_MESSAGES_ATTRIBUTES = + buildAttributes(true, false, true); + private static final List> PROCESS_DURATION_ATTRIBUTES = + buildAttributes(true, false, true); + + private static List> buildAttributes( + boolean includeConsumerAttributes, boolean includeOperationType, boolean includeErrorType) { + List> attributes = new ArrayList<>(); + attributes.add(MESSAGING_OPERATION_NAME); + attributes.add(MESSAGING_SYSTEM); + if (includeErrorType) { + attributes.add(ERROR_TYPE); + } + if (includeConsumerAttributes) { + attributes.add(MESSAGING_CONSUMER_GROUP_NAME); + } + attributes.add(MESSAGING_DESTINATION_NAME); + if (includeConsumerAttributes) { + attributes.add(MESSAGING_DESTINATION_SUBSCRIPTION_NAME); + } + attributes.add(MESSAGING_DESTINATION_TEMPLATE); + if (includeOperationType) { + attributes.add(MESSAGING_OPERATION_TYPE); + } + attributes.add(MESSAGING_DESTINATION_PARTITION_ID); + return unmodifiableList(attributes); + } + + static Attributes filterAttributes(Attributes attributes) { + if (attributes.get(MESSAGING_DESTINATION_TEMPLATE) == null + && !Boolean.TRUE.equals(attributes.get(MESSAGING_DESTINATION_ANONYMOUS)) + && !Boolean.TRUE.equals(attributes.get(MESSAGING_DESTINATION_TEMPORARY))) { + return attributes; + } + return attributes.toBuilder().remove(MESSAGING_DESTINATION_NAME).build(); + } + + static void applyOldDurationAdvice(DoubleHistogramBuilder builder) { if (!(builder instanceof ExtendedDoubleHistogramBuilder)) { return; } - ((ExtendedDoubleHistogramBuilder) builder).setAttributesAdvice(MESSAGING_ATTRIBUTES); + ((ExtendedDoubleHistogramBuilder) builder).setAttributesAdvice(OLD_ATTRIBUTES); } - static void applyReceiveDurationAdvice(DoubleHistogramBuilder builder) { + static void applyClientOperationDurationAdvice(DoubleHistogramBuilder builder) { if (!(builder instanceof ExtendedDoubleHistogramBuilder)) { return; } - ((ExtendedDoubleHistogramBuilder) builder).setAttributesAdvice(MESSAGING_ATTRIBUTES); + ((ExtendedDoubleHistogramBuilder) builder) + .setAttributesAdvice(CLIENT_OPERATION_DURATION_ATTRIBUTES); + } + + static void applyProcessDurationAdvice(DoubleHistogramBuilder builder) { + if (!(builder instanceof ExtendedDoubleHistogramBuilder)) { + return; + } + ((ExtendedDoubleHistogramBuilder) builder).setAttributesAdvice(PROCESS_DURATION_ATTRIBUTES); + } + + static void applyOldMessagesAdvice(LongCounterBuilder builder) { + if (!(builder instanceof ExtendedLongCounterBuilder)) { + return; + } + ((ExtendedLongCounterBuilder) builder).setAttributesAdvice(OLD_ATTRIBUTES); + } + + static void applySentMessagesAdvice(LongCounterBuilder builder) { + if (!(builder instanceof ExtendedLongCounterBuilder)) { + return; + } + ((ExtendedLongCounterBuilder) builder).setAttributesAdvice(SENT_MESSAGES_ATTRIBUTES); } - static void applyReceiveMessagesAdvice(LongCounterBuilder builder) { + static void applyConsumedMessagesAdvice(LongCounterBuilder builder) { if (!(builder instanceof ExtendedLongCounterBuilder)) { return; } - ((ExtendedLongCounterBuilder) builder).setAttributesAdvice(MESSAGING_ATTRIBUTES); + ((ExtendedLongCounterBuilder) builder).setAttributesAdvice(CONSUMED_MESSAGES_ATTRIBUTES); } private MessagingMetricsAdvice() {} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingOperationType.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingOperationType.java new file mode 100644 index 000000000000..53b90807068b --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingOperationType.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.messaging; + +/** + * Represents a messaging + * operation type. + */ +public enum MessagingOperationType { + CREATE("create", "create"), + SEND("send", "publish"), + RECEIVE("receive", "receive"), + PROCESS("process", "process"), + SETTLE("settle", "settle"); + + private final String value; + private final String defaultOperationName; + + MessagingOperationType(String value, String defaultOperationName) { + this.value = value; + this.defaultOperationName = defaultOperationName; + } + + String value() { + return value; + } + + String defaultOperationName() { + return defaultOperationName; + } +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingProcessMetrics.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingProcessMetrics.java new file mode 100644 index 000000000000..287741bea625 --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingProcessMetrics.java @@ -0,0 +1,97 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.messaging; + +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; +import static java.util.concurrent.TimeUnit.SECONDS; +import static java.util.logging.Level.FINE; + +import com.google.auto.value.AutoValue; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.DoubleHistogramBuilder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.instrumentation.api.instrumenter.OperationListener; +import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; +import io.opentelemetry.instrumentation.api.internal.OperationMetricsUtil; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +/** + * {@link OperationListener} which keeps track of message + * processing metrics. + */ +public final class MessagingProcessMetrics implements OperationListener { + private static final double NANOS_PER_S = SECONDS.toNanos(1); + + private static final ContextKey MESSAGING_PROCESS_METRICS_STATE = + ContextKey.named("messaging-process-metrics-state"); + private static final Logger logger = Logger.getLogger(MessagingProcessMetrics.class.getName()); + + @Nullable private final DoubleHistogram processDurationHistogram; + + private MessagingProcessMetrics(Meter meter) { + processDurationHistogram = emitStableMessagingSemconv() ? buildProcessDuration(meter) : null; + } + + public static OperationMetrics get() { + return OperationMetricsUtil.create("messaging process", MessagingProcessMetrics::new); + } + + @Override + @CanIgnoreReturnValue + public Context onStart(Context context, Attributes startAttributes, long startNanos) { + if (processDurationHistogram == null) { + return context; + } + return context.with( + MESSAGING_PROCESS_METRICS_STATE, + new AutoValue_MessagingProcessMetrics_State(startAttributes, startNanos)); + } + + @Override + public void onEnd(Context context, Attributes endAttributes, long endNanos) { + if (processDurationHistogram == null) { + return; + } + State state = context.get(MESSAGING_PROCESS_METRICS_STATE); + if (state == null) { + logger.log( + FINE, + "No state present when ending context {0}. Cannot record messaging process metrics.", + context); + return; + } + + Attributes attributes = state.startAttributes().toBuilder().putAll(endAttributes).build(); + processDurationHistogram.record( + (endNanos - state.startTimeNanos()) / NANOS_PER_S, + MessagingMetricsAdvice.filterAttributes(attributes), + context); + } + + private static DoubleHistogram buildProcessDuration(Meter meter) { + DoubleHistogramBuilder builder = + meter + .histogramBuilder("messaging.process.duration") + .setDescription("Duration of processing operation.") + .setExplicitBucketBoundariesAdvice(MessagingMetricsAdvice.DURATION_SECONDS_BUCKETS) + .setUnit("s"); + MessagingMetricsAdvice.applyProcessDurationAdvice(builder); + return builder.build(); + } + + @AutoValue + abstract static class State { + abstract Attributes startAttributes(); + + abstract long startTimeNanos(); + } +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingProducerMetrics.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingProducerMetrics.java index ee79fd0c224e..cd022e15a96a 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingProducerMetrics.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingProducerMetrics.java @@ -5,14 +5,19 @@ package io.opentelemetry.instrumentation.api.incubator.semconv.messaging; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitOldMessagingSemconv; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.logging.Level.FINE; import com.google.auto.value.AutoValue; import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.DoubleHistogram; import io.opentelemetry.api.metrics.DoubleHistogramBuilder; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.LongCounterBuilder; import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.context.Context; import io.opentelemetry.context.ContextKey; @@ -20,34 +25,52 @@ import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; import io.opentelemetry.instrumentation.api.internal.OperationMetricsUtil; import java.util.logging.Logger; +import javax.annotation.Nullable; /** * {@link OperationListener} which keeps track of Producer + * href="https://github.com/open-telemetry/semantic-conventions/blob/v1.43.0/docs/messaging/messaging-metrics.md#producer-metrics">producer * metrics. */ public final class MessagingProducerMetrics implements OperationListener { private static final double NANOS_PER_S = SECONDS.toNanos(1); + // copied from MessagingIncubatingAttributes + private static final AttributeKey MESSAGING_BATCH_MESSAGE_COUNT = + AttributeKey.longKey("messaging.batch.message_count"); + private static final AttributeKey MESSAGING_OPERATION = + AttributeKey.stringKey("messaging.operation"); + private static final AttributeKey MESSAGING_OPERATION_TYPE = + AttributeKey.stringKey("messaging.operation.type"); private static final ContextKey MESSAGING_PRODUCER_METRICS_STATE = ContextKey.named("messaging-producer-metrics-state"); private static final Logger logger = Logger.getLogger(MessagingProducerMetrics.class.getName()); - private final DoubleHistogram publishDurationHistogram; + private final boolean supportsStableSemconv; + @Nullable private final DoubleHistogram publishDurationHistogram; + @Nullable private final DoubleHistogram clientOperationDurationHistogram; + @Nullable private final LongCounter sentMessagesCounter; - private MessagingProducerMetrics(Meter meter) { - DoubleHistogramBuilder durationBuilder = - meter - .histogramBuilder("messaging.publish.duration") - .setDescription("Measures the duration of publish operation.") - .setExplicitBucketBoundariesAdvice(MessagingMetricsAdvice.DURATION_SECONDS_BUCKETS) - .setUnit("s"); - MessagingMetricsAdvice.applyPublishDurationAdvice(durationBuilder); - publishDurationHistogram = durationBuilder.build(); + private MessagingProducerMetrics(Meter meter, boolean supportsStableSemconv) { + this.supportsStableSemconv = supportsStableSemconv; + boolean emitOldSemconv = !supportsStableSemconv || emitOldMessagingSemconv(); + boolean emitStableSemconv = supportsStableSemconv && emitStableMessagingSemconv(); + publishDurationHistogram = emitOldSemconv ? buildPublishDuration(meter) : null; + clientOperationDurationHistogram = + emitStableSemconv ? buildClientOperationDuration(meter) : null; + sentMessagesCounter = emitStableSemconv ? buildSentMessages(meter) : null; } + /** Returns metrics for extractors configured with {@link MessageOperation}. */ public static OperationMetrics get() { - return OperationMetricsUtil.create("messaging producer", MessagingProducerMetrics::new); + return OperationMetricsUtil.create( + "messaging producer", meter -> new MessagingProducerMetrics(meter, false)); + } + + /** Returns metrics for extractors configured with {@link MessagingOperationType}. */ + public static OperationMetrics getForOperationType() { + return OperationMetricsUtil.create( + "messaging producer", meter -> new MessagingProducerMetrics(meter, true)); } @Override @@ -70,9 +93,64 @@ public void onEnd(Context context, Attributes endAttributes, long endNanos) { } Attributes attributes = state.startAttributes().toBuilder().putAll(endAttributes).build(); + double duration = (endNanos - state.startTimeNanos()) / NANOS_PER_S; - publishDurationHistogram.record( - (endNanos - state.startTimeNanos()) / NANOS_PER_S, attributes, context); + if (publishDurationHistogram != null + && (!supportsStableSemconv + || "publish".equals(attributes.get(MESSAGING_OPERATION)) + || MessagingOperationType.SEND + .value() + .equals(attributes.get(MESSAGING_OPERATION_TYPE)))) { + publishDurationHistogram.record(duration, attributes, context); + } + Attributes filteredAttributes = + clientOperationDurationHistogram != null || sentMessagesCounter != null + ? MessagingMetricsAdvice.filterAttributes(attributes) + : attributes; + if (clientOperationDurationHistogram != null) { + clientOperationDurationHistogram.record(duration, filteredAttributes, context); + } + if (sentMessagesCounter != null + && MessagingOperationType.SEND.value().equals(attributes.get(MESSAGING_OPERATION_TYPE))) { + Long batchMessageCount = attributes.get(MESSAGING_BATCH_MESSAGE_COUNT); + long sentMessagesCount = batchMessageCount == null ? 1 : batchMessageCount; + if (sentMessagesCount > 0) { + sentMessagesCounter.add(sentMessagesCount, filteredAttributes, context); + } + } + } + + private static DoubleHistogram buildPublishDuration(Meter meter) { + DoubleHistogramBuilder builder = + meter + .histogramBuilder("messaging.publish.duration") + .setDescription("Measures the duration of publish operation.") + .setExplicitBucketBoundariesAdvice(MessagingMetricsAdvice.DURATION_SECONDS_BUCKETS) + .setUnit("s"); + MessagingMetricsAdvice.applyOldDurationAdvice(builder); + return builder.build(); + } + + private static DoubleHistogram buildClientOperationDuration(Meter meter) { + DoubleHistogramBuilder builder = + meter + .histogramBuilder("messaging.client.operation.duration") + .setDescription( + "Duration of messaging operation initiated by a producer or consumer client.") + .setExplicitBucketBoundariesAdvice(MessagingMetricsAdvice.DURATION_SECONDS_BUCKETS) + .setUnit("s"); + MessagingMetricsAdvice.applyClientOperationDurationAdvice(builder); + return builder.build(); + } + + private static LongCounter buildSentMessages(Meter meter) { + LongCounterBuilder builder = + meter + .counterBuilder("messaging.client.sent.messages") + .setDescription("Number of messages producer attempted to send to the broker.") + .setUnit("{message}"); + MessagingMetricsAdvice.applySentMessagesAdvice(builder); + return builder.build(); } @AutoValue diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingSpanKindExtractor.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingSpanKindExtractor.java new file mode 100644 index 000000000000..4a7008c1420a --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingSpanKindExtractor.java @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.messaging; + +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; + +/** Selects messaging span kinds according to the configured semantic convention version. */ +public final class MessagingSpanKindExtractor { + + /** + * Returns a span kind extractor following the v1.43 + * messaging span kind conventions. + */ + public static SpanKindExtractor create(MessagingOperationType operationType) { + return create(operationType, true); + } + + /** + * Returns a span kind extractor following the v1.43 + * messaging span kind conventions. + * + * @param isSpanContextPropagated whether the context of a {@link MessagingOperationType#SEND} + * span is propagated as the message creation context; ignored for other operation types + */ + public static SpanKindExtractor create( + MessagingOperationType operationType, boolean isSpanContextPropagated) { + SpanKind spanKind; + switch (operationType) { + case CREATE: + spanKind = SpanKind.PRODUCER; + break; + case SEND: + spanKind = + emitStableMessagingSemconv() && !isSpanContextPropagated + ? SpanKind.CLIENT + : SpanKind.PRODUCER; + break; + case RECEIVE: + spanKind = emitStableMessagingSemconv() ? SpanKind.CLIENT : SpanKind.CONSUMER; + break; + case PROCESS: + spanKind = SpanKind.CONSUMER; + break; + case SETTLE: + spanKind = SpanKind.CLIENT; + break; + default: + throw new IllegalStateException("Can't possibly happen"); + } + SpanKind result = spanKind; + return request -> result; + } + + /** Returns a span kind extractor for the given operation. */ + public static SpanKindExtractor create(MessageOperation operation) { + SpanKind spanKind; + switch (operation) { + case PUBLISH: + spanKind = SpanKind.PRODUCER; + break; + case RECEIVE: + case PROCESS: + spanKind = SpanKind.CONSUMER; + break; + default: + throw new IllegalStateException("Can't possibly happen"); + } + return request -> spanKind; + } + + private MessagingSpanKindExtractor() {} +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingSpanNameExtractor.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingSpanNameExtractor.java index 624d3b8e00bf..a6d6aa679747 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingSpanNameExtractor.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingSpanNameExtractor.java @@ -5,35 +5,75 @@ package io.opentelemetry.instrumentation.api.incubator.semconv.messaging; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; + import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; public final class MessagingSpanNameExtractor implements SpanNameExtractor { /** * Returns a {@link SpanNameExtractor} that constructs the span name according to - * messaging semantic conventions: {@code }. + * href="https://github.com/open-telemetry/semantic-conventions/blob/v1.43.0/docs/messaging/messaging-spans.md#span-name"> + * messaging semantic conventions. * * @see MessagingAttributesGetter#getDestination(Object) used to extract {@code }. - * @see MessageOperation used to extract {@code }. + * @see MessagingOperationType used to extract {@code }. */ + public static SpanNameExtractor create( + MessagingAttributesGetter getter, MessagingOperationType operationType) { + return builder(getter, operationType).build(); + } + + /** Returns a messaging span name extractor for the given operation. */ public static SpanNameExtractor create( MessagingAttributesGetter getter, MessageOperation operation) { - return new MessagingSpanNameExtractor<>(getter, operation); + return builder(getter, operation).build(); } - private final MessagingAttributesGetter getter; - private final MessageOperation operation; + /** + * Returns a new {@link MessagingSpanNameExtractorBuilder} that can be used to configure the + * messaging span name extractor. + */ + public static MessagingSpanNameExtractorBuilder builder( + MessagingAttributesGetter getter, MessagingOperationType operationType) { + return new MessagingSpanNameExtractorBuilder<>(getter, operationType, true); + } - private MessagingSpanNameExtractor( + /** Returns a messaging span name extractor builder for the given operation. */ + public static MessagingSpanNameExtractorBuilder builder( MessagingAttributesGetter getter, MessageOperation operation) { + return new MessagingSpanNameExtractorBuilder<>(getter, operation.type(), false); + } + + private final MessagingAttributesGetter getter; + private final MessagingOperationType operationType; + private final String operationName; + private final boolean supportsStableSemconv; + + MessagingSpanNameExtractor( + MessagingAttributesGetter getter, + MessagingOperationType operationType, + String operationName, + boolean supportsStableSemconv) { this.getter = getter; - this.operation = operation; + this.operationType = operationType; + this.operationName = operationName; + this.supportsStableSemconv = supportsStableSemconv; } @Override public String extract(REQUEST request) { + if (supportsStableSemconv && emitStableMessagingSemconv()) { + String destinationName = getter.getDestinationTemplate(request); + if (destinationName == null + && !getter.isTemporaryDestination(request) + && !getter.isAnonymousDestination(request)) { + destinationName = getter.getDestination(request); + } + return destinationName == null ? operationName : operationName + " " + destinationName; + } + String destinationName = getter.isTemporaryDestination(request) ? MessagingAttributesExtractor.TEMP_DESTINATION_NAME @@ -42,6 +82,6 @@ public String extract(REQUEST request) { destinationName = "unknown"; } - return destinationName + " " + operation.operationName(); + return destinationName + " " + operationType.defaultOperationName(); } } diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingSpanNameExtractorBuilder.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingSpanNameExtractorBuilder.java new file mode 100644 index 000000000000..665ceaa58888 --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingSpanNameExtractorBuilder.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.messaging; + +import static java.util.Objects.requireNonNull; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; + +/** A builder of {@link MessagingSpanNameExtractor}. */ +public final class MessagingSpanNameExtractorBuilder { + + private final MessagingAttributesGetter getter; + private final MessagingOperationType operationType; + private final boolean supportsStableSemconv; + private String operationName; + + MessagingSpanNameExtractorBuilder( + MessagingAttributesGetter getter, + MessagingOperationType operationType, + boolean supportsStableSemconv) { + this.getter = getter; + this.operationType = requireNonNull(operationType, "operationType"); + this.supportsStableSemconv = supportsStableSemconv; + this.operationName = operationType.defaultOperationName(); + } + + /** Configures the system-specific operation name used in the v1.43 messaging span name. */ + @CanIgnoreReturnValue + public MessagingSpanNameExtractorBuilder setOperationName(String operationName) { + if (!supportsStableSemconv) { + throw new IllegalStateException("Operation name is not configurable for legacy builders"); + } + this.operationName = requireNonNull(operationName, "operationName"); + return this; + } + + /** + * Returns a new {@link MessagingSpanNameExtractor} with the settings of this {@link + * MessagingSpanNameExtractorBuilder}. + */ + public SpanNameExtractor build() { + return new MessagingSpanNameExtractor<>( + getter, operationType, operationName, supportsStableSemconv); + } +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/internal/MessagingProcessContextCustomizer.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/internal/MessagingProcessContextCustomizer.java new file mode 100644 index 000000000000..03b45ddf5c86 --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/internal/MessagingProcessContextCustomizer.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.messaging.internal; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer; +import java.util.function.BiFunction; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class MessagingProcessContextCustomizer implements ContextCustomizer { + + public static ContextCustomizer create( + TextMapPropagator propagator, TextMapGetter getter) { + return create((parentContext, request) -> propagator.extract(parentContext, request, getter)); + } + + public static ContextCustomizer create( + BiFunction producerContextExtractor) { + return new MessagingProcessContextCustomizer<>(producerContextExtractor); + } + + private final BiFunction producerContextExtractor; + + private MessagingProcessContextCustomizer( + BiFunction producerContextExtractor) { + this.producerContextExtractor = producerContextExtractor; + } + + @Override + public Context onStart(Context parentContext, REQUEST request, Attributes startAttributes) { + Context extractedContext = producerContextExtractor.apply(parentContext, request); + Span parentSpan = Span.fromContext(parentContext); + if (!parentSpan.getSpanContext().isValid()) { + return extractedContext; + } + return extractedContext.with(parentSpan); + } +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/internal/MessagingProcessInstrumenterFactory.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/internal/MessagingProcessInstrumenterFactory.java new file mode 100644 index 000000000000..726e5ae92194 --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/internal/MessagingProcessInstrumenterFactory.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.messaging.internal; + +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; +import io.opentelemetry.instrumentation.api.internal.PropagatorBasedSpanLinksExtractor; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class MessagingProcessInstrumenterFactory { + + public static Instrumenter create( + InstrumenterBuilder builder, + TextMapPropagator propagator, + TextMapGetter getter, + boolean receiveInstrumentationEnabled) { + if (emitStableMessagingSemconv()) { + builder.addSpanLinksExtractor( + (spanLinks, parentContext, request) -> { + SpanContext parentSpanContext = Span.fromContext(parentContext).getSpanContext(); + SpanContext producerSpanContext = + Span.fromContext(propagator.extract(parentContext, request, getter)) + .getSpanContext(); + if (parentSpanContext.isValid() + && producerSpanContext.isValid() + && (!producerSpanContext.getTraceId().equals(parentSpanContext.getTraceId()) + || !producerSpanContext.getSpanId().equals(parentSpanContext.getSpanId()))) { + spanLinks.addLink(producerSpanContext); + } + }); + builder.addContextCustomizer(MessagingProcessContextCustomizer.create(propagator, getter)); + return builder.buildInstrumenter(SpanKindExtractor.alwaysConsumer()); + } + if (receiveInstrumentationEnabled) { + builder.addSpanLinksExtractor(new PropagatorBasedSpanLinksExtractor<>(propagator, getter)); + return builder.buildInstrumenter(SpanKindExtractor.alwaysConsumer()); + } + return builder.buildConsumerInstrumenter(getter); + } + + private MessagingProcessInstrumenterFactory() {} +} diff --git a/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingAttributesExtractorTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingAttributesExtractorTest.java index a8d56f7e4f98..2a7959df2b08 100644 --- a/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingAttributesExtractorTest.java +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingAttributesExtractorTest.java @@ -6,7 +6,10 @@ package io.opentelemetry.instrumentation.api.incubator.semconv.messaging; import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitOldMessagingSemconv; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.semconv.ErrorAttributes.ERROR_TYPE; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_BATCH_MESSAGE_COUNT; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_ANONYMOUS; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME; @@ -17,15 +20,21 @@ import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_MESSAGE_ENVELOPE_SIZE; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION_NAME; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION_TYPE; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_SYSTEM; import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.entry; +import static org.junit.jupiter.params.provider.Arguments.argumentSet; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.internal.SpanKey; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -47,8 +56,8 @@ void shouldExtractAllAvailableAttributes( boolean temporary, boolean anonymous, String destination, - MessageOperation operation, - String expectedDestination) { + MessagingOperationType operationType, + String operationName) { // given Map request = new HashMap<>(); request.put("system", "myQueue"); @@ -63,13 +72,16 @@ void shouldExtractAllAvailableAttributes( } request.put("url", "http://broker/topic"); request.put("conversationId", "42"); + request.put("messageId", "42"); request.put("bodySize", "100"); request.put("envelopeSize", "120"); request.put("clientId", "43"); request.put("batchMessageCount", "2"); AttributesExtractor, String> underTest = - MessagingAttributesExtractor.create(TestGetter.INSTANCE, operation); + MessagingAttributesExtractor.builderForOperationType(TestGetter.INSTANCE, operationType) + .setOperationName(operationName) + .build(); Context context = Context.root(); @@ -78,16 +90,22 @@ void shouldExtractAllAvailableAttributes( underTest.onStart(startAttributes, context, request); AttributesBuilder endAttributes = Attributes.builder(); - underTest.onEnd(endAttributes, context, request, "42", null); + underTest.onEnd(endAttributes, context, request, "42", new IllegalStateException()); // then List, Object>> expectedEntries = new ArrayList<>(); expectedEntries.add(entry(MESSAGING_SYSTEM, "myQueue")); - expectedEntries.add(entry(MESSAGING_DESTINATION_NAME, expectedDestination)); if (temporary) { expectedEntries.add(entry(MESSAGING_DESTINATION_TEMPORARY, true)); + if (emitStableMessagingSemconv()) { + expectedEntries.add(entry(MESSAGING_DESTINATION_NAME, destination)); + expectedEntries.add(entry(MESSAGING_DESTINATION_TEMPLATE, destination)); + } else { + expectedEntries.add(entry(MESSAGING_DESTINATION_NAME, "(temporary)")); + } } else { - expectedEntries.add(entry(MESSAGING_DESTINATION_TEMPLATE, expectedDestination)); + expectedEntries.add(entry(MESSAGING_DESTINATION_NAME, destination)); + expectedEntries.add(entry(MESSAGING_DESTINATION_TEMPLATE, destination)); } if (anonymous) { expectedEntries.add(entry(MESSAGING_DESTINATION_ANONYMOUS, true)); @@ -95,22 +113,154 @@ void shouldExtractAllAvailableAttributes( expectedEntries.add(entry(MESSAGING_MESSAGE_CONVERSATION_ID, "42")); expectedEntries.add(entry(MESSAGING_MESSAGE_BODY_SIZE, 100L)); expectedEntries.add(entry(MESSAGING_MESSAGE_ENVELOPE_SIZE, 120L)); - expectedEntries.add(entry(stringKey("messaging.client_id"), "43")); - expectedEntries.add(entry(MESSAGING_OPERATION, operation.operationName())); + if (emitOldMessagingSemconv()) { + expectedEntries.add(entry(stringKey("messaging.client_id"), "43")); + expectedEntries.add(entry(MESSAGING_OPERATION, operationType.defaultOperationName())); + } + if (emitStableMessagingSemconv()) { + expectedEntries.add(entry(stringKey("messaging.client.id"), "43")); + expectedEntries.add(entry(MESSAGING_OPERATION_NAME, operationName)); + expectedEntries.add(entry(MESSAGING_OPERATION_TYPE, operationType.value())); + } @SuppressWarnings({"unchecked", "rawtypes"}) MapEntry, ?>[] expectedEntriesArr = expectedEntries.toArray(new MapEntry[0]); assertThat(startAttributes.build()).containsOnly(expectedEntriesArr); - assertThat(endAttributes.build()) - .containsOnly(entry(MESSAGING_MESSAGE_ID, "42"), entry(MESSAGING_BATCH_MESSAGE_COUNT, 2L)); + if (emitStableMessagingSemconv()) { + assertThat(endAttributes.build()) + .containsOnly( + entry(MESSAGING_MESSAGE_ID, "42"), + entry(MESSAGING_BATCH_MESSAGE_COUNT, 2L), + entry(ERROR_TYPE, IllegalStateException.class.getName())); + } else { + assertThat(endAttributes.build()) + .containsOnly( + entry(MESSAGING_MESSAGE_ID, "42"), entry(MESSAGING_BATCH_MESSAGE_COUNT, 2L)); + } } static Stream destinations() { return Stream.of( - Arguments.of(false, false, "destination", MessageOperation.RECEIVE, "destination"), - Arguments.of(true, true, null, MessageOperation.PROCESS, "(temporary)")); + argumentSet( + "create operation", + false, + false, + "destination", + MessagingOperationType.CREATE, + "create"), + argumentSet( + "regular destination", + false, + false, + "destination", + MessagingOperationType.RECEIVE, + "poll"), + argumentSet( + "temporary anonymous destination", + true, + true, + "generated-destination", + MessagingOperationType.PROCESS, + "process"), + argumentSet( + "settle operation", + false, + false, + "destination", + MessagingOperationType.SETTLE, + "settle")); + } + + @ParameterizedTest + @MethodSource("spanKeys") + void shouldReturnSpanKey(MessagingOperationType operationType, SpanKey spanKey) { + MessagingAttributesExtractor, String> underTest = + new MessagingAttributesExtractor<>( + TestGetter.INSTANCE, + operationType, + operationType.defaultOperationName(), + true, + new ArrayList<>()); + + assertThat(underTest.internalGetSpanKey()).isSameAs(spanKey); + } + + static Stream spanKeys() { + return Stream.of( + argumentSet("create", MessagingOperationType.CREATE, SpanKey.PRODUCER_CREATE), + argumentSet("send", MessagingOperationType.SEND, SpanKey.PRODUCER), + argumentSet("receive", MessagingOperationType.RECEIVE, SpanKey.CONSUMER_RECEIVE), + argumentSet("process", MessagingOperationType.PROCESS, SpanKey.CONSUMER_PROCESS), + argumentSet("settle", MessagingOperationType.SETTLE, SpanKey.CONSUMER_SETTLE)); + } + + @SuppressWarnings("deprecation") // testing deprecated API + @Test + void shouldSupportDeprecatedMessageOperation() { + AttributesExtractor, String> underTest = + MessagingAttributesExtractor.create(TestGetter.INSTANCE, MessageOperation.PUBLISH); + + AttributesBuilder attributes = Attributes.builder(); + underTest.onStart(attributes, Context.root(), singletonMap("anonymousDestination", "y")); + + assertThat(attributes.build()) + .containsOnly( + entry(MESSAGING_DESTINATION_ANONYMOUS, true), entry(MESSAGING_OPERATION, "publish")); + } + + @SuppressWarnings("deprecation") // testing deprecated API + @Test + void shouldRejectOperationNameForDeprecatedMessageOperation() { + assertThatThrownBy( + () -> + MessagingAttributesExtractor.builder(TestGetter.INSTANCE, MessageOperation.PUBLISH) + .setOperationName("send")) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Operation name is not configurable for legacy builders"); + } + + @Test + void shouldExtractOperationNameWithoutOperationType() { + MessagingOperationType operationType = null; + AttributesExtractor, String> underTest = + MessagingAttributesExtractor.builderForOperationType(TestGetter.INSTANCE, operationType) + .setOperationName("ack") + .build(); + + AttributesBuilder attributes = Attributes.builder(); + underTest.onStart(attributes, Context.root(), emptyMap()); + + Attributes expected = + emitStableMessagingSemconv() + ? Attributes.of(MESSAGING_OPERATION_NAME, "ack") + : Attributes.empty(); + assertThat(attributes.build()).isEqualTo(expected); + } + + @Test + void shouldRequireOperationNameForStableSemconv() { + assertThatThrownBy( + () -> + MessagingAttributesExtractor.builderForOperationType(TestGetter.INSTANCE, null) + .build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("operationName"); + } + + @Test + void shouldExtractErrorTypeFromResponse() { + AttributesExtractor, String> underTest = + MessagingAttributesExtractor.createForOperationType( + TestGetter.INSTANCE, MessagingOperationType.RECEIVE); + + AttributesBuilder attributes = Attributes.builder(); + underTest.onEnd(attributes, Context.root(), emptyMap(), "failure", null); + + Attributes expected = + emitStableMessagingSemconv() ? Attributes.of(ERROR_TYPE, "failure") : Attributes.empty(); + assertThat(attributes.build()).isEqualTo(expected); } @Test @@ -118,20 +268,24 @@ void shouldExtractNoAttributesIfNoneAreAvailable() { // given AttributesExtractor, String> underTest = MessagingAttributesExtractor.create(TestGetter.INSTANCE, null); + AttributesExtractor, String> builtUnderTest = + MessagingAttributesExtractor.builder(TestGetter.INSTANCE, null).build(); Context context = Context.root(); // when AttributesBuilder startAttributes = Attributes.builder(); underTest.onStart(startAttributes, context, emptyMap()); + AttributesBuilder builtStartAttributes = Attributes.builder(); + builtUnderTest.onStart(builtStartAttributes, context, emptyMap()); AttributesBuilder endAttributes = Attributes.builder(); underTest.onEnd(endAttributes, context, emptyMap(), null, null); // then - assertThat(startAttributes.build().isEmpty()).isTrue(); - - assertThat(endAttributes.build().isEmpty()).isTrue(); + assertThat(startAttributes.build()).isEmpty(); + assertThat(builtStartAttributes.build()).isEmpty(); + assertThat(endAttributes.build()).isEmpty(); } enum TestGetter implements MessagingAttributesGetter, String> { @@ -184,7 +338,7 @@ public Long getMessageEnvelopeSize(Map request) { @Override public String getMessageId(Map request, String response) { - return response; + return request.get("messageId"); } @Nullable @@ -199,5 +353,10 @@ public Long getBatchMessageCount(Map request, @Nullable String r String payloadSize = request.get("batchMessageCount"); return payloadSize == null ? null : Long.valueOf(payloadSize); } + + @Override + public String getErrorType(Map request, String response, Throwable error) { + return "failure".equals(response) ? response : null; + } } } diff --git a/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingConsumerMetricsTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingConsumerMetricsTest.java new file mode 100644 index 000000000000..07579a6db3fe --- /dev/null +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingConsumerMetricsTest.java @@ -0,0 +1,359 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.messaging; + +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitOldMessagingSemconv; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.semconv.ErrorAttributes.ERROR_TYPE; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_BATCH_MESSAGE_COUNT; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_CONSUMER_GROUP_NAME; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_SUBSCRIPTION_NAME; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_TEMPLATE; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION_NAME; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION_TYPE; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_SYSTEM; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.junit.jupiter.params.provider.Arguments.argumentSet; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.OperationListener; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +import java.util.Collection; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class MessagingConsumerMetricsTest { + + private static final double[] DURATION_BUCKETS = + MessagingMetricsAdvice.DURATION_SECONDS_BUCKETS.stream().mapToDouble(d -> d).toArray(); + + @Test + @SuppressWarnings("deprecation") // using deprecated semconv + void collectsMetricsAndCountsBatchOnce() { + InMemoryMetricReader metricReader = InMemoryMetricReader.createDelta(); + SdkMeterProvider meterProvider = + SdkMeterProvider.builder().registerMetricReader(metricReader).build(); + OperationListener listener = + MessagingConsumerMetrics.getForOperationType().create(meterProvider.get("test")); + + Attributes requestAttributes = + Attributes.builder() + .put(MESSAGING_SYSTEM, "pulsar") + .put(MESSAGING_DESTINATION_NAME, "topic") + .put(MESSAGING_DESTINATION_TEMPLATE, "topic-{id}") + .put(MESSAGING_OPERATION, emitOldMessagingSemconv() ? "receive" : null) + .put(MESSAGING_OPERATION_NAME, emitStableMessagingSemconv() ? "receive" : null) + .put(MESSAGING_OPERATION_TYPE, emitStableMessagingSemconv() ? "receive" : null) + .put(MESSAGING_CONSUMER_GROUP_NAME, "group") + .put(MESSAGING_DESTINATION_SUBSCRIPTION_NAME, "subscription") + .build(); + Attributes responseAttributes = + Attributes.builder() + .put(MESSAGING_BATCH_MESSAGE_COUNT, 3) + .put( + ERROR_TYPE, + emitStableMessagingSemconv() ? IllegalStateException.class.getName() : null) + .build(); + + Context context = listener.onStart(Context.root(), requestAttributes, nanos(100)); + listener.onEnd(context, responseAttributes, nanos(300)); + + Collection metrics = metricReader.collectAllMetrics(); + assertThat(metrics) + .hasSize((emitOldMessagingSemconv() ? 2 : 0) + (emitStableMessagingSemconv() ? 2 : 0)); + + if (emitOldMessagingSemconv()) { + assertThat(metrics) + .anySatisfy( + metric -> + assertThat(metric) + .hasName("messaging.receive.duration") + .hasUnit("s") + .hasDescription("Measures the duration of receive operation.") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(0.2) + .hasBucketBoundaries(DURATION_BUCKETS) + .hasAttributesSatisfyingExactly( + equalTo(MESSAGING_SYSTEM, "pulsar"), + equalTo(MESSAGING_DESTINATION_NAME, "topic"), + equalTo(MESSAGING_DESTINATION_TEMPLATE, "topic-{id}"), + equalTo(MESSAGING_OPERATION, "receive"), + equalTo( + ERROR_TYPE, + emitStableMessagingSemconv() + ? IllegalStateException.class.getName() + : null))))) + .anySatisfy( + metric -> + assertThat(metric) + .hasName("messaging.receive.messages") + .hasUnit("{message}") + .hasDescription("Measures the number of received messages.") + .hasLongSumSatisfying( + sum -> + sum.hasPointsSatisfying( + point -> + point + .hasValue(3) + .hasAttributesSatisfyingExactly( + equalTo(MESSAGING_SYSTEM, "pulsar"), + equalTo(MESSAGING_DESTINATION_NAME, "topic"), + equalTo(MESSAGING_DESTINATION_TEMPLATE, "topic-{id}"), + equalTo(MESSAGING_OPERATION, "receive"), + equalTo( + ERROR_TYPE, + emitStableMessagingSemconv() + ? IllegalStateException.class.getName() + : null))))); + } + if (emitStableMessagingSemconv()) { + assertThat(metrics) + .anySatisfy( + metric -> + assertThat(metric) + .hasName("messaging.client.operation.duration") + .hasUnit("s") + .hasDescription( + "Duration of messaging operation initiated by a producer or consumer client.") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(0.2) + .hasBucketBoundaries(DURATION_BUCKETS) + .hasAttributesSatisfyingExactly( + equalTo(MESSAGING_OPERATION_NAME, "receive"), + equalTo(MESSAGING_SYSTEM, "pulsar"), + equalTo(MESSAGING_CONSUMER_GROUP_NAME, "group"), + equalTo(MESSAGING_DESTINATION_TEMPLATE, "topic-{id}"), + equalTo( + MESSAGING_DESTINATION_SUBSCRIPTION_NAME, + "subscription"), + equalTo( + ERROR_TYPE, + IllegalStateException.class.getName()), + equalTo(MESSAGING_OPERATION_TYPE, "receive"))))) + .anySatisfy( + metric -> + assertThat(metric) + .hasName("messaging.client.consumed.messages") + .hasUnit("{message}") + .hasDescription("Number of messages that were delivered to the application.") + .hasLongSumSatisfying( + sum -> + sum.hasPointsSatisfying( + point -> + point + .hasValue(3) + .hasAttributesSatisfyingExactly( + equalTo(MESSAGING_OPERATION_NAME, "receive"), + equalTo(MESSAGING_SYSTEM, "pulsar"), + equalTo( + ERROR_TYPE, + IllegalStateException.class.getName()), + equalTo(MESSAGING_CONSUMER_GROUP_NAME, "group"), + equalTo(MESSAGING_DESTINATION_TEMPLATE, "topic-{id}"), + equalTo( + MESSAGING_DESTINATION_SUBSCRIPTION_NAME, + "subscription"))))); + } + } + + @Test + @SuppressWarnings("deprecation") // using deprecated semconv + void failedReceiveWithoutBatchCountCountsNoConsumedMessages() { + InMemoryMetricReader metricReader = InMemoryMetricReader.createDelta(); + SdkMeterProvider meterProvider = + SdkMeterProvider.builder().registerMetricReader(metricReader).build(); + OperationListener listener = + MessagingConsumerMetrics.getForOperationType().create(meterProvider.get("test")); + + Attributes requestAttributes = + Attributes.builder() + .put(MESSAGING_SYSTEM, "pulsar") + .put(MESSAGING_OPERATION, emitOldMessagingSemconv() ? "receive" : null) + .put(MESSAGING_OPERATION_NAME, emitStableMessagingSemconv() ? "receive" : null) + .put(MESSAGING_OPERATION_TYPE, emitStableMessagingSemconv() ? "receive" : null) + .build(); + Attributes responseAttributes = + Attributes.of(ERROR_TYPE, IllegalStateException.class.getName()); + + Context context = listener.onStart(Context.root(), requestAttributes, nanos(100)); + listener.onEnd(context, responseAttributes, nanos(300)); + + Collection metrics = metricReader.collectAllMetrics(); + if (emitOldMessagingSemconv()) { + assertThat(metrics) + .anySatisfy( + metric -> + assertThat(metric) + .hasName("messaging.receive.messages") + .hasLongSumSatisfying( + sum -> sum.hasPointsSatisfying(point -> point.hasValue(1)))); + } + if (emitStableMessagingSemconv()) { + assertThat(metrics) + .anySatisfy(metric -> assertThat(metric).hasName("messaging.client.operation.duration")) + .noneSatisfy(metric -> assertThat(metric).hasName("messaging.client.consumed.messages")); + } + } + + @Test + @SuppressWarnings("deprecation") // using deprecated semconv + void consumedMessagesOnlyCountsFailedDelivery() { + InMemoryMetricReader metricReader = InMemoryMetricReader.createDelta(); + SdkMeterProvider meterProvider = + SdkMeterProvider.builder().registerMetricReader(metricReader).build(); + OperationListener listener = + MessagingConsumerMetrics.getConsumedMessages().create(meterProvider.get("test")); + + Attributes attributes = + Attributes.builder() + .put(MESSAGING_SYSTEM, "pulsar") + .put(MESSAGING_DESTINATION_NAME, "topic") + .put(MESSAGING_OPERATION, emitOldMessagingSemconv() ? "process" : null) + .put(MESSAGING_OPERATION_NAME, emitStableMessagingSemconv() ? "process" : null) + .put(MESSAGING_OPERATION_TYPE, emitStableMessagingSemconv() ? "process" : null) + .build(); + Context context = listener.onStart(Context.root(), attributes, nanos(100)); + listener.onEnd( + context, Attributes.of(ERROR_TYPE, IllegalStateException.class.getName()), nanos(300)); + + Collection metrics = metricReader.collectAllMetrics(); + if (!emitStableMessagingSemconv()) { + assertThat(metrics).isEmpty(); + return; + } + assertThat(metrics) + .satisfiesExactly( + metric -> + assertThat(metric) + .hasName("messaging.client.consumed.messages") + .hasLongSumSatisfying( + sum -> + sum.hasPointsSatisfying( + point -> + point + .hasValue(1) + .hasAttributesSatisfyingExactly( + equalTo(MESSAGING_OPERATION_NAME, "process"), + equalTo(MESSAGING_SYSTEM, "pulsar"), + equalTo(MESSAGING_DESTINATION_NAME, "topic"), + equalTo( + ERROR_TYPE, + IllegalStateException.class.getName()))))); + } + + @ParameterizedTest + @MethodSource("nonReceiveOperations") + @SuppressWarnings("deprecation") // using deprecated semconv + void selectsStableMetricsByOperationType(String operationType, boolean recordsClientDuration) { + InMemoryMetricReader metricReader = InMemoryMetricReader.createDelta(); + SdkMeterProvider meterProvider = + SdkMeterProvider.builder().registerMetricReader(metricReader).build(); + OperationListener listener = + MessagingConsumerMetrics.getForOperationType().create(meterProvider.get("test")); + + Attributes attributes = + Attributes.builder() + .put(MESSAGING_OPERATION, emitOldMessagingSemconv() ? operationType : null) + .put(MESSAGING_OPERATION_NAME, emitStableMessagingSemconv() ? operationType : null) + .put(MESSAGING_OPERATION_TYPE, emitStableMessagingSemconv() ? operationType : null) + .build(); + Context context = listener.onStart(Context.root(), attributes, nanos(100)); + listener.onEnd(context, Attributes.empty(), nanos(300)); + + Collection metrics = metricReader.collectAllMetrics(); + assertThat( + metrics.stream() + .filter(metric -> metric.getName().equals("messaging.client.operation.duration")) + .count()) + .isEqualTo(emitStableMessagingSemconv() && recordsClientDuration ? 1 : 0); + assertThat( + metrics.stream() + .filter(metric -> metric.getName().equals("messaging.client.consumed.messages")) + .count()) + .isZero(); + assertThat( + metrics.stream() + .filter(metric -> metric.getName().equals("messaging.receive.duration")) + .count()) + .isZero(); + assertThat( + metrics.stream() + .filter(metric -> metric.getName().equals("messaging.receive.messages")) + .count()) + .isZero(); + } + + @Test + @SuppressWarnings("deprecation") // using deprecated semconv + void zeroBatchDoesNotCountReceivedMessages() { + InMemoryMetricReader metricReader = InMemoryMetricReader.createDelta(); + SdkMeterProvider meterProvider = + SdkMeterProvider.builder().registerMetricReader(metricReader).build(); + OperationListener listener = + MessagingConsumerMetrics.getForOperationType().create(meterProvider.get("test")); + + Attributes attributes = + Attributes.builder() + .put(MESSAGING_OPERATION, emitOldMessagingSemconv() ? "receive" : null) + .put(MESSAGING_OPERATION_NAME, emitStableMessagingSemconv() ? "receive" : null) + .put(MESSAGING_OPERATION_TYPE, emitStableMessagingSemconv() ? "receive" : null) + .put(MESSAGING_BATCH_MESSAGE_COUNT, 0) + .build(); + Context context = listener.onStart(Context.root(), attributes, nanos(100)); + listener.onEnd(context, Attributes.empty(), nanos(300)); + + assertThat(metricReader.collectAllMetrics()) + .noneSatisfy(metric -> assertThat(metric).hasName("messaging.receive.messages")); + } + + private static Stream nonReceiveOperations() { + return Stream.of( + argumentSet("process", "process", false), argumentSet("settle", "settle", true)); + } + + @Test + @SuppressWarnings("deprecation") // using deprecated semconv + void legacyEntryPointAlwaysCollectsLegacyMetrics() { + InMemoryMetricReader metricReader = InMemoryMetricReader.createDelta(); + SdkMeterProvider meterProvider = + SdkMeterProvider.builder().registerMetricReader(metricReader).build(); + OperationListener listener = MessagingConsumerMetrics.get().create(meterProvider.get("test")); + + Context context = + listener.onStart( + Context.root(), + Attributes.of(MESSAGING_SYSTEM, "pulsar", MESSAGING_OPERATION, "receive"), + nanos(100)); + listener.onEnd(context, Attributes.empty(), nanos(300)); + + assertThat(metricReader.collectAllMetrics()) + .hasSize(2) + .anySatisfy(metric -> assertThat(metric).hasName("messaging.receive.duration")) + .anySatisfy(metric -> assertThat(metric).hasName("messaging.receive.messages")); + } + + private static long nanos(int millis) { + return MILLISECONDS.toNanos(millis); + } +} diff --git a/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingMetricsAdviceTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingMetricsAdviceTest.java new file mode 100644 index 000000000000..d070e72b9998 --- /dev/null +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingMetricsAdviceTest.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.messaging; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_ANONYMOUS; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_TEMPLATE; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_TEMPORARY; + +import io.opentelemetry.api.common.Attributes; +import org.junit.jupiter.api.Test; + +class MessagingMetricsAdviceTest { + + @Test + @SuppressWarnings("deprecation") // using deprecated semconv + void filtersHighCardinalityDestinationNames() { + Attributes lowCardinality = Attributes.of(MESSAGING_DESTINATION_NAME, "orders"); + assertThat(MessagingMetricsAdvice.filterAttributes(lowCardinality)).isSameAs(lowCardinality); + + assertThat( + MessagingMetricsAdvice.filterAttributes( + Attributes.builder() + .put(MESSAGING_DESTINATION_NAME, "orders-42") + .put(MESSAGING_DESTINATION_TEMPLATE, "orders-{id}") + .build())) + .isEqualTo(Attributes.of(MESSAGING_DESTINATION_TEMPLATE, "orders-{id}")); + assertThat( + MessagingMetricsAdvice.filterAttributes( + Attributes.builder() + .put(MESSAGING_DESTINATION_NAME, "tmp-42") + .put(MESSAGING_DESTINATION_TEMPORARY, true) + .build())) + .isEqualTo(Attributes.of(MESSAGING_DESTINATION_TEMPORARY, true)); + assertThat( + MessagingMetricsAdvice.filterAttributes( + Attributes.builder() + .put(MESSAGING_DESTINATION_NAME, "generated-42") + .put(MESSAGING_DESTINATION_ANONYMOUS, true) + .build())) + .isEqualTo(Attributes.of(MESSAGING_DESTINATION_ANONYMOUS, true)); + } +} diff --git a/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingProcessMetricsTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingProcessMetricsTest.java new file mode 100644 index 000000000000..19b20b4888e6 --- /dev/null +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingProcessMetricsTest.java @@ -0,0 +1,93 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.messaging; + +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitOldMessagingSemconv; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.semconv.ErrorAttributes.ERROR_TYPE; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_TEMPLATE; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION_NAME; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION_TYPE; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_SYSTEM; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.OperationListener; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +import org.junit.jupiter.api.Test; + +class MessagingProcessMetricsTest { + + private static final double[] DURATION_BUCKETS = + MessagingMetricsAdvice.DURATION_SECONDS_BUCKETS.stream().mapToDouble(d -> d).toArray(); + + @Test + @SuppressWarnings("deprecation") // using deprecated semconv + void emitsProcessDurationAccordingToConfiguration() { + InMemoryMetricReader metricReader = InMemoryMetricReader.createDelta(); + SdkMeterProvider meterProvider = + SdkMeterProvider.builder().registerMetricReader(metricReader).build(); + OperationListener listener = MessagingProcessMetrics.get().create(meterProvider.get("test")); + + Attributes attributes = + Attributes.builder() + .put(MESSAGING_SYSTEM, "pulsar") + .put(MESSAGING_DESTINATION_NAME, "topic") + .put(MESSAGING_DESTINATION_TEMPLATE, "topic-{id}") + .put(MESSAGING_OPERATION, emitOldMessagingSemconv() ? "process" : null) + .put(MESSAGING_OPERATION_NAME, emitStableMessagingSemconv() ? "process" : null) + .put(MESSAGING_OPERATION_TYPE, emitStableMessagingSemconv() ? "process" : null) + .build(); + + Context root = Context.root(); + Context context = listener.onStart(root, attributes, nanos(100)); + Attributes endAttributes = + Attributes.builder() + .put( + ERROR_TYPE, + emitStableMessagingSemconv() ? IllegalStateException.class.getName() : null) + .build(); + listener.onEnd(context, endAttributes, nanos(350)); + + if (!emitStableMessagingSemconv()) { + assertThat(context).isSameAs(root); + assertThat(metricReader.collectAllMetrics()).isEmpty(); + return; + } + + assertThat(metricReader.collectAllMetrics()) + .satisfiesExactly( + metric -> + assertThat(metric) + .hasName("messaging.process.duration") + .hasUnit("s") + .hasDescription("Duration of processing operation.") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(0.25) + .hasBucketBoundaries(DURATION_BUCKETS) + .hasAttributesSatisfyingExactly( + equalTo(MESSAGING_OPERATION_NAME, "process"), + equalTo(MESSAGING_SYSTEM, "pulsar"), + equalTo(MESSAGING_DESTINATION_TEMPLATE, "topic-{id}"), + equalTo( + ERROR_TYPE, + IllegalStateException.class.getName()))))); + } + + private static long nanos(int millis) { + return MILLISECONDS.toNanos(millis); + } +} diff --git a/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingProducerMetricsTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingProducerMetricsTest.java index f155a613b349..7b0fd694862e 100644 --- a/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingProducerMetricsTest.java +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingProducerMetricsTest.java @@ -5,26 +5,30 @@ package io.opentelemetry.instrumentation.api.incubator.semconv.messaging; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitOldMessagingSemconv; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.semconv.ErrorAttributes.ERROR_TYPE; import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS; import static io.opentelemetry.semconv.ServerAttributes.SERVER_PORT; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_BATCH_MESSAGE_COUNT; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID; -import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_TEMPLATE; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION_NAME; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION_TYPE; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_SYSTEM; import static java.util.concurrent.TimeUnit.MILLISECONDS; import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.api.trace.SpanContext; -import io.opentelemetry.api.trace.TraceFlags; -import io.opentelemetry.api.trace.TraceState; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.OperationListener; import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.data.MetricData; import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +import java.util.Collection; import org.junit.jupiter.api.Test; class MessagingProducerMetricsTest { @@ -32,89 +36,196 @@ class MessagingProducerMetricsTest { private static final double[] DURATION_BUCKETS = MessagingMetricsAdvice.DURATION_SECONDS_BUCKETS.stream().mapToDouble(d -> d).toArray(); - @SuppressWarnings("deprecation") // using deprecated semconv @Test + @SuppressWarnings("deprecation") // using deprecated semconv void collectsMetrics() { - InMemoryMetricReader metricReader = InMemoryMetricReader.create(); + InMemoryMetricReader metricReader = InMemoryMetricReader.createDelta(); SdkMeterProvider meterProvider = SdkMeterProvider.builder().registerMetricReader(metricReader).build(); - - OperationListener listener = MessagingProducerMetrics.get().create(meterProvider.get("test")); + OperationListener listener = + MessagingProducerMetrics.getForOperationType().create(meterProvider.get("test")); Attributes requestAttributes = Attributes.builder() .put(MESSAGING_SYSTEM, "pulsar") - .put(MESSAGING_DESTINATION_NAME, "persistent://public/default/topic") - .put(MESSAGING_OPERATION, "publish") - .put(SERVER_PORT, 6650) + .put(MESSAGING_DESTINATION_NAME, "topic") + .put(MESSAGING_DESTINATION_TEMPLATE, "topic-{id}") + .put(MESSAGING_OPERATION, emitOldMessagingSemconv() ? "publish" : null) + .put(MESSAGING_OPERATION_NAME, emitStableMessagingSemconv() ? "send" : null) + .put(MESSAGING_OPERATION_TYPE, emitStableMessagingSemconv() ? "send" : null) .put(SERVER_ADDRESS, "localhost") + .put(SERVER_PORT, 6650) .build(); - Attributes responseAttributes = Attributes.builder() - .put(MESSAGING_MESSAGE_ID, "1:1:0:0") .put(MESSAGING_DESTINATION_PARTITION_ID, "1") + .put(MESSAGING_BATCH_MESSAGE_COUNT, 2) + .put( + ERROR_TYPE, + emitStableMessagingSemconv() ? IllegalStateException.class.getName() : null) .build(); - Context parent = - Context.root() - .with( - Span.wrap( - SpanContext.create( - "ff01020304050600ff0a0b0c0d0e0f00", - "090a0b0c0d0e0f00", - TraceFlags.getSampled(), - TraceState.getDefault()))); - - Context context1 = listener.onStart(parent, requestAttributes, nanos(100)); + Context context = listener.onStart(Context.root(), requestAttributes, nanos(100)); + listener.onEnd(context, responseAttributes, nanos(250)); + + Collection metrics = metricReader.collectAllMetrics(); + assertThat(metrics) + .hasSize((emitOldMessagingSemconv() ? 1 : 0) + (emitStableMessagingSemconv() ? 2 : 0)); + + if (emitOldMessagingSemconv()) { + assertThat(metrics) + .anySatisfy( + metric -> + assertThat(metric) + .hasName("messaging.publish.duration") + .hasUnit("s") + .hasDescription("Measures the duration of publish operation.") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(0.15) + .hasBucketBoundaries(DURATION_BUCKETS) + .hasAttributesSatisfyingExactly( + equalTo(MESSAGING_SYSTEM, "pulsar"), + equalTo(MESSAGING_DESTINATION_NAME, "topic"), + equalTo(MESSAGING_OPERATION, "publish"), + equalTo(MESSAGING_DESTINATION_PARTITION_ID, "1"), + equalTo(MESSAGING_DESTINATION_TEMPLATE, "topic-{id}"), + equalTo( + ERROR_TYPE, + emitStableMessagingSemconv() + ? IllegalStateException.class.getName() + : null), + equalTo(SERVER_PORT, 6650), + equalTo(SERVER_ADDRESS, "localhost"))))); + } + if (emitStableMessagingSemconv()) { + assertThat(metrics) + .anySatisfy( + metric -> + assertThat(metric) + .hasName("messaging.client.operation.duration") + .hasUnit("s") + .hasDescription( + "Duration of messaging operation initiated by a producer or consumer client.") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(0.15) + .hasBucketBoundaries(DURATION_BUCKETS) + .hasAttributesSatisfyingExactly( + equalTo(MESSAGING_OPERATION_NAME, "send"), + equalTo(MESSAGING_SYSTEM, "pulsar"), + equalTo(MESSAGING_DESTINATION_TEMPLATE, "topic-{id}"), + equalTo(MESSAGING_OPERATION_TYPE, "send"), + equalTo( + ERROR_TYPE, + IllegalStateException.class.getName()), + equalTo(MESSAGING_DESTINATION_PARTITION_ID, "1"))))) + .anySatisfy( + metric -> + assertThat(metric) + .hasName("messaging.client.sent.messages") + .hasUnit("{message}") + .hasDescription( + "Number of messages producer attempted to send to the broker.") + .hasLongSumSatisfying( + sum -> + sum.hasPointsSatisfying( + point -> + point + .hasValue(2) + .hasAttributesSatisfyingExactly( + equalTo(MESSAGING_OPERATION_NAME, "send"), + equalTo(MESSAGING_SYSTEM, "pulsar"), + equalTo( + ERROR_TYPE, + IllegalStateException.class.getName()), + equalTo(MESSAGING_DESTINATION_TEMPLATE, "topic-{id}"), + equalTo(MESSAGING_DESTINATION_PARTITION_ID, "1"))))); + } + } - assertThat(metricReader.collectAllMetrics()).isEmpty(); + @Test + @SuppressWarnings("deprecation") // using deprecated semconv + void createDoesNotCountSentMessages() { + InMemoryMetricReader metricReader = InMemoryMetricReader.createDelta(); + SdkMeterProvider meterProvider = + SdkMeterProvider.builder().registerMetricReader(metricReader).build(); + OperationListener listener = + MessagingProducerMetrics.getForOperationType().create(meterProvider.get("test")); - Context context2 = listener.onStart(Context.root(), requestAttributes, nanos(150)); + Attributes attributes = + Attributes.builder() + .put(MESSAGING_OPERATION, emitOldMessagingSemconv() ? "create" : null) + .put(MESSAGING_OPERATION_NAME, emitStableMessagingSemconv() ? "create" : null) + .put(MESSAGING_OPERATION_TYPE, emitStableMessagingSemconv() ? "create" : null) + .build(); + Context context = listener.onStart(Context.root(), attributes, nanos(100)); + listener.onEnd(context, Attributes.empty(), nanos(250)); + + Collection metrics = metricReader.collectAllMetrics(); + assertThat( + metrics.stream() + .filter(metric -> metric.getName().equals("messaging.client.operation.duration")) + .count()) + .isEqualTo(emitStableMessagingSemconv() ? 1 : 0); + assertThat( + metrics.stream() + .filter(metric -> metric.getName().equals("messaging.client.sent.messages")) + .count()) + .isZero(); + assertThat( + metrics.stream() + .filter(metric -> metric.getName().equals("messaging.publish.duration")) + .count()) + .isZero(); + } - assertThat(metricReader.collectAllMetrics()).isEmpty(); + @Test + @SuppressWarnings("deprecation") // using deprecated semconv + void zeroBatchDoesNotCountSentMessages() { + InMemoryMetricReader metricReader = InMemoryMetricReader.createDelta(); + SdkMeterProvider meterProvider = + SdkMeterProvider.builder().registerMetricReader(metricReader).build(); + OperationListener listener = + MessagingProducerMetrics.getForOperationType().create(meterProvider.get("test")); - listener.onEnd(context1, responseAttributes, nanos(250)); + Attributes attributes = + Attributes.builder() + .put(MESSAGING_OPERATION, emitOldMessagingSemconv() ? "publish" : null) + .put(MESSAGING_OPERATION_NAME, emitStableMessagingSemconv() ? "send" : null) + .put(MESSAGING_OPERATION_TYPE, emitStableMessagingSemconv() ? "send" : null) + .put(MESSAGING_BATCH_MESSAGE_COUNT, 0) + .build(); + Context context = listener.onStart(Context.root(), attributes, nanos(100)); + listener.onEnd(context, Attributes.empty(), nanos(250)); assertThat(metricReader.collectAllMetrics()) - .satisfiesExactlyInAnyOrder( - metric -> - assertThat(metric) - .hasName("messaging.publish.duration") - .hasUnit("s") - .hasDescription("Measures the duration of publish operation.") - .hasHistogramSatisfying( - histogram -> - histogram.hasPointsSatisfying( - point -> - point - .hasSum(0.15 /* seconds */) - .hasAttributesSatisfying( - equalTo(MESSAGING_SYSTEM, "pulsar"), - equalTo(MESSAGING_DESTINATION_PARTITION_ID, "1"), - equalTo( - MESSAGING_DESTINATION_NAME, - "persistent://public/default/topic"), - equalTo(SERVER_PORT, 6650), - equalTo(SERVER_ADDRESS, "localhost")) - .hasExemplarsSatisfying( - exemplar -> - exemplar - .hasTraceId("ff01020304050600ff0a0b0c0d0e0f00") - .hasSpanId("090a0b0c0d0e0f00")) - .hasBucketBoundaries(DURATION_BUCKETS)))); - - listener.onEnd(context2, responseAttributes, nanos(300)); + .noneSatisfy(metric -> assertThat(metric).hasName("messaging.client.sent.messages")); + } + + @Test + @SuppressWarnings("deprecation") // using deprecated semconv + void legacyEntryPointAlwaysCollectsLegacyMetrics() { + InMemoryMetricReader metricReader = InMemoryMetricReader.createDelta(); + SdkMeterProvider meterProvider = + SdkMeterProvider.builder().registerMetricReader(metricReader).build(); + OperationListener listener = MessagingProducerMetrics.get().create(meterProvider.get("test")); + + Context context = + listener.onStart( + Context.root(), + Attributes.of(MESSAGING_SYSTEM, "pulsar", MESSAGING_OPERATION, "publish"), + nanos(100)); + listener.onEnd(context, Attributes.empty(), nanos(250)); assertThat(metricReader.collectAllMetrics()) - .satisfiesExactlyInAnyOrder( - metric -> - assertThat(metric) - .hasName("messaging.publish.duration") - .hasHistogramSatisfying( - histogram -> - histogram.hasPointsSatisfying( - point -> point.hasSum(0.3 /* seconds */)))); + .satisfiesExactly(metric -> assertThat(metric).hasName("messaging.publish.duration")); } private static long nanos(int millis) { diff --git a/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingSpanKindExtractorTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingSpanKindExtractorTest.java new file mode 100644 index 000000000000..a7dac2e5d31d --- /dev/null +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingSpanKindExtractorTest.java @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.messaging; + +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.params.provider.Arguments.argumentSet; + +import io.opentelemetry.api.trace.SpanKind; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class MessagingSpanKindExtractorTest { + + @ParameterizedTest + @MethodSource("spanKinds") + void extractsSpanKind( + MessagingOperationType operationType, + boolean isSpanContextPropagated, + SpanKind oldKind, + SpanKind kind) { + SpanKind actualKind = + MessagingSpanKindExtractor.create(operationType, isSpanContextPropagated) + .extract(new Object()); + + assertThat(actualKind).isEqualTo(emitStableMessagingSemconv() ? kind : oldKind); + } + + @Test + void sendDefaultsToPropagatedSpanContext() { + SpanKind spanKind = + MessagingSpanKindExtractor.create(MessagingOperationType.SEND).extract(new Object()); + + assertThat(spanKind).isEqualTo(SpanKind.PRODUCER); + } + + @Test + void messageOperationUsesLegacySpanKind() { + SpanKind receiveKind = + MessagingSpanKindExtractor.create(MessageOperation.RECEIVE).extract(new Object()); + + assertThat(receiveKind).isEqualTo(SpanKind.CONSUMER); + } + + private static Stream spanKinds() { + return Stream.of( + argumentSet( + "create", MessagingOperationType.CREATE, true, SpanKind.PRODUCER, SpanKind.PRODUCER), + argumentSet( + "send with propagated context", + MessagingOperationType.SEND, + true, + SpanKind.PRODUCER, + SpanKind.PRODUCER), + argumentSet( + "send without propagated context", + MessagingOperationType.SEND, + false, + SpanKind.PRODUCER, + SpanKind.CLIENT), + argumentSet( + "receive", MessagingOperationType.RECEIVE, true, SpanKind.CONSUMER, SpanKind.CLIENT), + argumentSet( + "process", MessagingOperationType.PROCESS, true, SpanKind.CONSUMER, SpanKind.CONSUMER), + argumentSet( + "settle", MessagingOperationType.SETTLE, true, SpanKind.CLIENT, SpanKind.CLIENT)); + } +} diff --git a/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingSpanNameExtractorTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingSpanNameExtractorTest.java index 2d658df59f87..4abd8e84a7a6 100644 --- a/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingSpanNameExtractorTest.java +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingSpanNameExtractorTest.java @@ -5,11 +5,17 @@ package io.opentelemetry.instrumentation.api.incubator.semconv.messaging; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.params.provider.Arguments.argumentSet; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; import java.util.stream.Stream; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -22,36 +28,128 @@ class MessagingSpanNameExtractorTest { @Mock MessagingAttributesGetter getter; + @Test + void shouldKeepLegacyNameForMessageOperation() { + Message message = new Message(); + when(getter.isTemporaryDestination(message)).thenReturn(false); + when(getter.getDestination(message)).thenReturn("destination"); + + SpanNameExtractor underTest = + MessagingSpanNameExtractor.create(getter, MessageOperation.PUBLISH); + + assertThat(underTest.extract(message)).isEqualTo("destination publish"); + } + + @Test + void shouldRejectOperationNameForMessageOperation() { + assertThatThrownBy( + () -> + MessagingSpanNameExtractor.builder(getter, MessageOperation.PUBLISH) + .setOperationName("send")) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Operation name is not configurable for legacy builders"); + } + @ParameterizedTest @MethodSource("spanNameParams") void shouldExtractSpanName( boolean isTemporaryQueue, + boolean isAnonymousQueue, String destinationName, - MessageOperation operation, - String expectedSpanName) { + String destinationTemplate, + MessagingOperationType operationType, + String operationName, + String oldSpanName, + String spanName) { // given Message message = new Message(); - if (isTemporaryQueue) { - when(getter.isTemporaryDestination(message)).thenReturn(true); + if (emitStableMessagingSemconv()) { + when(getter.getDestinationTemplate(message)).thenReturn(destinationTemplate); + if (destinationTemplate == null) { + when(getter.isTemporaryDestination(message)).thenReturn(isTemporaryQueue); + if (!isTemporaryQueue) { + when(getter.isAnonymousDestination(message)).thenReturn(isAnonymousQueue); + if (!isAnonymousQueue) { + when(getter.getDestination(message)).thenReturn(destinationName); + } + } + } } else { - when(getter.getDestination(message)).thenReturn(destinationName); + when(getter.isTemporaryDestination(message)).thenReturn(isTemporaryQueue); + if (!isTemporaryQueue) { + when(getter.getDestination(message)).thenReturn(destinationName); + } } - SpanNameExtractor underTest = MessagingSpanNameExtractor.create(getter, operation); + SpanNameExtractor underTest = + MessagingSpanNameExtractor.builder(getter, operationType) + .setOperationName(operationName) + .build(); // when - String spanName = underTest.extract(message); + String actualSpanName = underTest.extract(message); // then - assertThat(spanName).isEqualTo(expectedSpanName); + assertThat(actualSpanName).isEqualTo(emitStableMessagingSemconv() ? spanName : oldSpanName); + if (emitStableMessagingSemconv() && destinationTemplate != null) { + verify(getter, never()).isTemporaryDestination(message); + verify(getter, never()).isAnonymousDestination(message); + } } static Stream spanNameParams() { return Stream.of( - Arguments.of(false, "destination", MessageOperation.PUBLISH, "destination publish"), - Arguments.of(true, null, MessageOperation.PROCESS, "(temporary) process"), - Arguments.of(false, null, MessageOperation.RECEIVE, "unknown receive")); + argumentSet( + "operation name override", + false, + false, + "destination", + null, + MessagingOperationType.SEND, + "send", + "destination publish", + "send destination"), + argumentSet( + "temporary destination", + true, + false, + "generated", + "generated-{id}", + MessagingOperationType.PROCESS, + "process", + "(temporary) process", + "process generated-{id}"), + argumentSet( + "missing destination", + false, + false, + null, + null, + MessagingOperationType.RECEIVE, + "receive", + "unknown receive", + "receive"), + argumentSet( + "destination template", + false, + false, + "customer-42", + "customer-{id}", + MessagingOperationType.SEND, + "send", + "customer-42 publish", + "send customer-{id}"), + argumentSet( + "anonymous destination", + false, + true, + "generated", + "generated-{id}", + MessagingOperationType.PROCESS, + "process", + "generated process", + "process generated-{id}")); } static class Message {} diff --git a/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/internal/MessagingProcessContextCustomizerTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/internal/MessagingProcessContextCustomizerTest.java new file mode 100644 index 000000000000..ebb92062489a --- /dev/null +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/internal/MessagingProcessContextCustomizerTest.java @@ -0,0 +1,90 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.messaging.internal; + +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.SpanContext; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class MessagingProcessContextCustomizerTest { + + private static final TextMapGetter> getter = + new TextMapGetter>() { + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Override + public String get(Map carrier, String key) { + return carrier.get(key); + } + }; + + private static final ContextCustomizer> underTest = + MessagingProcessContextCustomizer.create(W3CTraceContextPropagator.getInstance(), getter); + + @Test + void preservesAmbientParent() { + Context ambient = context("11111111111111111111111111111111", "1111111111111111"); + Span ambientSpan = Span.fromContext(ambient); + + Context result = underTest.onStart(ambient, carrier(), Attributes.empty()); + + assertThat(Span.fromContext(result)).isSameAs(ambientSpan); + } + + @Test + void preservesPropagatedFieldsWithAmbientParent() { + ContextKey propagatedKey = ContextKey.named("propagated"); + ContextCustomizer customizer = + MessagingProcessContextCustomizer.create( + (parent, request) -> parent.with(propagatedKey, request)); + Context ambient = context("11111111111111111111111111111111", "1111111111111111"); + + Context result = customizer.onStart(ambient, "value", Attributes.empty()); + + assertThat(result.get(propagatedKey)).isEqualTo("value"); + assertThat(Span.fromContext(result).getSpanContext()) + .isEqualTo(Span.fromContext(ambient).getSpanContext()); + } + + @Test + void usesProducerWhenAmbientParentIsMissing() { + Context result = underTest.onStart(Context.root(), carrier(), Attributes.empty()); + + assertThat(Span.fromContext(result).getSpanContext()) + .isEqualTo(spanContext("22222222222222222222222222222222", "2222222222222222")); + } + + private static Map carrier() { + Map carrier = new HashMap<>(); + carrier.put("traceparent", "00-22222222222222222222222222222222-2222222222222222-01"); + return Collections.unmodifiableMap(carrier); + } + + private static Context context(String traceId, String spanId) { + return Context.root().with(Span.wrap(spanContext(traceId, spanId))); + } + + private static SpanContext spanContext(String traceId, String spanId) { + return SpanContext.createFromRemoteParent( + traceId, spanId, TraceFlags.getSampled(), TraceState.getDefault()); + } +} diff --git a/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/internal/MessagingProcessInstrumenterFactoryTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/internal/MessagingProcessInstrumenterFactoryTest.java new file mode 100644 index 000000000000..81f007d9d377 --- /dev/null +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/internal/MessagingProcessInstrumenterFactoryTest.java @@ -0,0 +1,166 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.messaging.internal; + +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; +import static java.util.Collections.singletonMap; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.junit.jupiter.params.provider.Arguments.argumentSet; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceFlags; +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.Instrumenter; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.LinkData; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +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 MessagingProcessInstrumenterFactoryTest { + + private static final SpanContext ambientParent = + spanContext("11111111111111111111111111111111", "1111111111111111"); + private static final SpanContext producer = + spanContext("22222222222222222222222222222222", "2222222222222222"); + + private static final TextMapGetter> getter = + new TextMapGetter>() { + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Override + public String get(Map carrier, String key) { + return carrier.get(key); + } + }; + + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + @Test + void stableUsesProducerAsParentWithoutLink() { + assumeTrue(emitStableMessagingSemconv()); + Instrumenter, Void> instrumenter = + MessagingProcessInstrumenterFactory.create( + Instrumenter., Void>builder( + otelTesting.getOpenTelemetry(), "test", unused -> "process"), + W3CTraceContextPropagator.getInstance(), + getter, + false); + + Map carrier = + singletonMap("traceparent", "00-22222222222222222222222222222222-2222222222222222-01"); + Context context = instrumenter.start(Context.root(), carrier); + instrumenter.end(context, carrier, null, null); + + otelTesting + .assertTraces() + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("process") + .hasKind(SpanKind.CONSUMER) + .hasTraceId(producer.getTraceId()) + .hasParentSpanId(producer.getSpanId()) + .hasLinks())); + } + + @Test + void stableDoesNotLinkAmbientParent() { + assumeTrue(emitStableMessagingSemconv()); + SpanContext localProducer = + SpanContext.create( + producer.getTraceId(), + producer.getSpanId(), + producer.getTraceFlags(), + producer.getTraceState()); + Instrumenter, Void> instrumenter = + MessagingProcessInstrumenterFactory.create( + Instrumenter., Void>builder( + otelTesting.getOpenTelemetry(), "test", unused -> "process"), + W3CTraceContextPropagator.getInstance(), + getter, + false); + + Map carrier = + singletonMap("traceparent", "00-22222222222222222222222222222222-2222222222222222-01"); + Context context = instrumenter.start(Context.root().with(Span.wrap(localProducer)), carrier); + instrumenter.end(context, carrier, null, null); + + otelTesting + .assertTraces() + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("process") + .hasKind(SpanKind.CONSUMER) + .hasTraceId(producer.getTraceId()) + .hasParentSpanId(producer.getSpanId()) + .hasLinks())); + } + + @ParameterizedTest + @MethodSource("receiveInstrumentationSettings") + void usesExpectedParentAndLink(boolean receiveInstrumentationEnabled, boolean producerIsParent) { + Instrumenter, Void> instrumenter = + MessagingProcessInstrumenterFactory.create( + Instrumenter., Void>builder( + otelTesting.getOpenTelemetry(), "test", unused -> "process"), + W3CTraceContextPropagator.getInstance(), + getter, + receiveInstrumentationEnabled); + + Map carrier = + singletonMap("traceparent", "00-22222222222222222222222222222222-2222222222222222-01"); + Context context = instrumenter.start(Context.root().with(Span.wrap(ambientParent)), carrier); + instrumenter.end(context, carrier, null, null); + + SpanContext expectedParent = producerIsParent ? producer : ambientParent; + otelTesting + .assertTraces() + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + span -> { + span.hasName("process") + .hasKind(SpanKind.CONSUMER) + .hasTraceId(expectedParent.getTraceId()) + .hasParentSpanId(expectedParent.getSpanId()); + if (producerIsParent) { + span.hasLinks(); + } else { + span.hasLinks(LinkData.create(producer)); + } + })); + } + + private static Stream receiveInstrumentationSettings() { + boolean stable = emitStableMessagingSemconv(); + String semconv = stable ? "stable" : "old"; + return Stream.of( + argumentSet(semconv + " receive disabled", false, !stable), + argumentSet(semconv + " receive enabled", true, false)); + } + + private static SpanContext spanContext(String traceId, String spanId) { + return SpanContext.createFromRemoteParent( + traceId, spanId, TraceFlags.getSampled(), TraceState.getDefault()); + } +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/SemconvStability.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/SemconvStability.java index 8efdbfe4ee0b..d4465850c5ad 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/SemconvStability.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/SemconvStability.java @@ -169,11 +169,14 @@ private static boolean emitStable(SemconvMode mode) { return mode.version() >= 1; } - public static boolean emitOldMessagingSemconv() { + public static boolean emitOldMessagingSemconv() { // to be removed in 3.0 return emitOldMessagingSemconv; } - public static boolean emitStableMessagingSemconv() { + // Returns whether the selected v1 experimental messaging semantic conventions should be emitted. + // The method name follows the existing pattern; it does not indicate that the messaging + // conventions are stable. + public static boolean emitStableMessagingSemconv() { // to be removed in 3.0 return emitStableMessagingSemconv; } diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/SpanKey.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/SpanKey.java index 0cf43cc6162a..a7e705ad8d30 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/SpanKey.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/SpanKey.java @@ -43,12 +43,16 @@ public final class SpanKey { private static final ContextKey DB_CLIENT_KEY = ContextKey.named("opentelemetry-traces-span-key-db-client"); + private static final ContextKey PRODUCER_CREATE_KEY = + ContextKey.named("opentelemetry-traces-span-key-producer-create"); private static final ContextKey PRODUCER_KEY = ContextKey.named("opentelemetry-traces-span-key-producer"); private static final ContextKey CONSUMER_RECEIVE_KEY = ContextKey.named("opentelemetry-traces-span-key-consumer-receive"); private static final ContextKey CONSUMER_PROCESS_KEY = ContextKey.named("opentelemetry-traces-span-key-consumer-process"); + private static final ContextKey CONSUMER_SETTLE_KEY = + ContextKey.named("opentelemetry-traces-span-key-consumer-settle"); /* Span keys */ @@ -66,9 +70,11 @@ public final class SpanKey { public static final SpanKey RPC_CLIENT = new SpanKey(RPC_CLIENT_KEY); public static final SpanKey DB_CLIENT = new SpanKey(DB_CLIENT_KEY); + public static final SpanKey PRODUCER_CREATE = new SpanKey(PRODUCER_CREATE_KEY); public static final SpanKey PRODUCER = new SpanKey(PRODUCER_KEY); public static final SpanKey CONSUMER_RECEIVE = new SpanKey(CONSUMER_RECEIVE_KEY); public static final SpanKey CONSUMER_PROCESS = new SpanKey(CONSUMER_PROCESS_KEY); + public static final SpanKey CONSUMER_SETTLE = new SpanKey(CONSUMER_SETTLE_KEY); private final ContextKey key; diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/SemconvStabilityTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/SemconvStabilityTest.java index 32e316457df2..bc1d672f08bd 100644 --- a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/SemconvStabilityTest.java +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/SemconvStabilityTest.java @@ -9,6 +9,7 @@ import static java.util.Collections.emptyList; import static java.util.Collections.emptySet; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.params.provider.Arguments.argumentSet; import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; import io.opentelemetry.common.ComponentLoader; @@ -22,6 +23,8 @@ import java.util.stream.Collectors; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; class SemconvStabilityTest { @@ -379,6 +382,69 @@ void previewFallbackAppliesToPreviewDomains(boolean v3Preview) { assertThat(messaging).isEqualTo(SemconvMode.V1_EXPERIMENTAL); } + @ParameterizedTest + @MethodSource("messagingSelectionModes") + void messagingSelectionMatrix( + boolean v3Preview, Set stableOptIn, Set preview, SemconvMode expectedMode) { + SemconvMode messaging = + new SemconvSelectionResolver(general(), v3Preview, stableOptIn, preview).messaging(); + + assertThat(messaging).isEqualTo(expectedMode); + } + + private static List messagingSelectionModes() { + return asList( + argumentSet("legacy default", false, noStableOptIn(), noPreview(), SemconvMode.V0_STABLE), + argumentSet( + "legacy opt-in property", + false, + stableOptIn("messaging"), + noPreview(), + SemconvMode.V1_EXPERIMENTAL), + argumentSet( + "preview property", + false, + noStableOptIn(), + preview("messaging"), + SemconvMode.V1_EXPERIMENTAL), + argumentSet( + "legacy opt-in dual emit", + false, + stableOptIn("messaging/dup"), + noPreview(), + SemconvMode.V1_EXPERIMENTAL.withDualEmit()), + argumentSet( + "preview dual emit", + false, + noStableOptIn(), + preview("messaging/dup"), + SemconvMode.V1_EXPERIMENTAL.withDualEmit()), + argumentSet( + "v3 activation remains staged", + true, + noStableOptIn(), + noPreview(), + SemconvMode.V0_STABLE), + argumentSet( + "v3 ignores legacy opt-in property", + true, + stableOptIn("messaging"), + noPreview(), + SemconvMode.V0_STABLE), + argumentSet( + "v3 ignores legacy opt-in dual emit", + true, + stableOptIn("messaging/dup"), + noPreview(), + SemconvMode.V0_STABLE), + argumentSet( + "v3 with explicit preview", + true, + noStableOptIn(), + preview("messaging"), + SemconvMode.V1_EXPERIMENTAL)); + } + @SafeVarargs private static DeclarativeConfigProperties general(Entry... entries) { Map result = new HashMap(); diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/build.gradle.kts b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/build.gradle.kts index bbae53c120b0..0df59fcd47bb 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/build.gradle.kts +++ b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/build.gradle.kts @@ -53,6 +53,28 @@ tasks { include("**/KafkaClientSuppressReceiveSpansTest.*") } + val testMessagingPreview = register("testMessagingPreview") { + testClassesDirs = sourceSets.test.get().output.classesDirs + classpath = sourceSets.test.get().runtimeClasspath + filter { + includeTestsMatching("KafkaClientDefaultTest.testKafkaProducerAndConsumerSpan") + includeTestsMatching("KafkaClientDefaultTest.testAbandonedIteratorDoesNotParentNextProcessSpan") + includeTestsMatching("KafkaClientDefaultTest.testReceiveDoesNotParentProcessSpan") + } + jvmArgs("-Dotel.semconv-stability.preview=messaging") + systemProperty("metadataConfig", "otel.semconv-stability.opt-in=messaging") + } + + val testMessagingPreviewDup = register("testMessagingPreviewDup") { + testClassesDirs = sourceSets.test.get().output.classesDirs + classpath = sourceSets.test.get().runtimeClasspath + filter { + includeTestsMatching("KafkaClientDefaultTest.testReceiveDoesNotParentProcessSpan") + } + jvmArgs("-Dotel.semconv-stability.preview=messaging/dup") + systemProperty("metadataConfig", "otel.semconv-stability.opt-in=messaging/dup") + } + val testExperimental = register("testExperimental") { testClassesDirs = sourceSets.test.get().output.classesDirs classpath = sourceSets.test.get().runtimeClasspath @@ -67,23 +89,50 @@ tasks { systemProperty("metadataConfig", "otel.instrumentation.kafka.experimental-span-attributes=true") } - val testStableSemconv = register("testStableSemconv") { + val testV3Preview = register("testV3Preview") { testClassesDirs = sourceSets.test.get().output.classesDirs classpath = sourceSets.test.get().runtimeClasspath filter { excludeTestsMatching("KafkaClientPropagationDisabledTest") excludeTestsMatching("KafkaClientSuppressReceiveSpansTest") } - jvmArgs("-Dotel.instrumentation.messaging.experimental.receive-telemetry.enabled=true") jvmArgs("-Dotel.semconv-stability.preview=messaging") jvmArgs("-Dotel.instrumentation.common.v3-preview=true") // kafka metrics are disabled by default with v3-preview enabled jvmArgs("-Dotel.instrumentation.kafka-clients-metrics.enabled=true") - systemProperty("metadataConfig", "otel.semconv-stability.opt-in=messaging") + systemProperty("metadataConfig", "otel.instrumentation.common.v3-preview=true") + } + + val testV3PreviewReceiveSpansDisabled = register("testV3PreviewReceiveSpansDisabled") { + testClassesDirs = sourceSets.test.get().output.classesDirs + classpath = sourceSets.test.get().runtimeClasspath + filter { + includeTestsMatching("KafkaClientSuppressReceiveSpansTest") + } + include("**/KafkaClientSuppressReceiveSpansTest.*") + jvmArgs("-Dotel.instrumentation.messaging.experimental.receive-telemetry.enabled=false") + jvmArgs("-Dotel.semconv-stability.preview=messaging") + jvmArgs("-Dotel.instrumentation.common.v3-preview=true") + systemProperty("metadataConfig", "otel.instrumentation.common.v3-preview=true") + } + + val testBothSemconv = register("testBothSemconv") { + testClassesDirs = sourceSets.test.get().output.classesDirs + classpath = sourceSets.test.get().runtimeClasspath + filter { + excludeTestsMatching("KafkaClientPropagationDisabledTest") + excludeTestsMatching("KafkaClientSuppressReceiveSpansTest") + } + jvmArgs("-Dotel.instrumentation.messaging.experimental.receive-telemetry.enabled=true") + jvmArgs("-Dotel.semconv-stability.preview=messaging/dup") + jvmArgs("-Dotel.instrumentation.common.v3-preview=true") + // kafka metrics are disabled by default with v3-preview enabled + jvmArgs("-Dotel.instrumentation.kafka-clients-metrics.enabled=true") + systemProperty("metadataConfig", "otel.semconv-stability.preview=messaging/dup") } check { - dependsOn(testStableSemconv) + dependsOn(testV3Preview, testV3PreviewReceiveSpansDisabled, testBothSemconv) } test { @@ -95,7 +144,13 @@ tasks { } check { - dependsOn(testPropagationDisabled, testReceiveSpansDisabled, testExperimental) + dependsOn( + testPropagationDisabled, + testReceiveSpansDisabled, + testMessagingPreview, + testMessagingPreviewDup, + testExperimental, + ) } } diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaConsumerInstrumentation.java b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaConsumerInstrumentation.java index fd4c5f5de016..7d7f2e5e3ef2 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaConsumerInstrumentation.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaConsumerInstrumentation.java @@ -5,6 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.kafkaclients.v0_11; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; import static io.opentelemetry.javaagent.instrumentation.kafkaclients.v0_11.KafkaSingletons.consumerReceiveInstrumenter; import static net.bytebuddy.matcher.ElementMatchers.isPublic; @@ -67,15 +68,15 @@ public static void onExit( return; } - Context parentContext = currentContext(); + Context parentContext = KafkaConsumerContextUtil.withoutLeakedProcessSpan(currentContext()); KafkaReceiveRequest request = KafkaReceiveRequest.create(records, consumer); // disable process tracing and store the receive span for each individual record too boolean previousValue = KafkaClientsConsumerProcessTracing.setWrappingEnabled(false); try { - Context context = null; + Context receiveContext = null; if (consumerReceiveInstrumenter().shouldStart(parentContext, request)) { - context = + receiveContext = InstrumenterUtil.startAndEnd( consumerReceiveInstrumenter(), parentContext, @@ -86,16 +87,14 @@ public static void onExit( timer.now()); } - // we're storing the context of the receive span so that process spans can use it as - // parent context even though the span has ended - // this is the suggested behavior according to the spec batch receive scenario: - // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/messaging/messaging-spans.md#batch-receiving + Context processParentContext = + emitStableMessagingSemconv() ? parentContext : receiveContext; // we're attaching the consumer to the records to be able to retrieve things like consumer // group or clientId later - KafkaConsumerContextUtil.set(records, context, consumer); + KafkaConsumerContextUtil.set(records, processParentContext, consumer); for (ConsumerRecord record : records) { - KafkaConsumerContextUtil.set(record, context, consumer); + KafkaConsumerContextUtil.set(record, processParentContext, consumer); } } finally { KafkaClientsConsumerProcessTracing.setWrappingEnabled(previousValue); diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaProducerInstrumentation.java b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaProducerInstrumentation.java index c405c3a46a87..ee7ee274580a 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaProducerInstrumentation.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaProducerInstrumentation.java @@ -6,6 +6,7 @@ package io.opentelemetry.javaagent.instrumentation.kafkaclients.v0_11; import static io.opentelemetry.javaagent.instrumentation.kafkaclients.v0_11.KafkaSingletons.PRODUCER_PROPAGATION_ENABLED; +import static io.opentelemetry.javaagent.instrumentation.kafkaclients.v0_11.KafkaSingletons.PRODUCER_SPAN_CONTEXT_PROPAGATION_ENABLED; import static io.opentelemetry.javaagent.instrumentation.kafkaclients.v0_11.KafkaSingletons.producerInstrumenter; import static net.bytebuddy.matcher.ElementMatchers.isPublic; import static net.bytebuddy.matcher.ElementMatchers.named; @@ -79,8 +80,8 @@ public Callback wrapCallback(@Nullable Callback originalCallback) { } public ProducerRecord propagateContext( - ApiVersions apiVersions, ProducerRecord record) { - if (PRODUCER_PROPAGATION_ENABLED && KafkaPropagation.shouldPropagate(apiVersions)) { + ProducerRecord record, boolean shouldPropagate) { + if (shouldPropagate) { return KafkaPropagation.propagateContext(context, record); } return record; @@ -112,13 +113,20 @@ public static Object[] onEnter( String bootstrapServers = KafkaUtil.extractBootstrapServers( producerConfig.getList(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG)); + boolean shouldPropagate = + PRODUCER_PROPAGATION_ENABLED && KafkaPropagation.shouldPropagate(apiVersions); KafkaProducerRequest request = - KafkaProducerRequest.create(record, clientId, bootstrapServers); + KafkaProducerRequest.create( + record, + clientId, + bootstrapServers, + PRODUCER_SPAN_CONTEXT_PROPAGATION_ENABLED + && KafkaPropagation.shouldPropagate(apiVersions)); AdviceScope adviceScope = AdviceScope.start(request); if (adviceScope == null) { return new Object[] {null, record, callback}; } - record = adviceScope.propagateContext(apiVersions, record); + record = adviceScope.propagateContext(record, shouldPropagate); callback = adviceScope.wrapCallback(callback); return new Object[] {adviceScope, record, callback}; } diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaSingletons.java b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaSingletons.java index 9835aae995d9..3cbf9795570c 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaSingletons.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaSingletons.java @@ -6,11 +6,13 @@ package io.opentelemetry.javaagent.instrumentation.kafkaclients.v0_11; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; import io.opentelemetry.instrumentation.api.incubator.config.internal.DeclarativeConfigUtil; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.kafkaclients.common.v0_11.internal.KafkaInstrumenterFactory; import io.opentelemetry.instrumentation.kafkaclients.common.v0_11.internal.KafkaProcessRequest; import io.opentelemetry.instrumentation.kafkaclients.common.v0_11.internal.KafkaProducerRequest; +import io.opentelemetry.instrumentation.kafkaclients.common.v0_11.internal.KafkaPropagation; import io.opentelemetry.instrumentation.kafkaclients.common.v0_11.internal.KafkaReceiveRequest; import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; import org.apache.kafka.clients.producer.RecordMetadata; @@ -22,20 +24,30 @@ public class KafkaSingletons { DeclarativeConfigUtil.getInstrumentationConfig(GlobalOpenTelemetry.get(), "kafka") .get("producer_propagation") .getBoolean("enabled", true); + public static final boolean PRODUCER_SPAN_CONTEXT_PROPAGATION_ENABLED = + PRODUCER_PROPAGATION_ENABLED + && KafkaPropagation.propagatesSpanContext( + GlobalOpenTelemetry.getPropagators().getTextMapPropagator()); private static final Instrumenter producerInstrumenter; private static final Instrumenter consumerReceiveInstrumenter; private static final Instrumenter consumerProcessInstrumenter; static { + DeclarativeConfigProperties commonConfig = + DeclarativeConfigUtil.getInstrumentationConfig(GlobalOpenTelemetry.get(), "common"); + Boolean messagingReceiveInstrumentationEnabled = + commonConfig.get("messaging").get("receive_telemetry/development").getBoolean("enabled"); KafkaInstrumenterFactory instrumenterFactory = new KafkaInstrumenterFactory(GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME) .setCapturedHeaders(ExperimentalConfig.get().getMessagingHeaders()) .setCaptureExperimentalSpanAttributes( DeclarativeConfigUtil.getInstrumentationConfig(GlobalOpenTelemetry.get(), "kafka") - .getBoolean("experimental_span_attributes/development", false)) - .setMessagingReceiveTelemetryEnabled( - ExperimentalConfig.get().messagingReceiveInstrumentationEnabled()); + .getBoolean("experimental_span_attributes/development", false)); + if (messagingReceiveInstrumentationEnabled != null) { + instrumenterFactory.setMessagingReceiveTelemetryEnabled( + messagingReceiveInstrumentationEnabled); + } producerInstrumenter = instrumenterFactory.createProducerInstrumenter(); consumerReceiveInstrumenter = instrumenterFactory.createConsumerReceiveInstrumenter(); consumerProcessInstrumenter = instrumenterFactory.createConsumerProcessInstrumenter(); diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaClientDefaultTest.java b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaClientDefaultTest.java index fbcb2b086025..1638a82600a5 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaClientDefaultTest.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaClientDefaultTest.java @@ -5,12 +5,16 @@ package io.opentelemetry.javaagent.instrumentation.kafkaclients.v0_11; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; import static io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil.orderByRootSpanKind; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.kafkaclients.common.v0_11.internal.KafkaClientBaseTest; import io.opentelemetry.instrumentation.kafkaclients.common.v0_11.internal.KafkaClientPropagationBaseTest; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; @@ -18,6 +22,7 @@ import io.opentelemetry.sdk.trace.data.LinkData; import io.opentelemetry.sdk.trace.data.SpanData; import java.time.Duration; +import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.concurrent.atomic.AtomicReference; @@ -76,13 +81,53 @@ void testKafkaProducerAndConsumerSpan(boolean testHeaders) throws Exception { }); } AtomicReference producerSpan = new AtomicReference<>(); + if (emitStableMessagingSemconv()) { + testing.waitAndAssertSortedTraces( + orderByRootSpanKind(SpanKind.INTERNAL, SpanKind.CLIENT), + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> { + span.hasName("send " + SHARED_TOPIC) + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + sendAttributes("10", greeting, testHeaders)); + producerSpan.set(span.actual()); + }, + span -> + span.hasName("process " + SHARED_TOPIC) + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + processAttributes("10", greeting, testHeaders, false)), + span -> span.hasName("processing").hasParent(trace.getSpan(2)), + span -> + span.hasName("producer callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("poll " + SHARED_TOPIC) + .hasKind(SpanKind.CLIENT) + .hasNoParent() + .hasLinks(LinkData.create(producerSpan.get().getSpanContext())) + .hasAttributesSatisfyingExactly(receiveAttributes(testHeaders)))); + return; + } + testing.waitAndAssertSortedTraces( - orderByRootSpanKind(SpanKind.INTERNAL, SpanKind.CONSUMER), + orderByRootSpanKind( + SpanKind.INTERNAL, emitStableMessagingSemconv() ? SpanKind.CLIENT : SpanKind.CONSUMER), trace -> { trace.hasSpansSatisfyingExactly( span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), span -> - span.hasName(SHARED_TOPIC + " publish") + span.hasName( + emitStableMessagingSemconv() + ? "send " + SHARED_TOPIC + : SHARED_TOPIC + " publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly(sendAttributes("10", greeting, testHeaders)), @@ -95,12 +140,18 @@ void testKafkaProducerAndConsumerSpan(boolean testHeaders) throws Exception { trace -> trace.hasSpansSatisfyingExactly( span -> - span.hasName(SHARED_TOPIC + " receive") - .hasKind(SpanKind.CONSUMER) + span.hasName( + emitStableMessagingSemconv() + ? "poll " + SHARED_TOPIC + : SHARED_TOPIC + " receive") + .hasKind(emitStableMessagingSemconv() ? SpanKind.CLIENT : SpanKind.CONSUMER) .hasNoParent() .hasAttributesSatisfyingExactly(receiveAttributes(testHeaders)), span -> - span.hasName(SHARED_TOPIC + " process") + span.hasName( + emitStableMessagingSemconv() + ? "process " + SHARED_TOPIC + : SHARED_TOPIC + " process") .hasKind(SpanKind.CONSUMER) .hasLinks(LinkData.create(producerSpan.get().getSpanContext())) .hasParent(trace.getSpan(0)) @@ -109,6 +160,79 @@ void testKafkaProducerAndConsumerSpan(boolean testHeaders) throws Exception { span -> span.hasName("processing").hasParent(trace.getSpan(1)))); } + @Test + void testReceiveDoesNotParentProcessSpan() throws Exception { + assumeTrue(emitStableMessagingSemconv()); + producer.send(new ProducerRecord<>(SHARED_TOPIC, 10, "Hello Kafka!")).get(5, SECONDS); + + awaitUntilConsumerIsReady(); + ConsumerRecords records = poll(Duration.ofSeconds(5)); + assertThat(records.count()).isEqualTo(1); + + for (ConsumerRecord ignored : records) { + testing.runWithSpan("processing", () -> {}); + } + + AtomicReference producerSpan = new AtomicReference<>(); + testing.waitAndAssertSortedTraces( + orderByRootSpanKind(SpanKind.PRODUCER, SpanKind.CLIENT), + trace -> + trace.hasSpansSatisfyingExactly( + span -> { + span.hasName("send " + SHARED_TOPIC).hasKind(SpanKind.PRODUCER).hasNoParent(); + producerSpan.set(span.actual()); + }, + span -> + span.hasName("process " + SHARED_TOPIC) + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(0)), + span -> span.hasName("processing").hasParent(trace.getSpan(1))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("poll " + SHARED_TOPIC).hasKind(SpanKind.CLIENT).hasNoParent())); + } + + @Test + void testAbandonedIteratorDoesNotParentNextProcessSpan() throws Exception { + assumeTrue(emitStableMessagingSemconv()); + producer.send(new ProducerRecord<>(SHARED_TOPIC, "first")).get(5, SECONDS); + awaitUntilConsumerIsReady(); + testing.runWithSpan( + "parent", + () -> { + Iterator> firstIterator = + poll(Duration.ofSeconds(5)).iterator(); + assertThat(firstIterator.hasNext()).isTrue(); + firstIterator.next(); + + try (Scope ignored = Context.root().makeCurrent()) { + producer.send(new ProducerRecord<>(SHARED_TOPIC, "second")).get(5, SECONDS); + } + Iterator> secondIterator = + poll(Duration.ofSeconds(5)).iterator(); + assertThat(secondIterator.hasNext()).isTrue(); + secondIterator.next(); + assertThat(secondIterator.hasNext()).isFalse(); + assertThat(firstIterator.hasNext()).isFalse(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("send " + SHARED_TOPIC).hasNoParent()), + trace -> + trace.hasSpansSatisfyingExactlyInAnyOrder( + span -> span.hasName("parent").hasNoParent(), + span -> span.hasName("poll " + SHARED_TOPIC).hasParent(trace.getSpan(0)), + span -> span.hasName("process " + SHARED_TOPIC).hasParent(trace.getSpan(0)), + span -> span.hasName("poll " + SHARED_TOPIC).hasParent(trace.getSpan(0)), + span -> span.hasName("process " + SHARED_TOPIC).hasParent(trace.getSpan(0))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("send " + SHARED_TOPIC).hasNoParent())); + } + @DisplayName("test pass through tombstone") @Test void testPassThroughTombstone() throws Exception { @@ -124,12 +248,44 @@ void testPassThroughTombstone() throws Exception { } AtomicReference producerSpan = new AtomicReference<>(); + if (emitStableMessagingSemconv()) { + testing.waitAndAssertSortedTraces( + orderByRootSpanKind(SpanKind.PRODUCER, SpanKind.CLIENT), + trace -> + trace.hasSpansSatisfyingExactly( + span -> { + span.hasName("send " + SHARED_TOPIC) + .hasKind(SpanKind.PRODUCER) + .hasNoParent() + .hasAttributesSatisfyingExactly(sendAttributes(null, null, false)); + producerSpan.set(span.actual()); + }, + span -> + span.hasName("process " + SHARED_TOPIC) + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + processAttributes(null, null, false, false))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("poll " + SHARED_TOPIC) + .hasKind(SpanKind.CLIENT) + .hasNoParent() + .hasAttributesSatisfyingExactly(receiveAttributes(false)))); + return; + } + testing.waitAndAssertSortedTraces( - orderByRootSpanKind(SpanKind.INTERNAL, SpanKind.CONSUMER), + orderByRootSpanKind( + SpanKind.INTERNAL, emitStableMessagingSemconv() ? SpanKind.CLIENT : SpanKind.CONSUMER), trace -> { trace.hasSpansSatisfyingExactly( span -> - span.hasName(SHARED_TOPIC + " publish") + span.hasName( + emitStableMessagingSemconv() + ? "send " + SHARED_TOPIC + : SHARED_TOPIC + " publish") .hasKind(SpanKind.PRODUCER) .hasNoParent() .hasAttributesSatisfyingExactly(sendAttributes(null, null, false))); @@ -138,12 +294,18 @@ void testPassThroughTombstone() throws Exception { trace -> trace.hasSpansSatisfyingExactly( span -> - span.hasName(SHARED_TOPIC + " receive") - .hasKind(SpanKind.CONSUMER) + span.hasName( + emitStableMessagingSemconv() + ? "poll " + SHARED_TOPIC + : SHARED_TOPIC + " receive") + .hasKind(emitStableMessagingSemconv() ? SpanKind.CLIENT : SpanKind.CONSUMER) .hasNoParent() .hasAttributesSatisfyingExactly(receiveAttributes(false)), span -> - span.hasName(SHARED_TOPIC + " process") + span.hasName( + emitStableMessagingSemconv() + ? "process " + SHARED_TOPIC + : SHARED_TOPIC + " process") .hasKind(SpanKind.CONSUMER) .hasLinks(LinkData.create(producerSpan.get().getSpanContext())) .hasParent(trace.getSpan(0)) @@ -183,12 +345,44 @@ void testRecordsWithTopicPartitionKafkaConsume(boolean testListIterator) throws } AtomicReference producerSpan = new AtomicReference<>(); + if (emitStableMessagingSemconv()) { + testing.waitAndAssertSortedTraces( + orderByRootSpanKind(SpanKind.PRODUCER, SpanKind.CLIENT), + trace -> + trace.hasSpansSatisfyingExactly( + span -> { + span.hasName("send " + SHARED_TOPIC) + .hasKind(SpanKind.PRODUCER) + .hasNoParent() + .hasAttributesSatisfyingExactly(sendAttributes(null, greeting, false)); + producerSpan.set(span.actual()); + }, + span -> + span.hasName("process " + SHARED_TOPIC) + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + processAttributes(null, greeting, false, false))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("poll " + SHARED_TOPIC) + .hasKind(SpanKind.CLIENT) + .hasNoParent() + .hasAttributesSatisfyingExactly(receiveAttributes(false)))); + return; + } + testing.waitAndAssertSortedTraces( - orderByRootSpanKind(SpanKind.INTERNAL, SpanKind.CONSUMER), + orderByRootSpanKind( + SpanKind.INTERNAL, emitStableMessagingSemconv() ? SpanKind.CLIENT : SpanKind.CONSUMER), trace -> { trace.hasSpansSatisfyingExactly( span -> - span.hasName(SHARED_TOPIC + " publish") + span.hasName( + emitStableMessagingSemconv() + ? "send " + SHARED_TOPIC + : SHARED_TOPIC + " publish") .hasKind(SpanKind.PRODUCER) .hasNoParent() .hasAttributesSatisfyingExactly(sendAttributes(null, greeting, false))); @@ -197,12 +391,18 @@ void testRecordsWithTopicPartitionKafkaConsume(boolean testListIterator) throws trace -> trace.hasSpansSatisfyingExactly( span -> - span.hasName(SHARED_TOPIC + " receive") - .hasKind(SpanKind.CONSUMER) + span.hasName( + emitStableMessagingSemconv() + ? "poll " + SHARED_TOPIC + : SHARED_TOPIC + " receive") + .hasKind(emitStableMessagingSemconv() ? SpanKind.CLIENT : SpanKind.CONSUMER) .hasNoParent() .hasAttributesSatisfyingExactly(receiveAttributes(false)), span -> - span.hasName(SHARED_TOPIC + " process") + span.hasName( + emitStableMessagingSemconv() + ? "process " + SHARED_TOPIC + : SHARED_TOPIC + " process") .hasKind(SpanKind.CONSUMER) .hasLinks(LinkData.create(producerSpan.get().getSpanContext())) .hasParent(trace.getSpan(0)) @@ -247,13 +447,51 @@ void testKafkaHeaderNull() throws Exception { }); } AtomicReference producerSpan = new AtomicReference<>(); + if (emitStableMessagingSemconv()) { + testing.waitAndAssertSortedTraces( + orderByRootSpanKind(SpanKind.INTERNAL, SpanKind.CLIENT), + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> { + span.hasName("send " + SHARED_TOPIC) + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly(sendAttributes("10", greeting, false)); + producerSpan.set(span.actual()); + }, + span -> + span.hasName("process " + SHARED_TOPIC) + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + processAttributes("10", greeting, false, false)), + span -> span.hasName("processing").hasParent(trace.getSpan(2)), + span -> + span.hasName("producer callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("poll " + SHARED_TOPIC) + .hasKind(SpanKind.CLIENT) + .hasNoParent() + .hasAttributesSatisfyingExactly(receiveAttributes(false)))); + return; + } + testing.waitAndAssertSortedTraces( - orderByRootSpanKind(SpanKind.INTERNAL, SpanKind.CONSUMER), + orderByRootSpanKind( + SpanKind.INTERNAL, emitStableMessagingSemconv() ? SpanKind.CLIENT : SpanKind.CONSUMER), trace -> { trace.hasSpansSatisfyingExactly( span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), span -> - span.hasName(SHARED_TOPIC + " publish") + span.hasName( + emitStableMessagingSemconv() + ? "send " + SHARED_TOPIC + : SHARED_TOPIC + " publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly(sendAttributes("10", greeting, false)), @@ -266,12 +504,18 @@ void testKafkaHeaderNull() throws Exception { trace -> trace.hasSpansSatisfyingExactly( span -> - span.hasName(SHARED_TOPIC + " receive") - .hasKind(SpanKind.CONSUMER) + span.hasName( + emitStableMessagingSemconv() + ? "poll " + SHARED_TOPIC + : SHARED_TOPIC + " receive") + .hasKind(emitStableMessagingSemconv() ? SpanKind.CLIENT : SpanKind.CONSUMER) .hasNoParent() .hasAttributesSatisfyingExactly(receiveAttributes(false)), span -> - span.hasName(SHARED_TOPIC + " process") + span.hasName( + emitStableMessagingSemconv() + ? "process " + SHARED_TOPIC + : SHARED_TOPIC + " process") .hasKind(SpanKind.CONSUMER) .hasLinks(LinkData.create(producerSpan.get().getSpanContext())) .hasParent(trace.getSpan(0)) diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaClientSuppressReceiveSpansTest.java b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaClientSuppressReceiveSpansTest.java index 4f8a584b926a..7e805ade4218 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaClientSuppressReceiveSpansTest.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaClientSuppressReceiveSpansTest.java @@ -5,6 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.kafkaclients.v0_11; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; @@ -67,12 +68,12 @@ void testKafkaProduceAndConsume() throws InterruptedException { trace.hasSpansSatisfyingExactly( span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), span -> - span.hasName(SHARED_TOPIC + " publish") + span.hasName(spanName("publish", "send")) .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly(sendAttributes("10", greeting, false)), span -> - span.hasName(SHARED_TOPIC + " process") + span.hasName(spanName("process", "process")) .hasKind(SpanKind.CONSUMER) .hasParent(trace.getSpan(1)) .hasAttributesSatisfyingExactly( @@ -104,12 +105,12 @@ void testPassThroughTombstone() throws Exception { trace -> trace.hasSpansSatisfyingExactly( span -> - span.hasName(SHARED_TOPIC + " publish") + span.hasName(spanName("publish", "send")) .hasKind(SpanKind.PRODUCER) .hasNoParent() .hasAttributesSatisfyingExactly(sendAttributes(null, null, false)), span -> - span.hasName(SHARED_TOPIC + " process") + span.hasName(spanName("process", "process")) .hasKind(SpanKind.CONSUMER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( @@ -139,15 +140,21 @@ void testRecordsWithTopicPartitionKafkaConsume() throws Exception { trace -> trace.hasSpansSatisfyingExactly( span -> - span.hasName(SHARED_TOPIC + " publish") + span.hasName(spanName("publish", "send")) .hasKind(SpanKind.PRODUCER) .hasNoParent() .hasAttributesSatisfyingExactly(sendAttributes(null, greeting, false)), span -> - span.hasName(SHARED_TOPIC + " process") + span.hasName(spanName("process", "process")) .hasKind(SpanKind.CONSUMER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( processAttributes(null, greeting, false, false)))); } + + private static String spanName(String oldOperation, String operationName) { + return emitStableMessagingSemconv() + ? operationName + " " + SHARED_TOPIC + : SHARED_TOPIC + " " + oldOperation; + } } diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/testing/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaClientBaseTest.java b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/testing/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaClientBaseTest.java index 8dde894d4afd..80d7c5fe4534 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/testing/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaClientBaseTest.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/testing/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaClientBaseTest.java @@ -14,6 +14,7 @@ import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_BATCH_MESSAGE_COUNT; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_CONSUMER_GROUP_NAME; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP; @@ -23,6 +24,8 @@ import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_KAFKA_OFFSET; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION_NAME; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION_TYPE; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_SYSTEM; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Arrays.asList; @@ -74,7 +77,7 @@ public abstract class KafkaClientBaseTest { @RegisterExtension final AutoCleanupExtension cleanup = AutoCleanupExtension.create(); protected static final String SHARED_TOPIC = "shared.topic"; - protected static final AttributeKey MESSAGING_CLIENT_ID = + protected static final AttributeKey MESSAGING_CLIENT_ID_OLD = AttributeKey.stringKey("messaging.client_id"); private KafkaContainer kafka; @@ -181,9 +184,11 @@ protected static List sendAttributes( asList( equalTo(MESSAGING_SYSTEM, "kafka"), equalTo(MESSAGING_DESTINATION_NAME, SHARED_TOPIC), - equalTo(MESSAGING_OPERATION, "publish"), - satisfies(MESSAGING_CLIENT_ID, val -> val.startsWith("producer")), + equalTo(MESSAGING_OPERATION, emitOldMessagingSemconv() ? "publish" : null), + equalTo(MESSAGING_OPERATION_NAME, emitStableMessagingSemconv() ? "send" : null), + equalTo(MESSAGING_OPERATION_TYPE, emitStableMessagingSemconv() ? "send" : null), satisfies(MESSAGING_DESTINATION_PARTITION_ID, AbstractStringAssert::isNotEmpty))); + addClientIdAssertions(assertions, "producer"); if (emitOldMessagingSemconv()) { assertions.add(satisfies(MESSAGING_KAFKA_MESSAGE_OFFSET, AbstractLongAssert::isNotNegative)); } @@ -215,12 +220,17 @@ protected static List receiveAttributes(boolean testHeaders) asList( equalTo(MESSAGING_SYSTEM, "kafka"), equalTo(MESSAGING_DESTINATION_NAME, SHARED_TOPIC), - equalTo(MESSAGING_OPERATION, "receive"), - satisfies(MESSAGING_CLIENT_ID, val -> val.startsWith("consumer")), + equalTo(MESSAGING_OPERATION, emitOldMessagingSemconv() ? "receive" : null), + equalTo(MESSAGING_OPERATION_NAME, emitStableMessagingSemconv() ? "poll" : null), + equalTo(MESSAGING_OPERATION_TYPE, emitStableMessagingSemconv() ? "receive" : null), satisfies(MESSAGING_BATCH_MESSAGE_COUNT, AbstractLongAssert::isPositive))); + addClientIdAssertions(assertions, "consumer"); // consumer group is not available in version 0.11 if (testLatestDeps()) { - assertions.add(equalTo(MESSAGING_KAFKA_CONSUMER_GROUP, "test")); + assertions.add( + equalTo(MESSAGING_KAFKA_CONSUMER_GROUP, emitOldMessagingSemconv() ? "test" : null)); + assertions.add( + equalTo(MESSAGING_CONSUMER_GROUP_NAME, emitStableMessagingSemconv() ? "test" : null)); } if (testHeaders) { assertions.add(equalTo(headerAttributeKey("Test-Message-Header"), singletonList("test"))); @@ -236,9 +246,11 @@ protected static List processAttributes( asList( equalTo(MESSAGING_SYSTEM, "kafka"), equalTo(MESSAGING_DESTINATION_NAME, SHARED_TOPIC), - equalTo(MESSAGING_OPERATION, "process"), - satisfies(MESSAGING_CLIENT_ID, val -> val.startsWith("consumer")), + equalTo(MESSAGING_OPERATION, emitOldMessagingSemconv() ? "process" : null), + equalTo(MESSAGING_OPERATION_NAME, emitStableMessagingSemconv() ? "process" : null), + equalTo(MESSAGING_OPERATION_TYPE, emitStableMessagingSemconv() ? "process" : null), satisfies(MESSAGING_DESTINATION_PARTITION_ID, AbstractStringAssert::isNotEmpty))); + addClientIdAssertions(assertions, "consumer"); if (EXPERIMENTAL_ATTRIBUTES) { assertions.add( satisfies(longKey("kafka.record.queue_time_ms"), AbstractLongAssert::isNotNegative)); @@ -251,7 +263,10 @@ protected static List processAttributes( } // consumer group is not available in version 0.11 if (testLatestDeps()) { - assertions.add(equalTo(MESSAGING_KAFKA_CONSUMER_GROUP, "test")); + assertions.add( + equalTo(MESSAGING_KAFKA_CONSUMER_GROUP, emitOldMessagingSemconv() ? "test" : null)); + assertions.add( + equalTo(MESSAGING_CONSUMER_GROUP_NAME, emitStableMessagingSemconv() ? "test" : null)); } if (messageKey != null) { assertions.add(equalTo(MESSAGING_KAFKA_MESSAGE_KEY, messageKey)); @@ -271,4 +286,15 @@ protected static List processAttributes( } return assertions; } + + private static void addClientIdAssertions( + List assertions, String clientIdPrefix) { + if (emitOldMessagingSemconv()) { + assertions.add(satisfies(MESSAGING_CLIENT_ID_OLD, val -> val.startsWith(clientIdPrefix))); + } + if (emitStableMessagingSemconv()) { + assertions.add( + satisfies(stringKey("messaging.client.id"), val -> val.startsWith(clientIdPrefix))); + } + } } diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/build.gradle.kts b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/build.gradle.kts index 421f6f1aac53..d4689bb594ee 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/build.gradle.kts +++ b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/build.gradle.kts @@ -34,8 +34,16 @@ tasks { systemProperty("metadataConfig", "otel.semconv.exception.signal.preview=logs") } + val testV3Preview = register("testV3Preview") { + testClassesDirs = sourceSets.test.get().output.classesDirs + classpath = sourceSets.test.get().runtimeClasspath + jvmArgs("-Dotel.instrumentation.common.v3-preview=true") + jvmArgs("-Dotel.semconv-stability.preview=messaging") + systemProperty("metadataConfig", "otel.instrumentation.common.v3-preview=true") + } + check { - dependsOn(testExceptionSignalLogs) + dependsOn(testExceptionSignalLogs, testV3Preview) } } diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/KafkaTelemetryBuilder.java b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/KafkaTelemetryBuilder.java index 28f82b4b4a63..4f5880c8f96d 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/KafkaTelemetryBuilder.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/KafkaTelemetryBuilder.java @@ -34,6 +34,7 @@ public final class KafkaTelemetryBuilder { private boolean captureExperimentalSpanAttributes = false; private boolean propagationEnabled = true; private boolean messagingReceiveInstrumentationEnabled = false; + private boolean messagingReceiveInstrumentationConfigured = false; KafkaTelemetryBuilder(OpenTelemetry openTelemetry) { this.openTelemetry = requireNonNull(openTelemetry); @@ -106,6 +107,7 @@ public KafkaTelemetryBuilder setPropagationEnabled(boolean propagationEnabled) { public KafkaTelemetryBuilder setMessagingReceiveTelemetryEnabled( boolean messagingReceiveInstrumentationEnabled) { this.messagingReceiveInstrumentationEnabled = messagingReceiveInstrumentationEnabled; + this.messagingReceiveInstrumentationConfigured = true; return this; } @@ -113,8 +115,11 @@ public KafkaTelemetry build() { KafkaInstrumenterFactory instrumenterFactory = new KafkaInstrumenterFactory(openTelemetry, INSTRUMENTATION_NAME) .setCapturedHeaders(capturedHeaders) - .setCaptureExperimentalSpanAttributes(captureExperimentalSpanAttributes) - .setMessagingReceiveTelemetryEnabled(messagingReceiveInstrumentationEnabled); + .setCaptureExperimentalSpanAttributes(captureExperimentalSpanAttributes); + if (messagingReceiveInstrumentationConfigured) { + instrumenterFactory.setMessagingReceiveTelemetryEnabled( + messagingReceiveInstrumentationEnabled); + } return new KafkaTelemetry( openTelemetry, diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/internal/KafkaConsumerTelemetry.java b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/internal/KafkaConsumerTelemetry.java index df5f398488e0..49994ad135c0 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/internal/KafkaConsumerTelemetry.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/internal/KafkaConsumerTelemetry.java @@ -5,6 +5,7 @@ package io.opentelemetry.instrumentation.kafkaclients.v2_6.internal; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; import static java.util.Collections.emptyMap; import io.opentelemetry.context.Context; @@ -12,6 +13,7 @@ import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil; import io.opentelemetry.instrumentation.api.internal.Timer; import io.opentelemetry.instrumentation.kafkaclients.common.v0_11.internal.KafkaConsumerContext; +import io.opentelemetry.instrumentation.kafkaclients.common.v0_11.internal.KafkaConsumerContextUtil; import io.opentelemetry.instrumentation.kafkaclients.common.v0_11.internal.KafkaProcessRequest; import io.opentelemetry.instrumentation.kafkaclients.common.v0_11.internal.KafkaReceiveRequest; import io.opentelemetry.instrumentation.kafkaclients.common.v0_11.internal.KafkaUtil; @@ -75,11 +77,11 @@ public Context buildAndFinishSpan( if (records.isEmpty()) { return null; } - Context parentContext = Context.current(); + Context parentContext = KafkaConsumerContextUtil.withoutLeakedProcessSpan(Context.current()); KafkaReceiveRequest request = KafkaReceiveRequest.create(records, consumerGroup, clientId); - Context context = null; + Context receiveContext = null; if (consumerReceiveInstrumenter.shouldStart(parentContext, request)) { - context = + receiveContext = InstrumenterUtil.startAndEnd( consumerReceiveInstrumenter, parentContext, @@ -90,16 +92,12 @@ public Context buildAndFinishSpan( timer.now()); } - // we're returning the context of the receive span so that process spans can use it as - // parent context even though the span has ended - // this is the suggested behavior according to the spec batch receive scenario: - // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/messaging/messaging-spans.md#batch-receiving - return context; + return emitStableMessagingSemconv() ? parentContext : receiveContext; } public void buildAndFinishErrorSpan( Consumer consumer, Timer timer, Throwable error) { - Context parentContext = Context.current(); + Context parentContext = KafkaConsumerContextUtil.withoutLeakedProcessSpan(Context.current()); ConsumerRecords records = new ConsumerRecords<>(emptyMap()); KafkaReceiveRequest request = KafkaReceiveRequest.create( diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/internal/KafkaProducerTelemetry.java b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/internal/KafkaProducerTelemetry.java index 034e46907732..b45a40953499 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/internal/KafkaProducerTelemetry.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/internal/KafkaProducerTelemetry.java @@ -29,6 +29,7 @@ public class KafkaProducerTelemetry { private final TextMapPropagator propagator; private final Instrumenter producerInstrumenter; private final boolean producerPropagationEnabled; + private final boolean producerSpanContextPropagationEnabled; public KafkaProducerTelemetry( TextMapPropagator propagator, @@ -37,6 +38,8 @@ public KafkaProducerTelemetry( this.propagator = propagator; this.producerInstrumenter = producerInstrumenter; this.producerPropagationEnabled = producerPropagationEnabled; + this.producerSpanContextPropagationEnabled = + producerPropagationEnabled && KafkaPropagation.propagatesSpanContext(propagator); } /** @@ -48,7 +51,9 @@ public ProducerRecord buildAndInjectSpan( ProducerRecord record, @Nullable String clientId, @Nullable String bootstrapServers) { Context parentContext = Context.current(); - KafkaProducerRequest request = KafkaProducerRequest.create(record, clientId, bootstrapServers); + KafkaProducerRequest request = + KafkaProducerRequest.create( + record, clientId, bootstrapServers, producerSpanContextPropagationEnabled); if (!producerInstrumenter.shouldStart(parentContext, request)) { return record; } @@ -79,7 +84,9 @@ public Future buildAndInjectSpan( String bootstrapServers) { Context parentContext = Context.current(); - KafkaProducerRequest request = KafkaProducerRequest.create(record, producer, bootstrapServers); + KafkaProducerRequest request = + KafkaProducerRequest.create( + record, producer, bootstrapServers, producerSpanContextPropagationEnabled); if (!producerInstrumenter.shouldStart(parentContext, request)) { return sendFn.apply(record, callback); } diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/AbstractInterceptorsTest.java b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/AbstractInterceptorsTest.java index a62c8e475f19..dff57ca1622b 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/AbstractInterceptorsTest.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/AbstractInterceptorsTest.java @@ -7,17 +7,23 @@ import static io.opentelemetry.api.common.AttributeKey.longKey; import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitOldMessagingSemconv; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; import static io.opentelemetry.instrumentation.testing.junit.message.MessageHeaderUtil.headerAttributeKey; import static io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil.orderByRootSpanName; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_BATCH_MESSAGE_COUNT; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_CONSUMER_GROUP_NAME; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_KAFKA_OFFSET; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION_NAME; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION_TYPE; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_SYSTEM; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Arrays.asList; @@ -32,6 +38,7 @@ import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; import io.opentelemetry.sdk.trace.data.LinkData; import java.time.Duration; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; @@ -112,6 +119,53 @@ void testInterceptors() throws InterruptedException { void assertTraces() { AtomicReference producerSpanContext = new AtomicReference<>(); + + if (emitStableMessagingSemconv()) { + testing.waitAndAssertSortedTraces( + orderByRootSpanName("parent", "poll " + SHARED_TOPIC, "producer callback"), + trace -> { + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("send " + SHARED_TOPIC) + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + publishAttributes(captureExperimentalSpanAttributes())), + span -> + span.hasName("process " + SHARED_TOPIC) + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(1)) + .hasLinks() + .hasAttributesSatisfyingExactly( + processAttributes(captureExperimentalSpanAttributes())), + span -> + span.hasName("process child") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(2))); + SpanContext spanContext = trace.getSpan(1).getSpanContext(); + producerSpanContext.set( + SpanContext.createFromRemoteParent( + spanContext.getTraceId(), + spanContext.getSpanId(), + spanContext.getTraceFlags(), + spanContext.getTraceState())); + }, + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("poll " + SHARED_TOPIC) + .hasKind(SpanKind.CLIENT) + .hasNoParent() + .hasLinks(LinkData.create(producerSpanContext.get())) + .hasAttributesSatisfyingExactly(receiveAttributes())), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("producer callback").hasKind(SpanKind.INTERNAL).hasNoParent())); + return; + } + testing.waitAndAssertSortedTraces( orderByRootSpanName("parent", SHARED_TOPIC + " receive", "producer callback"), trace -> { @@ -159,49 +213,86 @@ void assertTraces() { } private static List publishAttributes(boolean experimental) { - return asList( - equalTo(headerAttributeKey("Test-Message-Header"), singletonList("test")), - equalTo(MESSAGING_SYSTEM, "kafka"), - equalTo(MESSAGING_DESTINATION_NAME, SHARED_TOPIC), - equalTo(MESSAGING_OPERATION, "publish"), - satisfies(MESSAGING_CLIENT_ID, val -> val.startsWith("producer")), - satisfies( - stringKey("messaging.kafka.bootstrap.servers"), - val -> { - if (experimental) { - val.matches("^localhost:\\d+(,localhost:\\d+)*$"); - } - })); + List assertions = + new ArrayList<>( + asList( + equalTo(headerAttributeKey("Test-Message-Header"), singletonList("test")), + equalTo(MESSAGING_SYSTEM, "kafka"), + equalTo(MESSAGING_DESTINATION_NAME, SHARED_TOPIC), + equalTo(MESSAGING_OPERATION, emitOldMessagingSemconv() ? "publish" : null), + equalTo(MESSAGING_OPERATION_NAME, emitStableMessagingSemconv() ? "send" : null), + equalTo(MESSAGING_OPERATION_TYPE, emitStableMessagingSemconv() ? "send" : null), + satisfies( + stringKey("messaging.kafka.bootstrap.servers"), + val -> { + if (experimental) { + val.matches("^localhost:\\d+(,localhost:\\d+)*$"); + } + }))); + addClientIdAssertions(assertions, "producer"); + return assertions; } private static List receiveAttributes() { - return asList( - equalTo(headerAttributeKey("Test-Message-Header"), singletonList("test")), - equalTo(MESSAGING_SYSTEM, "kafka"), - equalTo(MESSAGING_DESTINATION_NAME, SHARED_TOPIC), - equalTo(MESSAGING_OPERATION, "receive"), - equalTo(MESSAGING_KAFKA_CONSUMER_GROUP, "test"), - satisfies(MESSAGING_CLIENT_ID, val -> val.startsWith("consumer")), - equalTo(MESSAGING_BATCH_MESSAGE_COUNT, 1)); + List assertions = + new ArrayList<>( + asList( + equalTo(headerAttributeKey("Test-Message-Header"), singletonList("test")), + equalTo(MESSAGING_SYSTEM, "kafka"), + equalTo(MESSAGING_DESTINATION_NAME, SHARED_TOPIC), + equalTo(MESSAGING_OPERATION, emitOldMessagingSemconv() ? "receive" : null), + equalTo(MESSAGING_OPERATION_NAME, emitStableMessagingSemconv() ? "poll" : null), + equalTo(MESSAGING_OPERATION_TYPE, emitStableMessagingSemconv() ? "receive" : null), + equalTo(MESSAGING_KAFKA_CONSUMER_GROUP, emitOldMessagingSemconv() ? "test" : null), + equalTo( + MESSAGING_CONSUMER_GROUP_NAME, emitStableMessagingSemconv() ? "test" : null), + equalTo(MESSAGING_BATCH_MESSAGE_COUNT, 1))); + addClientIdAssertions(assertions, "consumer"); + return assertions; } private static List processAttributes(boolean experimental) { - return asList( - equalTo(headerAttributeKey("Test-Message-Header"), singletonList("test")), - equalTo(MESSAGING_SYSTEM, "kafka"), - equalTo(MESSAGING_DESTINATION_NAME, SHARED_TOPIC), - equalTo(MESSAGING_OPERATION, "process"), - equalTo(MESSAGING_MESSAGE_BODY_SIZE, greeting.getBytes(UTF_8).length), - satisfies(MESSAGING_DESTINATION_PARTITION_ID, AbstractStringAssert::isNotEmpty), - satisfies(MESSAGING_KAFKA_MESSAGE_OFFSET, AbstractLongAssert::isNotNegative), - equalTo(MESSAGING_KAFKA_CONSUMER_GROUP, "test"), - satisfies(MESSAGING_CLIENT_ID, val -> val.startsWith("consumer")), - satisfies( - longKey("kafka.record.queue_time_ms"), - val -> { - if (experimental) { - val.isNotNegative(); - } - })); + List assertions = + new ArrayList<>( + asList( + equalTo(headerAttributeKey("Test-Message-Header"), singletonList("test")), + equalTo(MESSAGING_SYSTEM, "kafka"), + equalTo(MESSAGING_DESTINATION_NAME, SHARED_TOPIC), + equalTo(MESSAGING_OPERATION, emitOldMessagingSemconv() ? "process" : null), + equalTo(MESSAGING_OPERATION_NAME, emitStableMessagingSemconv() ? "process" : null), + equalTo(MESSAGING_OPERATION_TYPE, emitStableMessagingSemconv() ? "process" : null), + equalTo(MESSAGING_MESSAGE_BODY_SIZE, greeting.getBytes(UTF_8).length), + satisfies(MESSAGING_DESTINATION_PARTITION_ID, AbstractStringAssert::isNotEmpty), + equalTo(MESSAGING_KAFKA_CONSUMER_GROUP, emitOldMessagingSemconv() ? "test" : null), + equalTo( + MESSAGING_CONSUMER_GROUP_NAME, emitStableMessagingSemconv() ? "test" : null), + satisfies( + longKey("kafka.record.queue_time_ms"), + val -> { + if (experimental) { + val.isNotNegative(); + } + }))); + if (emitOldMessagingSemconv()) { + assertions.add(satisfies(MESSAGING_KAFKA_MESSAGE_OFFSET, AbstractLongAssert::isNotNegative)); + } + if (emitStableMessagingSemconv()) { + assertions.add(satisfies(MESSAGING_KAFKA_OFFSET, AbstractLongAssert::isNotNegative)); + assertions.add(equalTo(stringKey("test-baggage-key-1"), "test-baggage-value-1")); + assertions.add(equalTo(stringKey("test-baggage-key-2"), "test-baggage-value-2")); + } + addClientIdAssertions(assertions, "consumer"); + return assertions; + } + + private static void addClientIdAssertions( + List assertions, String clientIdPrefix) { + if (emitOldMessagingSemconv()) { + assertions.add(satisfies(MESSAGING_CLIENT_ID_OLD, val -> val.startsWith(clientIdPrefix))); + } + if (emitStableMessagingSemconv()) { + assertions.add( + satisfies(stringKey("messaging.client.id"), val -> val.startsWith(clientIdPrefix))); + } } } diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/InterceptorsSuppressReceiveSpansTest.java b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/InterceptorsSuppressReceiveSpansTest.java index 8555895bd51c..b4aa4ca6892d 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/InterceptorsSuppressReceiveSpansTest.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/InterceptorsSuppressReceiveSpansTest.java @@ -6,25 +6,38 @@ package io.opentelemetry.instrumentation.kafkaclients.v2_6; import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitOldMessagingSemconv; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_CONSUMER_GROUP_NAME; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_KAFKA_OFFSET; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION_NAME; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION_TYPE; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_SYSTEM; import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Arrays.asList; import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import java.util.ArrayList; +import java.util.List; import org.assertj.core.api.AbstractLongAssert; import org.assertj.core.api.AbstractStringAssert; +@SuppressWarnings("deprecation") // using deprecated semconv class InterceptorsSuppressReceiveSpansTest extends AbstractInterceptorsTest { private static final KafkaTelemetry kafkaTelemetry = - KafkaTelemetry.create(testing.getOpenTelemetry()); + KafkaTelemetry.builder(testing.getOpenTelemetry()) + .setMessagingReceiveTelemetryEnabled(false) + .build(); @Override protected KafkaTelemetry kafkaTelemetry() { @@ -36,7 +49,6 @@ protected boolean captureExperimentalSpanAttributes() { return false; } - @SuppressWarnings("deprecation") // using deprecated semconv @Override void assertTraces() { testing.waitAndAssertTraces( @@ -44,32 +56,21 @@ void assertTraces() { trace.hasSpansSatisfyingExactly( span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), span -> - span.hasName(SHARED_TOPIC + " publish") + span.hasName( + emitStableMessagingSemconv() + ? "send " + SHARED_TOPIC + : SHARED_TOPIC + " publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly( - equalTo(MESSAGING_SYSTEM, "kafka"), - equalTo(MESSAGING_DESTINATION_NAME, SHARED_TOPIC), - equalTo(MESSAGING_OPERATION, "publish"), - satisfies(MESSAGING_CLIENT_ID, val -> val.startsWith("producer"))), + .hasAttributesSatisfyingExactly(sendAttributes()), span -> - span.hasName(SHARED_TOPIC + " process") + span.hasName( + emitStableMessagingSemconv() + ? "process " + SHARED_TOPIC + : SHARED_TOPIC + " process") .hasKind(SpanKind.CONSUMER) .hasParent(trace.getSpan(1)) - .hasAttributesSatisfyingExactly( - equalTo(MESSAGING_SYSTEM, "kafka"), - equalTo(MESSAGING_DESTINATION_NAME, SHARED_TOPIC), - equalTo(MESSAGING_OPERATION, "process"), - equalTo(MESSAGING_MESSAGE_BODY_SIZE, greeting.getBytes(UTF_8).length), - satisfies( - MESSAGING_DESTINATION_PARTITION_ID, - AbstractStringAssert::isNotEmpty), - satisfies( - MESSAGING_KAFKA_MESSAGE_OFFSET, AbstractLongAssert::isNotNegative), - equalTo(MESSAGING_KAFKA_CONSUMER_GROUP, "test"), - satisfies(MESSAGING_CLIENT_ID, val -> val.startsWith("consumer")), - equalTo(stringKey("test-baggage-key-1"), "test-baggage-value-1"), - equalTo(stringKey("test-baggage-key-2"), "test-baggage-value-2")), + .hasAttributesSatisfyingExactly(processAttributes()), span -> span.hasName("process child") .hasKind(SpanKind.INTERNAL) @@ -81,4 +82,54 @@ void assertTraces() { span -> span.hasName("producer callback").hasKind(SpanKind.INTERNAL).hasNoParent())); } + + private static List sendAttributes() { + List assertions = + new ArrayList<>( + asList( + equalTo(MESSAGING_SYSTEM, "kafka"), + equalTo(MESSAGING_DESTINATION_NAME, SHARED_TOPIC), + equalTo(MESSAGING_OPERATION, emitOldMessagingSemconv() ? "publish" : null), + equalTo(MESSAGING_OPERATION_NAME, emitStableMessagingSemconv() ? "send" : null), + equalTo(MESSAGING_OPERATION_TYPE, emitStableMessagingSemconv() ? "send" : null))); + addClientIdAssertion(assertions, "producer"); + return assertions; + } + + private static List processAttributes() { + List assertions = + new ArrayList<>( + asList( + equalTo(MESSAGING_SYSTEM, "kafka"), + equalTo(MESSAGING_DESTINATION_NAME, SHARED_TOPIC), + equalTo(MESSAGING_OPERATION, emitOldMessagingSemconv() ? "process" : null), + equalTo(MESSAGING_OPERATION_NAME, emitStableMessagingSemconv() ? "process" : null), + equalTo(MESSAGING_OPERATION_TYPE, emitStableMessagingSemconv() ? "process" : null), + equalTo(MESSAGING_MESSAGE_BODY_SIZE, greeting.getBytes(UTF_8).length), + satisfies(MESSAGING_DESTINATION_PARTITION_ID, AbstractStringAssert::isNotEmpty), + equalTo(MESSAGING_KAFKA_CONSUMER_GROUP, emitOldMessagingSemconv() ? "test" : null), + equalTo( + MESSAGING_CONSUMER_GROUP_NAME, emitStableMessagingSemconv() ? "test" : null), + equalTo(stringKey("test-baggage-key-1"), "test-baggage-value-1"), + equalTo(stringKey("test-baggage-key-2"), "test-baggage-value-2"))); + if (emitOldMessagingSemconv()) { + assertions.add(satisfies(MESSAGING_KAFKA_MESSAGE_OFFSET, AbstractLongAssert::isNotNegative)); + } + if (emitStableMessagingSemconv()) { + assertions.add(satisfies(MESSAGING_KAFKA_OFFSET, AbstractLongAssert::isNotNegative)); + } + addClientIdAssertion(assertions, "consumer"); + return assertions; + } + + private static void addClientIdAssertion( + List assertions, String clientIdPrefix) { + if (emitOldMessagingSemconv()) { + assertions.add(satisfies(MESSAGING_CLIENT_ID_OLD, val -> val.startsWith(clientIdPrefix))); + } + if (emitStableMessagingSemconv()) { + assertions.add( + satisfies(stringKey("messaging.client.id"), val -> val.startsWith(clientIdPrefix))); + } + } } diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/WrapperPropagationDisabledTest.java b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/WrapperPropagationDisabledTest.java index 542758cde145..51fa71e26c06 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/WrapperPropagationDisabledTest.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/WrapperPropagationDisabledTest.java @@ -5,6 +5,8 @@ package io.opentelemetry.instrumentation.kafkaclients.v2_6; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; + import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; import java.util.List; @@ -18,13 +20,56 @@ void configure(KafkaTelemetryBuilder builder) { @Override void assertTraces(boolean testHeaders, boolean testExperimental) { + if (emitStableMessagingSemconv()) { + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("send " + SHARED_TOPIC) + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + sendAttributes(testHeaders, testExperimental)), + span -> + span.hasName("producer callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("poll " + SHARED_TOPIC) + .hasKind(SpanKind.CLIENT) + .hasNoParent() + .hasLinks() + .hasAttributesSatisfyingExactly( + WrapperTest.receiveAttributes(testHeaders))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("process " + SHARED_TOPIC) + .hasKind(SpanKind.CONSUMER) + .hasNoParent() + .hasLinks() + .hasAttributesSatisfyingExactly( + processAttributes(greeting, testHeaders, testExperimental)), + span -> + span.hasName("process child") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + return; + } + testing.waitAndAssertTraces( trace -> trace.hasSpansSatisfyingExactly( span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), span -> - span.hasName(SHARED_TOPIC + " publish") - .hasKind(SpanKind.PRODUCER) + span.hasName( + emitStableMessagingSemconv() + ? "send " + SHARED_TOPIC + : SHARED_TOPIC + " publish") + .hasKind(emitStableMessagingSemconv() ? SpanKind.CLIENT : SpanKind.PRODUCER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( sendAttributes(testHeaders, testExperimental)), @@ -35,7 +80,10 @@ void assertTraces(boolean testHeaders, boolean testExperimental) { trace -> trace.hasSpansSatisfyingExactly( span -> - span.hasName(SHARED_TOPIC + " process") + span.hasName( + emitStableMessagingSemconv() + ? "process " + SHARED_TOPIC + : SHARED_TOPIC + " process") .hasKind(SpanKind.CONSUMER) .hasNoParent() .hasLinks() diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/WrapperSendExceptionTest.java b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/WrapperSendExceptionTest.java index 81d232ddeea5..ec2eb0a40581 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/WrapperSendExceptionTest.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/WrapperSendExceptionTest.java @@ -5,6 +5,7 @@ package io.opentelemetry.instrumentation.kafkaclients.v2_6; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; @@ -46,7 +47,8 @@ void producerSpanEndedWhenSendThrowsSynchronously() { trace.hasSpansSatisfyingExactly( span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), span -> - span.hasName("test-topic publish") + span.hasName( + emitStableMessagingSemconv() ? "send test-topic" : "test-topic publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)) .hasStatus(StatusData.error()))); diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/WrapperSuppressReceiveSpansTest.java b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/WrapperSuppressReceiveSpansTest.java index a549ce1aa7f4..0355bd13037a 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/WrapperSuppressReceiveSpansTest.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/WrapperSuppressReceiveSpansTest.java @@ -5,28 +5,11 @@ package io.opentelemetry.instrumentation.kafkaclients.v2_6; -import static io.opentelemetry.api.common.AttributeKey.longKey; -import static io.opentelemetry.api.common.AttributeKey.stringKey; -import static io.opentelemetry.instrumentation.testing.junit.message.MessageHeaderUtil.headerAttributeKey; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; -import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME; -import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID; -import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP; -import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET; -import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE; -import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION; -import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_SYSTEM; -import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.Arrays.asList; -import static java.util.Collections.singletonList; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; -import java.util.ArrayList; import java.util.List; -import org.assertj.core.api.AbstractLongAssert; -import org.assertj.core.api.AbstractStringAssert; @SuppressWarnings("deprecation") // using deprecated semconv class WrapperSuppressReceiveSpansTest extends AbstractWrapperTest { @@ -43,13 +26,19 @@ void assertTraces(boolean testHeaders, boolean testExperimental) { trace.hasSpansSatisfyingExactly( span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), span -> - span.hasName(SHARED_TOPIC + " publish") + span.hasName( + emitStableMessagingSemconv() + ? "send " + SHARED_TOPIC + : SHARED_TOPIC + " publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( sendAttributes(testHeaders, testExperimental)), span -> - span.hasName(SHARED_TOPIC + " process") + span.hasName( + emitStableMessagingSemconv() + ? "process " + SHARED_TOPIC + : SHARED_TOPIC + " process") .hasKind(SpanKind.CONSUMER) .hasParent(trace.getSpan(1)) .hasAttributesSatisfyingExactly( @@ -65,47 +54,11 @@ void assertTraces(boolean testHeaders, boolean testExperimental) { } static List sendAttributes(boolean testHeaders, boolean testExperimental) { - List assertions = - new ArrayList<>( - asList( - equalTo(MESSAGING_SYSTEM, "kafka"), - equalTo(MESSAGING_DESTINATION_NAME, SHARED_TOPIC), - equalTo(MESSAGING_OPERATION, "publish"), - satisfies(MESSAGING_CLIENT_ID, val -> val.startsWith("producer")), - satisfies(MESSAGING_DESTINATION_PARTITION_ID, AbstractStringAssert::isNotEmpty), - satisfies(MESSAGING_KAFKA_MESSAGE_OFFSET, AbstractLongAssert::isNotNegative))); - if (testHeaders) { - assertions.add(equalTo(headerAttributeKey("Test-Message-Header"), singletonList("test"))); - } - if (testExperimental) { - assertions.add( - satisfies( - stringKey("messaging.kafka.bootstrap.servers"), - val -> val.matches("^localhost:\\d+(,localhost:\\d+)*$"))); - } - return assertions; + return WrapperTest.sendAttributes(testHeaders, testExperimental); } static List processAttributes( String greeting, boolean testHeaders, boolean testExperimental) { - List assertions = - new ArrayList<>( - asList( - equalTo(MESSAGING_SYSTEM, "kafka"), - equalTo(MESSAGING_DESTINATION_NAME, SHARED_TOPIC), - equalTo(MESSAGING_OPERATION, "process"), - equalTo(MESSAGING_MESSAGE_BODY_SIZE, greeting.getBytes(UTF_8).length), - satisfies(MESSAGING_DESTINATION_PARTITION_ID, AbstractStringAssert::isNotEmpty), - satisfies(MESSAGING_KAFKA_MESSAGE_OFFSET, AbstractLongAssert::isNotNegative), - equalTo(MESSAGING_KAFKA_CONSUMER_GROUP, "test"), - satisfies(MESSAGING_CLIENT_ID, val -> val.startsWith("consumer")))); - if (testHeaders) { - assertions.add(equalTo(headerAttributeKey("Test-Message-Header"), singletonList("test"))); - } - if (testExperimental) { - assertions.add( - satisfies(longKey("kafka.record.queue_time_ms"), AbstractLongAssert::isNotNegative)); - } - return assertions; + return WrapperTest.processAttributes(greeting, testHeaders, testExperimental); } } diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/WrapperTest.java b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/WrapperTest.java index c9f784651554..e09a4cdba314 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/WrapperTest.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/WrapperTest.java @@ -9,16 +9,23 @@ import static io.opentelemetry.api.common.AttributeKey.stringKey; import static io.opentelemetry.instrumentation.api.internal.SemconvExceptionSignal.emitExceptionAsLogs; import static io.opentelemetry.instrumentation.api.internal.SemconvExceptionSignal.emitExceptionAsSpanEvents; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitOldMessagingSemconv; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static io.opentelemetry.semconv.ErrorAttributes.ERROR_TYPE; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_BATCH_MESSAGE_COUNT; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_CONSUMER_GROUP_NAME; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_KAFKA_OFFSET; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION_NAME; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION_TYPE; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_SYSTEM; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Arrays.asList; @@ -55,6 +62,52 @@ void configure(KafkaTelemetryBuilder builder) { @Override void assertTraces(boolean testHeaders, boolean testExperimental) { AtomicReference producerSpanContext = new AtomicReference<>(); + + if (emitStableMessagingSemconv()) { + testing.waitAndAssertTraces( + trace -> { + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("send " + SHARED_TOPIC) + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + sendAttributes(testHeaders, testExperimental)), + span -> + span.hasName("process " + SHARED_TOPIC) + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(1)) + .hasLinks() + .hasAttributesSatisfyingExactly( + processAttributes(greeting, testHeaders, testExperimental)), + span -> + span.hasName("process child") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(2)), + span -> + span.hasName("producer callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0))); + SpanContext spanContext = trace.getSpan(1).getSpanContext(); + producerSpanContext.set( + SpanContext.createFromRemoteParent( + spanContext.getTraceId(), + spanContext.getSpanId(), + spanContext.getTraceFlags(), + spanContext.getTraceState())); + }, + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("poll " + SHARED_TOPIC) + .hasKind(SpanKind.CLIENT) + .hasNoParent() + .hasLinks(LinkData.create(producerSpanContext.get())) + .hasAttributesSatisfyingExactly(receiveAttributes(testHeaders)))); + return; + } + testing.waitAndAssertTraces( trace -> { trace.hasSpansSatisfyingExactly( @@ -105,10 +158,12 @@ protected static List sendAttributes( asList( equalTo(MESSAGING_SYSTEM, "kafka"), equalTo(MESSAGING_DESTINATION_NAME, SHARED_TOPIC), - equalTo(MESSAGING_OPERATION, "publish"), - satisfies(MESSAGING_CLIENT_ID, val -> val.startsWith("producer")), - satisfies(MESSAGING_DESTINATION_PARTITION_ID, AbstractStringAssert::isNotEmpty), - satisfies(MESSAGING_KAFKA_MESSAGE_OFFSET, AbstractLongAssert::isNotNegative))); + equalTo(MESSAGING_OPERATION, emitOldMessagingSemconv() ? "publish" : null), + equalTo(MESSAGING_OPERATION_NAME, emitStableMessagingSemconv() ? "send" : null), + equalTo(MESSAGING_OPERATION_TYPE, emitStableMessagingSemconv() ? "send" : null), + satisfies(MESSAGING_DESTINATION_PARTITION_ID, AbstractStringAssert::isNotEmpty))); + addClientIdAssertions(assertions, "producer"); + addOffsetAssertions(assertions); if (testHeaders) { assertions.add( equalTo( @@ -123,19 +178,23 @@ protected static List sendAttributes( return assertions; } - private static List processAttributes( + static List processAttributes( String greeting, boolean testHeaders, boolean testExperimental) { List assertions = new ArrayList<>( asList( equalTo(MESSAGING_SYSTEM, "kafka"), equalTo(MESSAGING_DESTINATION_NAME, SHARED_TOPIC), - equalTo(MESSAGING_OPERATION, "process"), + equalTo(MESSAGING_OPERATION, emitOldMessagingSemconv() ? "process" : null), + equalTo(MESSAGING_OPERATION_NAME, emitStableMessagingSemconv() ? "process" : null), + equalTo(MESSAGING_OPERATION_TYPE, emitStableMessagingSemconv() ? "process" : null), equalTo(MESSAGING_MESSAGE_BODY_SIZE, greeting.getBytes(UTF_8).length), satisfies(MESSAGING_DESTINATION_PARTITION_ID, AbstractStringAssert::isNotEmpty), - satisfies(MESSAGING_KAFKA_MESSAGE_OFFSET, AbstractLongAssert::isNotNegative), - equalTo(MESSAGING_KAFKA_CONSUMER_GROUP, "test"), - satisfies(MESSAGING_CLIENT_ID, val -> val.startsWith("consumer")))); + equalTo(MESSAGING_KAFKA_CONSUMER_GROUP, emitOldMessagingSemconv() ? "test" : null), + equalTo( + MESSAGING_CONSUMER_GROUP_NAME, emitStableMessagingSemconv() ? "test" : null))); + addClientIdAssertions(assertions, "consumer"); + addOffsetAssertions(assertions); if (testHeaders) { assertions.add( equalTo( @@ -154,10 +213,14 @@ protected static List receiveAttributes(boolean testHeaders) asList( equalTo(MESSAGING_SYSTEM, "kafka"), equalTo(MESSAGING_DESTINATION_NAME, SHARED_TOPIC), - equalTo(MESSAGING_OPERATION, "receive"), - equalTo(MESSAGING_KAFKA_CONSUMER_GROUP, "test"), - satisfies(MESSAGING_CLIENT_ID, val -> val.startsWith("consumer")), + equalTo(MESSAGING_OPERATION, emitOldMessagingSemconv() ? "receive" : null), + equalTo(MESSAGING_OPERATION_NAME, emitStableMessagingSemconv() ? "poll" : null), + equalTo(MESSAGING_OPERATION_TYPE, emitStableMessagingSemconv() ? "receive" : null), + equalTo(MESSAGING_KAFKA_CONSUMER_GROUP, emitOldMessagingSemconv() ? "test" : null), + equalTo( + MESSAGING_CONSUMER_GROUP_NAME, emitStableMessagingSemconv() ? "test" : null), equalTo(MESSAGING_BATCH_MESSAGE_COUNT, 1))); + addClientIdAssertions(assertions, "consumer"); if (testHeaders) { assertions.add( equalTo( @@ -166,6 +229,26 @@ protected static List receiveAttributes(boolean testHeaders) return assertions; } + private static void addClientIdAssertions( + List assertions, String clientIdPrefix) { + if (emitOldMessagingSemconv()) { + assertions.add(satisfies(MESSAGING_CLIENT_ID_OLD, val -> val.startsWith(clientIdPrefix))); + } + if (emitStableMessagingSemconv()) { + assertions.add( + satisfies(stringKey("messaging.client.id"), val -> val.startsWith(clientIdPrefix))); + } + } + + private static void addOffsetAssertions(List assertions) { + if (emitOldMessagingSemconv()) { + assertions.add(satisfies(MESSAGING_KAFKA_MESSAGE_OFFSET, AbstractLongAssert::isNotNegative)); + } + if (emitStableMessagingSemconv()) { + assertions.add(satisfies(MESSAGING_KAFKA_OFFSET, AbstractLongAssert::isNotNegative)); + } + } + @Test void testConsumerError() { KafkaTelemetryBuilder telemetryBuilder = KafkaTelemetry.builder(testing.getOpenTelemetry()); @@ -182,14 +265,26 @@ void testConsumerError() { trace -> trace.hasSpansSatisfyingExactly( span -> - span.hasName("unknown receive") - .hasKind(SpanKind.CONSUMER) + span.hasName(emitStableMessagingSemconv() ? "poll" : "unknown receive") + .hasKind(emitStableMessagingSemconv() ? SpanKind.CLIENT : SpanKind.CONSUMER) .hasNoParent() .hasStatus(StatusData.error()) .hasException(emitExceptionAsSpanEvents() ? error : null) .hasAttributesSatisfyingExactly( equalTo(MESSAGING_SYSTEM, "kafka"), - equalTo(MESSAGING_OPERATION, "receive"), + equalTo( + MESSAGING_OPERATION, emitOldMessagingSemconv() ? "receive" : null), + equalTo( + MESSAGING_OPERATION_NAME, + emitStableMessagingSemconv() ? "poll" : null), + equalTo( + MESSAGING_OPERATION_TYPE, + emitStableMessagingSemconv() ? "receive" : null), + equalTo( + ERROR_TYPE, + emitStableMessagingSemconv() + ? IllegalStateException.class.getName() + : null), equalTo(MESSAGING_BATCH_MESSAGE_COUNT, 0)))); if (emitExceptionAsLogs()) { diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/internal/KafkaConsumerContextUtilTest.java b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/internal/KafkaConsumerContextUtilTest.java new file mode 100644 index 000000000000..54e4d1a86621 --- /dev/null +++ b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/internal/KafkaConsumerContextUtilTest.java @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.kafkaclients.v2_6.internal; + +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.kafkaclients.common.v0_11.internal.KafkaConsumerContextUtil; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class KafkaConsumerContextUtilTest { + + @RegisterExtension static final OpenTelemetryExtension testing = OpenTelemetryExtension.create(); + + @Test + void restoresProcessParentWithoutSuppressionMarker() { + assumeTrue(emitStableMessagingSemconv()); + Span parentSpan = + testing.getOpenTelemetry().getTracer("test").spanBuilder("parent").startSpan(); + Span processSpan = + testing.getOpenTelemetry().getTracer("test").spanBuilder("process").startSpan(); + try { + Context parentContext = Context.root().with(parentSpan); + Context processContext = + KafkaConsumerContextUtil.withProcessParentSpan( + parentContext.with(processSpan), parentContext); + + Context restored = KafkaConsumerContextUtil.withoutLeakedProcessSpan(processContext); + + assertThat(Span.fromContext(restored)).isSameAs(parentSpan); + assertThat(Span.fromContext(restored).isRecording()).isTrue(); + } finally { + processSpan.end(); + parentSpan.end(); + } + } + + @Test + void leavesUnrelatedCurrentSpanUntouched() { + assumeTrue(emitStableMessagingSemconv()); + Span parentSpan = + testing.getOpenTelemetry().getTracer("test").spanBuilder("parent").startSpan(); + Span processSpan = + testing.getOpenTelemetry().getTracer("test").spanBuilder("process").startSpan(); + Span unrelatedSpan = + testing.getOpenTelemetry().getTracer("test").spanBuilder("unrelated").startSpan(); + try { + Context parentContext = Context.root().with(parentSpan); + Context processContext = + KafkaConsumerContextUtil.withProcessParentSpan( + parentContext.with(processSpan), parentContext); + Context unrelatedContext = processContext.with(unrelatedSpan); + + assertThat(KafkaConsumerContextUtil.withoutLeakedProcessSpan(unrelatedContext)) + .isSameAs(unrelatedContext); + } finally { + unrelatedSpan.end(); + processSpan.end(); + parentSpan.end(); + } + } +} diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/internal/KafkaPropagationTest.java b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/internal/KafkaPropagationTest.java new file mode 100644 index 000000000000..3cdb523377f2 --- /dev/null +++ b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/internal/KafkaPropagationTest.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.kafkaclients.v2_6.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.instrumentation.kafkaclients.common.v0_11.internal.KafkaPropagation; +import org.junit.jupiter.api.Test; + +class KafkaPropagationTest { + + @Test + void detectsSpanContextPropagation() { + assertThat(KafkaPropagation.propagatesSpanContext(W3CTraceContextPropagator.getInstance())) + .isTrue(); + assertThat(KafkaPropagation.propagatesSpanContext(TextMapPropagator.noop())).isFalse(); + assertThat(KafkaPropagation.propagatesSpanContext(W3CBaggagePropagator.getInstance())) + .isFalse(); + } +} diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaConsumerAttributesExtractor.java b/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaConsumerAttributesExtractor.java index f1c226cfd8b6..a37bf3d1766e 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaConsumerAttributesExtractor.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaConsumerAttributesExtractor.java @@ -24,6 +24,8 @@ final class KafkaConsumerAttributesExtractor AttributeKey.stringKey("messaging.destination.partition.id"); private static final AttributeKey MESSAGING_KAFKA_CONSUMER_GROUP = AttributeKey.stringKey("messaging.kafka.consumer.group"); + private static final AttributeKey MESSAGING_CONSUMER_GROUP_NAME = + AttributeKey.stringKey("messaging.consumer.group.name"); private static final AttributeKey MESSAGING_KAFKA_MESSAGE_KEY = AttributeKey.stringKey("messaging.kafka.message.key"); private static final AttributeKey MESSAGING_KAFKA_MESSAGE_OFFSET = @@ -54,7 +56,12 @@ public void onStart( attributes.put(MESSAGING_KAFKA_MESSAGE_TOMBSTONE, true); } - attributes.put(MESSAGING_KAFKA_CONSUMER_GROUP, request.getConsumerGroup()); + if (emitOldMessagingSemconv()) { + attributes.put(MESSAGING_KAFKA_CONSUMER_GROUP, request.getConsumerGroup()); + } + if (emitStableMessagingSemconv()) { + attributes.put(MESSAGING_CONSUMER_GROUP_NAME, request.getConsumerGroup()); + } } private static boolean canSerialize(Class keyClass) { diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaConsumerContextUtil.java b/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaConsumerContextUtil.java index d11a423d2d4d..43b6525faabd 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaConsumerContextUtil.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaConsumerContextUtil.java @@ -5,7 +5,11 @@ package io.opentelemetry.instrumentation.kafkaclients.common.v0_11.internal; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; + +import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; import io.opentelemetry.instrumentation.api.util.VirtualField; import javax.annotation.Nullable; import org.apache.kafka.clients.consumer.Consumer; @@ -17,6 +21,10 @@ * any time. */ public final class KafkaConsumerContextUtil { + private static final ContextKey processSpanKey = + ContextKey.named("opentelemetry-kafka-process-span"); + private static final ContextKey processParentSpanKey = + ContextKey.named("opentelemetry-kafka-process-parent-span"); // these fields can be used for multiple instrumentations because of that we don't use a helper // class as field type private static final VirtualField, Context> recordContextField = @@ -28,6 +36,26 @@ public final class KafkaConsumerContextUtil { private static final VirtualField, String[]> recordsConsumerInfoField = VirtualField.find(ConsumerRecords.class, String[].class); + public static Context withoutLeakedProcessSpan(Context context) { + if (!emitStableMessagingSemconv()) { + return context; + } + + Span currentSpan = Span.fromContext(context); + if (currentSpan != context.get(processSpanKey)) { + return context; + } + + Span parentSpan = context.get(processParentSpanKey); + return context.with(parentSpan != null ? parentSpan : Span.getInvalid()); + } + + public static Context withProcessParentSpan(Context context, Context parentContext) { + return context + .with(processSpanKey, Span.fromContext(context)) + .with(processParentSpanKey, Span.fromContext(parentContext)); + } + public static KafkaConsumerContext get(ConsumerRecord records) { Context receiveContext = recordContextField.get(records); String consumerGroup = null; diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaInstrumenterFactory.java b/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaInstrumenterFactory.java index 3a0654f4b34c..154963a95568 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaInstrumenterFactory.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaInstrumenterFactory.java @@ -8,20 +8,23 @@ import static io.opentelemetry.instrumentation.api.incubator.semconv.messaging.internal.MessagingExceptionEventExtractors.setMessagingProcessExceptionEventExtractor; import static io.opentelemetry.instrumentation.api.incubator.semconv.messaging.internal.MessagingExceptionEventExtractors.setMessagingReceiveExceptionEventExtractor; import static io.opentelemetry.instrumentation.api.incubator.semconv.messaging.internal.MessagingExceptionEventExtractors.setMessagingSendExceptionEventExtractor; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; import static java.util.Collections.emptyList; import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessageOperation; +import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesExtractor; import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingOperationType; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingSpanKindExtractor; import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingSpanNameExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.internal.MessagingProcessInstrumenterFactory; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.ErrorCauseExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; -import io.opentelemetry.instrumentation.api.internal.PropagatorBasedSpanLinksExtractor; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -33,12 +36,16 @@ */ public final class KafkaInstrumenterFactory { + private static final String SEND_OPERATION_NAME = "send"; + private static final String POLL_OPERATION_NAME = "poll"; + private final OpenTelemetry openTelemetry; private final String instrumentationName; private ErrorCauseExtractor errorCauseExtractor = ErrorCauseExtractor.getDefault(); private List capturedHeaders = emptyList(); private boolean captureExperimentalSpanAttributes = false; private boolean messagingReceiveInstrumentationEnabled = false; + private boolean messagingReceiveInstrumentationConfigured = false; public KafkaInstrumenterFactory(OpenTelemetry openTelemetry, String instrumentationName) { this.openTelemetry = openTelemetry; @@ -68,6 +75,7 @@ public KafkaInstrumenterFactory setCaptureExperimentalSpanAttributes( public KafkaInstrumenterFactory setMessagingReceiveTelemetryEnabled( boolean messagingReceiveInstrumentationEnabled) { this.messagingReceiveInstrumentationEnabled = messagingReceiveInstrumentationEnabled; + this.messagingReceiveInstrumentationConfigured = true; return this; } @@ -79,15 +87,18 @@ public Instrumenter createProducerInstrume Iterable> extractors) { KafkaProducerAttributesGetter getter = new KafkaProducerAttributesGetter(); - MessageOperation operation = MessageOperation.PUBLISH; + MessagingOperationType operationType = MessagingOperationType.SEND; InstrumenterBuilder builder = Instrumenter.builder( openTelemetry, instrumentationName, - MessagingSpanNameExtractor.create(getter, operation)) + MessagingSpanNameExtractor.builder(getter, operationType) + .setOperationName(SEND_OPERATION_NAME) + .build()) .addAttributesExtractor( - buildMessagingAttributesExtractor(getter, operation, capturedHeaders)) + buildMessagingAttributesExtractor( + getter, operationType, SEND_OPERATION_NAME, capturedHeaders)) .addAttributesExtractors(extractors) .addAttributesExtractor(new KafkaProducerAttributesExtractor()) .setErrorCauseExtractor(errorCauseExtractor); @@ -95,7 +106,11 @@ public Instrumenter createProducerInstrume builder.addAttributesExtractor(new KafkaProducerExperimentalAttributesExtractor()); } setMessagingSendExceptionEventExtractor(builder); - return builder.buildInstrumenter(SpanKindExtractor.alwaysProducer()); + return builder.buildInstrumenter( + request -> + emitStableMessagingSemconv() && !request.isSpanContextPropagated() + ? SpanKind.CLIENT + : SpanKind.PRODUCER); } public Instrumenter createConsumerReceiveInstrumenter() { @@ -105,21 +120,30 @@ public Instrumenter createConsumerReceiveInstrumenter public Instrumenter createConsumerReceiveInstrumenter( Iterable> extractors) { KafkaReceiveAttributesGetter getter = new KafkaReceiveAttributesGetter(); - MessageOperation operation = MessageOperation.RECEIVE; + MessagingOperationType operationType = MessagingOperationType.RECEIVE; + boolean receiveInstrumentationEnabled = receiveInstrumentationEnabled(); InstrumenterBuilder builder = Instrumenter.builder( openTelemetry, instrumentationName, - MessagingSpanNameExtractor.create(getter, operation)) + MessagingSpanNameExtractor.builder(getter, operationType) + .setOperationName(POLL_OPERATION_NAME) + .build()) .addAttributesExtractor( - buildMessagingAttributesExtractor(getter, operation, capturedHeaders)) + buildMessagingAttributesExtractor( + getter, operationType, POLL_OPERATION_NAME, capturedHeaders)) .addAttributesExtractor(new KafkaReceiveAttributesExtractor()) .addAttributesExtractors(extractors) .setErrorCauseExtractor(errorCauseExtractor) - .setEnabled(messagingReceiveInstrumentationEnabled); + .setEnabled(receiveInstrumentationEnabled); + if (emitStableMessagingSemconv()) { + builder.addSpanLinksExtractor( + new KafkaBatchProcessSpanLinksExtractor( + openTelemetry.getPropagators().getTextMapPropagator())); + } setMessagingReceiveExceptionEventExtractor(builder); - return builder.buildInstrumenter(SpanKindExtractor.alwaysConsumer()); + return builder.buildInstrumenter(MessagingSpanKindExtractor.create(operationType)); } public Instrumenter createConsumerProcessInstrumenter() { @@ -129,15 +153,15 @@ public Instrumenter createConsumerProcessInstrumenter public Instrumenter createConsumerProcessInstrumenter( Iterable> extractors) { KafkaConsumerAttributesGetter getter = new KafkaConsumerAttributesGetter(); - MessageOperation operation = MessageOperation.PROCESS; + MessagingOperationType operationType = MessagingOperationType.PROCESS; InstrumenterBuilder builder = Instrumenter.builder( openTelemetry, instrumentationName, - MessagingSpanNameExtractor.create(getter, operation)) + MessagingSpanNameExtractor.create(getter, operationType)) .addAttributesExtractor( - buildMessagingAttributesExtractor(getter, operation, capturedHeaders)) + buildMessagingAttributesExtractor(getter, operationType, capturedHeaders)) .addAttributesExtractor(new KafkaConsumerAttributesExtractor()) .addAttributesExtractors(extractors) .setErrorCauseExtractor(errorCauseExtractor); @@ -146,28 +170,30 @@ public Instrumenter createConsumerProcessInstrumenter } setMessagingProcessExceptionEventExtractor(builder); - if (messagingReceiveInstrumentationEnabled) { - builder.addSpanLinksExtractor( - new PropagatorBasedSpanLinksExtractor<>( - openTelemetry.getPropagators().getTextMapPropagator(), - new KafkaConsumerRecordGetter())); - return builder.buildInstrumenter(SpanKindExtractor.alwaysConsumer()); - } else { - return builder.buildConsumerInstrumenter(new KafkaConsumerRecordGetter()); - } + return MessagingProcessInstrumenterFactory.create( + builder, + openTelemetry.getPropagators().getTextMapPropagator(), + new KafkaConsumerRecordGetter(), + receiveInstrumentationEnabled()); + } + + private boolean receiveInstrumentationEnabled() { + return messagingReceiveInstrumentationConfigured + ? messagingReceiveInstrumentationEnabled + : emitStableMessagingSemconv(); } public Instrumenter createBatchProcessInstrumenter() { KafkaReceiveAttributesGetter getter = new KafkaReceiveAttributesGetter(); - MessageOperation operation = MessageOperation.PROCESS; + MessagingOperationType operationType = MessagingOperationType.PROCESS; InstrumenterBuilder builder = Instrumenter.builder( openTelemetry, instrumentationName, - MessagingSpanNameExtractor.create(getter, operation)) + MessagingSpanNameExtractor.create(getter, operationType)) .addAttributesExtractor( - buildMessagingAttributesExtractor(getter, operation, capturedHeaders)) + buildMessagingAttributesExtractor(getter, operationType, capturedHeaders)) .addAttributesExtractor(new KafkaReceiveAttributesExtractor()) .addSpanLinksExtractor( new KafkaBatchProcessSpanLinksExtractor( @@ -180,9 +206,21 @@ public Instrumenter createBatchProcessInstrumenter() private static AttributesExtractor buildMessagingAttributesExtractor( MessagingAttributesGetter getter, - MessageOperation operation, + MessagingOperationType operationType, + String operationName, + List capturedHeaders) { + return MessagingAttributesExtractor.builderForOperationType(getter, operationType) + .setOperationName(operationName) + .setCapturedHeaders(capturedHeaders) + .build(); + } + + private static + AttributesExtractor buildMessagingAttributesExtractor( + MessagingAttributesGetter getter, + MessagingOperationType operationType, List capturedHeaders) { - return MessagingAttributesExtractor.builder(getter, operation) + return MessagingAttributesExtractor.builderForOperationType(getter, operationType) .setCapturedHeaders(capturedHeaders) .build(); } diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaProducerRequest.java b/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaProducerRequest.java index 67ffd1b623d1..25995243c9bb 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaProducerRequest.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaProducerRequest.java @@ -22,22 +22,43 @@ public final class KafkaProducerRequest { private final ProducerRecord record; @Nullable private final String clientId; @Nullable private final String bootstrapServers; + private final boolean spanContextPropagated; public static KafkaProducerRequest create( ProducerRecord record, Producer producer, @Nullable String bootstrapServers) { - return create(record, extractClientId(producer), bootstrapServers); + return create(record, extractClientId(producer), bootstrapServers, true); + } + + public static KafkaProducerRequest create( + ProducerRecord record, + Producer producer, + @Nullable String bootstrapServers, + boolean spanContextPropagated) { + return create(record, extractClientId(producer), bootstrapServers, spanContextPropagated); } public static KafkaProducerRequest create( ProducerRecord record, @Nullable String clientId, @Nullable String bootstrapServers) { - return new KafkaProducerRequest(record, clientId, bootstrapServers); + return create(record, clientId, bootstrapServers, true); + } + + public static KafkaProducerRequest create( + ProducerRecord record, + @Nullable String clientId, + @Nullable String bootstrapServers, + boolean spanContextPropagated) { + return new KafkaProducerRequest(record, clientId, bootstrapServers, spanContextPropagated); } private KafkaProducerRequest( - ProducerRecord record, @Nullable String clientId, @Nullable String bootstrapServers) { + ProducerRecord record, + @Nullable String clientId, + @Nullable String bootstrapServers, + boolean spanContextPropagated) { this.record = record; this.clientId = clientId; this.bootstrapServers = bootstrapServers; + this.spanContextPropagated = spanContextPropagated; } public ProducerRecord getRecord() { @@ -54,6 +75,10 @@ public String getBootstrapServers() { return bootstrapServers; } + public boolean isSpanContextPropagated() { + return spanContextPropagated; + } + @Nullable private static String extractClientId(Producer producer) { try { diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaPropagation.java b/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaPropagation.java index 8b4e8103f452..b48a2ebf3666 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaPropagation.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaPropagation.java @@ -6,9 +6,17 @@ package io.opentelemetry.instrumentation.kafkaclients.common.v0_11.internal; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; import io.opentelemetry.context.propagation.TextMapPropagator; import io.opentelemetry.javaagent.tooling.muzzle.NoMuzzle; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nullable; import org.apache.kafka.clients.ApiVersions; import org.apache.kafka.clients.producer.ProducerRecord; import org.apache.kafka.common.record.RecordBatch; @@ -34,6 +42,36 @@ public static boolean shouldPropagate(ApiVersions apiVersions) { || maxUsableProduceMagic(apiVersions) >= RecordBatch.MAGIC_VALUE_V2; } + public static boolean propagatesSpanContext(TextMapPropagator propagator) { + SpanContext expected = + SpanContext.create( + "00000000000000000000000000000001", + "0000000000000001", + TraceFlags.getSampled(), + TraceState.getDefault()); + Map carrier = new HashMap<>(); + propagator.inject(Context.root().with(Span.wrap(expected)), carrier, Map::put); + Context extracted = + propagator.extract( + Context.root(), + carrier, + new TextMapGetter>() { + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Nullable + @Override + public String get(@Nullable Map carrier, String key) { + return carrier == null ? null : carrier.get(key); + } + }); + SpanContext actual = Span.fromContext(extracted).getSpanContext(); + return expected.getTraceId().equals(actual.getTraceId()) + && expected.getSpanId().equals(actual.getSpanId()); + } + @NoMuzzle private static byte maxUsableProduceMagic(ApiVersions apiVersions) { return apiVersions.maxUsableProduceMagic(); diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaReceiveAttributesExtractor.java b/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaReceiveAttributesExtractor.java index e4470ee4f641..adaf62ca27e3 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaReceiveAttributesExtractor.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaReceiveAttributesExtractor.java @@ -5,6 +5,9 @@ package io.opentelemetry.instrumentation.kafkaclients.common.v0_11.internal; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitOldMessagingSemconv; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; + import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; @@ -17,11 +20,18 @@ final class KafkaReceiveAttributesExtractor // copied from MessagingIncubatingAttributes private static final AttributeKey MESSAGING_KAFKA_CONSUMER_GROUP = AttributeKey.stringKey("messaging.kafka.consumer.group"); + private static final AttributeKey MESSAGING_CONSUMER_GROUP_NAME = + AttributeKey.stringKey("messaging.consumer.group.name"); @Override public void onStart( AttributesBuilder attributes, Context parentContext, KafkaReceiveRequest request) { - attributes.put(MESSAGING_KAFKA_CONSUMER_GROUP, request.getConsumerGroup()); + if (emitOldMessagingSemconv()) { + attributes.put(MESSAGING_KAFKA_CONSUMER_GROUP, request.getConsumerGroup()); + } + if (emitStableMessagingSemconv()) { + attributes.put(MESSAGING_CONSUMER_GROUP_NAME, request.getConsumerGroup()); + } } @Override diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/TracingIterator.java b/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/TracingIterator.java index 33af6502abd7..e856c2e1f9cb 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/TracingIterator.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/TracingIterator.java @@ -44,7 +44,9 @@ private TracingIterator( Context receiveContext = consumerContext.getContext(); // use the receive CONSUMER as parent if it's available - this.parentContext = receiveContext != null ? receiveContext : Context.current(); + this.parentContext = + KafkaConsumerContextUtil.withoutLeakedProcessSpan( + receiveContext != null ? receiveContext : Context.current()); this.consumerContext = consumerContext; } @@ -80,6 +82,8 @@ public ConsumerRecord next() { if (next != null && wrappingEnabled.getAsBoolean()) { currentRequest = KafkaProcessRequest.create(consumerContext, next); currentContext = instrumenter.start(parentContext, currentRequest); + currentContext = + KafkaConsumerContextUtil.withProcessParentSpan(currentContext, parentContext); currentScope = currentContext.makeCurrent(); } return next; diff --git a/instrumentation/kafka/kafka-connect-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaconnect/v2_6/KafkaConnectSingletons.java b/instrumentation/kafka/kafka-connect-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaconnect/v2_6/KafkaConnectSingletons.java index 21d1fa944a28..777504060949 100644 --- a/instrumentation/kafka/kafka-connect-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaconnect/v2_6/KafkaConnectSingletons.java +++ b/instrumentation/kafka/kafka-connect-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaconnect/v2_6/KafkaConnectSingletons.java @@ -9,12 +9,12 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.context.propagation.TextMapPropagator; -import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessageOperation; import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingOperationType; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingSpanKindExtractor; import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingSpanNameExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; -import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; public class KafkaConnectSingletons { @@ -33,15 +33,17 @@ public class KafkaConnectSingletons { GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, MessagingSpanNameExtractor.create( - new KafkaConnectAttributesGetter(), MessageOperation.PROCESS)) + new KafkaConnectAttributesGetter(), MessagingOperationType.PROCESS)) .addAttributesExtractor( - MessagingAttributesExtractor.builder( - new KafkaConnectAttributesGetter(), MessageOperation.PROCESS) + MessagingAttributesExtractor.builderForOperationType( + new KafkaConnectAttributesGetter(), MessagingOperationType.PROCESS) .build()) .addSpanLinksExtractor(spanLinksExtractor); setMessagingProcessExceptionEventExtractor(builder); - instrumenter = builder.buildInstrumenter(SpanKindExtractor.alwaysConsumer()); + instrumenter = + builder.buildInstrumenter( + MessagingSpanKindExtractor.create(MessagingOperationType.PROCESS)); } public static Instrumenter instrumenter() { diff --git a/instrumentation/kafka/kafka-connect-2.6/testing/build.gradle.kts b/instrumentation/kafka/kafka-connect-2.6/testing/build.gradle.kts index bf090179c83a..f895686454ad 100644 --- a/instrumentation/kafka/kafka-connect-2.6/testing/build.gradle.kts +++ b/instrumentation/kafka/kafka-connect-2.6/testing/build.gradle.kts @@ -43,7 +43,15 @@ tasks { systemProperty("metadataConfig", "otel.semconv-stability.opt-in=database") } + val testV3Preview = register("testV3Preview") { + testClassesDirs = sourceSets.test.get().output.classesDirs + classpath = sourceSets.test.get().runtimeClasspath + jvmArgs("-Dotel.instrumentation.common.v3-preview=true") + jvmArgs("-Dotel.semconv-stability.preview=messaging") + systemProperty("metadataConfig", "otel.instrumentation.common.v3-preview=true") + } + check { - dependsOn(testStableSemconv) + dependsOn(testStableSemconv, testV3Preview) } } diff --git a/instrumentation/kafka/kafka-connect-2.6/testing/src/test/java/io/opentelemetry/instrumentation/kafkaconnect/v2_6/KafkaConnectSinkTaskBaseTest.java b/instrumentation/kafka/kafka-connect-2.6/testing/src/test/java/io/opentelemetry/instrumentation/kafkaconnect/v2_6/KafkaConnectSinkTaskBaseTest.java index d4eab1656cee..692f97df4412 100644 --- a/instrumentation/kafka/kafka-connect-2.6/testing/src/test/java/io/opentelemetry/instrumentation/kafkaconnect/v2_6/KafkaConnectSinkTaskBaseTest.java +++ b/instrumentation/kafka/kafka-connect-2.6/testing/src/test/java/io/opentelemetry/instrumentation/kafkaconnect/v2_6/KafkaConnectSinkTaskBaseTest.java @@ -5,13 +5,30 @@ package io.opentelemetry.instrumentation.kafkaconnect.v2_6; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitOldMessagingSemconv; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; +import static io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil.groupTraces; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_BATCH_MESSAGE_COUNT; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION_NAME; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION_TYPE; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_SYSTEM; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MessagingOperationTypeIncubatingValues.PROCESS; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MessagingSystemIncubatingValues.KAFKA; +import static io.opentelemetry.semconv.incubating.ThreadIncubatingAttributes.THREAD_ID; +import static io.opentelemetry.semconv.incubating.ThreadIncubatingAttributes.THREAD_NAME; import static io.restassured.RestAssured.given; import static java.lang.String.format; import static java.time.temporal.ChronoUnit.MINUTES; +import static java.util.Arrays.asList; import static org.awaitility.Awaitility.await; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; import io.opentelemetry.context.propagation.ContextPropagators; import io.opentelemetry.context.propagation.TextMapPropagator; @@ -21,7 +38,11 @@ import io.opentelemetry.instrumentation.test.utils.PortUtils; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import io.opentelemetry.sdk.testing.assertj.TraceAssert; +import io.opentelemetry.sdk.testing.assertj.TracesAssert; import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; import io.opentelemetry.smoketest.SmokeTestInstrumentationExtension; import io.opentelemetry.smoketest.TelemetryRetriever; @@ -29,8 +50,10 @@ import io.restassured.http.ContentType; import java.nio.file.Path; import java.time.Duration; +import java.util.List; import java.util.Locale; import java.util.Properties; +import java.util.function.Consumer; import java.util.stream.Stream; import org.apache.http.HttpStatus; import org.apache.kafka.clients.admin.AdminClient; @@ -130,6 +153,37 @@ protected String getKafkaBootstrapServers() { return kafka.getHost() + ":" + kafkaExposedPort; } + protected static String messagingSpanName( + String destinationName, String oldOperationName, String stableOperationName) { + if (emitStableMessagingSemconv()) { + return destinationName == null + ? stableOperationName + : stableOperationName + " " + destinationName; + } + return (destinationName == null ? "unknown" : destinationName) + " " + oldOperationName; + } + + @SafeVarargs + @SuppressWarnings("varargs") + protected final void waitAndAssertTracesIgnoringStableReceive( + Consumer... assertions) { + await() + .atMost(Duration.ofSeconds(60)) + .untilAsserted( + () -> { + List> traces = groupTraces(testing.spans()); + if (emitStableMessagingSemconv()) { + traces.removeIf( + trace -> + trace.size() == 1 + && trace.get(0).getKind() == SpanKind.CLIENT + && (trace.get(0).getName().equals("poll") + || trace.get(0).getName().startsWith("poll "))); + } + TracesAssert.assertThat(traces).hasTracesSatisfyingExactly(asList(assertions)); + }); + } + @Override public TelemetryRetriever getTelemetryRetriever() { return telemetryRetriever; @@ -263,9 +317,7 @@ private void setupKafkaConnect() { .withCopyFileToContainer( MountableFile.forHostPath(agentPath), "/opentelemetry-javaagent.jar") // Configure the agent to export spans to backend (like smoke tests) - .withEnv( - "JAVA_TOOL_OPTIONS", - "-javaagent:/opentelemetry-javaagent.jar " + "-Dotel.javaagent.debug=true") + .withEnv("JAVA_TOOL_OPTIONS", javaToolOptions()) // Disable test exporter and force OTLP exporter .withEnv("OTEL_TESTING_EXPORTER_ENABLED", "false") .withEnv("OTEL_TRACES_EXPORTER", "otlp") @@ -278,7 +330,9 @@ private void setupKafkaConnect() { .withEnv("OTEL_METRIC_EXPORT_INTERVAL", "1000") .withEnv( "OTEL_SEMCONV_STABILITY_OPT_IN", - System.getProperty("otel.semconv-stability.opt-in")) + emitStableMessagingSemconv() + ? "messaging" + : System.getProperty("otel.semconv-stability.opt-in")) .withEnv("CONNECT_BOOTSTRAP_SERVERS", getInternalKafkaBootstrapServers()) .withEnv("CONNECT_REST_ADVERTISED_HOST_NAME", KAFKA_CONNECT_NETWORK_ALIAS) .withEnv("CONNECT_PLUGIN_PATH", PLUGIN_PATH_CONTAINER) @@ -309,6 +363,62 @@ private void setupKafkaConnect() { + "/etc/confluent/docker/run 2>&1 | tee /var/log/kafka-connect/kafka-connect.log"); } + private static String javaToolOptions() { + StringBuilder options = + new StringBuilder("-javaagent:/opentelemetry-javaagent.jar -Dotel.javaagent.debug=true"); + appendSystemProperty(options, "otel.instrumentation.common.v3-preview"); + appendSystemProperty(options, "otel.semconv-stability.preview"); + return options.toString(); + } + + private static void appendSystemProperty(StringBuilder options, String propertyName) { + String value = System.getProperty(propertyName); + if (value != null) { + options.append(" -D").append(propertyName).append('=').append(value); + } + } + + @SuppressWarnings("deprecation") // using deprecated semconv + protected static AttributeAssertion[] processAttributes(String destination, long batchSize) { + return processAttributes(destination, batchSize, true); + } + + @SuppressWarnings("deprecation") // using deprecated semconv + protected static AttributeAssertion[] processAttributes(long batchSize) { + return processAttributes("", batchSize, false); + } + + private static AttributeAssertion[] processAttributes( + String destination, long batchSize, boolean hasDestination) { + boolean v3Preview = Boolean.getBoolean("otel.instrumentation.common.v3-preview"); + return new AttributeAssertion[] { + equalTo(MESSAGING_BATCH_MESSAGE_COUNT, batchSize), + equalTo(MESSAGING_DESTINATION_NAME, hasDestination ? destination : null), + equalTo(MESSAGING_OPERATION, emitOldMessagingSemconv() ? PROCESS : null), + equalTo(MESSAGING_OPERATION_NAME, emitStableMessagingSemconv() ? PROCESS : null), + equalTo(MESSAGING_OPERATION_TYPE, emitStableMessagingSemconv() ? PROCESS : null), + equalTo(MESSAGING_SYSTEM, KAFKA), + satisfies( + THREAD_ID, + val -> { + if (v3Preview) { + val.isNull(); + } else { + val.isNotZero(); + } + }), + satisfies( + THREAD_NAME, + val -> { + if (v3Preview) { + val.isNull(); + } else { + val.isNotBlank(); + } + }) + }; + } + @BeforeEach void resetBase() throws Exception { deleteConnectorIfExists(); diff --git a/instrumentation/kafka/kafka-connect-2.6/testing/src/test/java/io/opentelemetry/instrumentation/kafkaconnect/v2_6/MongoKafkaConnectSinkTaskTest.java b/instrumentation/kafka/kafka-connect-2.6/testing/src/test/java/io/opentelemetry/instrumentation/kafkaconnect/v2_6/MongoKafkaConnectSinkTaskTest.java index b24171389ce3..e6e23ddae94d 100644 --- a/instrumentation/kafka/kafka-connect-2.6/testing/src/test/java/io/opentelemetry/instrumentation/kafkaconnect/v2_6/MongoKafkaConnectSinkTaskTest.java +++ b/instrumentation/kafka/kafka-connect-2.6/testing/src/test/java/io/opentelemetry/instrumentation/kafkaconnect/v2_6/MongoKafkaConnectSinkTaskTest.java @@ -7,16 +7,7 @@ import static io.opentelemetry.api.trace.SpanKind.CONSUMER; import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableDatabaseSemconv; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; -import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_BATCH_MESSAGE_COUNT; -import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME; -import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION; -import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_SYSTEM; -import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MessagingOperationTypeIncubatingValues.PROCESS; -import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MessagingSystemIncubatingValues.KAFKA; -import static io.opentelemetry.semconv.incubating.ThreadIncubatingAttributes.THREAD_ID; -import static io.opentelemetry.semconv.incubating.ThreadIncubatingAttributes.THREAD_NAME; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; import static io.restassured.RestAssured.given; import static java.lang.String.format; import static org.awaitility.Awaitility.await; @@ -123,40 +114,48 @@ void testSingleMessage() throws IOException { await().atMost(Duration.ofSeconds(60)).until(() -> getRecordCountFromMongo() >= 1); AtomicReference producerSpanContext = new AtomicReference<>(); - testing.waitAndAssertTraces( + waitAndAssertTracesIgnoringStableReceive( trace -> // producer is in a separate trace, linked to consumer with a span link trace.hasSpansSatisfyingExactly( span -> { - span.hasName(testTopicName + " publish").hasKind(SpanKind.PRODUCER).hasNoParent(); + span.hasName( + emitStableMessagingSemconv() + ? "send " + testTopicName + : testTopicName + " publish") + .hasKind(SpanKind.PRODUCER) + .hasNoParent(); producerSpanContext.set(span.actual().getSpanContext()); }), trace -> // kafka connect sends message to status topic while processing our message trace.hasSpansSatisfyingExactly( span -> - span.hasName("kafka-connect-status publish") + span.hasName( + emitStableMessagingSemconv() + ? "send kafka-connect-status" + : "kafka-connect-status publish") .hasKind(SpanKind.PRODUCER) .hasNoParent(), span -> - span.hasName("kafka-connect-status process") + span.hasName( + emitStableMessagingSemconv() + ? "process kafka-connect-status" + : "kafka-connect-status process") .hasKind(SpanKind.CONSUMER) .hasParent(trace.getSpan(0))), trace -> // kafka connect consumer trace, linked to producer span via a span link trace.hasSpansSatisfyingExactly( span -> - span.hasName(testTopicName + " process") + span.hasName( + emitStableMessagingSemconv() + ? "process " + testTopicName + : testTopicName + " process") .hasKind(CONSUMER) .hasNoParent() .hasLinks(LinkData.create(producerSpanContext.get())) - .hasAttributesSatisfyingExactly( - equalTo(MESSAGING_BATCH_MESSAGE_COUNT, 1), - equalTo(MESSAGING_DESTINATION_NAME, testTopicName), - equalTo(MESSAGING_OPERATION, PROCESS), - equalTo(MESSAGING_SYSTEM, KAFKA), - satisfies(THREAD_ID, val -> val.isNotZero()), - satisfies(THREAD_NAME, val -> val.isNotBlank())), + .hasAttributesSatisfyingExactly(processAttributes(testTopicName, 1)), span -> span.hasName( emitStableDatabaseSemconv() @@ -217,36 +216,51 @@ void testMultiTopic() throws IOException { // kafka connect sends message to status topic while processing our message trace.hasSpansSatisfyingExactly( span -> - span.hasName("kafka-connect-status publish") + span.hasName( + emitStableMessagingSemconv() + ? "send kafka-connect-status" + : "kafka-connect-status publish") .hasKind(SpanKind.PRODUCER) .hasNoParent(), span -> - span.hasName("kafka-connect-status process") + span.hasName( + emitStableMessagingSemconv() + ? "process kafka-connect-status" + : "kafka-connect-status process") .hasKind(SpanKind.CONSUMER) .hasParent(trace.getSpan(0))); AtomicReference producerSpanContext1 = new AtomicReference<>(); AtomicReference producerSpanContext2 = new AtomicReference<>(); AtomicReference producerSpanContext3 = new AtomicReference<>(); - testing.waitAndAssertTraces( + waitAndAssertTracesIgnoringStableReceive( trace -> // producer is in a separate trace, linked to consumer with a span link trace.hasSpansSatisfyingExactly( span -> span.hasName("parent").hasNoParent(), span -> { - span.hasName(topicName1 + " publish") + span.hasName( + emitStableMessagingSemconv() + ? "send " + topicName1 + : topicName1 + " publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)); producerSpanContext1.set(span.actual().getSpanContext()); }, span -> { - span.hasName(topicName2 + " publish") + span.hasName( + emitStableMessagingSemconv() + ? "send " + topicName2 + : topicName2 + " publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)); producerSpanContext2.set(span.actual().getSpanContext()); }, span -> { - span.hasName(topicName3 + " publish") + span.hasName( + emitStableMessagingSemconv() + ? "send " + topicName3 + : topicName3 + " publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)); producerSpanContext3.set(span.actual().getSpanContext()); @@ -258,19 +272,14 @@ void testMultiTopic() throws IOException { // kafka connect consumer trace, linked to producer span via a span link trace.hasSpansSatisfyingExactly( span -> - span.hasName("unknown process") + span.hasName(emitStableMessagingSemconv() ? "process" : "unknown process") .hasKind(CONSUMER) .hasNoParent() .hasLinks( LinkData.create(producerSpanContext1.get()), LinkData.create(producerSpanContext2.get()), LinkData.create(producerSpanContext3.get())) - .hasAttributesSatisfyingExactly( - equalTo(MESSAGING_BATCH_MESSAGE_COUNT, 3), - equalTo(MESSAGING_OPERATION, PROCESS), - equalTo(MESSAGING_SYSTEM, KAFKA), - satisfies(THREAD_ID, val -> val.isNotZero()), - satisfies(THREAD_NAME, val -> val.isNotBlank())), + .hasAttributesSatisfyingExactly(processAttributes(3)), span -> span.hasName( emitStableDatabaseSemconv() diff --git a/instrumentation/kafka/kafka-connect-2.6/testing/src/test/java/io/opentelemetry/instrumentation/kafkaconnect/v2_6/PostgresKafkaConnectSinkTaskTest.java b/instrumentation/kafka/kafka-connect-2.6/testing/src/test/java/io/opentelemetry/instrumentation/kafkaconnect/v2_6/PostgresKafkaConnectSinkTaskTest.java index e79f0e8720ca..da626a828d84 100644 --- a/instrumentation/kafka/kafka-connect-2.6/testing/src/test/java/io/opentelemetry/instrumentation/kafkaconnect/v2_6/PostgresKafkaConnectSinkTaskTest.java +++ b/instrumentation/kafka/kafka-connect-2.6/testing/src/test/java/io/opentelemetry/instrumentation/kafkaconnect/v2_6/PostgresKafkaConnectSinkTaskTest.java @@ -7,16 +7,7 @@ import static io.opentelemetry.api.trace.SpanKind.CONSUMER; import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableDatabaseSemconv; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; -import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_BATCH_MESSAGE_COUNT; -import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME; -import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION; -import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_SYSTEM; -import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MessagingOperationTypeIncubatingValues.PROCESS; -import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MessagingSystemIncubatingValues.KAFKA; -import static io.opentelemetry.semconv.incubating.ThreadIncubatingAttributes.THREAD_ID; -import static io.opentelemetry.semconv.incubating.ThreadIncubatingAttributes.THREAD_NAME; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; import static io.restassured.RestAssured.given; import static java.lang.String.format; import static org.assertj.core.api.Assertions.assertThat; @@ -140,23 +131,34 @@ void testSingleMessage() throws IOException { await().atMost(Duration.ofSeconds(60)).until(() -> getRecordCountFromPostgres() >= 1); AtomicReference producerSpanContext = new AtomicReference<>(); - testing.waitAndAssertTraces( + waitAndAssertTracesIgnoringStableReceive( trace -> // producer is in a separate trace, linked to consumer with a span link trace.hasSpansSatisfyingExactly( span -> { - span.hasName(testTopicName + " publish").hasKind(SpanKind.PRODUCER).hasNoParent(); + span.hasName( + emitStableMessagingSemconv() + ? "send " + testTopicName + : testTopicName + " publish") + .hasKind(SpanKind.PRODUCER) + .hasNoParent(); producerSpanContext.set(span.actual().getSpanContext()); }), trace -> // kafka connect sends message to status topic while processing our message trace.hasSpansSatisfyingExactly( span -> - span.hasName("kafka-connect-status publish") + span.hasName( + emitStableMessagingSemconv() + ? "send kafka-connect-status" + : "kafka-connect-status publish") .hasKind(SpanKind.PRODUCER) .hasNoParent(), span -> - span.hasName("kafka-connect-status process") + span.hasName( + emitStableMessagingSemconv() + ? "process kafka-connect-status" + : "kafka-connect-status process") .hasKind(SpanKind.CONSUMER) .hasParent(trace.getSpan(0))), trace -> { @@ -173,17 +175,14 @@ void testSingleMessage() throws IOException { trace.hasSpansSatisfyingExactly( span -> - span.hasName(testTopicName + " process") + span.hasName( + emitStableMessagingSemconv() + ? "process " + testTopicName + : testTopicName + " process") .hasKind(CONSUMER) .hasNoParent() .hasLinks(LinkData.create(producerSpanContext.get())) - .hasAttributesSatisfyingExactly( - equalTo(MESSAGING_BATCH_MESSAGE_COUNT, 1), - equalTo(MESSAGING_DESTINATION_NAME, testTopicName), - equalTo(MESSAGING_OPERATION, PROCESS), - equalTo(MESSAGING_SYSTEM, KAFKA), - satisfies(THREAD_ID, val -> val.isNotZero()), - satisfies(THREAD_NAME, val -> val.isNotBlank())), + .hasAttributesSatisfyingExactly(processAttributes(testTopicName, 1)), selectAssertion, selectAssertion, selectAssertion, @@ -256,36 +255,51 @@ void testMultiTopic() throws IOException { // kafka connect sends message to status topic while processing our message trace.hasSpansSatisfyingExactly( span -> - span.hasName("kafka-connect-status publish") + span.hasName( + emitStableMessagingSemconv() + ? "send kafka-connect-status" + : "kafka-connect-status publish") .hasKind(SpanKind.PRODUCER) .hasNoParent(), span -> - span.hasName("kafka-connect-status process") + span.hasName( + emitStableMessagingSemconv() + ? "process kafka-connect-status" + : "kafka-connect-status process") .hasKind(SpanKind.CONSUMER) .hasParent(trace.getSpan(0))); AtomicReference producerSpanContext1 = new AtomicReference<>(); AtomicReference producerSpanContext2 = new AtomicReference<>(); AtomicReference producerSpanContext3 = new AtomicReference<>(); - testing.waitAndAssertTraces( + waitAndAssertTracesIgnoringStableReceive( trace -> // producer is in a separate trace, linked to consumer with a span link trace.hasSpansSatisfyingExactly( span -> span.hasName("parent").hasNoParent(), span -> { - span.hasName(topicName1 + " publish") + span.hasName( + emitStableMessagingSemconv() + ? "send " + topicName1 + : topicName1 + " publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)); producerSpanContext1.set(span.actual().getSpanContext()); }, span -> { - span.hasName(topicName2 + " publish") + span.hasName( + emitStableMessagingSemconv() + ? "send " + topicName2 + : topicName2 + " publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)); producerSpanContext2.set(span.actual().getSpanContext()); }, span -> { - span.hasName(topicName3 + " publish") + span.hasName( + emitStableMessagingSemconv() + ? "send " + topicName3 + : topicName3 + " publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)); producerSpanContext3.set(span.actual().getSpanContext()); @@ -307,19 +321,14 @@ void testMultiTopic() throws IOException { trace.hasSpansSatisfyingExactly( span -> - span.hasName("unknown process") + span.hasName(emitStableMessagingSemconv() ? "process" : "unknown process") .hasKind(CONSUMER) .hasNoParent() .hasLinks( LinkData.create(producerSpanContext1.get()), LinkData.create(producerSpanContext2.get()), LinkData.create(producerSpanContext3.get())) - .hasAttributesSatisfyingExactly( - equalTo(MESSAGING_BATCH_MESSAGE_COUNT, 3), - equalTo(MESSAGING_OPERATION, PROCESS), - equalTo(MESSAGING_SYSTEM, KAFKA), - satisfies(THREAD_ID, val -> val.isNotZero()), - satisfies(THREAD_NAME, val -> val.isNotBlank())), + .hasAttributesSatisfyingExactly(processAttributes(3)), selectAssertion, selectAssertion, selectAssertion, diff --git a/instrumentation/opentelemetry-instrumentation-api/opentelemetry-instrumentation-api-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/v1_14/SpanKeyBridging.java b/instrumentation/opentelemetry-instrumentation-api/opentelemetry-instrumentation-api-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/v1_14/SpanKeyBridging.java index 24be05a6e8d0..e588faf6bd38 100644 --- a/instrumentation/opentelemetry-instrumentation-api/opentelemetry-instrumentation-api-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/v1_14/SpanKeyBridging.java +++ b/instrumentation/opentelemetry-instrumentation-api/opentelemetry-instrumentation-api-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/v1_14/SpanKeyBridging.java @@ -51,6 +51,7 @@ public class SpanKeyBridging { application.io.opentelemetry.instrumentation.api.internal.SpanKey.DB_CLIENT, SpanKey.DB_CLIENT); + putIfPresent(map, "PRODUCER_CREATE", SpanKey.PRODUCER_CREATE); map.put( application.io.opentelemetry.instrumentation.api.internal.SpanKey.PRODUCER, SpanKey.PRODUCER); @@ -60,9 +61,26 @@ public class SpanKeyBridging { map.put( application.io.opentelemetry.instrumentation.api.internal.SpanKey.CONSUMER_PROCESS, SpanKey.CONSUMER_PROCESS); + putIfPresent(map, "CONSUMER_SETTLE", SpanKey.CONSUMER_SETTLE); return map; } + private static void putIfPresent( + Map map, + String fieldName, + SpanKey agentSpanKey) { + try { + application.io.opentelemetry.instrumentation.api.internal.SpanKey applicationSpanKey = + (application.io.opentelemetry.instrumentation.api.internal.SpanKey) + application.io.opentelemetry.instrumentation.api.internal.SpanKey.class + .getField(fieldName) + .get(null); + map.put(applicationSpanKey, agentSpanKey); + } catch (ReflectiveOperationException ignored) { + // The application may use an instrumentation-api version from before this key was added. + } + } + @Nullable public static SpanKey toAgentOrNull( application.io.opentelemetry.instrumentation.api.internal.SpanKey applicationSpanKey) { diff --git a/instrumentation/opentelemetry-instrumentation-api/opentelemetry-instrumentation-api-1.14/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/v1_14/ContextBridgeTest.java b/instrumentation/opentelemetry-instrumentation-api/opentelemetry-instrumentation-api-1.14/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/v1_14/ContextBridgeTest.java index 60ecc0f4201b..7322e0d9569c 100644 --- a/instrumentation/opentelemetry-instrumentation-api/opentelemetry-instrumentation-api-1.14/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/v1_14/ContextBridgeTest.java +++ b/instrumentation/opentelemetry-instrumentation-api/opentelemetry-instrumentation-api-1.14/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/v1_14/ContextBridgeTest.java @@ -68,9 +68,11 @@ void testSpanKeyBridge() { SpanKey.HTTP_CLIENT, SpanKey.RPC_CLIENT, SpanKey.DB_CLIENT, + SpanKey.PRODUCER_CREATE, SpanKey.PRODUCER, SpanKey.CONSUMER_RECEIVE, - SpanKey.CONSUMER_PROCESS); + SpanKey.CONSUMER_PROCESS, + SpanKey.CONSUMER_SETTLE); spanKeys.forEach( spanKey -> assertThat(spanKey.fromContextOrNull(Context.current())).isNotNull()); diff --git a/instrumentation/opentelemetry-instrumentation-api/opentelemetry-instrumentation-api-1.14/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/testing/AgentSpanTestingInstrumenter.java b/instrumentation/opentelemetry-instrumentation-api/opentelemetry-instrumentation-api-1.14/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/testing/AgentSpanTestingInstrumenter.java index e13499836749..74a821731111 100644 --- a/instrumentation/opentelemetry-instrumentation-api/opentelemetry-instrumentation-api-1.14/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/testing/AgentSpanTestingInstrumenter.java +++ b/instrumentation/opentelemetry-instrumentation-api/opentelemetry-instrumentation-api-1.14/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/testing/AgentSpanTestingInstrumenter.java @@ -73,9 +73,11 @@ private static SpanKey[] getAllSpanKeys() { SpanKey.HTTP_CLIENT, SpanKey.RPC_CLIENT, SpanKey.DB_CLIENT, + SpanKey.PRODUCER_CREATE, SpanKey.PRODUCER, SpanKey.CONSUMER_RECEIVE, SpanKey.CONSUMER_PROCESS, + SpanKey.CONSUMER_SETTLE, }; } diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/message/SemconvMessagingStabilityUtil.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/message/SemconvMessagingStabilityUtil.java new file mode 100644 index 000000000000..e5444956add0 --- /dev/null +++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/message/SemconvMessagingStabilityUtil.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.testing.junit.message; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableMessagingSemconv; + +import io.opentelemetry.api.common.AttributeKey; +import java.util.HashMap; +import java.util.Map; + +// supports asserting on old messaging semconv, to be removed in 3.0 +public class SemconvMessagingStabilityUtil { + + private static final Map, AttributeKey> oldToNewMap = buildMap(); + + private static Map, AttributeKey> buildMap() { + Map, AttributeKey> map = new HashMap<>(); + map.put(stringKey("messaging.client_id"), stringKey("messaging.client.id")); + return map; + } + + @SuppressWarnings("unchecked") + public static AttributeKey effectiveKey(AttributeKey oldKey) { + // not testing messaging/dup + if (emitStableMessagingSemconv()) { + return (AttributeKey) oldToNewMap.getOrDefault(oldKey, oldKey); + } + return oldKey; + } + + private SemconvMessagingStabilityUtil() {} +}