@@ -119,6 +120,7 @@
2.3.0
1.5.0
0.11.5
+ 1.65.0
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..6fe91dbcc
--- /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
+ ${opentelemetry-api.version}
+
+
+ io.opentelemetry
+ opentelemetry-sdk
+ ${opentelemetry-api.version}
+
+
+ io.opentelemetry
+ opentelemetry-exporter-otlp
+ ${opentelemetry-api.version}
+
+
+ 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
+ ${opentelemetry-api.version}
+ 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..bf9f7ae44
--- /dev/null
+++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/TracingOpenTelemetry.java
@@ -0,0 +1,258 @@
+/*
+ * 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.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.Objects;
+import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor;
+import software.amazon.lambda.powertools.tracing.opentelemetry.context.LambdaEventContextExtractorResolver;
+import software.amazon.lambda.powertools.tracing.opentelemetry.internal.AttributesConstants;
+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 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) {
+
+ Objects.requireNonNull(name, "name must not be null");
+ Objects.requireNonNull(kind, "kind must not be null");
+ Objects.requireNonNull(attributes, "attributes must not be null");
+
+ Span span = tracer.spanBuilder(name)
+ .setSpanKind(kind)
+ .setAllAttributes(attributes)
+ .startSpan();
+
+ return new SpanScope(span);
+ }
+
+ public SpanScope addSpan(String name, SpanKind kind, Attributes attributes, Context parentContext) {
+
+ Objects.requireNonNull(parentContext, "parentContext must not be null");
+
+ Span span = tracer.spanBuilder(name)
+ .setParent(parentContext)
+ .setSpanKind(kind)
+ .setAllAttributes(attributes)
+ .startSpan();
+
+ return new SpanScope(span);
+ }
+
+
+ public T withSpan(String name, SpanOperation operation) throws Exception {
+
+ Objects.requireNonNull(operation, "operation must not be null");
+
+ try (SpanScope scope = addSpan(name)) {
+ try {
+ return operation.execute(scope.span());
+ } catch (Exception exception) {
+ scope.recordException(exception);
+ throw exception;
+ }
+ }
+ }
+
+ public T captureLambdaHandler(
+ String name,
+ com.amazonaws.services.lambda.runtime.Context lambdaContext,
+ io.opentelemetry.context.Context parentContext,
+ SpanOperation operation
+ ) throws Exception {
+
+ Objects.requireNonNull(name, "name must not be null");
+ Objects.requireNonNull(parentContext, "parentContext must not be null");
+ Objects.requireNonNull(operation, "operation must not be null");
+
+ Span span = tracer.spanBuilder(name)
+ .setParent(parentContext)
+ .setSpanKind(SpanKind.SERVER)
+ .setAttribute(AttributesConstants.AWS_LAMBDA_FUNCTION_ARN, LambdaHandlerProcessor.isColdStart())
+ .setAttribute(AttributesConstants.FAAS_INVOCATION_ID, lambdaContext.getAwsRequestId())
+ .startSpan();
+
+ try (SpanScope scope = new SpanScope(span)) {
+ try {
+ T result = operation.execute(span);
+
+ LambdaHandlerProcessor.coldStartDone();
+
+ return result;
+ } 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(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(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..d4126b632
--- /dev/null
+++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/ApiGatewayTraceContextExtractor.java
@@ -0,0 +1,97 @@
+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.context.Context;
+import io.opentelemetry.context.propagation.TextMapPropagator;
+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 Context extract(Object event, Context parentContext, TextMapPropagator propagator) {
+
+ APIGatewayProxyRequestEvent apiGatewayEvent = (APIGatewayProxyRequestEvent) event;
+
+ Map headers = apiGatewayEvent.getHeaders();
+
+ if (headers == null || headers.isEmpty()) {
+ return parentContext;
+ }
+
+ return propagator.extract(
+ parentContext,
+ headers,
+ OpenTelemetryProvider.textMapGetter()
+ );
+ }
+
+ @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/LambdaEventContextExtractor.java b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/LambdaEventContextExtractor.java
new file mode 100644
index 000000000..90eae00ae
--- /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);
+
+ Context extract(Object event, Context parentContext, TextMapPropagator propagator);
+
+ void enrichSpan(Object event, Span span);
+}
\ 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..3b12f3413
--- /dev/null
+++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/LambdaEventContextExtractorResolver.java
@@ -0,0 +1,46 @@
+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()
+ )
+ );
+ }
+
+ public Context extract(Object event, Context parentContext, TextMapPropagator propagator) {
+
+ return extractors.stream()
+ .filter(extractor -> extractor.supports(event))
+ .findFirst()
+ .map(extractor ->
+ extractor.extract(
+ event,
+ parentContext,
+ propagator))
+ .orElse(parentContext);
+ }
+
+ 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/SnsTraceContextExtractor.java b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/SnsTraceContextExtractor.java
new file mode 100644
index 000000000..f37235155
--- /dev/null
+++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/SnsTraceContextExtractor.java
@@ -0,0 +1,84 @@
+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.context.Context;
+import io.opentelemetry.context.propagation.TextMapPropagator;
+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 Context extract(Object event, Context parentContext, TextMapPropagator propagator) {
+
+ SNSEvent snsEvent = (SNSEvent) event;
+
+ if (snsEvent.getRecords() == null || snsEvent.getRecords().isEmpty()) {
+ return parentContext;
+ }
+
+ SNSEvent.SNSRecord record = snsEvent.getRecords().get(0);
+
+ Map attributes = record.getSNS().getMessageAttributes();
+
+ if (attributes == null || attributes.isEmpty()) {
+ return parentContext;
+ }
+
+ 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()
+ ));
+
+ return propagator.extract(
+ parentContext,
+ propagationAttributes,
+ OpenTelemetryProvider.textMapGetter()
+ );
+ }
+
+ @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().get(0);
+
+ if (record.getSNS() == null) {
+ return;
+ }
+
+ span.setAttribute("messaging.system", "aws.sns");
+
+ if (record.getSNS().getMessageId() != null) {
+ span.setAttribute("messaging.message.id", record.getSNS().getMessageId());
+ }
+
+ 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..f4342afb6
--- /dev/null
+++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/SqsTraceContextExtractor.java
@@ -0,0 +1,78 @@
+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.context.Context;
+import io.opentelemetry.context.propagation.TextMapPropagator;
+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 Context extract(Object event, Context parentContext, TextMapPropagator propagator) {
+
+ SQSEvent sqsEvent = (SQSEvent) event;
+
+ if (sqsEvent.getRecords() == null || sqsEvent.getRecords().isEmpty()) {
+ return parentContext;
+ }
+
+ SQSEvent.SQSMessage message = sqsEvent.getRecords().get(0);
+
+ Map attributes = message.getMessageAttributes();
+
+ if (attributes == null || attributes.isEmpty()) {
+ return parentContext;
+ }
+
+ Map propagationAttributes = attributes.entrySet()
+ .stream()
+ .filter(entry -> entry.getValue() != null)
+ .collect(Collectors.toMap(
+ Map.Entry::getKey,
+ entry -> entry.getValue().getStringValue()
+ ));
+
+ return propagator.extract(
+ parentContext,
+ propagationAttributes,
+ OpenTelemetryProvider.textMapGetter()
+ );
+ }
+
+ @Override
+ public void enrichSpan(Object event, Span span) {
+ SQSEvent sqsEvent = (SQSEvent) event;
+
+ if (sqsEvent.getRecords() == null || sqsEvent.getRecords().isEmpty()) {
+ return;
+ }
+
+ SQSEvent.SQSMessage message = sqsEvent.getRecords().get(0);
+
+ span.setAttribute("messaging.system", "aws.sqs");
+
+ if (message.getMessageId() != null) {
+ span.setAttribute("messaging.message.id", message.getMessageId());
+ }
+
+ 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/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..a9e980e1d
--- /dev/null
+++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/AttributesConstants.java
@@ -0,0 +1,42 @@
+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";
+}
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..5be39b9bb
--- /dev/null
+++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/TracingOpenTelemetryAspect.java
@@ -0,0 +1,229 @@
+/*
+ * 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 com.fasterxml.jackson.databind.ObjectMapper;
+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;
+
+@Aspect
+public final class TracingOpenTelemetryAspect {
+
+ // Cannot be final for testing purposes
+ private static TracingOpenTelemetry tracingOtel = TracingOpenTelemetry.create();
+
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+ @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 {
+
+ Context parentContext = extractParentContext(pjp);
+
+ try (SpanScope scope = tracingOtel.addSpan(
+ spanName,
+ SpanKind.SERVER,
+ handlerAttributes(),
+ parentContext)) {
+
+ 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 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 Context extractParentContext(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,
+ OBJECT_MAPPER.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..21e0b9da1
--- /dev/null
+++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/provider/OpenTelemetryProvider.java
@@ -0,0 +1,106 @@
+/*
+ * 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 io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
+import io.opentelemetry.context.propagation.TextMapGetter;
+import io.opentelemetry.context.propagation.TextMapPropagator;
+import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
+import io.opentelemetry.sdk.trace.SdkTracerProvider;
+import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
+import java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+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 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 SdkTracerProvider TRACER_PROVIDER =
+ createTracerProvider();
+
+ private static final TextMapGetter