diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/build.gradle.kts b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/build.gradle.kts index 48def6f44070..1dff107ed832 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/build.gradle.kts +++ b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/build.gradle.kts @@ -44,6 +44,7 @@ dependencies { testLibrary("com.amazonaws:aws-java-sdk-dynamodb:1.11.106") testLibrary("com.amazonaws:aws-java-sdk-ec2:1.11.106") testLibrary("com.amazonaws:aws-java-sdk-kinesis:1.11.106") + testLibrary("com.amazonaws:aws-java-sdk-lambda:1.11.106") testLibrary("com.amazonaws:aws-java-sdk-rds:1.11.106") testLibrary("com.amazonaws:aws-java-sdk-s3:1.11.106") testLibrary("com.amazonaws:aws-java-sdk-sns:1.11.106") diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/LambdaClientTest.java b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/LambdaClientTest.java new file mode 100644 index 000000000000..f8125725d62b --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/LambdaClientTest.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.awssdk.v1_11; + +import com.amazonaws.services.lambda.AWSLambdaClientBuilder; +import io.opentelemetry.instrumentation.awssdk.v1_11.AbstractLambdaClientTest; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class LambdaClientTest extends AbstractLambdaClientTest { + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } + + @Override + public AWSLambdaClientBuilder configureClient(AWSLambdaClientBuilder clientBuilder) { + return clientBuilder; + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/build.gradle.kts b/instrumentation/aws-sdk/aws-sdk-1.11/library/build.gradle.kts index 104e260218fe..8a3e396a43d8 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library/build.gradle.kts +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { testLibrary("com.amazonaws:aws-java-sdk-dynamodb:1.11.106") testLibrary("com.amazonaws:aws-java-sdk-ec2:1.11.106") testLibrary("com.amazonaws:aws-java-sdk-kinesis:1.11.106") + testLibrary("com.amazonaws:aws-java-sdk-lambda:1.11.106") testLibrary("com.amazonaws:aws-java-sdk-rds:1.11.106") testLibrary("com.amazonaws:aws-java-sdk-s3:1.11.106") testLibrary("com.amazonaws:aws-java-sdk-sns:1.11.106") diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsExperimentalAttributes.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsExperimentalAttributes.java index de5bc349cacf..41255a2fe3e8 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsExperimentalAttributes.java +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsExperimentalAttributes.java @@ -17,5 +17,11 @@ final class AwsExperimentalAttributes { static final AttributeKey AWS_STREAM_NAME = stringKey("aws.stream.name"); static final AttributeKey AWS_TABLE_NAME = stringKey("aws.table.name"); + // Work is underway to add these two keys to the SemConv AWS registry, in line with other AWS + // resources. + // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/registry/attributes/aws.md#amazon-lambda-attributes + static final AttributeKey AWS_LAMBDA_ARN = stringKey("aws.lambda.function.arn"); + static final AttributeKey AWS_LAMBDA_NAME = stringKey("aws.lambda.function.name"); + private AwsExperimentalAttributes() {} } diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkAttributesExtractor.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkAttributesExtractor.java index e267a958711b..00fef4ab4bbb 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkAttributesExtractor.java +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkAttributesExtractor.java @@ -26,6 +26,8 @@ class AwsSdkAttributesExtractor implements AttributesExtractor, Respo // Copied from AwsIncubatingAttributes private static final AttributeKey AWS_SECRETSMANAGER_SECRET_ARN = stringKey("aws.secretsmanager.secret.arn"); + private static final AttributeKey AWS_LAMBDA_RESOURCE_MAPPING_ID = + stringKey("aws.lambda.resource_mapping.id"); private static final AttributeKey AWS_SNS_TOPIC_ARN = stringKey("aws.sns.topic.arn"); private static final AttributeKey AWS_STEP_FUNCTIONS_ACTIVITY_ARN = stringKey("aws.step_functions.activity.arn"); @@ -46,6 +48,11 @@ private static boolean canGetResponseMetadata() { @Override public void onStart(AttributesBuilder attributes, Context parentContext, Request request) { Object originalRequest = request.getOriginalRequest(); + setAttribute( + attributes, + AWS_LAMBDA_RESOURCE_MAPPING_ID, + originalRequest, + RequestAccess::getLambdaResourceMappingId); setAttribute(attributes, AWS_SNS_TOPIC_ARN, originalRequest, RequestAccess::getSnsTopicArn); setAttribute( attributes, @@ -69,6 +76,11 @@ public void onEnd( Object awsResp = getAwsResponse(response); if (awsResp != null) { setAttribute(attributes, AWS_SECRETSMANAGER_SECRET_ARN, awsResp, RequestAccess::getSecretArn); + setAttribute( + attributes, + AWS_LAMBDA_RESOURCE_MAPPING_ID, + awsResp, + RequestAccess::getLambdaResourceMappingId); setAttribute(attributes, AWS_SNS_TOPIC_ARN, awsResp, RequestAccess::getSnsTopicArn); setAttribute( attributes, diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkExperimentalAttributesExtractor.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkExperimentalAttributesExtractor.java index 76a4576b2787..a30010fc58b7 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkExperimentalAttributesExtractor.java +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkExperimentalAttributesExtractor.java @@ -7,6 +7,8 @@ import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_AGENT; import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_BUCKET_NAME; +import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_LAMBDA_ARN; +import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_LAMBDA_NAME; import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_QUEUE_NAME; import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_QUEUE_URL; import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_STREAM_NAME; @@ -35,6 +37,7 @@ public void onStart(AttributesBuilder attributes, Context parentContext, Request setRequestAttribute(attributes, AWS_QUEUE_NAME, originalRequest, RequestAccess::getQueueName); setRequestAttribute(attributes, AWS_STREAM_NAME, originalRequest, RequestAccess::getStreamName); setRequestAttribute(attributes, AWS_TABLE_NAME, originalRequest, RequestAccess::getTableName); + setRequestAttribute(attributes, AWS_LAMBDA_NAME, originalRequest, RequestAccess::getLambdaName); } private static void setRequestAttribute( @@ -54,5 +57,17 @@ public void onEnd( Context context, Request request, @Nullable Response response, - @Nullable Throwable error) {} + @Nullable Throwable error) { + Object awsResponse = getAwsResponse(response); + if (awsResponse != null) { + setRequestAttribute(attributes, AWS_LAMBDA_ARN, awsResponse, RequestAccess::getLambdaArn); + } + } + + private static Object getAwsResponse(Response response) { + if (response == null) { + return null; + } + return response.getAwsResponse(); + } } diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/RequestAccess.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/RequestAccess.java index 537b9d997c58..6f852031549d 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/RequestAccess.java +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/RequestAccess.java @@ -11,6 +11,7 @@ import javax.annotation.Nullable; final class RequestAccess { + private static final String LAMBDA_REQUEST_CLASS_PREFIX = "com.amazonaws.services.lambda.model."; private static final String SECRETS_MANAGER_REQUEST_CLASS_PREFIX = "com.amazonaws.services.secretsmanager.model."; private static final String STEP_FUNCTIONS_REQUEST_CLASS_PREFIX = @@ -24,6 +25,30 @@ protected RequestAccess computeValue(Class type) { } }; + @Nullable + static String getLambdaArn(Object request) { + RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); + if (access.getLambdaConfiguration == null) { + return null; + } + Object config = invokeOrNull(access.getLambdaConfiguration, request, Object.class); + return config != null + ? invokeOrNull(LambdaFunctionConfigurationAccess.getLambdaArnFromConfiguration, config) + : null; + } + + @Nullable + static String getLambdaName(Object request) { + RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); + return invokeOrNull(access.getLambdaName, request); + } + + @Nullable + static String getLambdaResourceMappingId(Object request) { + RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); + return invokeOrNull(access.getLambdaResourceMappingId, request); + } + @Nullable static String getSecretArn(Object request) { RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); @@ -86,17 +111,26 @@ static String getTargetArn(Object request) { @Nullable private static String invokeOrNull(@Nullable MethodHandle method, Object obj) { + return invokeOrNull(method, obj, String.class); + } + + @Nullable + private static T invokeOrNull( + @Nullable MethodHandle method, Object obj, Class returnType) { if (method == null) { return null; } try { - return (String) method.invoke(obj); + return returnType.cast(method.invoke(obj)); } catch (Throwable t) { return null; } } @Nullable private final MethodHandle getBucketName; + @Nullable private final MethodHandle getLambdaConfiguration; + @Nullable private final MethodHandle getLambdaName; + @Nullable private final MethodHandle getLambdaResourceMappingId; @Nullable private final MethodHandle getQueueUrl; @Nullable private final MethodHandle getQueueName; @Nullable private final MethodHandle getSecretArn; @@ -116,6 +150,10 @@ private RequestAccess(Class clz) { getTopicArn = findAccessorOrNull(clz, "getTopicArn"); getTargetArn = findAccessorOrNull(clz, "getTargetArn"); + boolean isLambda = clz.getName().startsWith(LAMBDA_REQUEST_CLASS_PREFIX); + getLambdaConfiguration = isLambda ? findLambdaGetConfigurationMethod(clz) : null; + getLambdaName = isLambda ? findAccessorOrNull(clz, "getFunctionName") : null; + getLambdaResourceMappingId = isLambda ? findAccessorOrNull(clz, "getUUID") : null; boolean isSecretsManager = clz.getName().startsWith(SECRETS_MANAGER_REQUEST_CLASS_PREFIX); getSecretArn = isSecretsManager ? findAccessorOrNull(clz, "getARN") : null; boolean isStepFunction = clz.getName().startsWith(STEP_FUNCTIONS_REQUEST_CLASS_PREFIX); @@ -125,11 +163,43 @@ private RequestAccess(Class clz) { @Nullable private static MethodHandle findAccessorOrNull(Class clz, String methodName) { + return findAccessorOrNull(clz, methodName, String.class); + } + + @Nullable + private static MethodHandle findAccessorOrNull( + Class clz, String methodName, Class returnType) { try { return MethodHandles.publicLookup() - .findVirtual(clz, methodName, MethodType.methodType(String.class)); + .findVirtual(clz, methodName, MethodType.methodType(returnType)); } catch (Throwable t) { return null; } } + + @Nullable + private static MethodHandle findLambdaGetConfigurationMethod(Class clz) { + try { + Class returnType = + Class.forName("com.amazonaws.services.lambda.model.FunctionConfiguration"); + return findAccessorOrNull(clz, "getConfiguration", returnType); + } catch (Throwable t) { + return null; + } + } + + private static class LambdaFunctionConfigurationAccess { + static final MethodHandle getLambdaArnFromConfiguration = findGetLambdaArnMethod(); + + @Nullable + private static MethodHandle findGetLambdaArnMethod() { + try { + Class lambdaConfigurationClass = + Class.forName("com.amazonaws.services.lambda.model.FunctionConfiguration"); + return findAccessorOrNull(lambdaConfigurationClass, "getFunctionArn"); + } catch (Throwable t) { + return null; + } + } + } } diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v1_11/LambdaClientTest.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v1_11/LambdaClientTest.java new file mode 100644 index 000000000000..8733953bec36 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v1_11/LambdaClientTest.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11; + +import com.amazonaws.services.lambda.AWSLambdaClientBuilder; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class LambdaClientTest extends AbstractLambdaClientTest { + @RegisterExtension + private static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } + + @Override + public AWSLambdaClientBuilder configureClient(AWSLambdaClientBuilder clientBuilder) { + return clientBuilder.withRequestHandlers( + AwsSdkTelemetry.builder(testing().getOpenTelemetry()) + .setCaptureExperimentalSpanAttributes(true) + .build() + .newRequestHandler()); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/testing/build.gradle.kts b/instrumentation/aws-sdk/aws-sdk-1.11/testing/build.gradle.kts index 793c6603ddba..4ae37295f997 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/testing/build.gradle.kts +++ b/instrumentation/aws-sdk/aws-sdk-1.11/testing/build.gradle.kts @@ -10,6 +10,7 @@ dependencies { compileOnly("com.amazonaws:aws-java-sdk-dynamodb:1.11.106") compileOnly("com.amazonaws:aws-java-sdk-ec2:1.11.106") compileOnly("com.amazonaws:aws-java-sdk-kinesis:1.11.106") + compileOnly("com.amazonaws:aws-java-sdk-lambda:1.11.106") compileOnly("com.amazonaws:aws-java-sdk-rds:1.11.106") compileOnly("com.amazonaws:aws-java-sdk-s3:1.11.106") compileOnly("com.amazonaws:aws-java-sdk-secretsmanager:1.12.80") diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractLambdaClientTest.java b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractLambdaClientTest.java new file mode 100644 index 000000000000..9564aef4cf48 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractLambdaClientTest.java @@ -0,0 +1,188 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.semconv.incubating.AwsIncubatingAttributes.AWS_LAMBDA_RESOURCE_MAPPING_ID; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; + +import com.amazonaws.services.lambda.AWSLambda; +import com.amazonaws.services.lambda.AWSLambdaClientBuilder; +import com.amazonaws.services.lambda.model.CreateEventSourceMappingRequest; +import com.amazonaws.services.lambda.model.GetEventSourceMappingRequest; +import com.amazonaws.services.lambda.model.GetFunctionRequest; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +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 java.util.List; +import java.util.function.Function; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public abstract class AbstractLambdaClientTest extends AbstractBaseAwsClientTest { + private static final String lambdaCreateEventSourceMappingResponseBody = + "{" + + "\"UUID\": \"e31def54-5e5d-4c1b-8e0f-bf1b11c137b7\"," + + "\"BatchSize\": 10," + + "\"MaximumBatchingWindowInSeconds\": 0," + + "\"EventSourceArn\": \"arn:aws:sqs:us-west-2:123456789012:MyTestQueue.fifo\"," + + "\"FunctionArn\": \"arn:aws:lambda:us-west-2:123456789012:function:myFn01-Temp\"," + + "\"LastModified\": \"1.754882043E9\"," + + "\"State\": \"Creating\"," + + "\"StateTransitionReason\": \"USER_INITIATED\"," + + "\"FunctionResponseTypes\": []," + + "\"EventSourceMappingArn\": \"arn:aws:lambda:us-west-2:123456789012:event-source-mapping:e31def54-5e5d-4c1b-8e0f-bf1b11c137b7\"" + + "}"; + + private static final String lambdaGetEventSourceMappingResponseBody = + "{" + + "\"UUID\": \"e31def54-5e5d-4c1b-8e0f-bf1b11c138c8\"," + + "\"BatchSize\": 10," + + "\"MaximumBatchingWindowInSeconds\": 0," + + "\"EventSourceArn\": \"arn:aws:sqs:us-west-2:123456789012:MyTestQueue.fifo\"," + + "\"FunctionArn\": \"arn:aws:lambda:us-west-2:123456789012:function:myFn01-Temp\"," + + "\"LastModified\": \"1.755054843E9\"," + + "\"State\": \"Enabled\"," + + "\"StateTransitionReason\": \"USER_INITIATED\"," + + "\"FunctionResponseTypes\": []," + + "\"EventSourceMappingArn\": \"arn:aws:lambda:us-west-2:123456789012:event-source-mapping:e31def54-5e5d-4c1b-8e0f-bf1b11c138c8\"" + + "}"; + + private static final String lambdaGetFunctionResponseBody = + "{" + + "\"Configuration\": {" + + " \"FunctionName\": \"lambda-function-name-foo\"," + + " \"FunctionArn\": \"arn:aws:lambda:us-west-2:123456789012:function:lambda-function-name-foo\"," + + " \"Runtime\": \"nodejs22.x\"," + + " \"Role\": \"arn:aws:iam::123456789012:role/service-role/Fn-role-pr7kt0bf\"," + + " \"Handler\": \"index.handler\"," + + " \"CodeSize\": 295," + + " \"Description\": \"\"," + + " \"Timeout\": 3," + + " \"MemorySize\": 128," + + " \"LastModified\": \"1.743573094E9\"," + + " \"CodeSha256\": \"q8E7Nexf5xxhKT9/d4bGpAYOXJYFAUjJ0UDj8OivK8E=\"," + + " \"Version\": \"$LATEST\"," + + " \"Environment\": {" + + " \"Variables\": {" + + " \"AWS_LAMBDA_EXEC_WRAPPER\": \"/opt/otel-instrument\"" + + "}" + + " }," + + " \"TracingConfig\": {" + + " \"Mode\": \"PassThrough\"" + + " }," + + " \"RevisionId\": \"955247dc-c724-4653-8f8b-f702c6f9c389\"," + + " \"Layers\": [" + + " {" + + " \"Arn\": \"arn:aws:lambda:us-west-2:615299751070:layer:AWSOpenTelemetryDistroJs:6\"," + + " \"CodeSize\": 14455992" + + " }" + + " ]," + + " \"State\": \"Active\"," + + " \"LastUpdateStatus\": \"Successful\"," + + " \"PackageType\": \"Zip\"," + + " \"Architectures\": [" + + " \"x86_64\"" + + " ]," + + " \"EphemeralStorage\": {" + + " \"Size\": 512" + + " }," + + " \"SnapStart\": {" + + " \"ApplyOn\": \"None\"," + + " \"OptimizationStatus\": \"Off\"" + + " }," + + " \"RuntimeVersionConfig\": {" + + " \"RuntimeVersionArn\": \"arn:aws:lambda:us-west-2::runtime:fd2e05b324f99edd3c6e17800b2535deb79bcce74b7506d595a94870b3d9bd2e\"" + + " }," + + " \"LoggingConfig\": {" + + " \"LogFormat\": \"Text\"," + + " \"LogGroup\": \"/aws/lambda/mlambda-function-name-foo\"" + + " }" + + " }," + + " \"Code\": {" + + " \"RepositoryType\": \"S3\"," + + " \"Location\": \"https://awslambda-us-west-2-tasks.s3.us-west-2.amazonaws.com/snapshots/123456789012/lambda-function-name-foo-47425b12\"" + + " }" + + "}"; + + public abstract AWSLambdaClientBuilder configureClient(AWSLambdaClientBuilder client); + + @Override + protected boolean hasRequestId() { + return false; + } + + private static Stream provideArguments() { + return Stream.of( + Arguments.of( + "CreateEventSourceMapping", + "POST", + lambdaCreateEventSourceMappingResponseBody, + asList( + equalTo(stringKey("aws.lambda.function.name"), "myFn01-Temp"), + equalTo(AWS_LAMBDA_RESOURCE_MAPPING_ID, "e31def54-5e5d-4c1b-8e0f-bf1b11c137b7")), + (Function) + c -> + c.createEventSourceMapping( + new CreateEventSourceMappingRequest() + .withFunctionName("myFn01-Temp") + .withEventSourceArn( + "arn:aws:sqs:us-west-2:123456789012:MyTestQueue.fifo") + .withEnabled(true) + .withBatchSize(10))), + Arguments.of( + "GetEventSourceMapping", + "GET", + lambdaGetEventSourceMappingResponseBody, + singletonList( + equalTo(AWS_LAMBDA_RESOURCE_MAPPING_ID, "e31def54-5e5d-4c1b-8e0f-bf1b11c138c8")), + (Function) + c -> + c.getEventSourceMapping( + new GetEventSourceMappingRequest() + .withUUID("e31def54-5e5d-4c1b-8e0f-bf1b11c138c8"))), + Arguments.of( + "GetFunction", + "GET", + lambdaGetFunctionResponseBody, + asList( + equalTo(stringKey("aws.lambda.function.name"), "lambda-function-name-foo"), + equalTo( + stringKey("aws.lambda.function.arn"), + "arn:aws:lambda:us-west-2:123456789012:function:lambda-function-name-foo")), + (Function) + c -> + c.getFunction( + new GetFunctionRequest().withFunctionName("lambda-function-name-foo")))); + } + + @ParameterizedTest + @MethodSource("provideArguments") + void testSendRequestWithMockedResponse( + String operation, + String method, + String responseBody, + List additionalAttributes, + Function call) + throws Exception { + AWSLambdaClientBuilder clientBuilder = AWSLambdaClientBuilder.standard(); + AWSLambda client = + configureClient(clientBuilder) + .withEndpointConfiguration(endpoint) + .withCredentials(credentialsProvider) + .build(); + + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, responseBody)); + Object response = call.apply(client); + assertRequestWithMockedResponse( + response, client, "AWSLambda", operation, method, additionalAttributes); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/build.gradle.kts b/instrumentation/aws-sdk/aws-sdk-2.2/library/build.gradle.kts index 2fe8f5ded759..c832ee002c1e 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/build.gradle.kts +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/build.gradle.kts @@ -24,6 +24,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:secretsmanager:2.2.0") diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/AwsExperimentalAttributes.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/AwsExperimentalAttributes.java new file mode 100644 index 000000000000..cd4055f3a12c --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/AwsExperimentalAttributes.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2.internal; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; + +import io.opentelemetry.api.common.AttributeKey; + +final class AwsExperimentalAttributes { + // Work is underway to add these two keys to the SemConv AWS registry, in line with other AWS + // resources. + // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/registry/attributes/aws.md#amazon-lambda-attributes + static final AttributeKey AWS_LAMBDA_ARN = stringKey("aws.lambda.function.arn"); + static final AttributeKey AWS_LAMBDA_NAME = stringKey("aws.lambda.function.name"); + + private AwsExperimentalAttributes() {} +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/AwsSdkRequest.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/AwsSdkRequest.java index 3f7fd57bb994..ae1f9f0f5adb 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/AwsSdkRequest.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/AwsSdkRequest.java @@ -8,6 +8,7 @@ import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsSdkRequestType.BEDROCK_RUNTIME; import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsSdkRequestType.DYNAMODB; import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsSdkRequestType.KINESIS; +import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsSdkRequestType.LAMBDA; import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsSdkRequestType.S3; import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsSdkRequestType.SECRETSMANAGER; import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsSdkRequestType.SNS; @@ -37,6 +38,7 @@ enum AwsSdkRequest { SnsRequest(SNS, "SnsRequest"), SqsRequest(SQS, "SqsRequest"), KinesisRequest(KINESIS, "KinesisRequest"), + LambdaRequest(LAMBDA, "LambdaRequest"), SecretsManagerRequest(SECRETSMANAGER, "SecretsManagerRequest"), StepFunctionsRequest(STEPFUNCTIONS, "SfnRequest"), // specific requests diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/AwsSdkRequestType.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/AwsSdkRequestType.java index 962eba594b52..cd71b596657d 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/AwsSdkRequestType.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/AwsSdkRequestType.java @@ -6,6 +6,8 @@ package io.opentelemetry.instrumentation.awssdk.v2_2.internal; import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsExperimentalAttributes.AWS_LAMBDA_ARN; +import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsExperimentalAttributes.AWS_LAMBDA_NAME; import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.FieldMapping.request; import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.FieldMapping.response; @@ -20,6 +22,11 @@ enum AwsSdkRequestType { KINESIS(request("aws.stream.name", "StreamName")), DYNAMODB(request("aws.table.name", "TableName")), BEDROCK_RUNTIME(), + LAMBDA( + request(AWS_LAMBDA_NAME.getKey(), "FunctionName"), + request(AttributeKeys.AWS_LAMBDA_RESOURCE_MAPPING_ID.getKey(), "UUID"), + response(AWS_LAMBDA_ARN.getKey(), "Configuration.FunctionArn"), + response(AttributeKeys.AWS_LAMBDA_RESOURCE_MAPPING_ID.getKey(), "UUID")), SECRETSMANAGER(response(AttributeKeys.AWS_SECRETSMANAGER_SECRET_ARN.getKey(), "ARN")), SNS( /* @@ -47,6 +54,8 @@ List fields(FieldMapping.Type type) { private static class AttributeKeys { // Copied from AwsIncubatingAttributes + private static final AttributeKey AWS_LAMBDA_RESOURCE_MAPPING_ID = + stringKey("aws.lambda.resource_mapping.id"); static final AttributeKey AWS_SECRETSMANAGER_SECRET_ARN = stringKey("aws.secretsmanager.secret.arn"); static final AttributeKey AWS_SNS_TOPIC_ARN = stringKey("aws.sns.topic.arn"); diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientTest.java index 6629076e47d2..51b23fd75f66 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientTest.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientTest.java @@ -13,6 +13,7 @@ import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS; import static io.opentelemetry.semconv.ServerAttributes.SERVER_PORT; import static io.opentelemetry.semconv.UrlAttributes.URL_FULL; +import static io.opentelemetry.semconv.incubating.AwsIncubatingAttributes.AWS_LAMBDA_RESOURCE_MAPPING_ID; import static io.opentelemetry.semconv.incubating.AwsIncubatingAttributes.AWS_REQUEST_ID; import static io.opentelemetry.semconv.incubating.AwsIncubatingAttributes.AWS_SECRETSMANAGER_SECRET_ARN; import static io.opentelemetry.semconv.incubating.AwsIncubatingAttributes.AWS_SNS_TOPIC_ARN; @@ -72,6 +73,13 @@ import software.amazon.awssdk.services.kinesis.KinesisClient; import software.amazon.awssdk.services.kinesis.KinesisClientBuilder; import software.amazon.awssdk.services.kinesis.model.DeleteStreamRequest; +import software.amazon.awssdk.services.lambda.LambdaAsyncClient; +import software.amazon.awssdk.services.lambda.LambdaAsyncClientBuilder; +import software.amazon.awssdk.services.lambda.LambdaClient; +import software.amazon.awssdk.services.lambda.LambdaClientBuilder; +import software.amazon.awssdk.services.lambda.model.CreateEventSourceMappingRequest; +import software.amazon.awssdk.services.lambda.model.GetEventSourceMappingRequest; +import software.amazon.awssdk.services.lambda.model.GetFunctionRequest; import software.amazon.awssdk.services.rds.RdsAsyncClient; import software.amazon.awssdk.services.rds.RdsAsyncClientBuilder; import software.amazon.awssdk.services.rds.RdsClient; @@ -128,6 +136,91 @@ public abstract class AbstractAws2ClientTest extends AbstractAws2ClientCoreTest + " standard" + ""; + private static final String lambdaCreateEventSourceMappingResponseBody = + "{" + + "\"UUID\": \"e31def54-5e5d-4c1b-8e0f-bf1b11c137b7\"," + + "\"BatchSize\": 10," + + "\"MaximumBatchingWindowInSeconds\": 0," + + "\"EventSourceArn\": \"arn:aws:sqs:us-west-2:123456789012:MyTestQueue.fifo\"," + + "\"FunctionArn\": \"arn:aws:lambda:us-west-2:123456789012:function:myFn01-Temp\"," + + "\"LastModified\": \"1.754882043E9\"," + + "\"State\": \"Creating\"," + + "\"StateTransitionReason\": \"USER_INITIATED\"," + + "\"FunctionResponseTypes\": []," + + "\"EventSourceMappingArn\": \"arn:aws:lambda:us-west-2:123456789012:event-source-mapping:e31def54-5e5d-4c1b-8e0f-bf1b11c137b7\"" + + "}"; + + private static final String lambdaGetEventSourceMappingResponseBody = + "{" + + "\"UUID\": \"e31def54-5e5d-4c1b-8e0f-bf1b11c138c8\"," + + "\"BatchSize\": 10," + + "\"MaximumBatchingWindowInSeconds\": 0," + + "\"EventSourceArn\": \"arn:aws:sqs:us-west-2:123456789012:MyTestQueue.fifo\"," + + "\"FunctionArn\": \"arn:aws:lambda:us-west-2:123456789012:function:myFn01-Temp\"," + + "\"LastModified\": \"1.755054843E9\"," + + "\"State\": \"Enabled\"," + + "\"StateTransitionReason\": \"USER_INITIATED\"," + + "\"FunctionResponseTypes\": []," + + "\"EventSourceMappingArn\": \"arn:aws:lambda:us-west-2:123456789012:event-source-mapping:e31def54-5e5d-4c1b-8e0f-bf1b11c138c8\"" + + "}"; + + private static final String lambdaGetFunctionResponseBody = + "{" + + "\"Configuration\": {" + + " \"FunctionName\": \"lambda-function-name-foo\"," + + " \"FunctionArn\": \"arn:aws:lambda:us-west-2:123456789012:function:lambda-function-name-foo\"," + + " \"Runtime\": \"nodejs22.x\"," + + " \"Role\": \"arn:aws:iam::123456789012:role/service-role/Fn-role-pr7kt0bf\"," + + " \"Handler\": \"index.handler\"," + + " \"CodeSize\": 295," + + " \"Description\": \"\"," + + " \"Timeout\": 3," + + " \"MemorySize\": 128," + + " \"LastModified\": \"1.743573094E9\"," + + " \"CodeSha256\": \"q8E7Nexf5xxhKT9/d4bGpAYOXJYFAUjJ0UDj8OivK8E=\"," + + " \"Version\": \"$LATEST\"," + + " \"Environment\": {" + + " \"Variables\": {" + + " \"AWS_LAMBDA_EXEC_WRAPPER\": \"/opt/otel-instrument\"" + + "}" + + " }," + + " \"TracingConfig\": {" + + " \"Mode\": \"PassThrough\"" + + " }," + + " \"RevisionId\": \"955247dc-c724-4653-8f8b-f702c6f9c389\"," + + " \"Layers\": [" + + " {" + + " \"Arn\": \"arn:aws:lambda:us-west-2:615299751070:layer:AWSOpenTelemetryDistroJs:6\"," + + " \"CodeSize\": 14455992" + + " }" + + " ]," + + " \"State\": \"Active\"," + + " \"LastUpdateStatus\": \"Successful\"," + + " \"PackageType\": \"Zip\"," + + " \"Architectures\": [" + + " \"x86_64\"" + + " ]," + + " \"EphemeralStorage\": {" + + " \"Size\": 512" + + " }," + + " \"SnapStart\": {" + + " \"ApplyOn\": \"None\"," + + " \"OptimizationStatus\": \"Off\"" + + " }," + + " \"RuntimeVersionConfig\": {" + + " \"RuntimeVersionArn\": \"arn:aws:lambda:us-west-2::runtime:fd2e05b324f99edd3c6e17800b2535deb79bcce74b7506d595a94870b3d9bd2e\"" + + " }," + + " \"LoggingConfig\": {" + + " \"LogFormat\": \"Text\"," + + " \"LogGroup\": \"/aws/lambda/mlambda-function-name-foo\"" + + " }" + + " }," + + " \"Code\": {" + + " \"RepositoryType\": \"S3\"," + + " \"Location\": \"https://awslambda-us-west-2-tasks.s3.us-west-2.amazonaws.com/snapshots/123456789012/lambda-function-name-foo-47425b12\"" + + " }" + + "}"; + private static final String rdsBodyContent = "" + " 0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99" @@ -307,6 +400,31 @@ private void clientAssertions( "arn:aws:secretsmanager:us-east-1:123456789012:secret:MySecretFromCLI-sNkBwD")); } + if (service.equals("Lambda")) { + switch (operation) { + case "GetFunction": + attributes.add( + equalTo(stringKey("aws.lambda.function.name"), "lambda-function-name-foo")); + attributes.add( + equalTo( + stringKey("aws.lambda.function.arn"), + "arn:aws:lambda:us-west-2:123456789012:function:lambda-function-name-foo")); + break; + case "CreateEventSourceMapping": + attributes.add(equalTo(stringKey("aws.lambda.function.name"), "myFn01-Temp")); + attributes.add( + equalTo(AWS_LAMBDA_RESOURCE_MAPPING_ID, "e31def54-5e5d-4c1b-8e0f-bf1b11c137b7")); + break; + case "GetEventSourceMapping": + attributes.add( + equalTo(AWS_LAMBDA_RESOURCE_MAPPING_ID, "e31def54-5e5d-4c1b-8e0f-bf1b11c138c8")); + break; + default: + attributes.add(equalTo(AWS_LAMBDA_RESOURCE_MAPPING_ID, "Bug-Unknown-Lambda-Operation")); + break; + } + } + String evaluatedOperation; SpanKind operationKind; if (operation.equals("SendMessage")) { @@ -948,4 +1066,120 @@ void testSecretsManagerAsyncSendOperationRequestWithBuilder() { client.getSecretValue(GetSecretValueRequest.builder().secretId("MySecretFromCLI").build()); clientAssertions("SecretsManager", "GetSecretValue", "POST", response, "UNKNOWN"); } + + private static Stream provideLambdaArguments() { + return Stream.of( + Arguments.of( + (Function) + c -> + c.getFunction( + GetFunctionRequest.builder() + .functionName("lambda-function-name-foo") + .build()), + "GetFunction", + "GET", + lambdaGetFunctionResponseBody, + "UNKNOWN"), + Arguments.of( + (Function) + c -> + c.createEventSourceMapping( + CreateEventSourceMappingRequest.builder() + .functionName("myFn01-Temp") + .eventSourceArn("arn:aws:sqs:us-west-2:123456789012:MyTestQueue.fifo") + .enabled(true) + .batchSize(10) + .build()), + "CreateEventSourceMapping", + "POST", + lambdaCreateEventSourceMappingResponseBody, + "UNKNOWN"), + Arguments.of( + (Function) + c -> + c.getEventSourceMapping( + GetEventSourceMappingRequest.builder() + .uuid("e31def54-5e5d-4c1b-8e0f-bf1b11c138c8") + .build()), + "GetEventSourceMapping", + "GET", + lambdaGetEventSourceMappingResponseBody, + "UNKNOWN")); + } + + @ParameterizedTest + @MethodSource("provideLambdaArguments") + void testLambdaSendOperationRequestWithBuilder( + Function call, + String operation, + String method, + String responseBody, + String requestId) { + LambdaClientBuilder builder = LambdaClient.builder(); + configureSdkClient(builder); + LambdaClient client = + builder + .endpointOverride(clientUri) + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, responseBody)); + Object response = call.apply(client); + assertThat(response.getClass().getSimpleName()) + .satisfiesAnyOf( + v -> + assertThat(response) + .isInstanceOf( + software.amazon.awssdk.services.lambda.model.GetFunctionResponse.class), + v -> + assertThat(response) + .isInstanceOf( + software.amazon.awssdk.services.lambda.model + .CreateEventSourceMappingResponse.class), + v -> + assertThat(response) + .isInstanceOf( + software.amazon.awssdk.services.lambda.model.GetEventSourceMappingResponse + .class)); + clientAssertions("Lambda", operation, method, response, requestId); + } + + @ParameterizedTest + @MethodSource("provideLambdaArguments") + void testLambdaAsyncSendOperationRequestWithBuilder( + Function call, + String operation, + String method, + String responseBody, + String requestId) { + LambdaAsyncClientBuilder builder = LambdaAsyncClient.builder(); + configureSdkClient(builder); + LambdaAsyncClient client = + builder + .endpointOverride(clientUri) + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, responseBody)); + Object response = call.apply(wrapClient(LambdaClient.class, LambdaAsyncClient.class, client)); + assertThat(response.getClass().getSimpleName()) + .satisfiesAnyOf( + v -> + assertThat(response) + .isInstanceOf( + software.amazon.awssdk.services.lambda.model.GetFunctionResponse.class), + v -> + assertThat(response) + .isInstanceOf( + software.amazon.awssdk.services.lambda.model + .CreateEventSourceMappingResponse.class), + v -> + assertThat(response) + .isInstanceOf( + software.amazon.awssdk.services.lambda.model.GetEventSourceMappingResponse + .class)); + clientAssertions("Lambda", operation, method, response, requestId); + } }