Propagate otel context through custom aws client context for lambda direct calls (#11675)
Co-authored-by: Lauri Tulmin <ltulmin@splunk.com>
This commit is contained in:
parent
ec91735598
commit
6b65447300
|
@ -11,6 +11,7 @@ import io.opentelemetry.context.propagation.TextMapGetter;
|
||||||
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
|
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
|
||||||
import io.opentelemetry.instrumentation.api.internal.ContextPropagationDebug;
|
import io.opentelemetry.instrumentation.api.internal.ContextPropagationDebug;
|
||||||
import io.opentelemetry.instrumentation.awslambdacore.v1_0.AwsLambdaRequest;
|
import io.opentelemetry.instrumentation.awslambdacore.v1_0.AwsLambdaRequest;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
@ -48,11 +49,20 @@ public class AwsLambdaFunctionInstrumenter {
|
||||||
|
|
||||||
public Context extract(AwsLambdaRequest input) {
|
public Context extract(AwsLambdaRequest input) {
|
||||||
ContextPropagationDebug.debugContextLeakIfEnabled();
|
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
|
return openTelemetry
|
||||||
.getPropagators()
|
.getPropagators()
|
||||||
.getTextMapPropagator()
|
.getTextMapPropagator()
|
||||||
.extract(Context.root(), input.getHeaders(), MapGetter.INSTANCE);
|
.extract(Context.root(), headers, MapGetter.INSTANCE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum MapGetter implements TextMapGetter<Map<String, String>> {
|
private enum MapGetter implements TextMapGetter<Map<String, String>> {
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ muzzle {
|
||||||
|
|
||||||
excludeInstrumentationName("aws-sdk-2.2-sqs")
|
excludeInstrumentationName("aws-sdk-2.2-sqs")
|
||||||
excludeInstrumentationName("aws-sdk-2.2-sns")
|
excludeInstrumentationName("aws-sdk-2.2-sns")
|
||||||
|
excludeInstrumentationName("aws-sdk-2.2-lambda")
|
||||||
|
|
||||||
// several software.amazon.awssdk artifacts are missing for this version
|
// several software.amazon.awssdk artifacts are missing for this version
|
||||||
skip("2.17.200")
|
skip("2.17.200")
|
||||||
|
@ -43,6 +44,7 @@ muzzle {
|
||||||
extraDependency("software.amazon.awssdk:protocol-core")
|
extraDependency("software.amazon.awssdk:protocol-core")
|
||||||
|
|
||||||
excludeInstrumentationName("aws-sdk-2.2-sns")
|
excludeInstrumentationName("aws-sdk-2.2-sns")
|
||||||
|
excludeInstrumentationName("aws-sdk-2.2-lambda")
|
||||||
|
|
||||||
// several software.amazon.awssdk artifacts are missing for this version
|
// several software.amazon.awssdk artifacts are missing for this version
|
||||||
skip("2.17.200")
|
skip("2.17.200")
|
||||||
|
@ -57,6 +59,21 @@ muzzle {
|
||||||
extraDependency("software.amazon.awssdk:protocol-core")
|
extraDependency("software.amazon.awssdk:protocol-core")
|
||||||
|
|
||||||
excludeInstrumentationName("aws-sdk-2.2-sqs")
|
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
|
// several software.amazon.awssdk artifacts are missing for this version
|
||||||
skip("2.17.200")
|
skip("2.17.200")
|
||||||
|
@ -81,6 +98,7 @@ dependencies {
|
||||||
testLibrary("software.amazon.awssdk:dynamodb:2.2.0")
|
testLibrary("software.amazon.awssdk:dynamodb:2.2.0")
|
||||||
testLibrary("software.amazon.awssdk:ec2:2.2.0")
|
testLibrary("software.amazon.awssdk:ec2:2.2.0")
|
||||||
testLibrary("software.amazon.awssdk:kinesis: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:rds:2.2.0")
|
||||||
testLibrary("software.amazon.awssdk:s3:2.2.0")
|
testLibrary("software.amazon.awssdk:s3:2.2.0")
|
||||||
testLibrary("software.amazon.awssdk:sqs:2.2.0")
|
testLibrary("software.amazon.awssdk:sqs:2.2.0")
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ dependencies {
|
||||||
testLibrary("software.amazon.awssdk:dynamodb:2.2.0")
|
testLibrary("software.amazon.awssdk:dynamodb:2.2.0")
|
||||||
testLibrary("software.amazon.awssdk:ec2:2.2.0")
|
testLibrary("software.amazon.awssdk:ec2:2.2.0")
|
||||||
testLibrary("software.amazon.awssdk:kinesis: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:rds:2.2.0")
|
||||||
testLibrary("software.amazon.awssdk:s3:2.2.0")
|
testLibrary("software.amazon.awssdk:s3:2.2.0")
|
||||||
testLibrary("software.amazon.awssdk:sqs:2.2.0")
|
testLibrary("software.amazon.awssdk:sqs:2.2.0")
|
||||||
|
|
|
@ -7,8 +7,11 @@ dependencies {
|
||||||
|
|
||||||
library("software.amazon.awssdk:aws-core:2.2.0")
|
library("software.amazon.awssdk:aws-core:2.2.0")
|
||||||
library("software.amazon.awssdk:sqs: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:sns:2.2.0")
|
||||||
library("software.amazon.awssdk:aws-json-protocol: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
|
compileOnly(project(":muzzle")) // For @NoMuzzle
|
||||||
|
|
||||||
testImplementation(project(":instrumentation:aws-sdk:aws-sdk-2.2:testing"))
|
testImplementation(project(":instrumentation:aws-sdk:aws-sdk-2.2:testing"))
|
||||||
|
@ -38,10 +41,24 @@ testing {
|
||||||
implementation("software.amazon.awssdk:aws-core:+")
|
implementation("software.amazon.awssdk:aws-core:+")
|
||||||
implementation("software.amazon.awssdk:aws-json-protocol:+")
|
implementation("software.amazon.awssdk:aws-json-protocol:+")
|
||||||
implementation("software.amazon.awssdk:dynamodb:+")
|
implementation("software.amazon.awssdk:dynamodb:+")
|
||||||
|
implementation("software.amazon.awssdk:lambda:+")
|
||||||
} else {
|
} else {
|
||||||
implementation("software.amazon.awssdk:aws-core:2.2.0")
|
implementation("software.amazon.awssdk:aws-core:2.2.0")
|
||||||
implementation("software.amazon.awssdk:aws-json-protocol: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: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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)));
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -212,6 +212,10 @@ final class TracingExecutionInterceptor implements ExecutionInterceptor {
|
||||||
if (modifiedRequest != null) {
|
if (modifiedRequest != null) {
|
||||||
return modifiedRequest;
|
return modifiedRequest;
|
||||||
}
|
}
|
||||||
|
modifiedRequest = LambdaAccess.modifyRequest(request, otelContext);
|
||||||
|
if (modifiedRequest != null) {
|
||||||
|
return modifiedRequest;
|
||||||
|
}
|
||||||
|
|
||||||
// Insert other special handling here, following the same pattern as SQS and SNS.
|
// Insert other special handling here, following the same pattern as SQS and SNS.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
/*
|
||||||
|
* Copyright The OpenTelemetry Authors
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.opentelemetry.instrumentation.awssdk.v2_2;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import io.opentelemetry.context.Context;
|
||||||
|
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
|
||||||
|
import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Base64;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||||
|
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
|
||||||
|
import software.amazon.awssdk.services.lambda.model.InvokeRequest;
|
||||||
|
|
||||||
|
class Aws2LambdaTest extends AbstractAws2LambdaTest {
|
||||||
|
|
||||||
|
@RegisterExtension
|
||||||
|
private static final LibraryInstrumentationExtension testing =
|
||||||
|
LibraryInstrumentationExtension.create();
|
||||||
|
|
||||||
|
private static Context context;
|
||||||
|
private static AwsSdkTelemetry telemetry;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void setup() {
|
||||||
|
testing.runWithHttpServerSpan(
|
||||||
|
() -> {
|
||||||
|
context = Context.current();
|
||||||
|
});
|
||||||
|
|
||||||
|
telemetry = AwsSdkTelemetry.create(testing.getOpenTelemetry());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected InstrumentationExtension getTesting() {
|
||||||
|
return testing;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder() {
|
||||||
|
return ClientOverrideConfiguration.builder()
|
||||||
|
.addExecutionInterceptor(telemetry.newExecutionInterceptor());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String base64ify(String json) {
|
||||||
|
return Base64.getEncoder().encodeToString(json.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void noExistingClientContext() {
|
||||||
|
InvokeRequest request = InvokeRequest.builder().build();
|
||||||
|
|
||||||
|
InvokeRequest newRequest =
|
||||||
|
(InvokeRequest) LambdaImpl.modifyOrAddCustomContextHeader(request, context);
|
||||||
|
|
||||||
|
String newClientContext = newRequest.clientContext();
|
||||||
|
newClientContext =
|
||||||
|
new String(Base64.getDecoder().decode(newClientContext), StandardCharsets.UTF_8);
|
||||||
|
assertThat(newClientContext.contains("traceparent")).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void withExistingClientContext() {
|
||||||
|
String clientContext =
|
||||||
|
base64ify(
|
||||||
|
"{\"otherStuff\": \"otherValue\", \"custom\": {\"preExisting\": \"somevalue\"} }");
|
||||||
|
InvokeRequest request = InvokeRequest.builder().clientContext(clientContext).build();
|
||||||
|
|
||||||
|
InvokeRequest newRequest =
|
||||||
|
(InvokeRequest) LambdaImpl.modifyOrAddCustomContextHeader(request, context);
|
||||||
|
|
||||||
|
String newClientContext = newRequest.clientContext();
|
||||||
|
newClientContext =
|
||||||
|
new String(Base64.getDecoder().decode(newClientContext), StandardCharsets.UTF_8);
|
||||||
|
assertThat(newClientContext.contains("traceparent")).isTrue();
|
||||||
|
assertThat(newClientContext.contains("preExisting")).isTrue();
|
||||||
|
assertThat(newClientContext.contains("otherStuff")).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void exceedingMaximumLengthDoesNotModify() {
|
||||||
|
// awkward way to build a valid json that is almost but not quite too long
|
||||||
|
StringBuilder buffer = new StringBuilder("x");
|
||||||
|
String long64edClientContext = "";
|
||||||
|
while (true) {
|
||||||
|
buffer.append("x");
|
||||||
|
String newClientContext = base64ify("{\"" + buffer + "\": \"" + buffer + "\"}");
|
||||||
|
if (newClientContext.length() >= LambdaImpl.MAX_CLIENT_CONTEXT_LENGTH) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
long64edClientContext = newClientContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
InvokeRequest request = InvokeRequest.builder().clientContext(long64edClientContext).build();
|
||||||
|
assertThat(request.clientContext().equals(long64edClientContext)).isTrue();
|
||||||
|
|
||||||
|
InvokeRequest newRequest =
|
||||||
|
(InvokeRequest) LambdaImpl.modifyOrAddCustomContextHeader(request, context);
|
||||||
|
assertThat(newRequest).isNull(); // null return means no modification performed
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ dependencies {
|
||||||
compileOnly("software.amazon.awssdk:dynamodb:2.2.0")
|
compileOnly("software.amazon.awssdk:dynamodb:2.2.0")
|
||||||
compileOnly("software.amazon.awssdk:ec2:2.2.0")
|
compileOnly("software.amazon.awssdk:ec2:2.2.0")
|
||||||
compileOnly("software.amazon.awssdk:kinesis:2.2.0")
|
compileOnly("software.amazon.awssdk:kinesis:2.2.0")
|
||||||
|
compileOnly("software.amazon.awssdk:lambda:2.2.0")
|
||||||
compileOnly("software.amazon.awssdk:rds:2.2.0")
|
compileOnly("software.amazon.awssdk:rds:2.2.0")
|
||||||
compileOnly("software.amazon.awssdk:s3:2.2.0")
|
compileOnly("software.amazon.awssdk:s3:2.2.0")
|
||||||
compileOnly("software.amazon.awssdk:sqs:2.2.0")
|
compileOnly("software.amazon.awssdk:sqs:2.2.0")
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
* Copyright The OpenTelemetry Authors
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.opentelemetry.instrumentation.awssdk.v2_2;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import io.opentelemetry.api.trace.SpanKind;
|
||||||
|
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
|
||||||
|
import io.opentelemetry.testing.internal.armeria.common.HttpResponse;
|
||||||
|
import io.opentelemetry.testing.internal.armeria.common.HttpStatus;
|
||||||
|
import io.opentelemetry.testing.internal.armeria.common.MediaType;
|
||||||
|
import io.opentelemetry.testing.internal.armeria.testing.junit5.server.mock.MockWebServerExtension;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Base64;
|
||||||
|
import org.junit.jupiter.api.Assumptions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||||
|
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
|
||||||
|
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
|
||||||
|
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
|
||||||
|
import software.amazon.awssdk.regions.Region;
|
||||||
|
import software.amazon.awssdk.services.lambda.LambdaClient;
|
||||||
|
import software.amazon.awssdk.services.lambda.LambdaClientBuilder;
|
||||||
|
import software.amazon.awssdk.services.lambda.model.InvokeRequest;
|
||||||
|
import software.amazon.awssdk.services.lambda.model.InvokeResponse;
|
||||||
|
|
||||||
|
public abstract class AbstractAws2LambdaTest {
|
||||||
|
|
||||||
|
@RegisterExtension
|
||||||
|
private static final MockWebServerExtension server = new MockWebServerExtension();
|
||||||
|
|
||||||
|
private static final StaticCredentialsProvider CREDENTIALS_PROVIDER =
|
||||||
|
StaticCredentialsProvider.create(
|
||||||
|
AwsBasicCredentials.create("my-access-key", "my-secret-key"));
|
||||||
|
|
||||||
|
protected abstract InstrumentationExtension getTesting();
|
||||||
|
|
||||||
|
protected abstract ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder();
|
||||||
|
|
||||||
|
protected boolean canTestLambdaInvoke() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInvokeLambda() {
|
||||||
|
Assumptions.assumeTrue(canTestLambdaInvoke());
|
||||||
|
|
||||||
|
LambdaClientBuilder builder = LambdaClient.builder();
|
||||||
|
builder
|
||||||
|
.overrideConfiguration(createOverrideConfigurationBuilder().build())
|
||||||
|
.endpointOverride(server.httpUri());
|
||||||
|
builder.region(Region.AP_NORTHEAST_1).credentialsProvider(CREDENTIALS_PROVIDER);
|
||||||
|
LambdaClient client = builder.build();
|
||||||
|
|
||||||
|
server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "ok"));
|
||||||
|
|
||||||
|
InvokeRequest request = InvokeRequest.builder().functionName("test").build();
|
||||||
|
InvokeResponse response = client.invoke(request);
|
||||||
|
assertThat(response.statusCode()).isEqualTo(200);
|
||||||
|
assertThat(response.payload().asUtf8String()).isEqualTo("ok");
|
||||||
|
|
||||||
|
String clientContextHeader =
|
||||||
|
server.takeRequest().request().headers().get("x-amz-client-context");
|
||||||
|
assertThat(clientContextHeader).isNotEmpty();
|
||||||
|
String clientContextJson =
|
||||||
|
new String(Base64.getDecoder().decode(clientContextHeader), StandardCharsets.UTF_8);
|
||||||
|
assertThat(clientContextJson).contains("traceparent");
|
||||||
|
|
||||||
|
getTesting()
|
||||||
|
.waitAndAssertTraces(
|
||||||
|
trace ->
|
||||||
|
trace.hasSpansSatisfyingExactly(
|
||||||
|
span -> span.hasName("Lambda.Invoke").hasKind(SpanKind.CLIENT).hasNoParent()));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue