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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import software.amazon.awssdk.core.interceptor.Context.BeforeTransmission;
import software.amazon.awssdk.core.interceptor.Context.FailedExecution;
import software.amazon.awssdk.core.interceptor.Context.ModifyHttpRequest;
import software.amazon.awssdk.core.interceptor.Context.ModifyResponse;
import software.amazon.awssdk.core.interceptor.ExecutionAttribute;
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
Expand Down Expand Up @@ -111,6 +112,22 @@ public void beforeTransmission(
}
}

@Override
public SdkResponse modifyResponse(
final ModifyResponse context, final ExecutionAttributes executionAttributes) {
final SdkResponse response = context.response();
if (!AWS_LEGACY_TRACING && isPollingRequest(context.request()) && isPollingResponse(response)) {
// Attach queueUrl before AWS SDK core rebuilds the response with
// toBuilder().sdkHttpResponse(...).build(). afterExecution sees this pre-rebuild response,
// not the final response returned to user code, so capturing queueUrl there is too late.
context
.request()
.getValueForField("QueueUrl", String.class)
.ifPresent(queueUrl -> responseQueueStore.put(response, queueUrl));
}
return response;
}

@Override
public void afterExecution(
final AfterExecution context, final ExecutionAttributes executionAttributes) {
Expand All @@ -124,13 +141,6 @@ public void afterExecution(
DECORATE.beforeFinish(span);
span.finish();
}
if (!AWS_LEGACY_TRACING && isPollingResponse(context.response())) {
// store queueUrl inside response for SqsReceiveResultInstrumentation
context
.request()
.getValueForField("QueueUrl", String.class)
.ifPresent(queueUrl -> responseQueueStore.put(context.response(), queueUrl));
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package datadog.trace.instrumentation.aws.v2.sqs;

import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;

import datadog.trace.agent.tooling.Instrumenter;
import net.bytebuddy.asm.Advice;

public final class SqsMd5ChecksumInterceptorInstrumentation
implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice {

@Override
public String instrumentedType() {
// The AWS SDK checksum interceptor reads ReceiveMessageResponse.messages() while finalizing
// the response. Mark that internal access so we only wrap messages for application code.
return "software.amazon.awssdk.services.sqs.internal.MessageMD5ChecksumInterceptor";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think a comment why we need to instrument this internal class would be nice

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yep, this is part of the commit message. It's needed to avoid wrapping messages during the SDK's internal MessageMD5ChecksumInterceptor pass to prevent creating consumer spans
before user code actually consumes the messages.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

yeah but when reading the code (in the editor, after merging), you don't go and look at the commit messages in the blame to understand what's going on, which is why I think having a comment in the code explaining things is better

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I agree with including this in the code; I just didn't want to run CI again just for a single comment line :)

}

@Override
public void methodAdvice(MethodTransformer transformer) {
transformer.applyAdvice(
isMethod().and(named("afterExecution")), getClass().getName() + "$AfterExecutionAdvice");
}

public static class AfterExecutionAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void onEnter() {
SqsReceiveResponseInternalAccess.enter();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

don't we need to register SqsReceiveResponseInternalAccess as a helper for this advice ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

}

@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void onExit() {
SqsReceiveResponseInternalAccess.exit();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import datadog.trace.agent.tooling.InstrumenterModule;
import datadog.trace.api.InstrumenterConfig;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

Expand All @@ -24,6 +23,7 @@ public String[] helperClassNames() {
"datadog.trace.instrumentation.aws.v2.sqs.MessageAttributeInjector",
"datadog.trace.instrumentation.aws.v2.sqs.MessageExtractAdapter",
"datadog.trace.instrumentation.aws.v2.sqs.SqsDecorator",
"datadog.trace.instrumentation.aws.v2.sqs.SqsReceiveResponseInternalAccess",
"datadog.trace.instrumentation.aws.v2.sqs.TracingIterator",
"datadog.trace.instrumentation.aws.v2.sqs.TracingList",
"datadog.trace.instrumentation.aws.v2.sqs.TracingListIterator"
Expand All @@ -32,17 +32,25 @@ public String[] helperClassNames() {

@Override
public Map<String, String> contextStore() {
return Collections.singletonMap(
Map<String, String> contextStore = new java.util.HashMap<>();
contextStore.put(
"software.amazon.awssdk.services.sqs.model.ReceiveMessageResponse", "java.lang.String");
contextStore.put(
"software.amazon.awssdk.services.sqs.model.ReceiveMessageResponse$BuilderImpl",
"java.lang.String");
return contextStore;
}

@Override
public List<Instrumenter> typeInstrumentations() {
final List<Instrumenter> ret = new ArrayList<>(4);
final List<Instrumenter> ret = new ArrayList<>(6);
ret.add(new SqsClientInstrumentation());
ret.add(new SqsReceiveRequestInstrumentation());
// we don't need to instrument messages when we're doing legacy AWS-SDK tracing
if (!InstrumenterConfig.get().isLegacyInstrumentationEnabled(false, "aws-sdk")) {
ret.add(new SqsMd5ChecksumInterceptorInstrumentation());
ret.add(new SqsReceiveResponseBuilderInstrumentation());
ret.add(new SqsReceiveResponseBuilderImplInstrumentation());
ret.add(new SqsReceiveResultInstrumentation());
}
return ret;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package datadog.trace.instrumentation.aws.v2.sqs;

import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.returns;
import static net.bytebuddy.matcher.ElementMatchers.takesNoArguments;

import datadog.trace.agent.tooling.Instrumenter;
import datadog.trace.bootstrap.ContextStore;
import datadog.trace.bootstrap.InstrumentationContext;
import net.bytebuddy.asm.Advice;
import software.amazon.awssdk.services.sqs.model.ReceiveMessageResponse;

public final class SqsReceiveResponseBuilderImplInstrumentation
implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice {

private static final String BUILDER_IMPL =
"software.amazon.awssdk.services.sqs.model.ReceiveMessageResponse$BuilderImpl";

@Override
public String instrumentedType() {
return BUILDER_IMPL;
}

@Override
public void methodAdvice(MethodTransformer transformer) {
transformer.applyAdvice(
isMethod()
.and(named("build"))
.and(takesNoArguments())
.and(
returns(named("software.amazon.awssdk.services.sqs.model.ReceiveMessageResponse"))),
getClass().getName() + "$BuildAdvice");
}

public static class BuildAdvice {
@Advice.OnMethodExit(suppress = Throwable.class)
public static void onExit(
@Advice.This Object builder, @Advice.Return ReceiveMessageResponse response) {
if (response == null) {
return;
}
ContextStore<Object, String> builderStore =
InstrumentationContext.get(BUILDER_IMPL, "java.lang.String");
String queueUrl = builderStore.get(builder);
if (queueUrl != null) {
// Complete the handoff from the pre-rebuild response to the final response user code
// sees.
InstrumentationContext.get(ReceiveMessageResponse.class, String.class)
.put(response, queueUrl);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package datadog.trace.instrumentation.aws.v2.sqs;

import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.returns;
import static net.bytebuddy.matcher.ElementMatchers.takesNoArguments;

import datadog.trace.agent.tooling.Instrumenter;
import datadog.trace.bootstrap.InstrumentationContext;
import net.bytebuddy.asm.Advice;
import software.amazon.awssdk.services.sqs.model.ReceiveMessageResponse;

public final class SqsReceiveResponseBuilderInstrumentation
implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice {

private static final String BUILDER_IMPL =
"software.amazon.awssdk.services.sqs.model.ReceiveMessageResponse$BuilderImpl";

@Override
public String instrumentedType() {
return "software.amazon.awssdk.services.sqs.model.ReceiveMessageResponse";
}

@Override
public void methodAdvice(MethodTransformer transformer) {
transformer.applyAdvice(
isMethod()
.and(named("toBuilder"))
.and(takesNoArguments())
.and(
returns(
named(
"software.amazon.awssdk.services.sqs.model.ReceiveMessageResponse$Builder"))),
getClass().getName() + "$ToBuilderAdvice");
}

public static class ToBuilderAdvice {
@Advice.OnMethodExit(suppress = Throwable.class)
public static void onExit(
@Advice.This ReceiveMessageResponse response, @Advice.Return Object builder) {
if (builder == null) {
return;
}
String queueUrl =
InstrumentationContext.get(ReceiveMessageResponse.class, String.class).get(response);
if (queueUrl != null) {
// AWS SDK core finalizes modeled responses through response.toBuilder().build().
// Carry queueUrl onto the builder so it survives that response instance change.
InstrumentationContext.get(BUILDER_IMPL, "java.lang.String").put(builder, queueUrl);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package datadog.trace.instrumentation.aws.v2.sqs;

import datadog.trace.bootstrap.CallDepthThreadLocalMap;

public final class SqsReceiveResponseInternalAccess {

private SqsReceiveResponseInternalAccess() {}

public static void enter() {
CallDepthThreadLocalMap.incrementCallDepth(SqsReceiveResponseInternalAccess.class);
}

public static void exit() {
CallDepthThreadLocalMap.decrementCallDepth(SqsReceiveResponseInternalAccess.class);
}

public static boolean active() {
return CallDepthThreadLocalMap.getCallDepth(SqsReceiveResponseInternalAccess.class) > 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ public static class GetMessagesAdvice {
public static void onExit(
@Advice.This ReceiveMessageResponse result,
@Advice.Return(readOnly = false) List<Message> messages) {
if (SqsReceiveResponseInternalAccess.active()) {
// AWS SDK's MD5 checksum interceptor calls messages() during afterExecution. That should
// not create consumer spans; those belong around application message processing.
return;
}
if (messages != null && !messages.isEmpty() && !(messages instanceof TracingList)) {
String queueUrl =
InstrumentationContext.get(ReceiveMessageResponse.class, String.class).get(result);
Expand Down
Loading