diff --git a/pom.xml b/pom.xml index 8e847983a..d6acfb8d3 100644 --- a/pom.xml +++ b/pom.xml @@ -14,8 +14,8 @@ --> + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 software.amazon.lambda @@ -25,7 +25,8 @@ Powertools for AWS Lambda (Java) - Parent - A suite of utilities for AWS Lambda Functions that makes tracing with AWS X-Ray, structured logging and creating custom metrics asynchronously easier. + A suite of utilities for AWS Lambda Functions that makes tracing with AWS X-Ray, structured logging and creating + custom metrics asynchronously easier. https://github.com/aws-powertools/powertools-lambda-java @@ -77,6 +78,7 @@ powertools-parameters/powertools-parameters-appconfig powertools-parameters/powertools-parameters-tests examples + powertools-tracing-opentelemetry @@ -119,6 +121,8 @@ 2.3.0 1.5.0 0.11.5 + 1.65.0 + 1.59.0-alpha @@ -313,6 +317,26 @@ commons-lang3 3.20.0 + + io.opentelemetry + opentelemetry-api + ${opentelemetry-api.version} + + + io.opentelemetry + opentelemetry-sdk + ${opentelemetry-api.version} + + + io.opentelemetry + opentelemetry-exporter-otlp + ${opentelemetry-api.version} + + + io.opentelemetry.contrib + opentelemetry-aws-xray-propagator + ${opentelemetry.aws.xray.propagator.version} + @@ -393,6 +417,12 @@ 3.13.2 test + + io.opentelemetry + opentelemetry-sdk-testing + ${opentelemetry-api.version} + test + @@ -471,7 +501,8 @@ true true - true + true + @@ -692,7 +723,9 @@ maven-surefire-plugin - --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED + --add-opens java.base/java.util=ALL-UNNAMED --add-opens + java.base/java.lang=ALL-UNNAMED + diff --git a/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/SystemWrapper.java b/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/SystemWrapper.java index 6dc4e9d9f..cc8ea39e9 100644 --- a/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/SystemWrapper.java +++ b/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/SystemWrapper.java @@ -22,6 +22,10 @@ public static String getenv(String name) { return System.getenv(name); } + public static boolean containsKey(String key) { + return System.getenv().containsKey(key); + } + public static String getProperty(String name) { return System.getProperty(name); } diff --git a/powertools-tracing-opentelemetry/pom.xml b/powertools-tracing-opentelemetry/pom.xml new file mode 100644 index 000000000..1878c964e --- /dev/null +++ b/powertools-tracing-opentelemetry/pom.xml @@ -0,0 +1,151 @@ + + + + 4.0.0 + + powertools-tracing-opentelemetry + jar + + + software.amazon.lambda + powertools-parent + 2.10.0 + + + Powertools for AWS Lambda (Java) - Tracing OpenTelemetry + + A suite of utilities for AWS Lambda Functions that makes tracing with OpenTelemetry, structured logging and + creating custom metrics asynchronously easier. + + + + + io.opentelemetry + opentelemetry-api + + + io.opentelemetry + opentelemetry-sdk + + + io.opentelemetry + opentelemetry-exporter-otlp + + + io.opentelemetry.contrib + opentelemetry-aws-xray-propagator + + + org.aspectj + aspectjrt + provided + + + software.amazon.lambda + powertools-common + + + software.amazon.awssdk + aws-core + + + software.amazon.awssdk + sdk-core + + + com.amazonaws + aws-lambda-java-core + + + com.amazonaws + aws-lambda-java-events + + + com.fasterxml.jackson.core + jackson-databind + + + + + io.opentelemetry + opentelemetry-sdk-testing + test + + + org.mockito + mockito-core + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + software.amazon.lambda + powertools-common + ${project.version} + test-jar + test + + + org.slf4j + slf4j-simple + test + + + org.junit-pioneer + junit-pioneer + test + + + org.apache.commons + commons-lang3 + test + + + org.aspectj + aspectjweaver + test + + + org.assertj + assertj-core + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + on-demand + + + + + + + \ No newline at end of file diff --git a/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/CaptureMode.java b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/CaptureMode.java new file mode 100644 index 000000000..d62c3b1ff --- /dev/null +++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/CaptureMode.java @@ -0,0 +1,32 @@ +package software.amazon.lambda.powertools.tracing.opentelemetry; + +/** + * Defines how method responses and errors are captured by tracing. + */ +public enum CaptureMode { + + /** + * Capture response and errors according to environment variables. + */ + ENVIRONMENT_VAR, + + /** + * Capture the method response. + */ + RESPONSE, + + /** + * Capture errors thrown by the method. + */ + ERROR, + + /** + * Capture both the method response and errors. + */ + RESPONSE_AND_ERROR, + + /** + * Disable response and error capture. + */ + DISABLED +} \ No newline at end of file diff --git a/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/Tracing.java b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/Tracing.java new file mode 100644 index 000000000..2d4d68608 --- /dev/null +++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/Tracing.java @@ -0,0 +1,43 @@ +package software.amazon.lambda.powertools.tracing.opentelemetry; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to enable OpenTelemetry tracing for the annotated method. + * Automatically creates and manages an OpenTelemetry span for the method invocation. + *

+ * This annotation allows configuration of the namespace, span name, and capture mode + * for tracing purposes. If no explicit configuration is provided, default values are used. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Tracing { + /** + * The namespace associated with the span. + * + *

If empty, the default Powertools service name is used. + * + * @return the namespace + */ + String namespace() default ""; + + /** + * The name of the span. + * + *

If empty, the annotated method name is used. + * + * @return the span name + */ + String spanName() default ""; + + /** + * Controls whether the method response and/or errors are captured + * as span data. + * + * @return the capture mode + */ + CaptureMode captureMode() default CaptureMode.ENVIRONMENT_VAR; +} diff --git a/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/TracingOpenTelemetry.java b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/TracingOpenTelemetry.java new file mode 100644 index 000000000..cd27b8497 --- /dev/null +++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/TracingOpenTelemetry.java @@ -0,0 +1,234 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools.tracing.opentelemetry; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.context.propagation.TextMapSetter; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import software.amazon.lambda.powertools.tracing.opentelemetry.context.LambdaEventContextExtractorResolver; +import software.amazon.lambda.powertools.tracing.opentelemetry.internal.SpanOperation; +import software.amazon.lambda.powertools.tracing.opentelemetry.internal.SpanScope; +import software.amazon.lambda.powertools.tracing.opentelemetry.provider.OpenTelemetryProvider; + + +public final class TracingOpenTelemetry { + + private final Tracer tracer; + private final TextMapPropagator propagator; + private final LambdaEventContextExtractorResolver eventContextExtractorResolver; + + private TracingOpenTelemetry(Builder builder) { + this.tracer = Objects.requireNonNull(builder.tracer, "tracer must not be null"); + this.propagator = Objects.requireNonNull(builder.propagator, "propagator must not be null"); + this.eventContextExtractorResolver = Objects.requireNonNull( + builder.eventContextExtractorResolver, + "eventContextExtractorResolver must not be null" + ); + } + + public TracingOpenTelemetry() { + this(OpenTelemetryProvider.tracer()); + } + + + public TracingOpenTelemetry(Tracer tracer) { + this(tracer, createDefaultPropagator(), createDefaultEventContextExtractorResolver()); + } + + + public TracingOpenTelemetry( + Tracer tracer, + TextMapPropagator propagator, + LambdaEventContextExtractorResolver eventContextExtractorResolver + ) { + + this.tracer = Objects.requireNonNull(tracer, "tracer must not be null"); + this.propagator = Objects.requireNonNull(propagator, "propagator must not be null"); + this.eventContextExtractorResolver = Objects.requireNonNull( + eventContextExtractorResolver, + "eventContextExtractorResolver must not be null" + ); + } + + public Tracer tracer() { + return tracer; + } + + public TextMapPropagator propagator() { + return propagator; + } + + public LambdaEventContextExtractorResolver eventContextExtractorResolver() { + return eventContextExtractorResolver; + } + + public Span currentSpan() { + return Span.current(); + } + + + public SpanScope addSpan(String name) { + return addSpan(name, SpanKind.INTERNAL); + } + + + public SpanScope addSpan(String name, SpanKind kind) { + + return addSpan(name, kind, Attributes.empty()); + } + + public SpanScope addSpan(String name, SpanKind kind, Attributes attributes) { + + return addSpan(name, kind, attributes, Context.current()); + } + + public SpanScope addSpan(String name, SpanKind kind, Attributes attributes, Context parentContext) { + + return addSpan(name, kind, attributes, parentContext, Collections.emptyList()); + } + + public SpanScope addSpan( + String name, + SpanKind kind, + Attributes attributes, + Context parentContext, + List spanContexts + ) { + + Objects.requireNonNull(name, "name must not be null"); + Objects.requireNonNull(kind, "kind must not be null"); + Objects.requireNonNull(attributes, "attributes must not be null"); + Objects.requireNonNull(parentContext, "parentContext must not be null"); + Objects.requireNonNull(spanContexts, "spanContexts must not be null"); + + SpanBuilder spanBuilder = tracer + .spanBuilder(name) + .setSpanKind(kind) + .setParent(parentContext) + .setAllAttributes(attributes); + + spanContexts.forEach(spanBuilder::addLink); + + return new SpanScope(spanBuilder.startSpan()); + } + + + public T withSpan(String name, SpanOperation operation) throws Exception { + + return withSpan(name, SpanKind.INTERNAL, Attributes.empty(), operation); + } + + public T withSpan( + String name, + SpanKind kind, + Attributes attributes, + SpanOperation operation + ) throws Exception { + Objects.requireNonNull(operation, "operation must not be null"); + + try (SpanScope scope = addSpan(name, kind, attributes)) { + try { + return operation.execute(scope.span()); + } catch (Exception exception) { + scope.recordException(exception); + throw exception; + } + } + } + + public Context extractContext(T carrier, TextMapGetter getter) { + + return extractContext(Context.current(), carrier, getter); + } + + public Context extractContext(Context context, T carrier, TextMapGetter getter) { + + Objects.requireNonNull(context, "context must not be null"); + Objects.requireNonNull(carrier, "carrier must not be null"); + Objects.requireNonNull(getter, "getter must not be null"); + + return propagator.extract(context, carrier, getter); + } + + public void injectContext(T carrier, TextMapSetter setter) { + + injectContext(Context.current(), carrier, setter); + } + + public void injectContext(Context context, T carrier, TextMapSetter setter) { + + Objects.requireNonNull(context, "context must not be null"); + Objects.requireNonNull(carrier, "carrier must not be null"); + Objects.requireNonNull(setter, "setter must not be null"); + + propagator.inject(context, carrier, setter); + } + + private static TextMapPropagator createDefaultPropagator() { + return OpenTelemetryProvider.propagator(); + } + + private static LambdaEventContextExtractorResolver createDefaultEventContextExtractorResolver() { + return LambdaEventContextExtractorResolver.create(); + } + + public static TracingOpenTelemetry create() { + return new TracingOpenTelemetry(); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + + private Tracer tracer; + private TextMapPropagator propagator = createDefaultPropagator(); + private LambdaEventContextExtractorResolver eventContextExtractorResolver = + createDefaultEventContextExtractorResolver(); + + public Builder tracer(Tracer tracer) { + this.tracer = tracer; + return this; + } + + public Builder propagator(TextMapPropagator propagator) { + this.propagator = propagator; + return this; + } + + public Builder eventContextExtractorResolver( + LambdaEventContextExtractorResolver eventContextExtractorResolver) { + this.eventContextExtractorResolver = eventContextExtractorResolver; + return this; + } + + public TracingOpenTelemetry build() { + return new TracingOpenTelemetry(this); + } + } +} \ No newline at end of file diff --git a/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/ApiGatewayTraceContextExtractor.java b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/ApiGatewayTraceContextExtractor.java new file mode 100644 index 000000000..a5a0d36a0 --- /dev/null +++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/ApiGatewayTraceContextExtractor.java @@ -0,0 +1,101 @@ +package software.amazon.lambda.powertools.tracing.opentelemetry.context; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapPropagator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import software.amazon.lambda.powertools.tracing.opentelemetry.provider.OpenTelemetryProvider; + +public final class ApiGatewayTraceContextExtractor implements LambdaEventContextExtractor { + + + @Override + public boolean supports(Object event) { + return event instanceof APIGatewayProxyRequestEvent; + } + + @Override + public ExtractedTraceContext extract(Object event, Context parentContext, TextMapPropagator propagator) { + + APIGatewayProxyRequestEvent apiGatewayEvent = (APIGatewayProxyRequestEvent) event; + + Map headers = apiGatewayEvent.getHeaders(); + + if (headers == null || headers.isEmpty()) { + return new ExtractedTraceContext(parentContext, List.of(), SpanKind.SERVER); + } + + Context context = propagator.extract( + parentContext, + headers, + OpenTelemetryProvider.textMapGetter() + ); + + return new ExtractedTraceContext(context, List.of(), SpanKind.SERVER); + } + + @Override + public void enrichSpan(Object event, Span span) { + + APIGatewayProxyRequestEvent apiGatewayEvent = (APIGatewayProxyRequestEvent) event; + + if (apiGatewayEvent.getHttpMethod() != null) { + span.setAttribute("http.request.method", apiGatewayEvent.getHttpMethod()); + } + + if (apiGatewayEvent.getPath() != null) { + span.setAttribute("url.path", apiGatewayEvent.getPath()); + } + + if (apiGatewayEvent.getQueryStringParameters() != null) { + + String queryString = apiGatewayEvent.getQueryStringParameters() + .entrySet() + .stream() + .map(entry -> entry.getKey() + "=" + entry.getValue()) + .collect(Collectors.joining("&")); + + span.setAttribute("url.query", queryString); + } + + if (apiGatewayEvent.getHeaders() != null) { + + apiGatewayEvent.getHeaders() + .entrySet() + .stream() + .filter(entry -> "user-agent".equalsIgnoreCase(entry.getKey())) + .map(Map.Entry::getValue) + .findFirst() + .ifPresent(userAgent -> span.setAttribute("user_agent.original", userAgent)); + } + + if (apiGatewayEvent.getRequestContext() != null) { + + APIGatewayProxyRequestEvent.ProxyRequestContext requestContext = + apiGatewayEvent.getRequestContext(); + + if (requestContext.getRequestId() != null) { + span.setAttribute("aws.request_id", requestContext.getRequestId()); + } + + if (requestContext.getStage() != null) { + span.setAttribute("aws.apigateway.stage", requestContext.getStage()); + } + + if (requestContext.getResourceId() != null) { + span.setAttribute("aws.apigateway.resource_id", requestContext.getResourceId()); + } + + if (requestContext.getResourcePath() != null) { + span.setAttribute("aws.apigateway.resource_path", requestContext.getResourcePath()); + } + } + + } + + +} \ No newline at end of file diff --git a/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/DynamoDbTraceContextExtractor.java b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/DynamoDbTraceContextExtractor.java new file mode 100644 index 000000000..3f04b6ace --- /dev/null +++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/DynamoDbTraceContextExtractor.java @@ -0,0 +1,67 @@ +package software.amazon.lambda.powertools.tracing.opentelemetry.context; + +import com.amazonaws.services.lambda.runtime.events.DynamodbEvent; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapPropagator; +import java.util.List; +import java.util.Objects; + +public final class DynamoDbTraceContextExtractor implements LambdaEventContextExtractor { + + @Override + public boolean supports(Object event) { + return event instanceof DynamodbEvent; + } + + @Override + public ExtractedTraceContext extract(Object event, Context parentContext, TextMapPropagator propagator) { + + /* + * DynamoDB Streams records do not expose message attributes + * that can be used for W3C trace context propagation. + * + * Do not assume that traceparent is stored inside the DynamoDB + * record payload. Propagation through DynamoDB Streams should be + * defined by a dedicated propagation strategy if supported in + * the future. + */ + return new ExtractedTraceContext(parentContext, List.of(), SpanKind.CONSUMER); + } + + @Override + public void enrichSpan(Object event, Span span) { + + DynamodbEvent dynamoDBEvent = (DynamodbEvent) event; + + if (dynamoDBEvent.getRecords() == null || dynamoDBEvent.getRecords().isEmpty()) { + return; + } + + DynamodbEvent.DynamodbStreamRecord record = dynamoDBEvent.getRecords() + .stream() + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + + if (record == null) { + return; + } + + span.setAttribute("messaging.system", "aws.dynamodb"); + + span.setAttribute("messaging.batch.message_count", dynamoDBEvent.getRecords().size()); + if (record.getEventSourceARN() != null) { + span.setAttribute("messaging.destination.name", extractStreamName(record.getEventSourceARN())); + } + } + + private String extractStreamName(String streamArn) { + int separator = streamArn.lastIndexOf('/'); + + return separator >= 0 + ? streamArn.substring(separator + 1) + : streamArn; + } +} diff --git a/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/ExtractedTraceContext.java b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/ExtractedTraceContext.java new file mode 100644 index 000000000..23657e5cc --- /dev/null +++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/ExtractedTraceContext.java @@ -0,0 +1,37 @@ +package software.amazon.lambda.powertools.tracing.opentelemetry.context; + +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import java.util.List; + +public final class ExtractedTraceContext { + + private final Context parentContext; + private final List spanContexts; + private final SpanKind spanKind; + + public ExtractedTraceContext(Context parentContext, List spanContexts, SpanKind spanKind) { + this.parentContext = parentContext; + this.spanContexts = spanContexts; + this.spanKind = spanKind; + } + + public ExtractedTraceContext(Context parentContext, List spanContexts) { + this.parentContext = parentContext; + this.spanContexts = spanContexts; + this.spanKind = SpanKind.SERVER; + } + + public Context context() { + return parentContext; + } + + public List spanContexts() { + return spanContexts; + } + + public SpanKind spanKind() { + return spanKind; + } +} diff --git a/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/KinesisTraceContextExtractor.java b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/KinesisTraceContextExtractor.java new file mode 100644 index 000000000..92c9bc977 --- /dev/null +++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/KinesisTraceContextExtractor.java @@ -0,0 +1,79 @@ +package software.amazon.lambda.powertools.tracing.opentelemetry.context; + +import com.amazonaws.services.lambda.runtime.events.KinesisEvent; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapPropagator; +import java.util.List; + +public final class KinesisTraceContextExtractor + implements LambdaEventContextExtractor { + + @Override + public boolean supports(Object event) { + return event instanceof KinesisEvent; + } + + @Override + public ExtractedTraceContext extract(Object event, Context parentContext, TextMapPropagator propagator) { + + /* + * Kinesis records do not expose message attributes + * that can be used for W3C trace context propagation. + * + * Do not assume that traceparent is stored inside the Kinesis + * record payload. Propagation through Kinesis should be + * defined by a dedicated propagation strategy if supported in + * the future. + */ + + return new ExtractedTraceContext(parentContext, List.of(), SpanKind.CONSUMER); + } + + @Override + public void enrichSpan(Object event, Span span) { + + KinesisEvent kinesisEvent = (KinesisEvent) event; + + if (kinesisEvent.getRecords() == null || kinesisEvent.getRecords().isEmpty()) { + return; + } + + KinesisEvent.KinesisEventRecord firstRecord = kinesisEvent.getRecords().get(0); + + if (firstRecord == null || firstRecord.getKinesis() == null) { + return; + } + + KinesisEvent.Record kinesis = firstRecord.getKinesis(); + + span.setAttribute("messaging.system", "aws.kinesis"); + + if (kinesis.getPartitionKey() != null) { + span.setAttribute("messaging.partition_key", kinesis.getPartitionKey()); + } + + if (kinesis.getSequenceNumber() != null) { + span.setAttribute("messaging.message.id", kinesis.getSequenceNumber()); + } + + if (kinesis.getApproximateArrivalTimestamp() != null) { + span.setAttribute("messaging.message.receive.timestamp", + kinesis.getApproximateArrivalTimestamp().getTime()); + } + + if (firstRecord.getEventSourceARN() != null) { + span.setAttribute("messaging.destination.name", extractStreamName(firstRecord.getEventSourceARN())); + } + } + + + private String extractStreamName(String arn) { + int separator = arn.lastIndexOf('/'); + + return separator >= 0 + ? arn.substring(separator + 1) + : arn; + } +} diff --git a/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/LambdaEventContextExtractor.java b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/LambdaEventContextExtractor.java new file mode 100644 index 000000000..b4d7e12cc --- /dev/null +++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/LambdaEventContextExtractor.java @@ -0,0 +1,14 @@ +package software.amazon.lambda.powertools.tracing.opentelemetry.context; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapPropagator; + +public interface LambdaEventContextExtractor { + + boolean supports(Object event); + + void enrichSpan(Object event, Span span); + + ExtractedTraceContext extract(Object event, Context parentContext, TextMapPropagator propagator); +} \ No newline at end of file diff --git a/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/LambdaEventContextExtractorResolver.java b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/LambdaEventContextExtractorResolver.java new file mode 100644 index 000000000..d0b6674e9 --- /dev/null +++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/LambdaEventContextExtractorResolver.java @@ -0,0 +1,49 @@ +package software.amazon.lambda.powertools.tracing.opentelemetry.context; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapPropagator; +import java.util.List; + +public final class LambdaEventContextExtractorResolver { + + private final List extractors; + + public LambdaEventContextExtractorResolver(List extractors) { + + this.extractors = List.copyOf(extractors); + } + + public static LambdaEventContextExtractorResolver create() { + return new LambdaEventContextExtractorResolver( + List.of( + new ApiGatewayTraceContextExtractor(), + new SqsTraceContextExtractor(), + new SnsTraceContextExtractor(), + new KinesisTraceContextExtractor(), + new DynamoDbTraceContextExtractor(), + new S3TraceContextExtractor() + ) + ); + } + + public ExtractedTraceContext extract(Object event, Context parentContext, TextMapPropagator propagator) { + + return extractors.stream() + .filter(extractor -> extractor.supports(event)) + .findFirst() + .map(extractor -> + extractor.extract( + event, + parentContext, + propagator)) + .orElse(new ExtractedTraceContext(parentContext, List.of())); + } + + public void enrichSpan(Object event, Span span) { + extractors.stream() + .filter(extractor -> extractor.supports(event)) + .findFirst() + .ifPresent(extractor -> extractor.enrichSpan(event, span)); + } +} diff --git a/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/S3TraceContextExtractor.java b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/S3TraceContextExtractor.java new file mode 100644 index 000000000..fee3ca909 --- /dev/null +++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/S3TraceContextExtractor.java @@ -0,0 +1,67 @@ +package software.amazon.lambda.powertools.tracing.opentelemetry.context; + +import com.amazonaws.services.lambda.runtime.events.S3Event; +import com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapPropagator; +import java.util.List; +import java.util.Objects; + +public final class S3TraceContextExtractor implements LambdaEventContextExtractor { + + @Override + public boolean supports(Object event) { + return event instanceof S3Event; + } + + @Override + public ExtractedTraceContext extract(Object event, Context parentContext, TextMapPropagator propagator) { + + /* + * S3 event notifications do not expose message attributes + * equivalent to SQS/SNS that can be passed directly to a + * TextMapPropagator. + * + * Do not assume that traceparent/tracestate are embedded + * inside the S3 event payload. + */ + return new ExtractedTraceContext(parentContext, List.of(), SpanKind.CONSUMER); + } + + @Override + public void enrichSpan(Object event, Span span) { + + S3Event s3Event = (S3Event) event; + + if (s3Event.getRecords() == null || s3Event.getRecords().isEmpty()) { + return; + } + + span.setAttribute("messaging.system", "aws.s3"); + + span.setAttribute("messaging.batch.message_count", s3Event.getRecords().size()); + + S3EventNotification.S3EventNotificationRecord record = + s3Event.getRecords() + .stream() + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + + if (record == null || record.getS3() == null) { + return; + } + + if (record.getS3().getBucket() != null + && record.getS3().getBucket().getName() != null) { + + span.setAttribute("messaging.destination.name", record.getS3().getBucket().getName()); + } + + if (record.getEventName() != null) { + span.setAttribute("messaging.event.type", record.getEventName()); + } + } +} diff --git a/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/SnsTraceContextExtractor.java b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/SnsTraceContextExtractor.java new file mode 100644 index 000000000..9184d4c73 --- /dev/null +++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/SnsTraceContextExtractor.java @@ -0,0 +1,111 @@ +package software.amazon.lambda.powertools.tracing.opentelemetry.context; + +import com.amazonaws.services.lambda.runtime.events.SNSEvent; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapPropagator; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import software.amazon.lambda.powertools.tracing.opentelemetry.provider.OpenTelemetryProvider; + +public final class SnsTraceContextExtractor implements LambdaEventContextExtractor { + + @Override + public boolean supports(Object event) { + return event instanceof SNSEvent; + } + + @Override + public ExtractedTraceContext extract(Object event, Context parentContext, TextMapPropagator propagator) { + + SNSEvent snsEvent = (SNSEvent) event; + + if (snsEvent.getRecords() == null || snsEvent.getRecords().isEmpty()) { + return new ExtractedTraceContext(parentContext, List.of(), SpanKind.CONSUMER); + } + + List spanContexts = new ArrayList<>(); + + for (SNSEvent.SNSRecord record : snsEvent.getRecords()) { + + if (record == null || record.getSNS() == null) { + continue; + } + + Map attributes = record.getSNS().getMessageAttributes(); + + if (attributes == null || attributes.isEmpty()) { + continue; + } + + Map propagationAttributes = attributes.entrySet() + .stream() + .filter(entry -> entry.getValue() != null) + .filter(entry -> entry.getValue().getValue() != null) + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> entry.getValue().getValue() + )); + + if (propagationAttributes.isEmpty()) { + continue; + } + + Context extractedContext = propagator.extract( + Context.root(), + propagationAttributes, + OpenTelemetryProvider.textMapGetter() + ); + + SpanContext spanContext = Span.fromContext(extractedContext).getSpanContext(); + + if (spanContext.isValid()) { + spanContexts.add(spanContext); + } + } + + Context parent = spanContexts.isEmpty() + ? parentContext + : Context.root().with(Span.wrap(spanContexts.get(0))); + + return new ExtractedTraceContext(parent, spanContexts, SpanKind.CONSUMER); + } + + @Override + public void enrichSpan(Object event, Span span) { + + SNSEvent snsEvent = (SNSEvent) event; + + if (snsEvent.getRecords() == null || snsEvent.getRecords().isEmpty()) { + return; + } + + SNSEvent.SNSRecord record = snsEvent.getRecords() + .stream() + .filter(r -> r != null && r.getSNS() != null) + .findFirst() + .orElse(null); + + if (record == null) { + return; + } + + span.setAttribute("messaging.system", "aws.sns"); + + if (record.getSNS().getTopicArn() != null) { + span.setAttribute("messaging.destination.name", extractTopicName(record.getSNS().getTopicArn())); + } + } + + private String extractTopicName(String topicArn) { + int separator = topicArn.lastIndexOf(':'); + + return separator >= 0 + ? topicArn.substring(separator + 1) + : topicArn; + } +} \ No newline at end of file diff --git a/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/SqsTraceContextExtractor.java b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/SqsTraceContextExtractor.java new file mode 100644 index 000000000..c7802b383 --- /dev/null +++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/SqsTraceContextExtractor.java @@ -0,0 +1,100 @@ +package software.amazon.lambda.powertools.tracing.opentelemetry.context; + +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapPropagator; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import software.amazon.lambda.powertools.tracing.opentelemetry.provider.OpenTelemetryProvider; + +public final class SqsTraceContextExtractor implements LambdaEventContextExtractor { + + @Override + public boolean supports(Object event) { + return event instanceof SQSEvent; + } + + @Override + public ExtractedTraceContext extract(Object event, Context parentContext, TextMapPropagator propagator) { + + SQSEvent sqsEvent = (SQSEvent) event; + + if (sqsEvent.getRecords() == null || sqsEvent.getRecords().isEmpty()) { + return new ExtractedTraceContext(parentContext, List.of(), SpanKind.CONSUMER); + } + + List spanContexts = new ArrayList<>(); + + for (SQSEvent.SQSMessage message : sqsEvent.getRecords()) { + + if (message == null || message.getMessageAttributes() == null) { + continue; + } + + Map attributes = message.getMessageAttributes(); + + if (attributes.isEmpty()) { + continue; + } + + Map propagationAttributes = attributes.entrySet() + .stream() + .filter(entry -> entry.getValue() != null) + .filter(entry -> entry.getValue().getStringValue() != null) + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> entry.getValue().getStringValue() + )); + + Context extractedContext = propagator.extract( + Context.root(), + propagationAttributes, + OpenTelemetryProvider.textMapGetter() + ); + + SpanContext spanContext = Span.fromContext(extractedContext).getSpanContext(); + + if (spanContext.isValid()) { + spanContexts.add(spanContext); + } + } + + Context parent = spanContexts.isEmpty() + ? parentContext + : Context.root().with(Span.wrap(spanContexts.get(0))); + + return new ExtractedTraceContext(parent, spanContexts, SpanKind.CONSUMER); + } + + @Override + public void enrichSpan(Object event, Span span) { + SQSEvent sqsEvent = (SQSEvent) event; + + if (sqsEvent.getRecords() == null || sqsEvent.getRecords().isEmpty()) { + return; + } + + span.setAttribute("messaging.system", "aws.sqs"); + + span.setAttribute("messaging.batch.message_count", sqsEvent.getRecords().size()); + + SQSEvent.SQSMessage message = sqsEvent.getRecords().get(0); + + if (message.getEventSourceArn() != null) { + span.setAttribute("messaging.destination.name", extractQueueName(message.getEventSourceArn())); + } + } + + private String extractQueueName(String arn) { + int separator = arn.lastIndexOf(':'); + + return separator >= 0 + ? arn.substring(separator + 1) + : arn; + } +} \ No newline at end of file diff --git a/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/TraceContextPropagationMode.java b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/TraceContextPropagationMode.java new file mode 100644 index 000000000..4e70a753b --- /dev/null +++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/TraceContextPropagationMode.java @@ -0,0 +1,6 @@ +package software.amazon.lambda.powertools.tracing.opentelemetry.context; + +public enum TraceContextPropagationMode { + PARENT, + LINK +} diff --git a/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/AttributesConstants.java b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/AttributesConstants.java new file mode 100644 index 000000000..a9d41acb0 --- /dev/null +++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/AttributesConstants.java @@ -0,0 +1,36 @@ +package software.amazon.lambda.powertools.tracing.opentelemetry.internal; + +public final class AttributesConstants { + + private AttributesConstants() { + // Constant holder class + } + + public static final String AWS_LAMBDA_FUNCTION_NAME = "AWS_LAMBDA_FUNCTION_NAME"; + + public static final String AWS_LAMBDA_FUNCTION_VERSION = "AWS_LAMBDA_FUNCTION_VERSION"; + + public static final String AWS_LAMBDA_FUNCTION_MEMORY_SIZE = "AWS_LAMBDA_FUNCTION_MEMORY_SIZE"; + + public static final String AWS_LAMBDA_LOG_STREAM_NAME = "AWS_LAMBDA_LOG_STREAM_NAME"; + + public static final String AWS_REGION = "AWS_REGION"; + + public static final String AWS_LAMBDA_FUNCTION_ARN = "AWS_LAMBDA_FUNCTION_ARN"; + + public static final String TELEMETRY_DISTRO_NAME = "powertools-for-aws-lambda"; + + public static final String FAAS_COLDSTART = "faas.coldstart"; + + public static final String FAAS_INVOCATION_ID = "faas.invocation_id"; + + public static final String RESPONSE_ATTRIBUTE = "aws.lambda.powertools.response"; + + public static final String CAPTURE_RESPONSE_ENV = "POWERTOOLS_TRACER_CAPTURE_RESPONSE"; + + public static final String CAPTURE_ERROR_ENV = "POWERTOOLS_TRACER_CAPTURE_ERROR"; + + public static final String TRACEPARENT = "traceparent"; + + public static final String TRACESTATE = "tracestate"; +} diff --git a/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/LambdaResource.java b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/LambdaResource.java new file mode 100644 index 000000000..1186c85f4 --- /dev/null +++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/LambdaResource.java @@ -0,0 +1,115 @@ +package software.amazon.lambda.powertools.tracing.opentelemetry.internal; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.resources.Resource; +import software.amazon.lambda.powertools.common.internal.SystemWrapper; + +public final class LambdaResource { + + private LambdaResource() { + } + + public static Resource create() { + AttributesBuilder attributes = Attributes.builder(); + + putIfPresent( + attributes, + "cloud.provider", + "aws" + ); + + putIfPresent( + attributes, + "cloud.region", + SystemWrapper.getenv(AttributesConstants.AWS_REGION) + ); + + putIfPresent( + attributes, + "service.name", + SystemWrapper.getenv(AttributesConstants.AWS_LAMBDA_FUNCTION_NAME) + ); + + putIfPresent( + attributes, + "service.version", + SystemWrapper.getenv(AttributesConstants.AWS_LAMBDA_FUNCTION_VERSION) + ); + + putIfPresent( + attributes, + "faas.name", + SystemWrapper.getenv(AttributesConstants.AWS_LAMBDA_FUNCTION_NAME) + ); + + putIfPresent( + attributes, + "faas.version", + SystemWrapper.getenv(AttributesConstants.AWS_LAMBDA_FUNCTION_VERSION) + ); + + putIfPresent( + attributes, + "faas.instance", + SystemWrapper.getenv(AttributesConstants.AWS_LAMBDA_LOG_STREAM_NAME) + ); + + String memory = SystemWrapper.getenv(AttributesConstants.AWS_LAMBDA_FUNCTION_MEMORY_SIZE); + + if (memory != null) { + attributes.put( + "faas.max_memory", + Long.parseLong(memory) + ); + } + + String functionArn = SystemWrapper.getenv(AttributesConstants.AWS_LAMBDA_FUNCTION_ARN); + + if (functionArn != null) { + String accountId = extractAccountId(functionArn); + + if (accountId != null) { + attributes.put( + "cloud.account.id", + accountId + ); + } + } + + attributes.put( + "telemetry.sdk.name", + "opentelemetry" + ); + + attributes.put( + "telemetry.distro.name", + AttributesConstants.TELEMETRY_DISTRO_NAME + ); + + attributes.put( + "telemetry.sdk.language", + "java" + ); + + return Resource.create(attributes.build()); + } + + private static void putIfPresent( + AttributesBuilder attributes, + String key, + String value) { + + if (value != null && !value.isBlank()) { + attributes.put(key, value); + } + } + + private static String extractAccountId(String arn) { + String[] parts = arn.split(":"); + + return parts.length > 4 + ? parts[4] + : null; + } +} \ No newline at end of file diff --git a/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/SpanOperation.java b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/SpanOperation.java new file mode 100644 index 000000000..a0d589db5 --- /dev/null +++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/SpanOperation.java @@ -0,0 +1,45 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools.tracing.opentelemetry.internal; + +import io.opentelemetry.api.trace.Span; + +/** + * Represents a functional interface used to execute a custom operation within + * the context of a given {@link Span}. This interface requires implementing a + * single method that performs an operation with the span and optionally + * returns a result. + * + *

+ * The {@code SpanOperation} interface enables tracing and manipulation of + * a span during its lifecycle, such as setting attributes, adding events, + * or updating status codes. It can be used alongside frameworks that support + * OpenTelemetry for distributed tracing. + * + * @param the type of result returned by the custom span operation + */ +@FunctionalInterface +public interface SpanOperation { + + /** + * Executes a custom operation within the context of the provided {@link Span}. + * This method allows for interaction with the span, such as adding events, + * setting attributes, or manipulating its status during the operation. + * + * @param span the {@link Span} within whose context the operation will be executed + * @throws Exception if an error occurs during the execution of the operation + */ + T execute(Span span) throws Exception; +} diff --git a/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/SpanScope.java b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/SpanScope.java new file mode 100644 index 000000000..fe757d840 --- /dev/null +++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/SpanScope.java @@ -0,0 +1,59 @@ +package software.amazon.lambda.powertools.tracing.opentelemetry.internal; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.context.Scope; + +/** + * A utility class that manages the lifecycle of a span and its associated context + * within a thread. It ensures that the span is properly closed and the thread context + * is restored when the scope is closed. + *

+ * This class is primarily used to work with OpenTelemetry spans, making them current + * in the thread context and managing their lifecycle, including recording exceptions + * and handling automatic cleanup of associated resources. + *

+ * It implements {@link AutoCloseable}, allowing it to be used in try-with-resources blocks + * to ensure proper cleanup of the span and scope. + */ +public final class SpanScope implements AutoCloseable { + + private final Span span; + private final Scope scope; + + public SpanScope(Span span) { + this.span = span; + this.scope = span.makeCurrent(); + } + + public Span span() { + return span; + } + + public SpanScope setStatus(StatusCode status) { + span.setStatus(status); + return this; + } + + public SpanScope addEvent(String name) { + span.addEvent(name); + return this; + } + + public SpanScope addEvent(String name, Attributes attributes) { + span.addEvent(name, attributes); + return this; + } + + public void recordException(Throwable throwable) { + span.recordException(throwable); + span.setStatus(StatusCode.ERROR); + } + + @Override + public void close() { + scope.close(); + span.end(); + } +} \ No newline at end of file diff --git a/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/TracingOpenTelemetryAspect.java b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/TracingOpenTelemetryAspect.java new file mode 100644 index 000000000..80888480f --- /dev/null +++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/TracingOpenTelemetryAspect.java @@ -0,0 +1,225 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 + */ + +package software.amazon.lambda.powertools.tracing.opentelemetry.internal; + +import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.coldStartDone; +import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.isColdStart; +import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.isHandlerMethod; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import java.util.Optional; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; +import software.amazon.lambda.powertools.common.internal.SystemWrapper; +import software.amazon.lambda.powertools.tracing.opentelemetry.Tracing; +import software.amazon.lambda.powertools.tracing.opentelemetry.TracingOpenTelemetry; +import software.amazon.lambda.powertools.tracing.opentelemetry.context.ExtractedTraceContext; +import software.amazon.lambda.powertools.tracing.opentelemetry.context.TraceContextPropagationMode; +import software.amazon.lambda.powertools.tracing.opentelemetry.provider.OpenTelemetryProvider; + +@Aspect +public final class TracingOpenTelemetryAspect { + + // Cannot be final for testing purposes + private static TracingOpenTelemetry tracingOtel = TracingOpenTelemetry.create(); + + @SuppressWarnings("EmptyMethod") + @Pointcut("@annotation(tracing)") + public void callAt(Tracing tracing) { + } + + @Around( + value = "callAt(tracing) && execution(@Tracing * *.*(..))", + argNames = "pjp,tracing" + ) + public Object around(ProceedingJoinPoint pjp, Tracing tracing) throws Throwable { + + String spanName = tracing.spanName().isEmpty() + ? pjp.getSignature().getName() + : tracing.spanName(); + + if (isHandlerMethod(pjp)) { + return traceHandler(pjp, tracing, spanName); + } + + return traceMethod(pjp, tracing, spanName); + } + + private Object traceHandler(ProceedingJoinPoint pjp, Tracing tracing, String spanName) throws Throwable { + + ExtractedTraceContext extractedTraceContext = extractTraceContext(pjp); + + try (SpanScope scope = addHandlerSpan(spanName, extractedTraceContext)) { + + Span span = scope.span(); + + tracingOtel.eventContextExtractorResolver().enrichSpan(pjp.getArgs()[0], span); + + addLambdaInvocationAttributes(pjp, span); + + try { + + Object result = pjp.proceed(pjp.getArgs()); + + captureResponse(span, tracing, result); + + coldStartDone(); + + return result; + + } catch (Throwable throwable) { + + captureError(scope, tracing, throwable); + + throw throwable; + } + } + } + + private SpanScope addHandlerSpan(String spanName, ExtractedTraceContext extractedTraceContext) { + + if (shouldUseSpanLinks(extractedTraceContext)) { + return tracingOtel.addSpan( + spanName, + extractedTraceContext.spanKind(), + handlerAttributes(), + Context.current(), + extractedTraceContext.spanContexts() + ); + } + + return tracingOtel.addSpan( + spanName, + extractedTraceContext.spanKind(), + handlerAttributes(), + extractedTraceContext.context() + ); + } + + private boolean shouldUseSpanLinks(ExtractedTraceContext extractedTraceContext) { + + return OpenTelemetryProvider.traceContextPropagationMode() == TraceContextPropagationMode.LINK + && !extractedTraceContext.spanContexts().isEmpty(); + } + + private Object traceMethod(ProceedingJoinPoint pjp, Tracing tracing, String spanName) throws Throwable { + + try (SpanScope scope = tracingOtel.addSpan(spanName, SpanKind.INTERNAL, Attributes.empty(), + Context.current())) { + + Span span = scope.span(); + + try { + Object result = pjp.proceed(pjp.getArgs()); + + captureResponse(span, tracing, result); + + return result; + + } catch (Throwable throwable) { + + captureError(scope, tracing, throwable); + + throw throwable; + } + } + } + + private ExtractedTraceContext extractTraceContext(ProceedingJoinPoint pjp) { + + return tracingOtel.eventContextExtractorResolver().extract( + pjp.getArgs()[0], + Context.current(), + tracingOtel.propagator() + ); + } + + private Attributes handlerAttributes() { + return Attributes.builder() + .put(AttributesConstants.FAAS_COLDSTART, isColdStart()) + .build(); + } + + private void addLambdaInvocationAttributes(ProceedingJoinPoint pjp, Span span) { + + Optional.ofNullable(LambdaHandlerProcessor.extractContext(pjp)) + .ifPresent( + context -> span.setAttribute(AttributesConstants.FAAS_INVOCATION_ID, context.getAwsRequestId() + ) + ); + } + + private void captureResponse(Span span, Tracing tracing, Object response) throws Exception { + + if (!isCaptureResponseEnabled(tracing)) { + return; + } + + span.setAttribute( + AttributesConstants.RESPONSE_ATTRIBUTE, + OpenTelemetryProvider.objectMapper().writeValueAsString(response) + ); + } + + private void captureError(SpanScope scope, Tracing tracing, Throwable throwable) { + + if (isCaptureErrorEnabled(tracing)) { + scope.recordException(throwable); + } + } + + private boolean isCaptureResponseEnabled(Tracing tracing) { + switch (tracing.captureMode()) { + case ENVIRONMENT_VAR: + return isEnvironmentVariableSet( + AttributesConstants.CAPTURE_RESPONSE_ENV) + && environmentVariable( + AttributesConstants.CAPTURE_RESPONSE_ENV); + + case RESPONSE: + case RESPONSE_AND_ERROR: + return true; + + case DISABLED: + case ERROR: + default: + return false; + } + } + + private boolean isCaptureErrorEnabled(Tracing tracing) { + switch (tracing.captureMode()) { + case ENVIRONMENT_VAR: + return isEnvironmentVariableSet( + AttributesConstants.CAPTURE_ERROR_ENV) + && environmentVariable( + AttributesConstants.CAPTURE_ERROR_ENV); + + case ERROR: + case RESPONSE_AND_ERROR: + return true; + + case DISABLED: + case RESPONSE: + default: + return false; + } + } + + private boolean environmentVariable(String key) { + return Boolean.parseBoolean(SystemWrapper.getenv(key)); + } + + private boolean isEnvironmentVariableSet(String key) { + return SystemWrapper.containsKey(key); + } +} \ No newline at end of file diff --git a/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/provider/OpenTelemetryProvider.java b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/provider/OpenTelemetryProvider.java new file mode 100644 index 000000000..43e662554 --- /dev/null +++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/provider/OpenTelemetryProvider.java @@ -0,0 +1,206 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + */ + +package software.amazon.lambda.powertools.tracing.opentelemetry.provider; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.contrib.awsxray.propagator.AwsXrayPropagator; +import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import software.amazon.lambda.powertools.common.internal.SystemWrapper; +import software.amazon.lambda.powertools.tracing.opentelemetry.context.TraceContextPropagationMode; +import software.amazon.lambda.powertools.tracing.opentelemetry.internal.LambdaResource; + +public final class OpenTelemetryProvider { + + private static final String INSTRUMENTATION_NAME = "aws-lambda-powertools"; + private static final String OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL"; + private static final String TRACE_CONTEXT_PROPAGATION_MODE_ENV = "POWERTOOLS_TRACE_CONTEXT_PROPAGATION_MODE"; + + private static final int MAX_EXPORT_BATCH_SIZE = 10; + private static final int MAX_QUEUE_SIZE = 100; + private static final long SCHEDULE_DELAY_MILLIS = 1_000; + private static final long EXPORT_TIMEOUT_MILLIS = 3_000; + + private static final OpenTelemetry OPEN_TELEMETRY = initializeOpenTelemetry(); + + private static final TraceContextPropagationMode TRACE_CONTEXT_PROPAGATION_MODE = retrieveTraceContextMode(); + + private static final TextMapGetter> TEXT_MAP_GETTER = createTextMapGetter(); + + private static final TextMapPropagator PROPAGATOR = createPropagator(); + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private OpenTelemetryProvider() { + } + + public static ObjectMapper objectMapper() { + return OBJECT_MAPPER; + } + + public static TraceContextPropagationMode traceContextPropagationMode() { + return TRACE_CONTEXT_PROPAGATION_MODE; + } + + public static Tracer tracer() { + return OPEN_TELEMETRY.getTracer(INSTRUMENTATION_NAME); + } + + public static TextMapPropagator propagator() { + return PROPAGATOR; + } + + public static TextMapGetter> textMapGetter() { + return TEXT_MAP_GETTER; + } + + /** + * Uses an already configured GlobalOpenTelemetry instance when one exists. + *

+ * This is important when running with the ADOT Lambda layer/javaagent, + * because the agent configures the global OpenTelemetry instance with + * its own TracerProvider, exporters, processors, resources, etc. + *

+ * If no global OpenTelemetry instance has been configured, Powertools + * creates its own Lambda-optimized default configuration. + */ + private static OpenTelemetry initializeOpenTelemetry() { + + OpenTelemetry globalOpenTelemetry = GlobalOpenTelemetry.get(); + + if (!isNoop(globalOpenTelemetry)) { + return globalOpenTelemetry; + } + + return createDefaultOpenTelemetry(); + } + + /** + * Determines whether GlobalOpenTelemetry has been configured. + *

+ * GlobalOpenTelemetry.get() returns OpenTelemetry.noop() when no + * SDK/global implementation has been registered. + */ + private static boolean isNoop(OpenTelemetry openTelemetry) { + return openTelemetry == OpenTelemetry.noop(); + } + + /** + * Creates the Powertools default OpenTelemetry configuration. + */ + private static OpenTelemetry createDefaultOpenTelemetry() { + + return OpenTelemetrySdk.builder() + .setTracerProvider(createTracerProvider()) + .setPropagators(ContextPropagators.create(PROPAGATOR)) + .build(); + } + + private static TraceContextPropagationMode retrieveTraceContextMode() { + + String value = SystemWrapper.getenv(TRACE_CONTEXT_PROPAGATION_MODE_ENV); + + if (value == null || value.isBlank()) { + return TraceContextPropagationMode.PARENT; + } + + try { + return TraceContextPropagationMode.valueOf(value.trim().toUpperCase()); + } catch (IllegalArgumentException exception) { + return TraceContextPropagationMode.PARENT; + } + } + + private static TextMapGetter> createTextMapGetter() { + + return new TextMapGetter<>() { + + @Override + public Iterable keys(Map carrier) { + + return carrier == null + ? Collections.emptyList() + : carrier.keySet(); + } + + @Override + public String get(Map carrier, String key) { + + return carrier == null + ? null + : carrier.get(key); + } + }; + } + + private static SdkTracerProvider createTracerProvider() { + + String protocol = SystemWrapper.getenv(OTEL_EXPORTER_OTLP_TRACES_PROTOCOL); + + SpanExporter exporter = createExporter(protocol); + + BatchSpanProcessor processor = BatchSpanProcessor.builder(exporter) + .setMaxExportBatchSize(MAX_EXPORT_BATCH_SIZE) + .setMaxQueueSize(MAX_QUEUE_SIZE) + .setScheduleDelay(SCHEDULE_DELAY_MILLIS, TimeUnit.MILLISECONDS) + .setExporterTimeout(EXPORT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS) + .build(); + + return SdkTracerProvider.builder() + .setResource(LambdaResource.create()) + .addSpanProcessor(processor) + .build(); + } + + private static SpanExporter createExporter(String protocol) { + + if (protocol == null || protocol.isBlank()) { + return OtlpGrpcSpanExporter.builder() + .setTimeout(EXPORT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS) + .build(); + } + + switch (protocol.trim().toLowerCase()) { + case "grpc": + return OtlpGrpcSpanExporter.builder() + .setTimeout(EXPORT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS) + .build(); + + case "http/protobuf": + return OtlpHttpSpanExporter.builder() + .setTimeout(EXPORT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS) + .build(); + + default: + throw new IllegalArgumentException( + "Unsupported OTLP protocol: " + protocol + ); + } + } + + private static TextMapPropagator createPropagator() { + + return TextMapPropagator.composite( + W3CTraceContextPropagator.getInstance(), + AwsXrayPropagator.getInstance() + ); + } +} \ No newline at end of file diff --git a/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/TracingOpenTelemetryTest.java b/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/TracingOpenTelemetryTest.java new file mode 100644 index 000000000..9b526c1de --- /dev/null +++ b/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/TracingOpenTelemetryTest.java @@ -0,0 +1,235 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools.tracing.opentelemetry; + +import io.opentelemetry.context.propagation.TextMapGetter; +import java.util.Map; + +class TracingOpenTelemetryTest { + + public static final TextMapGetter> MAP_GETTER = new TextMapGetter<>() { + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Override + public String get( + Map carrier, + String key) { + return carrier.get(key); + } + }; + +// @Test +// void shouldCreateAndMakeSpanCurrent() { +// SdkTracerProvider tracerProvider = SdkTracerProvider.builder().build(); +// +// Tracer tracer = tracerProvider.get("test-tracer"); +// +// TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer); +// +// try (SpanScope scope = tracing.addSpan("payment")) { +// assertThat(scope.span().getSpanContext().isValid()) +// .isTrue(); +// +// assertThat(Span.current()) +// .isEqualTo(scope.span()); +// } +// } +// +// @Test +// void shouldEndSpanWhenScopeIsClosed() { +// InMemorySpanExporter exporter = InMemorySpanExporter.create(); +// +// SdkTracerProvider tracerProvider = SdkTracerProvider.builder() +// .addSpanProcessor(SimpleSpanProcessor.create(exporter)) +// .build(); +// +// Tracer tracer = tracerProvider.get("test-tracer"); +// +// TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer); +// +// try (SpanScope ignored = tracing.addSpan("payment")) { +// assertThat(exporter.getFinishedSpanItems()) +// .isEmpty(); +// } +// +// assertThat(exporter.getFinishedSpanItems()) +// .hasSize(1); +// +// assertThat(exporter.getFinishedSpanItems().get(0).getName()) +// .isEqualTo("payment"); +// +// tracerProvider.close(); +// } +// +// @Test +// void shouldRestorePreviousSpanWhenScopeIsClosed() { +// +// SdkTracerProvider tracerProvider = SdkTracerProvider.builder().build(); +// +// Tracer tracer = tracerProvider.get("test-tracer"); +// +// TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer); +// +// try (SpanScope outer = tracing.addSpan("outer")) { +// +// assertThat(Span.current()).isEqualTo(outer.span()); +// +// try (SpanScope inner = tracing.addSpan("inner")) { +// assertThat(Span.current()).isEqualTo(inner.span()); +// } +// +// assertThat(Span.current()).isEqualTo(outer.span()); +// } +// } +// +// @Test +// void shouldRecordException() { +// InMemorySpanExporter exporter = InMemorySpanExporter.create(); +// +// SdkTracerProvider tracerProvider = SdkTracerProvider.builder() +// .addSpanProcessor(SimpleSpanProcessor.create(exporter)) +// .build(); +// +// Tracer tracer = tracerProvider.get("test-tracer"); +// +// TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer); +// +// RuntimeException exception = new RuntimeException("boom"); +// +// try (SpanScope scope = tracing.addSpan("payment")) { +// scope.recordException(exception); +// } +// +// assertThat(exporter.getFinishedSpanItems()) +// .hasSize(1); +// +// assertThat(exporter.getFinishedSpanItems().get(0).getEvents()) +// .hasSize(1); +// +// assertThat(exporter.getFinishedSpanItems().get(0).getEvents().get(0).getName()) +// .isEqualTo("exception"); +// +// assertThat(exporter.getFinishedSpanItems().get(0).getStatus().getStatusCode()) +// .isEqualTo(io.opentelemetry.api.trace.StatusCode.ERROR); +// +// tracerProvider.close(); +// } +// +// @Test +// void shouldRecordExceptionWhenUsingWithSpan() { +// InMemorySpanExporter exporter = InMemorySpanExporter.create(); +// +// SdkTracerProvider tracerProvider = SdkTracerProvider.builder() +// .addSpanProcessor(SimpleSpanProcessor.create(exporter)) +// .build(); +// +// Tracer tracer = tracerProvider.get("test-tracer"); +// +// TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer); +// +// RuntimeException exception = new RuntimeException("boom"); +// +// assertThatThrownBy(() -> +// tracing.withSpan("payment", span -> { +// throw exception; +// }) +// ).isSameAs(exception); +// +// assertThat(exporter.getFinishedSpanItems()) +// .hasSize(1); +// +// assertThat(exporter.getFinishedSpanItems().get(0).getEvents()) +// .hasSize(1); +// +// assertThat(exporter.getFinishedSpanItems().get(0).getEvents().get(0).getName()) +// .isEqualTo("exception"); +// +// assertThat(exporter.getFinishedSpanItems().get(0).getStatus().getStatusCode()) +// .isEqualTo(io.opentelemetry.api.trace.StatusCode.ERROR); +// +// tracerProvider.close(); +// } +// +// @Test +// void shouldExtractContext() { +// TextMapPropagator propagator = +// W3CTraceContextPropagator.getInstance(); +// +// SdkTracerProvider tracerProvider = SdkTracerProvider.builder().build(); +// +// Tracer tracer = tracerProvider.get("test-tracer"); +// +// TracingOpenTelemetry tracing = +// TracingOpenTelemetry.builder() +// .tracer(tracer) +// .propagator(propagator) +// .build(); +// +// Map headers = new HashMap<>(); +// headers.put( +// "traceparent", +// "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" +// ); +// +// Context context = tracing.extractContext( +// headers, +// MAP_GETTER +// ); +// +// SpanContext spanContext = Span.fromContext(context).getSpanContext(); +// +// assertThat(spanContext.isValid()).isTrue(); +// assertThat(spanContext.isRemote()).isTrue(); +// +// assertThat(spanContext.getTraceId()) +// .isEqualTo("4bf92f3577b34da6a3ce929d0e0e4736"); +// +// assertThat(spanContext.getSpanId()) +// .isEqualTo("00f067aa0ba902b7"); +// +// assertThat(spanContext.getTraceFlags().isSampled()) +// .isTrue(); +// } +// +// @Test +// void shouldReturnInvalidContextWhenTraceparentIsMissing() { +// TextMapPropagator propagator = +// W3CTraceContextPropagator.getInstance(); +// +// SdkTracerProvider tracerProvider = +// SdkTracerProvider.builder().build(); +// +// TracingOpenTelemetry tracing = +// TracingOpenTelemetry.builder() +// .tracer(tracerProvider.get("test-tracer")) +// .propagator(propagator) +// .build(); +// +// Map headers = new HashMap<>(); +// +// Context context = tracing.extractContext( +// headers, +// MAP_GETTER +// ); +// +// assertThat(Span.fromContext(context).getSpanContext().isValid()) +// .isFalse(); +// } + + +} \ No newline at end of file diff --git a/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/ApiGatewayTraceContextExtractorTest.java b/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/ApiGatewayTraceContextExtractorTest.java new file mode 100644 index 000000000..f0534e626 --- /dev/null +++ b/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/ApiGatewayTraceContextExtractorTest.java @@ -0,0 +1,37 @@ +package software.amazon.lambda.powertools.tracing.opentelemetry.context; + +class ApiGatewayTraceContextExtractorTest { + +// @Test +// void shouldExtractTraceContextFromApiGatewayEvent() { +// +// String traceId = "4bf92f3577b34da6a3ce929d0e0e4736"; +// +// String spanId = "00f067aa0ba902b7"; +// +// APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent() +// .withHeaders(Map.of( +// "traceparent", +// "00-" + traceId + "-" + spanId + "-01" +// )); +// +// ApiGatewayTraceContextExtractor apiGatewayTraceContextExtractor = new ApiGatewayTraceContextExtractor(); +// +// Context parentContext = apiGatewayTraceContextExtractor.extract( +// event, +// Context.current(), +// W3CTraceContextPropagator.getInstance() +// ); +// +// SpanContext spanContext = Span.fromContext(parentContext).getSpanContext(); +// +// assertThat(spanContext.isValid()).isTrue(); +// assertThat(spanContext.isRemote()).isTrue(); +// +// assertThat(spanContext.getTraceId()).isEqualTo(traceId); +// +// assertThat(spanContext.getSpanId()).isEqualTo(spanId); +// } + + +} \ No newline at end of file diff --git a/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/SnsTraceContextExtractorTest.java b/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/SnsTraceContextExtractorTest.java new file mode 100644 index 000000000..55a0f57b8 --- /dev/null +++ b/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/SnsTraceContextExtractorTest.java @@ -0,0 +1,43 @@ +package software.amazon.lambda.powertools.tracing.opentelemetry.context; + +class SnsTraceContextExtractorTest { + +// @Test +// void shouldExtractTraceContextFromSnsEvent() { +// String traceId = +// "4bf92f3577b34da6a3ce929d0e0e4736"; +// +// String spanId = +// "00f067aa0ba902b7"; +// +// SNSEvent.MessageAttribute traceparent = new SNSEvent.MessageAttribute(); +// +// traceparent.setType("String"); +// traceparent.setValue("00-" + traceId + "-" + spanId + "-01"); +// +// SNSEvent.SNS sns = new SNSEvent.SNS(); +// +// sns.setMessageAttributes(Map.of("traceparent", traceparent)); +// +// SNSEvent.SNSRecord record = new SNSEvent.SNSRecord(); +// +// record.setSns(sns); +// +// SNSEvent event = new SNSEvent(); +// event.setRecords(List.of(record)); +// +// Context extracted = new SnsTraceContextExtractor().extract( +// event, +// Context.current(), +// W3CTraceContextPropagator.getInstance() +// ); +// +// SpanContext spanContext = Span.fromContext(extracted).getSpanContext(); +// +// assertThat(spanContext.isValid()).isTrue(); +// assertThat(spanContext.isRemote()).isTrue(); +// assertThat(spanContext.getTraceId()).isEqualTo(traceId); +// assertThat(spanContext.getSpanId()).isEqualTo(spanId); +// } + +} \ No newline at end of file diff --git a/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/SpanScopeTest.java b/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/SpanScopeTest.java new file mode 100644 index 000000000..e0710f765 --- /dev/null +++ b/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/SpanScopeTest.java @@ -0,0 +1,51 @@ +package software.amazon.lambda.powertools.tracing.opentelemetry.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Scope; +import org.junit.jupiter.api.Test; + +class SpanScopeTest { + + @Test + void span_returnsCurrentSpan() { + + Span mockSpan = mock(Span.class); + SpanScope spanScope = new SpanScope(mockSpan); + + Span result = spanScope.span(); + + assertEquals(mockSpan, result, "The span method should return the same Span instance."); + } + + @Test + void recordException_recordsThrowableAndSetsErrorStatus() { + Span mockSpan = mock(Span.class); + SpanScope spanScope = new SpanScope(mockSpan); + Throwable exception = new RuntimeException("Test exception"); + + spanScope.recordException(exception); + + verify(mockSpan).recordException(exception); + verify(mockSpan).setStatus(io.opentelemetry.api.trace.StatusCode.ERROR); + } + + @Test + void close_closesScopeAndEndsSpan() { + + Span mockSpan = mock(Span.class); + Scope mockScope = mock(Scope.class); + when(mockSpan.makeCurrent()).thenReturn(mockScope); + + SpanScope spanScope = new SpanScope(mockSpan); + + spanScope.close(); + + verify(mockScope).close(); + verify(mockSpan).end(); + } +} \ No newline at end of file diff --git a/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/TracingOpenTelemetryAspectTest.java b/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/TracingOpenTelemetryAspectTest.java new file mode 100644 index 000000000..6414833aa --- /dev/null +++ b/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/TracingOpenTelemetryAspectTest.java @@ -0,0 +1,105 @@ +package software.amazon.lambda.powertools.tracing.opentelemetry.internal; + +import static org.mockito.Mockito.mock; + +import org.apache.commons.lang3.reflect.FieldUtils; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.Signature; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import software.amazon.lambda.powertools.tracing.opentelemetry.Tracing; +import software.amazon.lambda.powertools.tracing.opentelemetry.TracingOpenTelemetry; + +class TracingOpenTelemetryAspectTest { + + private ProceedingJoinPoint pjp; + private Tracing tracing; + private TracingOpenTelemetry tracingOpenTelemetry; + private SpanScope spanScope; + private Signature signature; + private TracingOpenTelemetry originalTracing; + + @BeforeEach + void setUp() throws IllegalAccessException { + pjp = mock(ProceedingJoinPoint.class); + tracing = mock(Tracing.class); + tracingOpenTelemetry = mock(TracingOpenTelemetry.class); + spanScope = mock(SpanScope.class); + signature = mock(Signature.class); + + originalTracing = (TracingOpenTelemetry) FieldUtils + .readStaticField(TracingOpenTelemetryAspect.class, "tracing", true); + + FieldUtils.writeStaticField(TracingOpenTelemetryAspect.class, "tracing", tracingOpenTelemetry, true); + } + + @AfterEach + void tearDown() throws IllegalAccessException { + FieldUtils.writeStaticField(TracingOpenTelemetryAspect.class, "tracing", originalTracing, true); + } + +// @Test +// void testAroundMethodSuccessfulExecution() throws Throwable { +// +// when(tracingOpenTelemetry.addSpan(anyString())).thenReturn(spanScope); +// when(pjp.getSignature()).thenReturn(signature); +// when(signature.getName()).thenReturn("testMethod"); +// when(signature.getDeclaringType()).thenReturn(RequestHandler.class); +// Object[] args = new Object[0]; +// when(pjp.getArgs()).thenReturn(args); +// when(tracing.spanName()).thenReturn("testMethod"); +// when(tracing.namespace()).thenReturn("test"); +// when(tracing.captureMode()).thenReturn(CaptureMode.ENVIRONMENT_VAR); +// when(pjp.proceed(any(Object[].class))).thenReturn("Success"); +// +// TracingOpenTelemetryAspect aspect = new TracingOpenTelemetryAspect(); +// Object result = aspect.around(pjp, tracing); +// +// verify(tracingOpenTelemetry).addSpan("testMethod"); +// verify(pjp).proceed(any(Object[].class)); +// assertEquals("Success", result); +// } +// +// @Test +// void testAroundMethodExceptionFlow() throws Throwable { +// +// +// when(tracingOpenTelemetry.addSpan(anyString())).thenReturn(spanScope); +// when(pjp.getSignature()).thenReturn(signature); +// when(signature.getName()).thenReturn("testMethod"); +// when(signature.getDeclaringType()).thenReturn(RequestHandler.class); +// when(pjp.getArgs()).thenReturn(new Object[0]); +// Throwable mockThrowable = new RuntimeException("Test Exception"); +// when(tracing.spanName()).thenReturn("testMethod"); +// when(tracing.namespace()).thenReturn("test"); +// when(tracing.captureMode()).thenReturn(CaptureMode.ERROR); +// when(pjp.proceed(pjp.getArgs())).thenThrow(mockThrowable); +// +// TracingOpenTelemetryAspect aspect = new TracingOpenTelemetryAspect(); +// RuntimeException exception = assertThrows(RuntimeException.class, () -> aspect.around(pjp, tracing)); +// +// verify(tracingOpenTelemetry).addSpan("testMethod"); +// verify(spanScope).recordException(mockThrowable); +// assertEquals("Test Exception", exception.getMessage()); +// } +// +// @Test +// void testAddSpanIsCalledWithCorrectSignature() throws Throwable { +// +// when(tracingOpenTelemetry.addSpan(anyString())).thenReturn(spanScope); +// when(pjp.getSignature()).thenReturn(signature); +// Object[] args = new Object[0]; +// when(pjp.getArgs()).thenReturn(args); +// when(signature.getDeclaringType()).thenReturn(RequestHandler.class); +// when(signature.getName()).thenReturn("correctMethodSignature"); +// when(tracing.spanName()).thenReturn("correctMethodSignature"); +// when(tracing.captureMode()).thenReturn(CaptureMode.ENVIRONMENT_VAR); +// when(tracing.namespace()).thenReturn("test"); +// when(pjp.proceed()).thenReturn("Success"); +// +// TracingOpenTelemetryAspect aspect = new TracingOpenTelemetryAspect(); +// aspect.around(pjp, tracing); +// +// verify(tracingOpenTelemetry).addSpan("correctMethodSignature"); +// } +} \ No newline at end of file