Skip to content
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

Merged
merged 21 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
97286cf
Look for propagated context in custom lambda context in addition to h…
johnbley Jun 3, 2024
c396628
Working code to propagate through X-Amx-Client-Context
johnbley Jun 26, 2024
3bae213
Missed copyright on test file
johnbley Jun 26, 2024
1c98310
Clean up some debug comments, add code to handle no propagation happe…
johnbley Jun 26, 2024
fafb4ac
Remove more debug output
johnbley Jun 26, 2024
d2092d5
Remove yet more debugging printlns
johnbley Jun 26, 2024
0b11264
Merge branch 'open-telemetry:main' into lambda_direct_calls_custom_co…
johnbley Jun 26, 2024
7038146
Clean up context handling for tests
johnbley Jun 26, 2024
411cb6e
spotlessApply
johnbley Jun 26, 2024
c0ff012
Properly reset test
johnbley Jun 26, 2024
2309f77
checkstyle fixes
johnbley Jun 26, 2024
30462c9
spotlessApply again
johnbley Jun 26, 2024
64d5728
Properly depend on jackson for json manipulation
johnbley Jun 27, 2024
76ba8c2
Handful of cleanups from review feedback
johnbley Jun 27, 2024
416e3a6
Clean up test setup per review feedback
johnbley Jun 27, 2024
150c0bc
Adding lambda dependency to test as well; not sure why this is needed...
johnbley Jun 27, 2024
fb525c6
Another missing test dependency
johnbley Jun 27, 2024
5217e3b
And one more check for plugin/classloader safety
johnbley Jun 27, 2024
5b81407
Replace jackson databind with json parser from aws sdk
laurit Jul 5, 2024
1d8f0c8
Merge pull request #1 from laurit/aws-lambda
johnbley Jul 7, 2024
40d1264
Add muzzle build settings for new lambda call instrumentation
johnbley Jul 7, 2024
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 @@ -11,6 +11,7 @@
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.api.internal.ContextPropagationDebug;
import io.opentelemetry.instrumentation.awslambdacore.v1_0.AwsLambdaRequest;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import javax.annotation.Nullable;
Expand Down Expand Up @@ -48,11 +49,20 @@ public void end(

public Context extract(AwsLambdaRequest input) {
ContextPropagationDebug.debugContextLeakIfEnabled();
// Look in both the http headers and the custom client context
Map<String, String> headers = input.getHeaders();
if (input.getAwsContext() != null && input.getAwsContext().getClientContext() != null) {
Map<String, String> customContext = input.getAwsContext().getClientContext().getCustom();
if (customContext != null) {
headers = new HashMap<>(headers);
headers.putAll(customContext);
}
}

return openTelemetry
.getPropagators()
.getTextMapPropagator()
.extract(Context.root(), input.getHeaders(), MapGetter.INSTANCE);
.extract(Context.root(), headers, MapGetter.INSTANCE);
}

private enum MapGetter implements TextMapGetter<Map<String, String>> {
Expand Down
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");
}
}
18 changes: 18 additions & 0 deletions instrumentation/aws-sdk/aws-sdk-2.2/javaagent/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ muzzle {

excludeInstrumentationName("aws-sdk-2.2-sqs")
excludeInstrumentationName("aws-sdk-2.2-sns")
excludeInstrumentationName("aws-sdk-2.2-lambda")

// several software.amazon.awssdk artifacts are missing for this version
skip("2.17.200")
Expand Down Expand Up @@ -43,6 +44,7 @@ muzzle {
extraDependency("software.amazon.awssdk:protocol-core")

excludeInstrumentationName("aws-sdk-2.2-sns")
excludeInstrumentationName("aws-sdk-2.2-lambda")

// several software.amazon.awssdk artifacts are missing for this version
skip("2.17.200")
Expand All @@ -57,6 +59,21 @@ muzzle {
extraDependency("software.amazon.awssdk:protocol-core")

excludeInstrumentationName("aws-sdk-2.2-sqs")
excludeInstrumentationName("aws-sdk-2.2-lambda")

// several software.amazon.awssdk artifacts are missing for this version
skip("2.17.200")
}
pass {
group.set("software.amazon.awssdk")
module.set("lambda")
versions.set("[2.17.0,)")
// Used by all SDK services, the only case it isn't is an SDK extension such as a custom HTTP
// client, which is not target of instrumentation anyways.
extraDependency("software.amazon.awssdk:protocol-core")

excludeInstrumentationName("aws-sdk-2.2-sqs")
excludeInstrumentationName("aws-sdk-2.2-sns")

// several software.amazon.awssdk artifacts are missing for this version
skip("2.17.200")
Expand All @@ -81,6 +98,7 @@ dependencies {
testLibrary("software.amazon.awssdk:dynamodb:2.2.0")
testLibrary("software.amazon.awssdk:ec2:2.2.0")
testLibrary("software.amazon.awssdk:kinesis:2.2.0")
testLibrary("software.amazon.awssdk:lambda:2.2.0")
testLibrary("software.amazon.awssdk:rds:2.2.0")
testLibrary("software.amazon.awssdk:s3:2.2.0")
testLibrary("software.amazon.awssdk:sqs:2.2.0")
Expand Down
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
Expand Up @@ -17,6 +17,7 @@ dependencies {
testLibrary("software.amazon.awssdk:dynamodb:2.2.0")
testLibrary("software.amazon.awssdk:ec2:2.2.0")
testLibrary("software.amazon.awssdk:kinesis:2.2.0")
testLibrary("software.amazon.awssdk:lambda:2.2.0")
testLibrary("software.amazon.awssdk:rds:2.2.0")
testLibrary("software.amazon.awssdk:s3:2.2.0")
testLibrary("software.amazon.awssdk:sqs:2.2.0")
Expand Down
17 changes: 17 additions & 0 deletions instrumentation/aws-sdk/aws-sdk-2.2/library/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ dependencies {

library("software.amazon.awssdk:aws-core:2.2.0")
library("software.amazon.awssdk:sqs:2.2.0")
library("software.amazon.awssdk:lambda:2.2.0")
library("software.amazon.awssdk:sns:2.2.0")
library("software.amazon.awssdk:aws-json-protocol:2.2.0")
// json-utils was added in 2.17.0
compileOnly("software.amazon.awssdk:json-utils:2.17.0")
compileOnly(project(":muzzle")) // For @NoMuzzle

testImplementation(project(":instrumentation:aws-sdk:aws-sdk-2.2:testing"))
Expand Down Expand Up @@ -38,10 +41,24 @@ testing {
implementation("software.amazon.awssdk:aws-core:+")
implementation("software.amazon.awssdk:aws-json-protocol:+")
implementation("software.amazon.awssdk:dynamodb:+")
implementation("software.amazon.awssdk:lambda:+")
} else {
implementation("software.amazon.awssdk:aws-core:2.2.0")
implementation("software.amazon.awssdk:aws-json-protocol:2.2.0")
implementation("software.amazon.awssdk:dynamodb:2.2.0")
implementation("software.amazon.awssdk:lambda:2.2.0")
}
}
}

val testLambda by registering(JvmTestSuite::class) {
dependencies {
implementation(project())
implementation(project(":instrumentation:aws-sdk:aws-sdk-2.2:testing"))
if (findProperty("testLatestDeps") as Boolean) {
implementation("software.amazon.awssdk:lambda:+")
} else {
implementation("software.amazon.awssdk:lambda:2.17.0")
}
}
}
Expand Down
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();
Comment on lines +31 to +35
Copy link
Member

Choose a reason for hiding this comment

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

Fancy!

// 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)));
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Member

Choose a reason for hiding this comment

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

Nevermind. I see the isDirectLambdaInvocation check.

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();
}
}
Loading
Loading