-
Notifications
You must be signed in to change notification settings - Fork 869
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Propagate otel context through custom aws client context for lambda direct calls #11675
Changes from all commits
97286cf
c396628
3bae213
1c98310
fafb4ac
d2092d5
0b11264
7038146
411cb6e
c0ff012
2309f77
30462c9
64d5728
76ba8c2
416e3a6
150c0bc
fb525c6
5217e3b
5b81407
1d8f0c8
40d1264
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
/* | ||
* Copyright The OpenTelemetry Authors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package io.opentelemetry.instrumentation.awslambdacore.v1_0.internal; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
import static org.mockito.Mockito.mock; | ||
import static org.mockito.Mockito.when; | ||
|
||
import com.amazonaws.services.lambda.runtime.ClientContext; | ||
import io.opentelemetry.api.OpenTelemetry; | ||
import io.opentelemetry.api.trace.Span; | ||
import io.opentelemetry.api.trace.SpanContext; | ||
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; | ||
import io.opentelemetry.context.Context; | ||
import io.opentelemetry.context.propagation.ContextPropagators; | ||
import io.opentelemetry.instrumentation.awslambdacore.v1_0.AwsLambdaRequest; | ||
import java.util.HashMap; | ||
import org.junit.jupiter.api.Test; | ||
|
||
class InstrumenterExtractionTest { | ||
@Test | ||
public void useCustomContext() { | ||
AwsLambdaFunctionInstrumenter instr = | ||
AwsLambdaFunctionInstrumenterFactory.createInstrumenter( | ||
OpenTelemetry.propagating( | ||
ContextPropagators.create(W3CTraceContextPropagator.getInstance()))); | ||
com.amazonaws.services.lambda.runtime.Context awsContext = | ||
mock(com.amazonaws.services.lambda.runtime.Context.class); | ||
ClientContext clientContext = mock(ClientContext.class); | ||
when(awsContext.getClientContext()).thenReturn(clientContext); | ||
HashMap<String, String> customMap = new HashMap<>(); | ||
customMap.put("traceparent", "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"); | ||
when(clientContext.getCustom()).thenReturn(customMap); | ||
|
||
AwsLambdaRequest input = AwsLambdaRequest.create(awsContext, new HashMap<>(), new HashMap<>()); | ||
|
||
Context extracted = instr.extract(input); | ||
SpanContext spanContext = Span.fromContext(extracted).getSpanContext(); | ||
assertThat(spanContext.getTraceId()).isEqualTo("4bf92f3577b34da6a3ce929d0e0e4736"); | ||
assertThat(spanContext.getSpanId()).isEqualTo("00f067aa0ba902b7"); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
/* | ||
* Copyright The OpenTelemetry Authors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package io.opentelemetry.instrumentation.awssdk.v2_2; | ||
|
||
public final class LambdaAdviceBridge { | ||
private LambdaAdviceBridge() {} | ||
|
||
public static void referenceForMuzzleOnly() { | ||
throw new UnsupportedOperationException( | ||
LambdaImpl.class.getName() + " referencing for muzzle, should never be actually called"); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
/* | ||
* Copyright The OpenTelemetry Authors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package io.opentelemetry.javaagent.instrumentation.awssdk.v2_2; | ||
|
||
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; | ||
import static net.bytebuddy.matcher.ElementMatchers.none; | ||
|
||
import com.google.auto.service.AutoService; | ||
import io.opentelemetry.instrumentation.awssdk.v2_2.LambdaAdviceBridge; | ||
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; | ||
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; | ||
import net.bytebuddy.asm.Advice; | ||
import net.bytebuddy.matcher.ElementMatcher; | ||
|
||
@AutoService(InstrumentationModule.class) | ||
public class LambdaInstrumentationModule extends AbstractAwsSdkInstrumentationModule { | ||
|
||
public LambdaInstrumentationModule() { | ||
super("aws-sdk-2.2-lambda"); | ||
} | ||
|
||
@Override | ||
public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() { | ||
return hasClassesNamed( | ||
"software.amazon.awssdk.services.lambda.model.InvokeRequest", | ||
"software.amazon.awssdk.protocols.jsoncore.JsonNode"); | ||
} | ||
|
||
@Override | ||
public void doTransform(TypeTransformer transformer) { | ||
transformer.applyAdviceToMethod( | ||
none(), LambdaInstrumentationModule.class.getName() + "$RegisterAdvice"); | ||
} | ||
|
||
@SuppressWarnings("unused") | ||
public static class RegisterAdvice { | ||
@Advice.OnMethodExit(suppress = Throwable.class) | ||
public static void onExit() { | ||
// (indirectly) using LambdaImpl class here to make sure it is available from LambdaAccess | ||
// (injected into app classloader) and checked by Muzzle | ||
LambdaAdviceBridge.referenceForMuzzleOnly(); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
/* | ||
* Copyright The OpenTelemetry Authors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package io.opentelemetry.javaagent.instrumentation.awssdk.v2_2; | ||
|
||
import io.opentelemetry.instrumentation.awssdk.v2_2.AbstractAws2LambdaTest; | ||
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; | ||
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; | ||
import org.junit.jupiter.api.extension.RegisterExtension; | ||
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; | ||
|
||
class Aws2LambdaTest extends AbstractAws2LambdaTest { | ||
|
||
@RegisterExtension | ||
private static final AgentInstrumentationExtension testing = | ||
AgentInstrumentationExtension.create(); | ||
|
||
@Override | ||
protected InstrumentationExtension getTesting() { | ||
return testing; | ||
} | ||
|
||
@Override | ||
protected boolean canTestLambdaInvoke() { | ||
// only supported since 2.17.0 | ||
return Boolean.getBoolean("testLatestDeps"); | ||
} | ||
|
||
@Override | ||
protected ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder() { | ||
return ClientOverrideConfiguration.builder(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
/* | ||
* Copyright The OpenTelemetry Authors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package io.opentelemetry.instrumentation.awssdk.v2_2; | ||
|
||
import io.opentelemetry.context.Context; | ||
import io.opentelemetry.javaagent.tooling.muzzle.NoMuzzle; | ||
import software.amazon.awssdk.core.SdkRequest; | ||
|
||
final class LambdaAccess { | ||
private LambdaAccess() {} | ||
|
||
private static final boolean enabled = PluginImplUtil.isImplPresent("LambdaImpl"); | ||
|
||
@NoMuzzle | ||
public static SdkRequest modifyRequest(SdkRequest request, Context otelContext) { | ||
return enabled ? LambdaImpl.modifyRequest(request, otelContext) : null; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
/* | ||
* Copyright The OpenTelemetry Authors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package io.opentelemetry.instrumentation.awssdk.v2_2; | ||
|
||
import io.opentelemetry.api.GlobalOpenTelemetry; | ||
import java.nio.charset.StandardCharsets; | ||
import java.util.Base64; | ||
import java.util.LinkedHashMap; | ||
import java.util.Map; | ||
import javax.annotation.Nullable; | ||
import software.amazon.awssdk.core.SdkRequest; | ||
import software.amazon.awssdk.protocols.jsoncore.JsonNode; | ||
import software.amazon.awssdk.protocols.jsoncore.internal.ObjectJsonNode; | ||
import software.amazon.awssdk.protocols.jsoncore.internal.StringJsonNode; | ||
import software.amazon.awssdk.services.lambda.model.InvokeRequest; | ||
|
||
// this class is only used from LambdaAccess from method with @NoMuzzle annotation | ||
|
||
// Direct lambda invocations (e.g., not through an api gateway) currently strip | ||
// away the otel propagation headers (but leave x-ray ones intact). Use the | ||
// custom client context header as an additional propagation mechanism for this | ||
// very specific scenario. For reference, the header is named "X-Amz-Client-Context" but the api to | ||
// manipulate it abstracts that away. The client context field is documented in | ||
// https://docs.aws.amazon.com/lambda/latest/api/API_Invoke.html#API_Invoke_RequestParameters | ||
|
||
final class LambdaImpl { | ||
static { | ||
// Force loading of InvokeRequest; this ensures that an exception is thrown at this point when | ||
// the Lambda library is not present, which will cause DirectLambdaAccess to have | ||
// enabled=false in library mode. | ||
@SuppressWarnings("unused") | ||
String invokeRequestName = InvokeRequest.class.getName(); | ||
// was added in 2.17.0 | ||
@SuppressWarnings("unused") | ||
String jsonNodeName = JsonNode.class.getName(); | ||
} | ||
|
||
private static final String CLIENT_CONTEXT_CUSTOM_FIELDS_KEY = "custom"; | ||
static final int MAX_CLIENT_CONTEXT_LENGTH = 3583; // visible for testing | ||
|
||
private LambdaImpl() {} | ||
|
||
@Nullable | ||
static SdkRequest modifyRequest( | ||
SdkRequest request, io.opentelemetry.context.Context otelContext) { | ||
if (isDirectLambdaInvocation(request)) { | ||
return modifyOrAddCustomContextHeader((InvokeRequest) request, otelContext); | ||
} | ||
return null; | ||
} | ||
|
||
static boolean isDirectLambdaInvocation(SdkRequest request) { | ||
return request instanceof InvokeRequest; | ||
} | ||
|
||
static SdkRequest modifyOrAddCustomContextHeader( | ||
InvokeRequest request, io.opentelemetry.context.Context otelContext) { | ||
InvokeRequest.Builder builder = request.toBuilder(); | ||
// Unfortunately the value of this thing is a base64-encoded json with a character limit; also | ||
// therefore not comma-composable like many http headers | ||
String clientContextString = request.clientContext(); | ||
String clientContextJsonString = "{}"; | ||
if (clientContextString != null && !clientContextString.isEmpty()) { | ||
clientContextJsonString = | ||
new String(Base64.getDecoder().decode(clientContextString), StandardCharsets.UTF_8); | ||
} | ||
JsonNode jsonNode = JsonNode.parser().parse(clientContextJsonString); | ||
if (!jsonNode.isObject()) { | ||
return null; | ||
} | ||
JsonNode customNode = | ||
jsonNode | ||
.asObject() | ||
.computeIfAbsent( | ||
CLIENT_CONTEXT_CUSTOM_FIELDS_KEY, (k) -> new ObjectJsonNode(new LinkedHashMap<>())); | ||
if (!customNode.isObject()) { | ||
return null; | ||
} | ||
Map<String, JsonNode> map = customNode.asObject(); | ||
GlobalOpenTelemetry.getPropagators() | ||
.getTextMapPropagator() | ||
.inject(otelContext, map, (nodes, key, value) -> nodes.put(key, new StringJsonNode(value))); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the propagation injected into the custom block for everything? If not, how does it distinguish between a direct call vs other types? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nevermind. I see the |
||
if (map.isEmpty()) { | ||
return null; | ||
} | ||
|
||
// turn it back into a string (json encode) | ||
String newJson = jsonNode.toString(); | ||
|
||
// turn it back into a base64 string | ||
String newJson64 = Base64.getEncoder().encodeToString(newJson.getBytes(StandardCharsets.UTF_8)); | ||
// check it for length (err on the safe side with >=) | ||
if (newJson64.length() >= MAX_CLIENT_CONTEXT_LENGTH) { | ||
return null; | ||
} | ||
builder.clientContext(newJson64); | ||
return builder.build(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fancy!