aws-sdk-2.2: Support non-X-Ray propagation for SQS (#8405)

Co-authored-by: Mateusz Rzeszutek <mrzeszutek@splunk.com>
This commit is contained in:
Christian Neumüller 2023-05-16 02:28:15 +02:00 committed by GitHub
parent c0e220dcff
commit f98a97f671
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 692 additions and 54 deletions

View File

@ -1,5 +1,11 @@
# Settings for the AWS SDK instrumentation
For more information, see the respective public setters in the `AwsSdkTelemetryBuilder` classes:
* [SDK v1](./aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkTelemetryBuilder.java)
* [SDK v2](./aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkTelemetryBuilder.java)
| System property | Type | Default | Description |
|---|---|---|---|
|---|---|---|------------------------------------------------------------------------------------------------------------------------------------------------|
| `otel.instrumentation.aws-sdk.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. |
| `otel.instrumentation.aws-sdk.experimental-use-propagator-for-messaging` | Boolean | `false` | Enable propagation via message attributes using configured propagator (in addition to X-Ray). At the moment, Supports only SQS and the v2 SDK. |

View File

@ -32,9 +32,14 @@ public class TracingExecutionInterceptor implements ExecutionInterceptor {
ConfigPropertiesUtil.getBoolean(
"otel.instrumentation.aws-sdk.experimental-span-attributes", false);
private static final boolean USE_MESSAGING_PROPAGATOR =
ConfigPropertiesUtil.getBoolean(
"otel.instrumentation.aws-sdk.experimental-use-propagator-for-messaging", false);
private final ExecutionInterceptor delegate =
AwsSdkTelemetry.builder(GlobalOpenTelemetry.get())
.setCaptureExperimentalSpanAttributes(CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES)
.setUseConfiguredPropagatorForMessaging(USE_MESSAGING_PROPAGATOR)
.build()
.newExecutionInterceptor();

View File

@ -20,31 +20,43 @@ import software.amazon.awssdk.http.SdkHttpResponse;
final class AwsSdkInstrumenterFactory {
private static final String INSTRUMENTATION_NAME = "io.opentelemetry.aws-sdk-2.2";
static final AttributesExtractor<ExecutionAttributes, SdkHttpResponse> rpcAttributesExtractor =
RpcClientAttributesExtractor.create(AwsSdkRpcAttributesGetter.INSTANCE);
private static final AwsSdkExperimentalAttributesExtractor experimentalAttributesExtractor =
new AwsSdkExperimentalAttributesExtractor();
static final AwsSdkHttpAttributesGetter httpAttributesGetter = new AwsSdkHttpAttributesGetter();
private static final AwsSdkNetAttributesGetter netAttributesGetter =
new AwsSdkNetAttributesGetter();
static final AttributesExtractor<ExecutionAttributes, SdkHttpResponse> httpAttributesExtractor =
HttpClientAttributesExtractor.create(httpAttributesGetter, netAttributesGetter);
static final AttributesExtractor<ExecutionAttributes, SdkHttpResponse> rpcAttributesExtractor =
RpcClientAttributesExtractor.create(AwsSdkRpcAttributesGetter.INSTANCE);
private static final AwsSdkExperimentalAttributesExtractor experimentalAttributesExtractor =
new AwsSdkExperimentalAttributesExtractor();
private static final AwsSdkSpanKindExtractor spanKindExtractor = new AwsSdkSpanKindExtractor();
private static final List<AttributesExtractor<ExecutionAttributes, SdkHttpResponse>>
defaultAttributesExtractors = Arrays.asList(httpAttributesExtractor, rpcAttributesExtractor);
defaultAttributesExtractors = Arrays.asList(rpcAttributesExtractor);
private static final List<AttributesExtractor<ExecutionAttributes, SdkHttpResponse>>
extendedAttributesExtractors =
Arrays.asList(rpcAttributesExtractor, experimentalAttributesExtractor);
private static final List<AttributesExtractor<ExecutionAttributes, SdkHttpResponse>>
defaultConsumerAttributesExtractors =
Arrays.asList(rpcAttributesExtractor, httpAttributesExtractor);
private static final List<AttributesExtractor<ExecutionAttributes, SdkHttpResponse>>
extendedConsumerAttributesExtractors =
Arrays.asList(
httpAttributesExtractor, rpcAttributesExtractor, experimentalAttributesExtractor);
rpcAttributesExtractor, httpAttributesExtractor, experimentalAttributesExtractor);
static Instrumenter<ExecutionAttributes, SdkHttpResponse> requestInstrumenter(
OpenTelemetry openTelemetry, boolean captureExperimentalSpanAttributes) {
return createInstrumenter(
openTelemetry,
captureExperimentalSpanAttributes,
captureExperimentalSpanAttributes
? extendedAttributesExtractors
: defaultAttributesExtractors,
AwsSdkInstrumenterFactory.spanKindExtractor);
}
@ -52,20 +64,21 @@ final class AwsSdkInstrumenterFactory {
OpenTelemetry openTelemetry, boolean captureExperimentalSpanAttributes) {
return createInstrumenter(
openTelemetry, captureExperimentalSpanAttributes, SpanKindExtractor.alwaysConsumer());
openTelemetry,
captureExperimentalSpanAttributes
? extendedConsumerAttributesExtractors
: defaultConsumerAttributesExtractors,
SpanKindExtractor.alwaysConsumer());
}
private static Instrumenter<ExecutionAttributes, SdkHttpResponse> createInstrumenter(
OpenTelemetry openTelemetry,
boolean captureExperimentalSpanAttributes,
List<AttributesExtractor<ExecutionAttributes, SdkHttpResponse>> extractors,
SpanKindExtractor<ExecutionAttributes> spanKindExtractor) {
return Instrumenter.<ExecutionAttributes, SdkHttpResponse>builder(
openTelemetry, INSTRUMENTATION_NAME, AwsSdkInstrumenterFactory::spanName)
.addAttributesExtractors(
captureExperimentalSpanAttributes
? extendedAttributesExtractors
: defaultAttributesExtractors)
.addAttributesExtractors(extractors)
.buildInstrumenter(spanKindExtractor);
}

View File

@ -6,7 +6,9 @@
package io.opentelemetry.instrumentation.awssdk.v2_2;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.context.propagation.TextMapPropagator;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import javax.annotation.Nullable;
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
@ -42,8 +44,15 @@ public class AwsSdkTelemetry {
private final Instrumenter<ExecutionAttributes, SdkHttpResponse> requestInstrumenter;
private final Instrumenter<ExecutionAttributes, SdkHttpResponse> consumerInstrumenter;
private final boolean captureExperimentalSpanAttributes;
@Nullable private final TextMapPropagator messagingPropagator;
private final boolean useXrayPropagator;
AwsSdkTelemetry(OpenTelemetry openTelemetry, boolean captureExperimentalSpanAttributes) {
AwsSdkTelemetry(
OpenTelemetry openTelemetry,
boolean captureExperimentalSpanAttributes,
boolean useMessagingPropagator,
boolean useXrayPropagator) {
this.useXrayPropagator = useXrayPropagator;
this.requestInstrumenter =
AwsSdkInstrumenterFactory.requestInstrumenter(
openTelemetry, captureExperimentalSpanAttributes);
@ -51,6 +60,8 @@ public class AwsSdkTelemetry {
AwsSdkInstrumenterFactory.consumerInstrumenter(
openTelemetry, captureExperimentalSpanAttributes);
this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes;
this.messagingPropagator =
useMessagingPropagator ? openTelemetry.getPropagators().getTextMapPropagator() : null;
}
/**
@ -59,6 +70,10 @@ public class AwsSdkTelemetry {
*/
public ExecutionInterceptor newExecutionInterceptor() {
return new TracingExecutionInterceptor(
requestInstrumenter, consumerInstrumenter, captureExperimentalSpanAttributes);
requestInstrumenter,
consumerInstrumenter,
captureExperimentalSpanAttributes,
messagingPropagator,
useXrayPropagator);
}
}

View File

@ -15,6 +15,10 @@ public final class AwsSdkTelemetryBuilder {
private boolean captureExperimentalSpanAttributes;
private boolean useMessagingPropagator;
private boolean useXrayPropagator = true;
AwsSdkTelemetryBuilder(OpenTelemetry openTelemetry) {
this.openTelemetry = openTelemetry;
}
@ -31,10 +35,50 @@ public final class AwsSdkTelemetryBuilder {
return this;
}
/**
* Sets whether the {@link io.opentelemetry.context.propagation.TextMapPropagator} configured in
* the provided {@link OpenTelemetry} should be used to inject into supported messaging attributes
* (currently only SQS; SNS may follow).
*
* <p>In addition, the X-Ray propagator is always used.
*
* <p>Using the messaging propagator is needed if your tracing vendor requires special tracestate
* entries or legacy propagation information that cannot be transported via X-Ray headers. It may
* also be useful if you need to directly connect spans over messaging in your tracing backend,
* bypassing any intermediate spans/X-Ray segments that AWS may create in the delivery process.
*
* <p>This option is off by default. If enabled, on extraction the configured propagator will be
* preferred over X-Ray if it can extract anything.
*/
@CanIgnoreReturnValue
public AwsSdkTelemetryBuilder setUseConfiguredPropagatorForMessaging(
boolean useMessagingPropagator) {
this.useMessagingPropagator = useMessagingPropagator;
return this;
}
/**
* This setter implemented package-private for testing the messaging propagator, it does not seem
* too useful in general. The option is on by default.
*
* <p>If this needs to be exposed for non-testing use cases, consider if you need to refine this
* feature so that it disable this only for requests supported by {@link
* #setUseConfiguredPropagatorForMessaging(boolean)}
*/
@CanIgnoreReturnValue
AwsSdkTelemetryBuilder setUseXrayPropagator(boolean useMessagingPropagator) {
this.useXrayPropagator = useMessagingPropagator;
return this;
}
/**
* Returns a new {@link AwsSdkTelemetry} with the settings of this {@link AwsSdkTelemetryBuilder}.
*/
public AwsSdkTelemetry build() {
return new AwsSdkTelemetry(openTelemetry, captureExperimentalSpanAttributes);
return new AwsSdkTelemetry(
openTelemetry,
captureExperimentalSpanAttributes,
useMessagingPropagator,
useXrayPropagator);
}
}

View File

@ -12,6 +12,7 @@ import java.lang.invoke.MethodHandles;
import java.util.Collections;
import java.util.Map;
import javax.annotation.Nullable;
import software.amazon.awssdk.core.SdkPojo;
/**
* Reflective access to aws-sdk-java-sqs class Message.
@ -21,10 +22,18 @@ import javax.annotation.Nullable;
* prevent the entire instrumentation from loading when the plugin isn't available. We need to
* carefully check this class has all reflection errors result in no-op, and in the future we will
* hopefully come up with a better pattern.
*
* @see <a
* href="https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/sqs/model/Message.html">SDK
* Javadoc</a>
* @see <a
* href="https://github.com/aws/aws-sdk-java-v2/blob/2.2.0/services/sqs/src/main/resources/codegen-resources/service-2.json#L821-L856">Definition
* JSON</a>
*/
final class SqsMessageAccess {
@Nullable private static final MethodHandle GET_ATTRIBUTES;
@Nullable private static final MethodHandle GET_MESSAGE_ATTRIBUTES;
static {
Class<?> messageClass = null;
@ -43,8 +52,18 @@ final class SqsMessageAccess {
// Ignore
}
GET_ATTRIBUTES = getAttributes;
MethodHandle getMessageAttributes = null;
try {
getMessageAttributes =
lookup.findVirtual(messageClass, "messageAttributes", methodType(Map.class));
} catch (NoSuchMethodException | IllegalAccessException e) {
// Ignore
}
GET_MESSAGE_ATTRIBUTES = getMessageAttributes;
} else {
GET_ATTRIBUTES = null;
GET_MESSAGE_ATTRIBUTES = null;
}
}
@ -61,4 +80,16 @@ final class SqsMessageAccess {
}
private SqsMessageAccess() {}
@SuppressWarnings("unchecked")
public static Map<String, SdkPojo> getMessageAttributes(Object message) {
if (GET_MESSAGE_ATTRIBUTES == null) {
return Collections.emptyMap();
}
try {
return (Map<String, SdkPojo>) GET_MESSAGE_ATTRIBUTES.invoke(message);
} catch (Throwable t) {
return Collections.emptyMap();
}
}
}

View File

@ -0,0 +1,161 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.awssdk.v2_2;
import static java.lang.invoke.MethodType.methodType;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import javax.annotation.Nullable;
import software.amazon.awssdk.core.SdkPojo;
import software.amazon.awssdk.utils.builder.SdkBuilder;
/**
* Reflective access to aws-sdk-java-sqs class Message.
*
* <p>We currently don't have a good pattern of instrumenting a core library with various plugins
* that need plugin-specific instrumentation - if we accessed the class directly, Muzzle would
* prevent the entire instrumentation from loading when the plugin isn't available. We need to
* carefully check this class has all reflection errors result in no-op, and in the future we will
* hopefully come up with a better pattern.
*
* @see <a
* href="https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/sqs/model/MessageAttributeValue.html">SDK
* Javadoc</a>
* @see <a
* href="https://github.com/aws/aws-sdk-java-v2/blob/2.2.0/services/sqs/src/main/resources/codegen-resources/service-2.json#L866-L896">Definition
* JSON</a>
*/
final class SqsMessageAttributeValueAccess {
@Nullable private static final MethodHandle GET_STRING_VALUE;
@Nullable private static final MethodHandle STRING_VALUE;
@Nullable private static final MethodHandle DATA_TYPE;
@Nullable private static final MethodHandle BUILDER;
static {
Class<?> messageAttributeValueClass = null;
try {
messageAttributeValueClass =
Class.forName("software.amazon.awssdk.services.sqs.model.MessageAttributeValue");
} catch (Throwable t) {
// Ignore.
}
if (messageAttributeValueClass != null) {
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
MethodHandle getStringValue = null;
try {
getStringValue =
lookup.findVirtual(messageAttributeValueClass, "stringValue", methodType(String.class));
} catch (NoSuchMethodException | IllegalAccessException e) {
// Ignore
}
GET_STRING_VALUE = getStringValue;
} else {
GET_STRING_VALUE = null;
}
Class<?> builderClass = null;
if (messageAttributeValueClass != null) {
try {
builderClass =
Class.forName(
"software.amazon.awssdk.services.sqs.model.MessageAttributeValue$Builder");
} catch (Throwable t) {
// Ignore.
}
}
if (builderClass != null) {
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
MethodHandle stringValue = null;
try {
stringValue =
lookup.findVirtual(builderClass, "stringValue", methodType(builderClass, String.class));
} catch (NoSuchMethodException | IllegalAccessException e) {
// Ignore
}
STRING_VALUE = stringValue;
MethodHandle dataType = null;
try {
dataType =
lookup.findVirtual(builderClass, "dataType", methodType(builderClass, String.class));
} catch (NoSuchMethodException | IllegalAccessException e) {
// Ignore
}
DATA_TYPE = dataType;
MethodHandle builder = null;
try {
builder =
lookup.findStatic(messageAttributeValueClass, "builder", methodType(builderClass));
} catch (NoSuchMethodException | IllegalAccessException e) {
// Ignore
}
BUILDER = builder;
} else {
STRING_VALUE = null;
DATA_TYPE = null;
BUILDER = null;
}
}
@SuppressWarnings({"rawtypes"})
static String getStringValue(SdkPojo messageAttributeValue) {
if (GET_STRING_VALUE == null) {
return null;
}
try {
return (String) GET_STRING_VALUE.invoke(messageAttributeValue);
} catch (Throwable t) {
return null;
}
}
/**
* Note that this does not set the (required) dataType automatically, see {@link
* #dataType(SdkBuilder, String)} *
*/
@SuppressWarnings({"rawtypes", "unchecked"})
static SdkBuilder stringValue(SdkBuilder builder, String value) {
if (STRING_VALUE == null) {
return null;
}
try {
return (SdkBuilder) STRING_VALUE.invoke(builder, value);
} catch (Throwable t) {
return null;
}
}
@SuppressWarnings({"rawtypes", "unchecked"})
static SdkBuilder dataType(SdkBuilder builder, String dataType) {
if (DATA_TYPE == null) {
return null;
}
try {
return (SdkBuilder) DATA_TYPE.invoke(builder, dataType);
} catch (Throwable t) {
return null;
}
}
private SqsMessageAttributeValueAccess() {}
@SuppressWarnings({"rawtypes"})
public static SdkBuilder builder() {
if (BUILDER == null) {
return null;
}
try {
return (SdkBuilder) BUILDER.invoke();
} catch (Throwable e) {
return null;
}
}
}

View File

@ -7,13 +7,15 @@ package io.opentelemetry.instrumentation.awssdk.v2_2;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.TextMapGetter;
import io.opentelemetry.context.propagation.TextMapPropagator;
import io.opentelemetry.contrib.awsxray.propagator.AwsXrayPropagator;
import java.util.Collections;
import java.util.Map;
import software.amazon.awssdk.core.SdkPojo;
final class SqsParentContext {
enum MapGetter implements TextMapGetter<Map<String, String>> {
enum StringMapGetter implements TextMapGetter<Map<String, String>> {
INSTANCE;
@Override
@ -27,15 +29,42 @@ final class SqsParentContext {
}
}
enum MessageAttributeValueMapGetter implements TextMapGetter<Map<String, SdkPojo>> {
INSTANCE;
@Override
public Iterable<String> keys(Map<String, SdkPojo> map) {
return map.keySet();
}
@Override
public String get(Map<String, SdkPojo> map, String s) {
if (map == null) {
return null;
}
SdkPojo value = map.get(s);
if (value == null) {
return null;
}
return SqsMessageAttributeValueAccess.getStringValue(value);
}
}
static final String AWS_TRACE_SYSTEM_ATTRIBUTE = "AWSTraceHeader";
static Context ofMessageAttributes(
Map<String, SdkPojo> messageAttributes, TextMapPropagator propagator) {
return propagator.extract(
Context.root(), messageAttributes, MessageAttributeValueMapGetter.INSTANCE);
}
static Context ofSystemAttributes(Map<String, String> systemAttributes) {
String traceHeader = systemAttributes.get(AWS_TRACE_SYSTEM_ATTRIBUTE);
return AwsXrayPropagator.getInstance()
.extract(
Context.root(),
Collections.singletonMap("X-Amzn-Trace-Id", traceHeader),
MapGetter.INSTANCE);
StringMapGetter.INSTANCE);
}
private SqsParentContext() {}

View File

@ -10,7 +10,9 @@ import static java.lang.invoke.MethodType.methodType;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import javax.annotation.Nullable;
import software.amazon.awssdk.core.SdkRequest;
@ -22,10 +24,18 @@ import software.amazon.awssdk.core.SdkRequest;
* prevent the entire instrumentation from loading when the plugin isn't available. We need to
* carefully check this class has all reflection errors result in no-op, and in the future we will
* hopefully come up with a better pattern.
*
* @see <a
* href="https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/sqs/model/ReceiveMessageRequest.html">SDK
* Javadoc</a>
* @see <a
* href="https://github.com/aws/aws-sdk-java-v2/blob/2.2.0/services/sqs/src/main/resources/codegen-resources/service-2.json#L1076-L1110">Definition
* JSON</a>
*/
final class SqsReceiveMessageRequestAccess {
@Nullable private static final MethodHandle ATTRIBUTE_NAMES_WITH_STRINGS;
@Nullable private static final MethodHandle MESSAGE_ATTRIBUTE_NAMES;
static {
Class<?> receiveMessageRequestClass = null;
@ -48,8 +58,21 @@ final class SqsReceiveMessageRequestAccess {
// Ignore
}
ATTRIBUTE_NAMES_WITH_STRINGS = withAttributeNames;
MethodHandle messageAttributeNames = null;
try {
messageAttributeNames =
lookup.findVirtual(
receiveMessageRequestClass,
"messageAttributeNames",
methodType(receiveMessageRequestClass, Collection.class));
} catch (NoSuchMethodException | IllegalAccessException e) {
// Ignore
}
MESSAGE_ATTRIBUTE_NAMES = messageAttributeNames;
} else {
ATTRIBUTE_NAMES_WITH_STRINGS = null;
MESSAGE_ATTRIBUTE_NAMES = null;
}
}
@ -71,5 +94,29 @@ final class SqsReceiveMessageRequestAccess {
}
}
static void messageAttributeNames(
SdkRequest.Builder builder, List<String> messageAttributeNames) {
if (MESSAGE_ATTRIBUTE_NAMES == null) {
return;
}
try {
MESSAGE_ATTRIBUTE_NAMES.invoke(builder, messageAttributeNames);
} catch (Throwable throwable) {
// Ignore
}
}
private SqsReceiveMessageRequestAccess() {}
@SuppressWarnings({"rawtypes", "unchecked"})
static List<String> getAttributeNames(SdkRequest request) {
Optional<List> optional = request.getValueForField("AttributeNames", List.class);
return optional.isPresent() ? (List<String>) optional.get() : Collections.emptyList();
}
@SuppressWarnings({"rawtypes", "unchecked"})
static List<String> getMessageAttributeNames(SdkRequest request) {
Optional<List> optional = request.getValueForField("MessageAttributeNames", List.class);
return optional.isPresent() ? (List<String>) optional.get() : Collections.emptyList();
}
}

View File

@ -0,0 +1,91 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.awssdk.v2_2;
import static java.lang.invoke.MethodType.methodType;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Nullable;
import software.amazon.awssdk.core.SdkPojo;
import software.amazon.awssdk.core.SdkRequest;
/**
* Reflective access to aws-sdk-java-sqs class ReceiveMessageRequest.
*
* <p>We currently don't have a good pattern of instrumenting a core library with various plugins
* that need plugin-specific instrumentation - if we accessed the class directly, Muzzle would
* prevent the entire instrumentation from loading when the plugin isn't available. We need to
* carefully check this class has all reflection errors result in no-op, and in the future we will
* hopefully come up with a better pattern.
*
* @see <a
* href="https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/sqs/model/SendMessageRequest.html">SDK
* Javadoc</a>
* @see <a
* href="https://github.com/aws/aws-sdk-java-v2/blob/2.2.0/services/sqs/src/main/resources/codegen-resources/service-2.json#L1257-L1291">Definition
* JSON</a>
*/
final class SqsSendMessageRequestAccess {
@Nullable private static final MethodHandle MESSAGE_ATTRIBUTES;
static {
Class<?> sendMessageRequestClass = null;
try {
sendMessageRequestClass =
Class.forName("software.amazon.awssdk.services.sqs.model.SendMessageRequest$Builder");
} catch (Throwable t) {
// Ignore.
}
if (sendMessageRequestClass != null) {
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
MethodHandle messageAttributes = null;
try {
messageAttributes =
lookup.findVirtual(
sendMessageRequestClass,
"messageAttributes",
methodType(sendMessageRequestClass, Map.class));
} catch (NoSuchMethodException | IllegalAccessException e) {
// Ignore
}
MESSAGE_ATTRIBUTES = messageAttributes;
} else {
MESSAGE_ATTRIBUTES = null;
}
}
static boolean isInstance(SdkRequest request) {
return request
.getClass()
.getName()
.equals("software.amazon.awssdk.services.sqs.model.SendMessageRequest");
}
static void messageAttributes(
SdkRequest.Builder builder, Map<String, SdkPojo> messageAttributes) {
if (MESSAGE_ATTRIBUTES == null) {
return;
}
try {
MESSAGE_ATTRIBUTES.invoke(builder, messageAttributes);
} catch (Throwable throwable) {
// Ignore
}
}
@SuppressWarnings({"unchecked", "rawtypes"})
static Map<String, SdkPojo> messageAttributes(SdkRequest request) {
Optional<Map> optional = request.getValueForField("AttributeNames", Map.class);
return optional.isPresent() ? (Map<String, SdkPojo>) optional.get() : Collections.emptyMap();
}
private SqsSendMessageRequestAccess() {}
}

View File

@ -7,17 +7,24 @@ package io.opentelemetry.instrumentation.awssdk.v2_2;
import static io.opentelemetry.instrumentation.awssdk.v2_2.AwsSdkRequestType.DYNAMODB;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Scope;
import io.opentelemetry.context.propagation.TextMapPropagator;
import io.opentelemetry.contrib.awsxray.propagator.AwsXrayPropagator;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Nullable;
import software.amazon.awssdk.awscore.AwsResponse;
import software.amazon.awssdk.core.ClientType;
import software.amazon.awssdk.core.SdkPojo;
import software.amazon.awssdk.core.SdkRequest;
import software.amazon.awssdk.core.SdkResponse;
import software.amazon.awssdk.core.interceptor.Context;
@ -27,6 +34,7 @@ import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute;
import software.amazon.awssdk.http.SdkHttpRequest;
import software.amazon.awssdk.http.SdkHttpResponse;
import software.amazon.awssdk.utils.builder.SdkBuilder;
/** AWS request execution interceptor. */
final class TracingExecutionInterceptor implements ExecutionInterceptor {
@ -47,29 +55,38 @@ final class TracingExecutionInterceptor implements ExecutionInterceptor {
private final Instrumenter<ExecutionAttributes, SdkHttpResponse> requestInstrumenter;
private final Instrumenter<ExecutionAttributes, SdkHttpResponse> consumerInstrumenter;
private final boolean captureExperimentalSpanAttributes;
@Nullable private final TextMapPropagator messagingPropagator;
private final boolean useXrayPropagator;
private final FieldMapper fieldMapper;
TracingExecutionInterceptor(
Instrumenter<ExecutionAttributes, SdkHttpResponse> requestInstrumenter,
Instrumenter<ExecutionAttributes, SdkHttpResponse> consumerInstrumenter,
boolean captureExperimentalSpanAttributes) {
boolean captureExperimentalSpanAttributes,
TextMapPropagator messagingPropagator,
boolean useXrayPropagator) {
this.requestInstrumenter = requestInstrumenter;
this.consumerInstrumenter = consumerInstrumenter;
this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes;
this.messagingPropagator = messagingPropagator;
this.useXrayPropagator = useXrayPropagator;
this.fieldMapper = new FieldMapper();
}
@Override
public void afterMarshalling(
Context.AfterMarshalling context, ExecutionAttributes executionAttributes) {
public SdkRequest modifyRequest(
Context.ModifyRequest context, ExecutionAttributes executionAttributes) {
// This is the latest point where we can start the span, since we might need to inject
// it into the request payload. This means that HTTP attributes need to be captured later.
io.opentelemetry.context.Context parentOtelContext = io.opentelemetry.context.Context.current();
executionAttributes.putAttribute(SDK_REQUEST_ATTRIBUTE, context.request());
SdkHttpRequest httpRequest = context.httpRequest();
executionAttributes.putAttribute(SDK_HTTP_REQUEST_ATTRIBUTE, httpRequest);
SdkRequest request = context.request();
executionAttributes.putAttribute(SDK_REQUEST_ATTRIBUTE, request);
if (!requestInstrumenter.shouldStart(parentOtelContext, executionAttributes)) {
return;
// NB: We also skip injection in case we don't start.
return request;
}
io.opentelemetry.context.Context otelContext =
@ -96,38 +113,151 @@ final class TracingExecutionInterceptor implements ExecutionInterceptor {
clearAttributes(executionAttributes);
throw throwable;
}
}
@Override
public SdkRequest modifyRequest(
Context.ModifyRequest context, ExecutionAttributes executionAttributes) {
SdkRequest request = context.request();
if (SqsReceiveMessageRequestAccess.isInstance(request)) {
List<String> existingAttributeNames = getAttributeNames(request);
if (!existingAttributeNames.contains(SqsParentContext.AWS_TRACE_SYSTEM_ATTRIBUTE)) {
List<String> attributeNames = new ArrayList<>();
attributeNames.addAll(existingAttributeNames);
attributeNames.add(SqsParentContext.AWS_TRACE_SYSTEM_ATTRIBUTE);
SdkRequest.Builder builder = request.toBuilder();
SqsReceiveMessageRequestAccess.attributeNamesWithStrings(builder, attributeNames);
return builder.build();
return modifySqsReceiveMessageRequest(request);
} else if (messagingPropagator != null) {
if (SqsSendMessageRequestAccess.isInstance(request)) {
return injectIntoSqsSendMessageRequest(request, otelContext);
}
// TODO: Support SendMessageBatchRequest (and thus SendMessageBatchRequestEntry)
}
return request;
}
@SuppressWarnings({"rawtypes", "unchecked"})
private static List<String> getAttributeNames(SdkRequest request) {
Optional<List> optional = request.getValueForField("AttributeNames", List.class);
return optional.isPresent() ? (List<String>) optional.get() : Collections.emptyList();
private SdkRequest injectIntoSqsSendMessageRequest(
SdkRequest request, io.opentelemetry.context.Context otelContext) {
Map<String, SdkPojo> messageAttributes =
new HashMap<>(SqsSendMessageRequestAccess.messageAttributes(request));
messagingPropagator.inject(
otelContext,
messageAttributes,
(carrier, k, v) -> {
@SuppressWarnings("rawtypes")
SdkBuilder builder = SqsMessageAttributeValueAccess.builder();
if (builder == null) {
return;
}
builder = SqsMessageAttributeValueAccess.stringValue(builder, v);
if (builder == null) {
return;
}
builder = SqsMessageAttributeValueAccess.dataType(builder, "String");
if (builder == null) {
return;
}
carrier.put(k, (SdkPojo) builder.build());
});
if (messageAttributes.size() > 10) { // Too many attributes, we don't want to break the call.
return request;
}
SdkRequest.Builder builder = request.toBuilder();
SqsSendMessageRequestAccess.messageAttributes(builder, messageAttributes);
return builder.build();
}
private SdkRequest modifySqsReceiveMessageRequest(SdkRequest request) {
boolean hasXrayAttribute = true;
List<String> existingAttributeNames = null;
if (useXrayPropagator) {
existingAttributeNames = SqsReceiveMessageRequestAccess.getAttributeNames(request);
hasXrayAttribute =
existingAttributeNames.contains(SqsParentContext.AWS_TRACE_SYSTEM_ATTRIBUTE);
}
boolean hasMessageAttribute = true;
List<String> existingMessageAttributeNames = null;
if (messagingPropagator != null) {
existingMessageAttributeNames =
SqsReceiveMessageRequestAccess.getMessageAttributeNames(request);
hasMessageAttribute = existingMessageAttributeNames.containsAll(messagingPropagator.fields());
}
if (hasMessageAttribute && hasXrayAttribute) {
return request;
}
SdkRequest.Builder builder = request.toBuilder();
if (!hasXrayAttribute) {
List<String> attributeNames = new ArrayList<>(existingAttributeNames);
attributeNames.add(SqsParentContext.AWS_TRACE_SYSTEM_ATTRIBUTE);
SqsReceiveMessageRequestAccess.attributeNamesWithStrings(builder, attributeNames);
}
if (messagingPropagator != null) {
List<String> messageAttributeNames = new ArrayList<>(existingMessageAttributeNames);
for (String field : messagingPropagator.fields()) {
if (!existingMessageAttributeNames.contains(field)) {
messageAttributeNames.add(field);
}
}
SqsReceiveMessageRequestAccess.messageAttributeNames(builder, messageAttributeNames);
}
return builder.build();
}
@Override
public void afterMarshalling(
Context.AfterMarshalling context, ExecutionAttributes executionAttributes) {
// Since we merge the HTTP attributes into an already started span instead of creating a
// full child span, we have to do some dirty work here.
//
// As per HTTP conventions, we should actually only create spans for the "physical" requests but
// not for the encompassing logical request, see
// https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/trace/semantic_conventions/http.md#http-request-retries-and-redirects
// Specific AWS SDK conventions also don't mention this peculiar hybrid span convention, see
// https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/trace/semantic_conventions/instrumentation/aws-sdk.md
//
// TODO: Consider removing net+http conventions & relying on lower-level client instrumentation
io.opentelemetry.context.Context otelContext = getContext(executionAttributes);
if (otelContext == null) {
// No context, no sense in doing anything else (but this is not expected)
return;
}
SdkHttpRequest httpRequest = context.httpRequest();
executionAttributes.putAttribute(SDK_HTTP_REQUEST_ATTRIBUTE, httpRequest);
// We ought to pass the parent of otelContext here, but we didn't store it, and it shouldn't
// make a difference (unless we start supporting the http.resend_count attribute in this
// instrumentation, which, logically, we can't on this level of abstraction)
onHttpRequestAvailable(executionAttributes, otelContext, Span.fromContext(otelContext));
}
private static void onHttpResponseAvailable(
ExecutionAttributes executionAttributes,
io.opentelemetry.context.Context otelContext,
Span span,
SdkHttpResponse httpResponse) {
// For the httpAttributesExtractor dance, see afterMarshalling
AttributesBuilder builder = Attributes.builder(); // NB: UnsafeAttributes are package-private
AwsSdkInstrumenterFactory.httpAttributesExtractor.onEnd(
builder, otelContext, executionAttributes, httpResponse, null);
span.setAllAttributes(builder.build());
}
private static void onHttpRequestAvailable(
ExecutionAttributes executionAttributes,
io.opentelemetry.context.Context parentContext,
Span span) {
AttributesBuilder builder = Attributes.builder(); // NB: UnsafeAttributes are package-private
AwsSdkInstrumenterFactory.httpAttributesExtractor.onStart(
builder, parentContext, executionAttributes);
span.setAllAttributes(builder.build());
}
@Override
@SuppressWarnings("deprecation") // deprecated class to be updated once published in new location
public SdkHttpRequest modifyHttpRequest(
Context.ModifyHttpRequest context, ExecutionAttributes executionAttributes) {
SdkHttpRequest httpRequest = context.httpRequest();
if (!useXrayPropagator) {
return httpRequest;
}
io.opentelemetry.context.Context otelContext = getContext(executionAttributes);
if (otelContext == null) {
return httpRequest;
@ -170,7 +300,12 @@ final class TracingExecutionInterceptor implements ExecutionInterceptor {
Span span = Span.fromContext(otelContext);
onUserAgentHeaderAvailable(span, executionAttributes);
onSdkResponse(span, context.response(), executionAttributes);
requestInstrumenter.end(otelContext, executionAttributes, context.httpResponse(), null);
SdkHttpResponse httpResponse = context.httpResponse();
onHttpResponseAvailable(
executionAttributes, otelContext, Span.fromContext(otelContext), httpResponse);
requestInstrumenter.end(otelContext, executionAttributes, httpResponse, null);
}
clearAttributes(executionAttributes);
}
@ -184,19 +319,28 @@ final class TracingExecutionInterceptor implements ExecutionInterceptor {
}
}
@SuppressWarnings({"rawtypes", "unchecked"})
private static List<Object> getMessages(SdkResponse response) {
Optional<List> optional = response.getValueForField("Messages", List.class);
return optional.isPresent() ? optional.get() : Collections.emptyList();
}
private void createConsumerSpan(
Object message, ExecutionAttributes executionAttributes, SdkHttpResponse httpResponse) {
io.opentelemetry.context.Context parentContext =
SqsParentContext.ofSystemAttributes(SqsMessageAccess.getAttributes(message));
io.opentelemetry.context.Context parentContext = io.opentelemetry.context.Context.root();
if (messagingPropagator != null) {
parentContext =
SqsParentContext.ofMessageAttributes(
SqsMessageAccess.getMessageAttributes(message), messagingPropagator);
}
if (useXrayPropagator && parentContext == io.opentelemetry.context.Context.root()) {
parentContext = SqsParentContext.ofSystemAttributes(SqsMessageAccess.getAttributes(message));
}
if (consumerInstrumenter.shouldStart(parentContext, executionAttributes)) {
io.opentelemetry.context.Context context =
consumerInstrumenter.start(parentContext, executionAttributes);
// TODO: Even if we keep HTTP attributes (see afterMarshalling), does it make sense here
// per-message?
// TODO: Should we really create root spans if we can't extract anything, or should we attach
// to the current context?
consumerInstrumenter.end(context, executionAttributes, httpResponse, null);
}
}
@ -250,4 +394,10 @@ final class TracingExecutionInterceptor implements ExecutionInterceptor {
static io.opentelemetry.context.Context getContext(ExecutionAttributes attributes) {
return attributes.getAttribute(CONTEXT_ATTRIBUTE);
}
@SuppressWarnings({"rawtypes", "unchecked"})
static List<Object> getMessages(SdkResponse response) {
Optional<List> optional = response.getValueForField("Messages", List.class);
return optional.isPresent() ? optional.get() : Collections.emptyList();
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.awssdk.v2_2
import io.opentelemetry.instrumentation.test.LibraryTestTrait
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration
class Aws2SqsTracingTestWithW3CPropagator extends AbstractAws2SqsTracingTest implements LibraryTestTrait {
@Override
ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder() {
return ClientOverrideConfiguration.builder()
.addExecutionInterceptor(
AwsSdkTelemetry.builder(getOpenTelemetry())
.setCaptureExperimentalSpanAttributes(true)
.setUseConfiguredPropagatorForMessaging(true) // Difference to main test
.setUseXrayPropagator(false) // Disable to confirm messaging propagator actually works
.build()
.newExecutionInterceptor())
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.awssdk.v2_2
import io.opentelemetry.instrumentation.test.LibraryTestTrait
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration
/** We want to test the combination of W3C + Xray, as that's what you'll get in prod if you enable W3C. */
class Aws2SqsTracingTestWithW3CPropagatorAndXrayPropagator extends AbstractAws2SqsTracingTest implements LibraryTestTrait {
@Override
ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder() {
return ClientOverrideConfiguration.builder()
.addExecutionInterceptor(
AwsSdkTelemetry.builder(getOpenTelemetry())
.setCaptureExperimentalSpanAttributes(true)
.setUseConfiguredPropagatorForMessaging(true) // Difference to main test
.build()
.newExecutionInterceptor())
}
}