From fb6e20ddf6b376ccd3e1a17aae1715ae1804619d Mon Sep 17 00:00:00 2001
From: David-DAM <82216301+David-DAM@users.noreply.github.com>
Date: Sun, 9 Aug 2026 17:58:34 +0200
Subject: [PATCH 01/21] Add OpenTelemetry-based tracing module
(powertools-tracing-opentelemetry)
---
pom.xml | 2 +
powertools-tracing-opentelemetry/pom.xml | 138 ++++++++++++++
.../opentelemetry/TracingOpenTelemetry.java | 110 +++++++++++
.../opentelemetry/internal/SpanOperation.java | 46 +++++
.../opentelemetry/internal/SpanScope.java | 67 +++++++
.../TracingOpenTelemetryTest.java | 173 ++++++++++++++++++
6 files changed, 536 insertions(+)
create mode 100644 powertools-tracing-opentelemetry/pom.xml
create mode 100644 powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/TracingOpenTelemetry.java
create mode 100644 powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/SpanOperation.java
create mode 100644 powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/SpanScope.java
create mode 100644 powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/TracingOpenTelemetryTest.java
diff --git a/pom.xml b/pom.xml
index 8e847983a..227fdc8d6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -77,6 +77,7 @@
powertools-parameters/powertools-parameters-appconfigpowertools-parameters/powertools-parameters-testsexamples
+ powertools-tracing-opentelemetry
@@ -119,6 +120,7 @@
2.3.01.5.00.11.5
+ 1.65.0
diff --git a/powertools-tracing-opentelemetry/pom.xml b/powertools-tracing-opentelemetry/pom.xml
new file mode 100644
index 000000000..9062184d7
--- /dev/null
+++ b/powertools-tracing-opentelemetry/pom.xml
@@ -0,0 +1,138 @@
+
+
+
+ 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}
+
+
+ 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
+
+
+
+
+ io.opentelemetry
+ opentelemetry-sdk
+ ${opentelemetry-api.version}
+ test
+
+
+ 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/TracingOpenTelemetry.java b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/TracingOpenTelemetry.java
new file mode 100644
index 000000000..4fd01b3b8
--- /dev/null
+++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/TracingOpenTelemetry.java
@@ -0,0 +1,110 @@
+/*
+ * 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.trace.Span;
+import io.opentelemetry.api.trace.Tracer;
+import java.util.Objects;
+import software.amazon.lambda.powertools.tracing.opentelemetry.internal.SpanOperation;
+import software.amazon.lambda.powertools.tracing.opentelemetry.internal.SpanScope;
+
+/**
+ * A wrapper for OpenTelemetry's Tracer that simplifies the creation and management of spans.
+ * This class provides utility functions for starting and controlling spans and their contexts
+ * in the current execution thread.
+ *
+ * This is a final class and cannot be extended.
+ */
+public final class TracingOpenTelemetry {
+
+ private final Tracer tracer;
+
+ private TracingOpenTelemetry(Builder builder) {
+ this.tracer = builder.tracer;
+ }
+
+ /**
+ * Creates a new span with the specified name and makes it the current span in the thread context.
+ * The span must be manually closed to properly end it and revert the thread context.
+ *
+ * @param name the name of the span to be created
+ * @return an instance of {@link SpanScope}, which represents the created span and its associated context
+ */
+ public SpanScope addSpan(String name) {
+ return new SpanScope(tracer.spanBuilder(name).startSpan());
+ }
+
+ /**
+ * Retrieves the current active span in the execution context.
+ *
+ * @return the current {@link Span} if one is active, or a default no-op {@link Span} if none is active
+ */
+ public Span currentSpan() {
+ return Span.current();
+ }
+
+ /**
+ * Executes the specified operation within the context of a new span.
+ * The span is automatically managed and closed when the operation completes
+ * or an exception is thrown.
+ *
+ * @param name the name of the span to be created
+ * @param operation the operation to be executed within the span's context
+ * @throws Exception if the provided operation throws an exception during execution
+ */
+ public void withSpan(String name, SpanOperation operation) throws Exception {
+ try (SpanScope scope = addSpan(name)) {
+ try {
+ operation.execute(scope.span());
+ } catch (Exception e) {
+ scope.recordException(e);
+ throw e;
+ }
+ }
+ }
+
+ /**
+ * Creates and returns a new instance of the {@code Builder} class for constructing
+ * instances of {@code TracingOpenTelemetry}.
+ *
+ * @return a new {@code Builder} instance for configuring and building a {@code TracingOpenTelemetry} object
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static final class Builder {
+
+ private Tracer tracer;
+
+ public Builder tracer(Tracer tracer) {
+ this.tracer = tracer;
+ return this;
+ }
+
+ /**
+ * Builds and returns a {@code TracingOpenTelemetry} instance configured with the specified {@code Tracer}.
+ * The returned instance provides utilities for creating and managing spans.
+ *
+ * @return a fully constructed {@code TracingOpenTelemetry} object based on the builder's configuration
+ * @throws NullPointerException if the {@code tracer} has not been set
+ */
+ public TracingOpenTelemetry build() {
+ Objects.requireNonNull(tracer, "tracer must not be null");
+ 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/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..e93d080e0
--- /dev/null
+++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/SpanOperation.java
@@ -0,0 +1,46 @@
+/*
+ * 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 that encapsulates an operation to be performed
+ * within the context of an OpenTelemetry {@link Span}.
+ *
+ * This interface provides a contract for defining custom operations that take a
+ * {@link Span} as input and execute within its context. It is used in conjunction
+ * with utilities that manage OpenTelemetry spans, such as the {@code withSpan} method
+ * in the {@code TracingOpenTelemetry} class.
+ *
+ * Implementations of this interface enable the customization of behavior for spans,
+ * including adding events, setting attributes, or modifying the span's status.
+ *
+ * The operation defined by the {@code execute} method can throw an exception, which
+ * allows for handling of error scenarios and proper recording of exceptions in the span.
+ */
+@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
+ */
+ void 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..2326fbbca
--- /dev/null
+++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/SpanScope.java
@@ -0,0 +1,67 @@
+/*
+ * 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;
+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();
+ }
+
+ /**
+ * Retrieves the {@link Span} associated with this {@link SpanScope}.
+ *
+ * @return the {@link Span} managed by this {@link SpanScope}
+ */
+ public Span span() {
+ return span;
+ }
+
+ /**
+ * Records an exception in the span and sets its status to {@code StatusCode.ERROR}.
+ *
+ * @param throwable the {@link Throwable} instance to be recorded as an event in the span.
+ */
+ public void recordException(Throwable throwable) {
+ span.recordException(throwable);
+ span.setStatus(StatusCode.ERROR);
+ }
+
+ @Override
+ public void close() {
+ scope.close();
+ span.end();
+ }
+}
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..b6d37206c
--- /dev/null
+++ b/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/TracingOpenTelemetryTest.java
@@ -0,0 +1,173 @@
+/*
+ * 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 static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter;
+import io.opentelemetry.sdk.trace.SdkTracerProvider;
+import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
+import org.junit.jupiter.api.Test;
+import software.amazon.lambda.powertools.tracing.opentelemetry.internal.SpanScope;
+
+class TracingOpenTelemetryTest {
+
+ @Test
+ void shouldCreateAndMakeSpanCurrent() {
+ SdkTracerProvider tracerProvider = SdkTracerProvider.builder().build();
+
+ Tracer tracer = tracerProvider.get("test-tracer");
+
+ TracingOpenTelemetry tracing = TracingOpenTelemetry.builder()
+ .tracer(tracer)
+ .build();
+
+ 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 = TracingOpenTelemetry.builder()
+ .tracer(tracer)
+ .build();
+
+ try (SpanScope scope = 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 = TracingOpenTelemetry.builder()
+ .tracer(tracer)
+ .build();
+
+ 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 = TracingOpenTelemetry.builder()
+ .tracer(tracer)
+ .build();
+
+ 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() throws Exception {
+ InMemorySpanExporter exporter = InMemorySpanExporter.create();
+
+ SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
+ .addSpanProcessor(SimpleSpanProcessor.create(exporter))
+ .build();
+
+ Tracer tracer = tracerProvider.get("test-tracer");
+
+ TracingOpenTelemetry tracing = TracingOpenTelemetry.builder()
+ .tracer(tracer)
+ .build();
+
+ 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();
+ }
+
+
+}
\ No newline at end of file
From 4aa52290a7c773123f77702a3c520006e8698805 Mon Sep 17 00:00:00 2001
From: David-DAM <82216301+David-DAM@users.noreply.github.com>
Date: Sat, 15 Aug 2026 12:03:21 +0200
Subject: [PATCH 02/21] Introduce TracingOpenTelemetryAspect for
annotation-based span creation
---
.../common/internal/SystemWrapper.java | 4 +
powertools-tracing-opentelemetry/pom.xml | 4 +
.../tracing/opentelemetry/CaptureMode.java | 32 +++++
.../opentelemetry/TracingOpenTelemetry.java | 69 +++++-----
.../tracing/opentelemetry/TracingOtel.java | 36 +++++
.../opentelemetry/internal/SpanOperation.java | 27 ++--
.../internal/TracingOpenTelemetryAspect.java | 123 ++++++++++++++++++
.../TracingOpenTelemetryTest.java | 24 +---
.../opentelemetry/internal/SpanScopeTest.java | 51 ++++++++
.../TracingOpenTelemetryAspectTest.java | 114 ++++++++++++++++
10 files changed, 420 insertions(+), 64 deletions(-)
create mode 100644 powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/CaptureMode.java
create mode 100644 powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/TracingOtel.java
create mode 100644 powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/TracingOpenTelemetryAspect.java
create mode 100644 powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/SpanScopeTest.java
create mode 100644 powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/TracingOpenTelemetryAspectTest.java
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
index 9062184d7..7533345e1 100644
--- a/powertools-tracing-opentelemetry/pom.xml
+++ b/powertools-tracing-opentelemetry/pom.xml
@@ -58,6 +58,10 @@
com.amazonawsaws-lambda-java-core
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
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/TracingOpenTelemetry.java b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/TracingOpenTelemetry.java
index 4fd01b3b8..b3ab1cdd8 100644
--- 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
@@ -14,6 +14,7 @@
package software.amazon.lambda.powertools.tracing.opentelemetry;
+import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import java.util.Objects;
@@ -29,10 +30,31 @@
*/
public final class TracingOpenTelemetry {
+ private static final String INSTRUMENTATION_NAME =
+ "aws-lambda-powertools";
+
private final Tracer tracer;
- private TracingOpenTelemetry(Builder builder) {
- this.tracer = builder.tracer;
+ /**
+ * Creates a tracing instance using the provided tracer.
+ *
+ *
This constructor is primarily useful for testing.
+ *
+ * @param tracer the OpenTelemetry tracer
+ */
+ TracingOpenTelemetry(Tracer tracer) {
+ this.tracer = Objects.requireNonNull(tracer, "tracer must not be null");
+ }
+
+ /**
+ * Initializes a new instance of the {@code TracingOpenTelemetry} class, using
+ * the global OpenTelemetry tracer identified by the instrumentation name.
+ *
+ * This constructor simplifies the setup process for applications by
+ * automatically leveraging the globally configured instrumentation tracer.
+ */
+ public TracingOpenTelemetry() {
+ this(GlobalOpenTelemetry.getTracer(INSTRUMENTATION_NAME));
}
/**
@@ -43,7 +65,11 @@ private TracingOpenTelemetry(Builder builder) {
* @return an instance of {@link SpanScope}, which represents the created span and its associated context
*/
public SpanScope addSpan(String name) {
- return new SpanScope(tracer.spanBuilder(name).startSpan());
+ Span span = tracer
+ .spanBuilder(name)
+ .startSpan();
+
+ return new SpanScope(span);
}
/**
@@ -60,14 +86,14 @@ public Span currentSpan() {
* The span is automatically managed and closed when the operation completes
* or an exception is thrown.
*
- * @param name the name of the span to be created
+ * @param name the name of the span to be created
* @param operation the operation to be executed within the span's context
* @throws Exception if the provided operation throws an exception during execution
*/
- public void withSpan(String name, SpanOperation operation) throws Exception {
+ public T withSpan(String name, SpanOperation operation) throws Exception {
try (SpanScope scope = addSpan(name)) {
try {
- operation.execute(scope.span());
+ return operation.execute(scope.span());
} catch (Exception e) {
scope.recordException(e);
throw e;
@@ -76,35 +102,12 @@ public void withSpan(String name, SpanOperation operation) throws Exception {
}
/**
- * Creates and returns a new instance of the {@code Builder} class for constructing
- * instances of {@code TracingOpenTelemetry}.
+ * Creates a new tracing instance using the global OpenTelemetry tracer.
*
- * @return a new {@code Builder} instance for configuring and building a {@code TracingOpenTelemetry} object
+ * @return a new tracing instance
*/
- public static Builder builder() {
- return new Builder();
- }
-
- public static final class Builder {
-
- private Tracer tracer;
-
- public Builder tracer(Tracer tracer) {
- this.tracer = tracer;
- return this;
- }
-
- /**
- * Builds and returns a {@code TracingOpenTelemetry} instance configured with the specified {@code Tracer}.
- * The returned instance provides utilities for creating and managing spans.
- *
- * @return a fully constructed {@code TracingOpenTelemetry} object based on the builder's configuration
- * @throws NullPointerException if the {@code tracer} has not been set
- */
- public TracingOpenTelemetry build() {
- Objects.requireNonNull(tracer, "tracer must not be null");
- return new TracingOpenTelemetry(this);
- }
+ public static TracingOpenTelemetry create() {
+ return new TracingOpenTelemetry();
}
}
\ No newline at end of file
diff --git a/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/TracingOtel.java b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/TracingOtel.java
new file mode 100644
index 000000000..1112b53aa
--- /dev/null
+++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/TracingOtel.java
@@ -0,0 +1,36 @@
+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;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface TracingOtel {
+ /**
+ * 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/internal/SpanOperation.java b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/SpanOperation.java
index e93d080e0..a0d589db5 100644
--- 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
@@ -17,22 +17,21 @@
import io.opentelemetry.api.trace.Span;
/**
- * Represents a functional interface that encapsulates an operation to be performed
- * within the context of an OpenTelemetry {@link Span}.
- *
- * This interface provides a contract for defining custom operations that take a
- * {@link Span} as input and execute within its context. It is used in conjunction
- * with utilities that manage OpenTelemetry spans, such as the {@code withSpan} method
- * in the {@code TracingOpenTelemetry} class.
- *
- * Implementations of this interface enable the customization of behavior for spans,
- * including adding events, setting attributes, or modifying the span's status.
+ * 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.
+ *
*
+ * 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 TracingOtel {
From bac5e69c9c3b02cc5ff53d65843eba71dc838136 Mon Sep 17 00:00:00 2001
From: David-DAM <82216301+David-DAM@users.noreply.github.com>
Date: Sun, 16 Aug 2026 11:51:38 +0200
Subject: [PATCH 04/21] Refactor TracingOpenTelemetry for handler-specific
attributes, response/error capture, and contextual propagation
---
powertools-tracing-opentelemetry/pom.xml | 19 +-
.../{TracingOtel.java => Tracing.java} | 2 +-
.../opentelemetry/TracingOpenTelemetry.java | 268 ++++++++++++++----
.../internal/AttributesConstants.java | 42 +++
.../internal/LambdaResource.java | 115 ++++++++
.../internal/OpenTelemetryProvider.java | 79 ++++++
.../opentelemetry/internal/SpanScope.java | 42 ++-
.../internal/TracingOpenTelemetryAspect.java | 199 +++++++++----
.../TracingOpenTelemetryTest.java | 163 +++++++++++
.../TracingOpenTelemetryAspectTest.java | 30 +-
10 files changed, 800 insertions(+), 159 deletions(-)
rename powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/{TracingOtel.java => Tracing.java} (97%)
create mode 100644 powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/AttributesConstants.java
create mode 100644 powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/LambdaResource.java
create mode 100644 powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/OpenTelemetryProvider.java
diff --git a/powertools-tracing-opentelemetry/pom.xml b/powertools-tracing-opentelemetry/pom.xml
index 7533345e1..304ee0d42 100644
--- a/powertools-tracing-opentelemetry/pom.xml
+++ b/powertools-tracing-opentelemetry/pom.xml
@@ -28,7 +28,8 @@
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.
+ A suite of utilities for AWS Lambda Functions that makes tracing with OpenTelemetry, structured logging and
+ creating custom metrics asynchronously easier.
@@ -37,6 +38,16 @@
opentelemetry-api${opentelemetry-api.version}
+
+ io.opentelemetry
+ opentelemetry-sdk
+ ${opentelemetry-api.version}
+
+
+ io.opentelemetry
+ opentelemetry-exporter-otlp
+ ${opentelemetry-api.version}
+ org.aspectjaspectjrt
@@ -64,12 +75,6 @@
-
- io.opentelemetry
- opentelemetry-sdk
- ${opentelemetry-api.version}
- test
- io.opentelemetryopentelemetry-sdk-testing
diff --git a/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/TracingOtel.java b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/Tracing.java
similarity index 97%
rename from powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/TracingOtel.java
rename to powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/Tracing.java
index 33dc7ee2c..2d4d68608 100644
--- a/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/TracingOtel.java
+++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/Tracing.java
@@ -14,7 +14,7 @@
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
-public @interface TracingOtel {
+public @interface Tracing {
/**
* The namespace associated with the span.
*
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
index b3ab1cdd8..e96c525a5 100644
--- 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
@@ -2,8 +2,10 @@
* 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
+ * 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.
@@ -14,100 +16,240 @@
package software.amazon.lambda.powertools.tracing.opentelemetry;
-import io.opentelemetry.api.GlobalOpenTelemetry;
+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.internal.OpenTelemetryProvider;
import software.amazon.lambda.powertools.tracing.opentelemetry.internal.SpanOperation;
import software.amazon.lambda.powertools.tracing.opentelemetry.internal.SpanScope;
-/**
- * A wrapper for OpenTelemetry's Tracer that simplifies the creation and management of spans.
- * This class provides utility functions for starting and controlling spans and their contexts
- * in the current execution thread.
- *
- * This is a final class and cannot be extended.
- */
-public final class TracingOpenTelemetry {
- private static final String INSTRUMENTATION_NAME =
- "aws-lambda-powertools";
+public final class TracingOpenTelemetry {
private final Tracer tracer;
+ private final TextMapPropagator propagator;
+
+ 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"
+ );
+ }
- /**
- * Creates a tracing instance using the provided tracer.
- *
- *
This constructor is primarily useful for testing.
- *
- * @param tracer the OpenTelemetry tracer
- */
- TracingOpenTelemetry(Tracer tracer) {
- this.tracer = Objects.requireNonNull(tracer, "tracer must not be null");
- }
-
- /**
- * Initializes a new instance of the {@code TracingOpenTelemetry} class, using
- * the global OpenTelemetry tracer identified by the instrumentation name.
- *
- * This constructor simplifies the setup process for applications by
- * automatically leveraging the globally configured instrumentation tracer.
- */
public TracingOpenTelemetry() {
- this(GlobalOpenTelemetry.getTracer(INSTRUMENTATION_NAME));
+ this(OpenTelemetryProvider.tracer());
+ }
+
+
+ public TracingOpenTelemetry(Tracer tracer) {
+ this(tracer, createDefaultPropagator());
}
- /**
- * Creates a new span with the specified name and makes it the current span in the thread context.
- * The span must be manually closed to properly end it and revert the thread context.
- *
- * @param name the name of the span to be created
- * @return an instance of {@link SpanScope}, which represents the created span and its associated context
- */
+
+ public TracingOpenTelemetry(
+ Tracer tracer,
+ TextMapPropagator propagator) {
+
+ this.tracer = Objects.requireNonNull(
+ tracer,
+ "tracer must not be null"
+ );
+ this.propagator = Objects.requireNonNull(
+ propagator,
+ "propagator must not be null"
+ );
+ }
+
+
public SpanScope addSpan(String name) {
- Span span = tracer
- .spanBuilder(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);
}
- /**
- * Retrieves the current active span in the execution context.
- *
- * @return the current {@link Span} if one is active, or a default no-op {@link Span} if none is active
- */
+ 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 Span currentSpan() {
return Span.current();
}
- /**
- * Executes the specified operation within the context of a new span.
- * The span is automatically managed and closed when the operation completes
- * or an exception is thrown.
- *
- * @param name the name of the span to be created
- * @param operation the operation to be executed within the span's context
- * @throws Exception if the provided operation throws an exception during execution
- */
- public T withSpan(String name, SpanOperation operation) throws Exception {
+
+ 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 e) {
- scope.recordException(e);
- throw e;
+ } catch (Exception exception) {
+ scope.recordException(exception);
+ throw exception;
}
}
}
- /**
- * Creates a new tracing instance using the global OpenTelemetry tracer.
- *
- * @return a new tracing instance
- */
+ 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("faas.coldstart", LambdaHandlerProcessor.isColdStart())
+ .setAttribute("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();
+ }
+
+
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();
+
+ public Builder tracer(Tracer tracer) {
+ this.tracer = tracer;
+ return this;
+ }
+
+ public Builder propagator(TextMapPropagator propagator) {
+ this.propagator = propagator;
+ 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/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/OpenTelemetryProvider.java b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/OpenTelemetryProvider.java
new file mode 100644
index 000000000..41674821a
--- /dev/null
+++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/OpenTelemetryProvider.java
@@ -0,0 +1,79 @@
+/*
+ * 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.internal;
+
+import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
+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.concurrent.TimeUnit;
+
+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 OpenTelemetryProvider() {
+ }
+
+ public static Tracer tracer() {
+ return TRACER_PROVIDER.get(INSTRUMENTATION_NAME);
+ }
+
+ public static SdkTracerProvider tracerProvider() {
+ return TRACER_PROVIDER;
+ }
+
+ public static TextMapPropagator propagator() {
+ return createPropagator();
+ }
+
+ private static SdkTracerProvider createTracerProvider() {
+
+ OtlpGrpcSpanExporter exporter =
+ OtlpGrpcSpanExporter.builder()
+ .setTimeout(
+ EXPORT_TIMEOUT_MILLIS,
+ TimeUnit.MILLISECONDS
+ )
+ .build();
+
+ 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();
+ }
+
+ //TODO Pending adding AWS X-RAY propagation, the library opentelemetry-aws-xray-propagator is still in alpha
+ private static TextMapPropagator createPropagator() {
+ return W3CTraceContextPropagator.getInstance();
+ }
+}
\ No newline at end of file
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
index 2326fbbca..fe757d840 100644
--- 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
@@ -1,19 +1,6 @@
-/*
- * 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.common.Attributes;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.context.Scope;
@@ -40,20 +27,25 @@ public SpanScope(Span span) {
this.scope = span.makeCurrent();
}
- /**
- * Retrieves the {@link Span} associated with this {@link SpanScope}.
- *
- * @return the {@link Span} managed by this {@link SpanScope}
- */
public Span span() {
return span;
}
- /**
- * Records an exception in the span and sets its status to {@code StatusCode.ERROR}.
- *
- * @param throwable the {@link Throwable} instance to be recorded as an event in the 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);
@@ -64,4 +56,4 @@ 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
index fee0d1bfd..dcd410dee 100644
--- 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
@@ -1,95 +1,193 @@
+/*
+ * 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 static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.serviceName;
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;
-import software.amazon.lambda.powertools.tracing.opentelemetry.TracingOtel;
@Aspect
public final class TracingOpenTelemetryAspect {
- //tracing cannot be final for testing purposes
- private static TracingOpenTelemetry tracing =
- TracingOpenTelemetry.create();
-
- private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
- private static final String COLD_START_ATTRIBUTE =
- "aws.lambda.powertools.cold_start";
- private static final String SERVICE_ATTRIBUTE =
- "aws.lambda.powertools.service";
+ // Cannot be final for testing purposes
+ private static TracingOpenTelemetry tracingOtel = TracingOpenTelemetry.create();
- private static final String RESPONSE_ATTRIBUTE =
- "aws.lambda.powertools.response";
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
- @SuppressWarnings({"EmptyMethod"})
- @Pointcut("@annotation(tracingOtel)")
- public void callAt(TracingOtel tracingOtel) {
+ @SuppressWarnings("EmptyMethod")
+ @Pointcut("@annotation(tracing)")
+ public void callAt(Tracing tracing) {
}
@Around(
- value = "callAt(tracingOtel) && execution(@TracingOtel * *.*(..))",
- argNames = "pjp,tracingOtel"
+ value = "callAt(tracing) && execution(@Tracing * *.*(..))",
+ argNames = "pjp,tracing"
)
- public Object around(ProceedingJoinPoint pjp, TracingOtel tracingOtel) throws Throwable {
+ public Object around(
+ ProceedingJoinPoint pjp,
+ Tracing tracing) throws Throwable {
- String spanName = tracingOtel.spanName().isEmpty()
+ String spanName = tracing.spanName().isEmpty()
? pjp.getSignature().getName()
- : tracingOtel.spanName();
+ : tracing.spanName();
+
+ if (isHandlerMethod(pjp)) {
+ return traceHandler(pjp, tracing, spanName);
+ }
- String namespace = tracingOtel.namespace().isEmpty()
- ? serviceName()
- : tracingOtel.namespace();
+ return traceMethod(pjp, tracing, spanName);
+ }
+
+ private Object traceHandler(
+ ProceedingJoinPoint pjp,
+ Tracing tracing,
+ String spanName) throws Throwable {
+
+ Context parentContext = Context.current();
- try (SpanScope scope = tracing.addSpan(spanName)) {
+ try (SpanScope scope = tracingOtel.addSpan(
+ spanName,
+ SpanKind.SERVER,
+ handlerAttributes(),
+ parentContext)) {
Span span = scope.span();
- if (isHandlerMethod(pjp)) {
- span.setAttribute(COLD_START_ATTRIBUTE, isColdStart());
- span.setAttribute(SERVICE_ATTRIBUTE, namespace);
- }
+ addLambdaInvocationAttributes(pjp, span);
try {
-
Object result = pjp.proceed(pjp.getArgs());
- if (captureResponse(tracingOtel)) {
- span.setAttribute(RESPONSE_ATTRIBUTE, OBJECT_MAPPER.writeValueAsString(result));
- }
+ 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())) {
- if (isHandlerMethod(pjp)) {
- coldStartDone();
- }
+ Span span = scope.span();
+
+ try {
+ Object result = pjp.proceed(pjp.getArgs());
+
+ captureResponse(
+ span,
+ tracing,
+ result
+ );
return result;
} catch (Throwable throwable) {
+ captureError(
+ scope,
+ tracing,
+ throwable
+ );
- if (captureError(tracingOtel)) {
- scope.recordException(throwable);
- }
throw throwable;
}
}
}
- private boolean captureResponse(TracingOtel tracing) {
+ 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 (!captureResponse(tracing)) {
+ return;
+ }
+
+ span.setAttribute(
+ AttributesConstants.RESPONSE_ATTRIBUTE,
+ OBJECT_MAPPER.writeValueAsString(response)
+ );
+ }
+
+ private void captureError(
+ SpanScope scope,
+ Tracing tracing,
+ Throwable throwable) {
+
+ if (captureError(tracing)) {
+ scope.recordException(throwable);
+ }
+ }
+
+ private boolean captureResponse(Tracing tracing) {
switch (tracing.captureMode()) {
case ENVIRONMENT_VAR:
- return isEnvironmentVariableSet("POWERTOOLS_TRACER_CAPTURE_RESPONSE")
- && environmentVariable("POWERTOOLS_TRACER_CAPTURE_RESPONSE");
+ return isEnvironmentVariableSet(
+ AttributesConstants.CAPTURE_RESPONSE_ENV)
+ && environmentVariable(
+ AttributesConstants.CAPTURE_RESPONSE_ENV);
+
case RESPONSE:
case RESPONSE_AND_ERROR:
return true;
+
case DISABLED:
case ERROR:
default:
@@ -97,14 +195,18 @@ private boolean captureResponse(TracingOtel tracing) {
}
}
- private boolean captureError(TracingOtel tracing) {
+ private boolean captureError(Tracing tracing) {
switch (tracing.captureMode()) {
case ENVIRONMENT_VAR:
- return isEnvironmentVariableSet("POWERTOOLS_TRACER_CAPTURE_ERROR")
- && environmentVariable("POWERTOOLS_TRACER_CAPTURE_ERROR");
+ return isEnvironmentVariableSet(
+ AttributesConstants.CAPTURE_ERROR_ENV)
+ && environmentVariable(
+ AttributesConstants.CAPTURE_ERROR_ENV);
+
case ERROR:
case RESPONSE_AND_ERROR:
return true;
+
case DISABLED:
case RESPONSE:
default:
@@ -113,11 +215,12 @@ private boolean captureError(TracingOtel tracing) {
}
private boolean environmentVariable(String key) {
- return Boolean.parseBoolean(SystemWrapper.getenv(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/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
index cb64c11f3..8ca67d47a 100644
--- 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
@@ -18,15 +18,39 @@
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanContext;
+import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.propagation.TextMapGetter;
+import io.opentelemetry.context.propagation.TextMapPropagator;
import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
+import io.opentelemetry.sdk.trace.data.SpanData;
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
import org.junit.jupiter.api.Test;
import software.amazon.lambda.powertools.tracing.opentelemetry.internal.SpanScope;
class TracingOpenTelemetryTest {
+ public static final TextMapGetter
+ * 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.
@@ -153,9 +146,7 @@ public String get(Map carrier, String key) {
private static SdkTracerProvider createTracerProvider() {
- String protocol = SystemWrapper.getenv(OTEL_EXPORTER_OTLP_TRACES_PROTOCOL);
-
- SpanExporter exporter = createExporter(protocol);
+ SpanExporter exporter = createExporter();
BatchSpanProcessor processor = BatchSpanProcessor.builder(exporter)
.setMaxExportBatchSize(MAX_EXPORT_BATCH_SIZE)
@@ -170,11 +161,19 @@ private static SdkTracerProvider createTracerProvider() {
.build();
}
- private static SpanExporter createExporter(String protocol) {
+ private static SpanExporter createExporter() {
+
+ String protocol = SystemWrapper.getenv(OTEL_EXPORTER_OTLP_TRACES_PROTOCOL);
+ String endpoint = SystemWrapper.getenv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT);
+ String headers = SystemWrapper.getenv(OTEL_EXPORTER_OTLP_TRACES_HEADERS);
+
+ Map headerMap = parseHeaders(headers);
if (protocol == null || protocol.isBlank()) {
return OtlpGrpcSpanExporter.builder()
.setTimeout(EXPORT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
+ .setEndpoint(endpoint)
+ .setHeaders(() -> headerMap)
.build();
}
@@ -182,11 +181,15 @@ private static SpanExporter createExporter(String protocol) {
case "grpc":
return OtlpGrpcSpanExporter.builder()
.setTimeout(EXPORT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
+ .setEndpoint(endpoint)
+ .setHeaders(() -> headerMap)
.build();
case "http/protobuf":
return OtlpHttpSpanExporter.builder()
.setTimeout(EXPORT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
+ .setEndpoint(endpoint)
+ .setHeaders(() -> headerMap)
.build();
default:
@@ -196,6 +199,41 @@ private static SpanExporter createExporter(String protocol) {
}
}
+ private static Map parseHeaders(String headers) {
+
+ if (headers == null || headers.isBlank()) {
+ return Collections.emptyMap();
+ }
+
+ Map result = new HashMap<>();
+
+ String[] entries = headers.split(",");
+
+ for (String entry : entries) {
+
+ String[] parts = entry.split("=", 2);
+
+ if (parts.length != 2) {
+ throw new IllegalArgumentException(
+ "Invalid OTLP header: " + entry
+ );
+ }
+
+ String key = parts[0].trim();
+ String value = parts[1].trim();
+
+ if (key.isEmpty()) {
+ throw new IllegalArgumentException(
+ "OTLP header name cannot be empty"
+ );
+ }
+
+ result.put(key, value);
+ }
+
+ return result;
+ }
+
private static TextMapPropagator createPropagator() {
return TextMapPropagator.composite(
From d8880a27f5e6ab51ad26229dd14e7377b9ca3323 Mon Sep 17 00:00:00 2001
From: David-DAM <82216301+David-DAM@users.noreply.github.com>
Date: Sat, 29 Aug 2026 15:59:24 +0200
Subject: [PATCH 15/21] Add flush support in `TracingOpenTelemetry` and
`OpenTelemetryProvider`, refactor tracer provider initialization for improved
SDK management.
---
.../tracing/opentelemetry/TracingOpenTelemetry.java | 4 ++++
.../opentelemetry/provider/OpenTelemetryProvider.java | 11 ++++++++++-
2 files changed, 14 insertions(+), 1 deletion(-)
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
index cd27b8497..581e06201 100644
--- 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
@@ -26,6 +26,7 @@
import io.opentelemetry.context.propagation.TextMapGetter;
import io.opentelemetry.context.propagation.TextMapPropagator;
import io.opentelemetry.context.propagation.TextMapSetter;
+import io.opentelemetry.sdk.common.CompletableResultCode;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@@ -90,6 +91,9 @@ public Span currentSpan() {
return Span.current();
}
+ public CompletableResultCode flush() {
+ return OpenTelemetryProvider.forceFlush();
+ }
public SpanScope addSpan(String name) {
return addSpan(name, SpanKind.INTERNAL);
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
index b9f7a686c..ffaf79399 100644
--- 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
@@ -18,6 +18,7 @@
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.sdk.OpenTelemetrySdk;
+import io.opentelemetry.sdk.common.CompletableResultCode;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.sdk.trace.export.SpanExporter;
@@ -50,6 +51,8 @@ public final class OpenTelemetryProvider {
private static final TraceContextPropagationMode TRACE_CONTEXT_PROPAGATION_MODE = retrieveTraceContextMode();
+ private static final SdkTracerProvider SDK_TRACER_PROVIDER = createTracerProvider();
+
private static final OpenTelemetry OPEN_TELEMETRY = initializeOpenTelemetry();
@@ -95,6 +98,12 @@ private static OpenTelemetry initializeOpenTelemetry() {
return createDefaultOpenTelemetry();
}
+ public static CompletableResultCode forceFlush() {
+ if (GlobalOpenTelemetry.isSet()) {
+ return CompletableResultCode.ofSuccess();
+ }
+ return SDK_TRACER_PROVIDER.forceFlush();
+ }
/**
* Creates the Powertools default OpenTelemetry configuration.
@@ -102,7 +111,7 @@ private static OpenTelemetry initializeOpenTelemetry() {
private static OpenTelemetry createDefaultOpenTelemetry() {
return OpenTelemetrySdk.builder()
- .setTracerProvider(createTracerProvider())
+ .setTracerProvider(SDK_TRACER_PROVIDER)
.setPropagators(ContextPropagators.create(PROPAGATOR))
.build();
}
From f1d0ba64b27055f3ee9bb07e734c0d63ae244fa2 Mon Sep 17 00:00:00 2001
From: David-DAM <82216301+David-DAM@users.noreply.github.com>
Date: Sun, 30 Aug 2026 11:53:22 +0200
Subject: [PATCH 16/21] Add `flush` method with timeout in
`TracingOpenTelemetry`, update `TracingOpenTelemetryAspect` to ensure
flushing spans, and introduce reusable default instance.
---
.../tracing/opentelemetry/TracingOpenTelemetry.java | 10 ++++++++--
.../internal/TracingOpenTelemetryAspect.java | 7 +++++++
2 files changed, 15 insertions(+), 2 deletions(-)
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
index 581e06201..1b853fef4 100644
--- 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
@@ -30,6 +30,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Objects;
+import java.util.concurrent.TimeUnit;
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;
@@ -38,6 +39,7 @@
public final class TracingOpenTelemetry {
+ private static final TracingOpenTelemetry DEFAULT_INSTANCE = new TracingOpenTelemetry();
private final Tracer tracer;
private final TextMapPropagator propagator;
private final LambdaEventContextExtractorResolver eventContextExtractorResolver;
@@ -92,7 +94,11 @@ public Span currentSpan() {
}
public CompletableResultCode flush() {
- return OpenTelemetryProvider.forceFlush();
+ return flush(5, TimeUnit.SECONDS);
+ }
+
+ public CompletableResultCode flush(long timeout, TimeUnit unit) {
+ return OpenTelemetryProvider.forceFlush().join(timeout, unit);
}
public SpanScope addSpan(String name) {
@@ -201,7 +207,7 @@ private static LambdaEventContextExtractorResolver createDefaultEventContextExtr
}
public static TracingOpenTelemetry create() {
- return new TracingOpenTelemetry();
+ return DEFAULT_INSTANCE;
}
public static Builder builder() {
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
index 80888480f..4a8f0c8cc 100644
--- 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
@@ -13,6 +13,7 @@
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.context.Context;
+import java.util.Objects;
import java.util.Optional;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
@@ -32,6 +33,10 @@ public final class TracingOpenTelemetryAspect {
// Cannot be final for testing purposes
private static TracingOpenTelemetry tracingOtel = TracingOpenTelemetry.create();
+ public static void configure(TracingOpenTelemetry tracing) {
+ tracingOtel = Objects.requireNonNull(tracing);
+ }
+
@SuppressWarnings("EmptyMethod")
@Pointcut("@annotation(tracing)")
public void callAt(Tracing tracing) {
@@ -82,6 +87,8 @@ private Object traceHandler(ProceedingJoinPoint pjp, Tracing tracing, String spa
throw throwable;
}
+ } finally {
+ tracingOtel.flush();
}
}
From adfff2880edb4b4fff70c6d0db8d9ff1860e5017 Mon Sep 17 00:00:00 2001
From: David-DAM <82216301+David-DAM@users.noreply.github.com>
Date: Sun, 30 Aug 2026 17:35:22 +0200
Subject: [PATCH 17/21] Add detailed JavaDocs to improve documentation of
`TracingOpenTelemetry`, context extractors, and related classes and methods.
---
.../tracing/opentelemetry/CaptureMode.java | 16 ++
.../tracing/opentelemetry/Tracing.java | 16 ++
.../opentelemetry/TracingOpenTelemetry.java | 206 +++++++++++++++++-
.../ApiGatewayTraceContextExtractor.java | 23 ++
.../DynamoDbTraceContextExtractor.java | 30 +++
.../context/ExtractedTraceContext.java | 44 ++++
.../context/KinesisTraceContextExtractor.java | 38 ++++
.../context/LambdaEventContextExtractor.java | 58 +++++
.../LambdaEventContextExtractorResolver.java | 54 +++++
.../context/S3TraceContextExtractor.java | 24 ++
.../context/SnsTraceContextExtractor.java | 35 +++
.../context/SqsTraceContextExtractor.java | 37 ++++
.../context/TraceContextPropagationMode.java | 22 ++
.../internal/AttributesConstants.java | 24 ++
.../internal/LambdaResource.java | 44 ++++
.../opentelemetry/internal/SpanOperation.java | 4 +-
.../opentelemetry/internal/SpanScope.java | 81 ++++++-
.../internal/TracingOpenTelemetryAspect.java | 57 ++++-
.../provider/OpenTelemetryProvider.java | 110 +++++++++-
19 files changed, 897 insertions(+), 26 deletions(-)
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
index d62c3b1ff..40ad84307 100644
--- 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
@@ -1,3 +1,19 @@
+/*
+ * 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;
/**
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
index 2d4d68608..820026cc2 100644
--- 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
@@ -1,3 +1,19 @@
+/*
+ * 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 java.lang.annotation.ElementType;
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
index 1b853fef4..9585ca03f 100644
--- 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
@@ -36,7 +36,18 @@
import software.amazon.lambda.powertools.tracing.opentelemetry.internal.SpanScope;
import software.amazon.lambda.powertools.tracing.opentelemetry.provider.OpenTelemetryProvider;
-
+/**
+ * A utility class responsible for managing OpenTelemetry tracing functionality,
+ * including creating and managing spans, handling context propagation, and facilitating
+ * relevant operations for distributed tracing.
+ *
+ * This class provides methods to manage the life cycle of spans, propagate and extract
+ * context, flush telemetry data, and execute operations within spans. It also supports
+ * configuration via a builder pattern.
+ *
+ * The class is designed to be thread-safe and offers a default singleton instance
+ * for convenience.
+ */
public final class TracingOpenTelemetry {
private static final TracingOpenTelemetry DEFAULT_INSTANCE = new TracingOpenTelemetry();
@@ -77,50 +88,130 @@ public TracingOpenTelemetry(
);
}
+ /**
+ * Provides access to the current Tracer instance.
+ *
+ * @return the Tracer instance associated with the current context
+ */
public Tracer tracer() {
return tracer;
}
+ /**
+ * Provides the current TextMapPropagator instance.
+ *
+ * @return the TextMapPropagator instance used for propagating context information.
+ */
public TextMapPropagator propagator() {
return propagator;
}
+ /**
+ * Retrieves the instance of LambdaEventContextExtractorResolver.
+ *
+ * @return the resolver used to extract context from Lambda events.
+ */
public LambdaEventContextExtractorResolver eventContextExtractorResolver() {
return eventContextExtractorResolver;
}
+ /**
+ * Retrieves the current active span within the context.
+ *
+ * @return the currently active span, or null if there is no active span
+ */
public Span currentSpan() {
return Span.current();
}
+ /**
+ * Forces all pending spans and related telemetry data to be processed and exported.
+ * This method sends the pending data using the default timeout period.
+ *
+ * @return a {@code CompletableResultCode} indicating the success or failure of the flush operation
+ */
public CompletableResultCode flush() {
return flush(5, TimeUnit.SECONDS);
}
+ /**
+ * Forces all pending spans and related telemetry data to be processed and exported
+ * within a specified timeout period.
+ *
+ * @param timeout the maximum duration to wait for the flush operation to complete
+ * @param unit the time unit of the {@code timeout} parameter
+ * @return a {@code CompletableResultCode} indicating the success or failure of the flush operation
+ */
public CompletableResultCode flush(long timeout, TimeUnit unit) {
return OpenTelemetryProvider.forceFlush().join(timeout, unit);
}
+ /**
+ * Starts a new OpenTelemetry span with the given name and a default {@link SpanKind#INTERNAL} kind.
+ *
+ * @param name the name of the span to be created
+ * @return a {@link SpanScope} instance that manages the lifecycle of the span and its associated context
+ */
public SpanScope addSpan(String name) {
return addSpan(name, SpanKind.INTERNAL);
}
-
+ /**
+ * Starts a new OpenTelemetry span with the given name, kind, and default attributes.
+ *
+ * @param name the name of the span to be created
+ * @param kind the kind of the span, e.g., {@link SpanKind#INTERNAL}, {@link SpanKind#CLIENT}, etc.
+ * @return a {@link SpanScope} instance that manages the lifecycle of the span and its associated context
+ */
public SpanScope addSpan(String name, SpanKind kind) {
return addSpan(name, kind, Attributes.empty());
}
+ /**
+ * Starts a new OpenTelemetry span with the given name, kind, and attributes,
+ * using the current thread context as the parent context.
+ *
+ * @param name the name of the span to be created
+ * @param kind the kind of the span, such as {@code SpanKind.INTERNAL}, {@code SpanKind.CLIENT}, etc.
+ * @param attributes the attributes to associate with the span
+ * @return a {@code SpanScope} instance that manages the lifecycle of the span and its associated context
+ */
public SpanScope addSpan(String name, SpanKind kind, Attributes attributes) {
return addSpan(name, kind, attributes, Context.current());
}
+ /**
+ * Starts a new OpenTelemetry span with the given name, kind, attributes, and parent context.
+ * The span is returned encapsulated in a {@code SpanScope}, which manages the lifecycle
+ * of the span and its associated context.
+ *
+ * @param name the name of the span to be created
+ * @param kind the type of the span, such as {@code SpanKind.INTERNAL}, {@code SpanKind.CLIENT}, etc.
+ * @param attributes the attributes to associate with the span
+ * @param parentContext the parent context to use for the span
+ * @return a {@code SpanScope} instance that manages the lifecycle of the span and its related context
+ */
public SpanScope addSpan(String name, SpanKind kind, Attributes attributes, Context parentContext) {
return addSpan(name, kind, attributes, parentContext, Collections.emptyList());
}
+ /**
+ * Starts a new OpenTelemetry span with the given configuration, including name, kind, attributes,
+ * parent context, and links to other spans represented by their {@code SpanContext}s.
+ * The resulting span is encapsulated within a {@code SpanScope} for proper lifecycle management.
+ *
+ * @param name the name of the span to be created; must not be null
+ * @param kind the type of the span, such as {@code SpanKind.INTERNAL}, {@code SpanKind.CLIENT}, etc.;
+ * must not be null
+ * @param attributes the attributes to associate with the span; must not be null
+ * @param parentContext the parent context to use for the span; must not be null
+ * @param spanContexts the list of {@code SpanContext} instances to link to the created span; must not be null
+ * @return a {@code SpanScope} instance that manages the lifecycle of the span and its associated context
+ * @throws NullPointerException if any of the parameters are null
+ */
public SpanScope addSpan(
String name,
SpanKind kind,
@@ -146,12 +237,37 @@ public SpanScope addSpan(
return new SpanScope(spanBuilder.startSpan());
}
-
+ /**
+ * Executes a given operation within the context of an OpenTelemetry span with the specified name.
+ * The span is created with the default {@link SpanKind#INTERNAL} and no additional attributes.
+ * Any exceptions thrown during the operation will be recorded in the span.
+ *
+ * @param the type of result returned by the operation
+ * @param name the name of the span to be created; must not be null
+ * @param operation the operation to execute within the span context; must not be null
+ * @return the result of the operation
+ * @throws Exception if an error occurs during the execution of the operation
+ */
public T withSpan(String name, SpanOperation operation) throws Exception {
return withSpan(name, SpanKind.INTERNAL, Attributes.empty(), operation);
}
+ /**
+ * Executes a given operation within the context of an OpenTelemetry span
+ * with the specified name, kind, and attributes. The span is created and
+ * managed within the method. Any exceptions thrown during the operation
+ * are recorded in the span before being propagated.
+ *
+ * @param the type of result returned by the operation
+ * @param name the name of the span to be created; must not be null
+ * @param kind the kind of the span, such as {@code SpanKind.INTERNAL}
+ * or {@code SpanKind.CLIENT}; must not be null
+ * @param attributes the attributes to associate with the span; must not be null
+ * @param operation the operation to execute within the span context; must not be null
+ * @return the result of the operation
+ * @throws Exception if an error occurs during the execution of the operation
+ */
public T withSpan(
String name,
SpanKind kind,
@@ -170,11 +286,28 @@ public T withSpan(
}
}
+ /**
+ * Extracts a {@code Context} from the given carrier using the specified {@link TextMapGetter}.
+ *
+ * @param the type of the carrier from which the context is extracted
+ * @param carrier the carrier object that holds context propagation data; must not be null
+ * @param getter the {@link TextMapGetter} used to read propagation fields from the carrier; must not be null
+ * @return the extracted {@code Context}, or the current context if no context could be extracted
+ * @throws NullPointerException if the carrier or getter is null
+ */
public Context extractContext(T carrier, TextMapGetter getter) {
return extractContext(Context.current(), carrier, getter);
}
+ /**
+ * Extracts a {@link Context} from the given carrier using the specified {@link TextMapGetter}.
+ *
+ * @param context the initial {@link Context} used as the baseline for extraction; must not be null
+ * @param carrier the carrier of the propagation fields; must not be null
+ * @param getter the {@link TextMapGetter} used to read propagation fields from the carrier; must not be null
+ * @return the extracted {@link Context} containing the propagated values
+ */
public Context extractContext(Context context, T carrier, TextMapGetter getter) {
Objects.requireNonNull(context, "context must not be null");
@@ -184,11 +317,26 @@ public Context extractContext(Context context, T carrier, TextMapGetter g
return propagator.extract(context, carrier, getter);
}
+ /**
+ * Injects the current context into the specified carrier using the provided TextMapSetter.
+ *
+ * @param The type of the carrier into which the context will be injected.
+ * @param carrier The carrier object that will hold the injected context.
+ * @param setter The TextMapSetter implementation used to set the context into the carrier.
+ */
public void injectContext(T carrier, TextMapSetter setter) {
injectContext(Context.current(), carrier, setter);
}
+ /**
+ * Injects the provided {@code Context} into the specified carrier using the given {@code TextMapSetter}.
+ *
+ * @param context the context to inject; must not be null
+ * @param carrier the carrier into which the context will be injected; must not be null
+ * @param setter the {@code TextMapSetter} used to define how the context is set on the carrier; must not be null
+ * @param the type of the carrier
+ */
public void injectContext(Context context, T carrier, TextMapSetter setter) {
Objects.requireNonNull(context, "context must not be null");
@@ -206,14 +354,34 @@ private static LambdaEventContextExtractorResolver createDefaultEventContextExtr
return LambdaEventContextExtractorResolver.create();
}
+ /**
+ * Creates and returns the default instance of the TracingOpenTelemetry.
+ *
+ * @return The default instance of TracingOpenTelemetry.
+ */
public static TracingOpenTelemetry create() {
return DEFAULT_INSTANCE;
}
+ /**
+ * Creates and returns a new instance of the Builder.
+ *
+ * @return a new Builder instance
+ */
public static Builder builder() {
return new Builder();
}
+ /**
+ * Builder class for creating instances of TracingOpenTelemetry.
+ * This class provides a fluent API for configuring and constructing
+ * a TracingOpenTelemetry object.
+ *
+ * The Builder allows customization of the following components:
+ * - Tracer: A tracer instance used for tracing operations.
+ * - TextMapPropagator: A propagator responsible for context propagation.
+ * - LambdaEventContextExtractorResolver: A resolver for extracting context from Lambda events.
+ */
public static final class Builder {
private Tracer tracer;
@@ -221,22 +389,54 @@ public static final class Builder {
private LambdaEventContextExtractorResolver eventContextExtractorResolver =
createDefaultEventContextExtractorResolver();
+ /**
+ * Sets the tracer instance to be used for tracing operations.
+ * This method allows specifying a custom tracer, which will
+ * be used to create and manage spans in tracing contexts.
+ *
+ * @param tracer the tracer instance to be used for tracing
+ * @return the updated Builder instance for method chaining
+ */
public Builder tracer(Tracer tracer) {
this.tracer = tracer;
return this;
}
+ /**
+ * Sets the {@link TextMapPropagator} to be used for context propagation.
+ * This allows specifying a custom propagator to handle the injection and extraction
+ * of context data across process boundaries.
+ *
+ * @param propagator the {@link TextMapPropagator} instance to be used for context propagation
+ * @return the updated Builder instance for method chaining
+ */
public Builder propagator(TextMapPropagator propagator) {
this.propagator = propagator;
return this;
}
+ /**
+ * Sets the {@link LambdaEventContextExtractorResolver} to be used for extracting
+ * context from AWS Lambda events. This allows specifying a custom resolver
+ * to handle the extraction of trace context from various types of AWS Lambda
+ * event sources.
+ *
+ * @param eventContextExtractorResolver the {@link LambdaEventContextExtractorResolver} instance
+ * to be used for extracting trace context from Lambda events
+ * @return the updated Builder instance for method chaining
+ */
public Builder eventContextExtractorResolver(
LambdaEventContextExtractorResolver eventContextExtractorResolver) {
this.eventContextExtractorResolver = eventContextExtractorResolver;
return this;
}
+ /**
+ * Constructs a new instance of TracingOpenTelemetry using the current state of the Builder.
+ * This method finalizes the configuration and returns the configured TracingOpenTelemetry instance.
+ *
+ * @return a fully configured TracingOpenTelemetry instance
+ */
public TracingOpenTelemetry build() {
return new TracingOpenTelemetry(this);
}
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
index a5a0d36a0..a6aa98917 100644
--- 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
@@ -1,3 +1,19 @@
+/*
+ * 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.context;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
@@ -10,6 +26,13 @@
import java.util.stream.Collectors;
import software.amazon.lambda.powertools.tracing.opentelemetry.provider.OpenTelemetryProvider;
+/**
+ * An implementation of {@link LambdaEventContextExtractor} that extracts and enriches tracing context
+ * information from API Gateway events. This class supports distributed tracing by leveraging OpenTelemetry
+ * to propagate and enrich trace data from API Gateway-provided HTTP headers and metadata.
+ *
+ * This extractor handles events of type {@link APIGatewayProxyRequestEvent}.
+ */
public final class ApiGatewayTraceContextExtractor implements LambdaEventContextExtractor {
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
index 3f04b6ace..acdbe1bab 100644
--- 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
@@ -1,3 +1,19 @@
+/*
+ * 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.context;
import com.amazonaws.services.lambda.runtime.events.DynamodbEvent;
@@ -8,6 +24,20 @@
import java.util.List;
import java.util.Objects;
+/**
+ * A specialized implementation of {@link LambdaEventContextExtractor} for handling AWS DynamoDB Streams events.
+ * This class enables extraction of tracing context, enrichment of OpenTelemetry spans, and determination of
+ * compatibility with DynamoDB Streams events for distributed tracing purposes.
+ *
+ * Instances of this class focus on the following:
+ * - Verifying if an event is a DynamoDB Streams event.
+ * - Extracting trace context information in scenarios where trace context propagation is applicable.
+ * - Enriching OpenTelemetry spans with metadata derived from DynamoDB Streams events, such as stream names
+ * and record batch sizes.
+ *
+ * Note: Due to limitations in DynamoDB Streams metadata, W3C trace context propagation (e.g., `traceparent`)
+ * is not supported by default. Future enhancements for dedicated propagation strategies may be required.
+ */
public final class DynamoDbTraceContextExtractor implements LambdaEventContextExtractor {
@Override
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
index 23657e5cc..3a7abd2e7 100644
--- 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
@@ -1,3 +1,19 @@
+/*
+ * 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.context;
import io.opentelemetry.api.trace.SpanContext;
@@ -5,6 +21,13 @@
import io.opentelemetry.context.Context;
import java.util.List;
+/**
+ * Represents the trace context extracted during event processing in an OpenTelemetry-based tracing system.
+ * This class encapsulates the parent context, a collection of span contexts, and the span kind associated
+ * with the extracted trace.
+ *
+ * Instances of this class are immutable, ensuring thread-safety when utilized in multi-threaded environments.
+ */
public final class ExtractedTraceContext {
private final Context parentContext;
@@ -23,14 +46,35 @@ public ExtractedTraceContext(Context parentContext, List spanContex
this.spanKind = SpanKind.SERVER;
}
+ /**
+ * Returns the parent context associated with this extracted trace context.
+ * The parent context provides the linkage to the pre-existing context
+ * in the OpenTelemetry system, enabling context propagation.
+ *
+ * @return the parent {@link Context} of this extracted trace context
+ */
public Context context() {
return parentContext;
}
+ /**
+ * Returns the collection of {@link SpanContext} instances associated with this extracted trace context.
+ * Span contexts represent individual trace spans, enabling correlation and telemetry processing
+ * across distributed systems.
+ *
+ * @return a list of {@link SpanContext} instances associated with this trace context
+ */
public List spanContexts() {
return spanContexts;
}
+ /**
+ * Returns the span kind associated with this extracted trace context.
+ * The span kind indicates the role of the span in a distributed trace,
+ * such as SERVER, CLIENT, PRODUCER, or CONSUMER.
+ *
+ * @return the {@link SpanKind} of this extracted trace context
+ */
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
index 92c9bc977..739fcb9a7 100644
--- 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
@@ -1,3 +1,19 @@
+/*
+ * 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.context;
import com.amazonaws.services.lambda.runtime.events.KinesisEvent;
@@ -7,6 +23,28 @@
import io.opentelemetry.context.propagation.TextMapPropagator;
import java.util.List;
+/**
+ * An implementation of the {@link LambdaEventContextExtractor} interface designed
+ * for AWS Lambda functions that are triggered by Kinesis events. This class
+ * provides methods for extracting trace context, determining support for
+ * Kinesis events, and enriching spans with metadata specific to Kinesis.
+ *
+ * Trace propagation for Kinesis is limited, as Kinesis records do not inherently
+ * include W3C trace context propagation attributes. As such, this implementation
+ * assumes trace context is not present in the payload and instead defines how
+ * future propagation strategies could be supported.
+ *
+ * This class primarily handles the following responsibilities:
+ * - Identifies whether a given event is a Kinesis event.
+ * - Extracts minimal trace context from a Kinesis event, returning a consumer
+ * span kind without assuming additional trace attributes.
+ * - Enriches spans with Kinesis-specific attributes, such as partition key,
+ * sequence number, approximate arrival timestamp, and stream name.
+ *
+ * It is intended for use in distributed tracing scenarios within AWS Lambda
+ * functions, ensuring that spans generated for Kinesis events are annotated
+ * with meaningful metadata.
+ */
public final class KinesisTraceContextExtractor
implements LambdaEventContextExtractor {
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
index b4d7e12cc..8bc2c9741 100644
--- 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
@@ -1,14 +1,72 @@
+/*
+ * 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.context;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.TextMapPropagator;
+/**
+ * Defines a contract for extracting, enriching, and validating tracing context information
+ * from AWS Lambda event objects in order to support distributed tracing.
+ *
+ * Implementations of this interface are intended to handle specific types of AWS Lambda
+ * event sources, such as SQS, SNS, DynamoDB, Kinesis, or API Gateway events. The methods
+ * within this interface facilitate propagating and enriching trace information within
+ * OpenTelemetry spans and contexts.
+ */
public interface LambdaEventContextExtractor {
+ /**
+ * Determines whether the provided event is supported by this context extractor.
+ *
+ * @param event The AWS Lambda event to check for compatibility. Typically, this would
+ * be an event source object such as SQS, SNS, DynamoDB, Kinesis, API
+ * Gateway, or other supported AWS Lambda event types.
+ * @return true if the given event type is supported by this extractor;
+ * false otherwise.
+ */
boolean supports(Object event);
+ /**
+ * Enriches the provided OpenTelemetry span with metadata extracted from the given AWS Lambda event.
+ * This method is intended to populate the span with attributes that are specific to the event type,
+ * such as metadata about the source, destination, or other relevant contextual information.
+ *
+ * @param event The AWS Lambda event object containing the data from which span attributes are derived.
+ * This could be an event-specific object, such as an S3Event, SQS event, or API Gateway event.
+ * @param span The OpenTelemetry {@link Span} to be enriched with attributes based on the provided event.
+ */
void enrichSpan(Object event, Span span);
+ /**
+ * Extracts trace context information from the given AWS Lambda event to facilitate distributed tracing.
+ * This method utilizes the provided `TextMapPropagator` to extract trace context information and creates
+ * an {@link ExtractedTraceContext} object containing the extracted data.
+ *
+ * @param event The AWS Lambda event object from which trace context should be extracted. This could be
+ * an event-specific object like S3Event, SQS event, or API Gateway event.
+ * @param parentContext The parent OpenTelemetry {@link Context} that serves as the starting point for
+ * trace extraction. This is typically passed from the Lambda function's invocation.
+ * @param propagator A {@link TextMapPropagator} instance used to extract trace context from the event
+ * metadata or headers.
+ * @return An {@link ExtractedTraceContext} containing the extracted trace data, including the parent context,
+ * span contexts, and span kind. If no trace information is found, an {@link ExtractedTraceContext}
+ * with an empty list of span contexts is returned.
+ */
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
index d0b6674e9..a020207ff 100644
--- 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
@@ -1,3 +1,19 @@
+/*
+ * 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.context;
import io.opentelemetry.api.trace.Span;
@@ -5,6 +21,14 @@
import io.opentelemetry.context.propagation.TextMapPropagator;
import java.util.List;
+/**
+ * Resolves and delegates processing of Lambda-specific event contexts to the appropriate
+ * {@link LambdaEventContextExtractor} implementation based on the event type.
+ * This resolver allows for dynamic extraction and span enrichment tailored to
+ * various AWS Lambda event sources (e.g., API Gateway, SQS, SNS, etc.).
+ *
+ * This class is immutable and thread-safe.
+ */
public final class LambdaEventContextExtractorResolver {
private final List extractors;
@@ -14,6 +38,14 @@ public LambdaEventContextExtractorResolver(List ext
this.extractors = List.copyOf(extractors);
}
+ /**
+ * Creates and returns an instance of {@link LambdaEventContextExtractorResolver} configured with
+ * a predefined set of {@link LambdaEventContextExtractor} implementations. These extractors are specialized in
+ * processing different types of AWS Lambda event sources, such as API Gateway, SQS, SNS, Kinesis, DynamoDB, and S3.
+ *
+ * @return a new instance of {@link LambdaEventContextExtractorResolver} with predefined extractors for
+ * handling various AWS Lambda event contexts.
+ */
public static LambdaEventContextExtractorResolver create() {
return new LambdaEventContextExtractorResolver(
List.of(
@@ -27,6 +59,19 @@ public static LambdaEventContextExtractorResolver create() {
);
}
+ /**
+ * Extracts trace context information from a Lambda event using the appropriate
+ * {@link LambdaEventContextExtractor} implementation that supports the event type.
+ * This method delegates the extraction to the first extractor in the configured list
+ * that supports the provided event type. If no suitable extractor is found, a default
+ * {@link ExtractedTraceContext} is returned using the provided parent context.
+ *
+ * @param event the Lambda event from which to extract the trace context
+ * @param parentContext the parent {@link Context} to be used as the base for the extraction
+ * @param propagator the {@link TextMapPropagator} used to extract propagation information from the event
+ * @return an {@link ExtractedTraceContext} containing the extracted trace context or a default one
+ * if no supporting extractor is found
+ */
public ExtractedTraceContext extract(Object event, Context parentContext, TextMapPropagator propagator) {
return extractors.stream()
@@ -40,6 +85,15 @@ public ExtractedTraceContext extract(Object event, Context parentContext, TextMa
.orElse(new ExtractedTraceContext(parentContext, List.of()));
}
+ /**
+ * Enriches a given {@link Span} with contextual information extracted from
+ * the specified event. This method evaluates a list of configured extractors
+ * and delegates the enrichment process to the first extractor that supports
+ * the provided event type.
+ *
+ * @param event the event object containing context information to be added to the span
+ * @param span the {@link Span} instance to be enriched with extracted information
+ */
public void enrichSpan(Object event, Span span) {
extractors.stream()
.filter(extractor -> extractor.supports(event))
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
index fee3ca909..f6e314f5a 100644
--- 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
@@ -1,3 +1,19 @@
+/*
+ * 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.context;
import com.amazonaws.services.lambda.runtime.events.S3Event;
@@ -9,6 +25,14 @@
import java.util.List;
import java.util.Objects;
+/**
+ * A context extractor implementation for handling AWS S3 event notifications within an AWS Lambda environment.
+ * This extractor is responsible for determining if an event can be processed, extracting trace context
+ * information, and enriching spans with metadata related to the S3 event.
+ *
+ * This implementation assumes that S3 event payloads do not contain trace context attributes (e.g.,
+ * traceparent or tracestate) and handles them accordingly.
+ */
public final class S3TraceContextExtractor implements LambdaEventContextExtractor {
@Override
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
index 9184d4c73..3bd260554 100644
--- 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
@@ -1,3 +1,19 @@
+/*
+ * 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.context;
import com.amazonaws.services.lambda.runtime.events.SNSEvent;
@@ -12,6 +28,25 @@
import java.util.stream.Collectors;
import software.amazon.lambda.powertools.tracing.opentelemetry.provider.OpenTelemetryProvider;
+/**
+ * An implementation of {@link LambdaEventContextExtractor} specifically designed to handle AWS Simple Notification
+ * Service (SNS) events.
+ * This class provides mechanisms to extract trace context from SNS event records and enrich spans with relevant
+ * messaging attributes.
+ * It supports processing instances of {@code SNSEvent}.
+ *
+ *
+ *
{@code supports}: Determines if the given event is an instance of SNS event.
+ *
{@code extract}: Extracts trace context data from message attributes of the SNS event records and generates
+ * an {@link ExtractedTraceContext}.
+ *
{@code enrichSpan}: Enriches the span with attributes pertaining to the SNS messaging system, such as the
+ * topic name and messaging system specific values.
+ *
+ *
+ * This class also ensures trace propagation by parsing SNS message attributes and converting them into OpenTelemetry
+ * context.
+ * It supports multi-record SNS events and handles cases where certain records or attributes are invalid.
+ */
public final class SnsTraceContextExtractor implements LambdaEventContextExtractor {
@Override
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
index c7802b383..d8427e5f8 100644
--- 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
@@ -1,3 +1,19 @@
+/*
+ * 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.context;
import com.amazonaws.services.lambda.runtime.events.SQSEvent;
@@ -12,6 +28,27 @@
import java.util.stream.Collectors;
import software.amazon.lambda.powertools.tracing.opentelemetry.provider.OpenTelemetryProvider;
+/**
+ * SqsTraceContextExtractor is responsible for extracting and enriching trace context
+ * information from AWS SQS events in the context of AWS Lambda functions. It implements
+ * the {@code LambdaEventContextExtractor} interface, providing functionality to determine
+ * support for an event, extract trace context, and enrich spans with additional attributes.
+ *
+ * The class processes SQS events by iterating through the batch of SQS messages, extracting
+ * propagation headers from message attributes, and building trace context information to be
+ * propagated and used by OpenTelemetry.
+ *
+ * Key functionalities include:
+ * - Determining if the extractor supports the provided event.
+ * - Extracting trace context from propagation headers present in SQS message attributes.
+ * - Enriching spans with messaging system details, including the number of messages in a batch
+ * and the queue name from the event source.
+ *
+ * This class is intended for use with AWS Lambda functions processing SQS events for tracing
+ * distributed systems.
+ *
+ * Thread-safety: This class is immutable and thread-safe.
+ */
public final class SqsTraceContextExtractor implements LambdaEventContextExtractor {
@Override
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
index 4e70a753b..407ff58c9 100644
--- 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
@@ -1,5 +1,27 @@
+/*
+ * 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.context;
+/**
+ * Enum representing different modes of trace context propagation.
+ *
+ * Trace context propagation defines how tracing information is passed
+ * between distributed systems to capture the relationship between trace spans.
+ */
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
index a9d41acb0..d299b2b78 100644
--- 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
@@ -1,5 +1,29 @@
+/*
+ * 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;
+/**
+ * A utility class that holds constant values for various attribute names and configurations
+ * used in AWS Lambda and Powertools for AWS Lambda. These constants are mainly used for
+ * telemetry, tracing, and environment variable configuration within the application.
+ *
+ * This class is designed as a final class with a private constructor to prevent instantiation
+ * and ensure it acts solely as a container for constants.
+ */
public final class AttributesConstants {
private AttributesConstants() {
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
index 1186c85f4..1f8646377 100644
--- 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
@@ -1,3 +1,19 @@
+/*
+ * 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.common.Attributes;
@@ -5,11 +21,39 @@
import io.opentelemetry.sdk.resources.Resource;
import software.amazon.lambda.powertools.common.internal.SystemWrapper;
+/**
+ * The {@code LambdaResource} class is a utility for creating a representation of
+ * an AWS Lambda execution environment in the form of a {@code Resource} object.
+ * It extracts and structures metadata about the Lambda function's runtime environment,
+ * which is useful for telemetry and observability purposes.
+ *
+ *
Responsibilities:
+ *
+ * - Populates resource attributes based on AWS Lambda-specific environment variables.
+ * - Includes attributes related to the cloud provider, service, function details, and
+ * OpenTelemetry metadata.
+ * - Processes function execution context such as memory size and account ID from the
+ * AWS Lambda environment.
+ * - Ensures only relevant and non-empty values are added as attributes.
+ *
+ * This class is designed to be final and non-instantiable, serving purely as a
+ * container for a static method.
+ */
public final class LambdaResource {
private LambdaResource() {
}
+ /**
+ * Creates a Resource instance populated with attributes derived from the
+ * AWS Lambda environment. The attributes include cloud provider information,
+ * service details, function memory size, account ID, and OpenTelemetry metadata.
+ *
+ * It retrieves environment variables specific to AWS Lambda and processes
+ * them to build a comprehensive resource description.
+ *
+ * @return a Resource object containing attributes about the AWS Lambda environment
+ */
public static Resource create() {
AttributesBuilder attributes = Attributes.builder();
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
index a0d589db5..174829d5a 100644
--- 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
@@ -2,8 +2,10 @@
* 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
+ * 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.
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
index fe757d840..944927af5 100644
--- 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
@@ -1,3 +1,19 @@
+/*
+ * 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.common.Attributes;
@@ -6,16 +22,25 @@
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.
+ * A utility class that combines a {@link Span} and its associated {@link Scope},
+ * managing the lifecycle of both. This class ensures that the span is properly ended and the scope is
+ * closed when the {@code SpanScope} is no longer needed.
+ *
*
- * It implements {@link AutoCloseable}, allowing it to be used in try-with-resources blocks
- * to ensure proper cleanup of the span and scope.
+ * The {@code SpanScope} class facilitates interaction with the {@link Span} during its lifecycle by
+ * providing methods to set its status, add events, and record exceptions. Upon closing, the span is
+ * finalized, and the associated scope is released.
+ *
+ *
+ *
Thread Safety
+ * This class is not thread-safe and must be used only within the thread it was created.
+ *
+ *
Usage
+ * Instances of this class should be used in a try-with-resources block to ensure proper cleanup.
+ *
+ *
Important Notes
+ * - The {@link Span} should be created and managed by an OpenTelemetry tracer or similar system.
+ * - Always close the {@code SpanScope} to release resources and end the span.
*/
public final class SpanScope implements AutoCloseable {
@@ -27,30 +52,68 @@ public SpanScope(Span span) {
this.scope = span.makeCurrent();
}
+ /**
+ * Retrieves the {@link Span} associated with this {@code SpanScope}.
+ *
+ * @return the {@link Span} managed by this {@code SpanScope}.
+ */
public Span span() {
return span;
}
+ /**
+ * Updates the status of the associated span.
+ *
+ * @param status the {@link StatusCode} to set for the span
+ * @return the current {@code SpanScope} instance for method chaining
+ */
public SpanScope setStatus(StatusCode status) {
span.setStatus(status);
return this;
}
+ /**
+ * Adds an event to the associated {@link Span} with the specified name.
+ *
+ * @param name the name of the event to be added to the span
+ * @return the current {@code SpanScope} instance for method chaining
+ */
public SpanScope addEvent(String name) {
span.addEvent(name);
return this;
}
+ /**
+ * Adds an event with the specified name and attributes to the associated {@link Span}.
+ *
+ * @param name the name of the event to add
+ * @param attributes the attributes associated with the event
+ * @return the current {@code SpanScope} instance for method chaining
+ */
public SpanScope addEvent(String name, Attributes attributes) {
span.addEvent(name, attributes);
return this;
}
+ /**
+ * Records an exception in the associated {@link Span} and sets its status to {@code ERROR}.
+ * This method is used to log and signal the occurrence of an error condition within the span.
+ *
+ * @param throwable the {@link Throwable} instance representing the exception to record
+ */
public void recordException(Throwable throwable) {
span.recordException(throwable);
span.setStatus(StatusCode.ERROR);
}
+ /**
+ * Closes the underlying resources associated with this {@code SpanScope}.
+ * This method ensures that the {@code scope} is closed to release any associated
+ * context and marks the end of the {@code span}'s lifecycle by calling its {@code end()} method.
+ *
+ * This method should be invoked to properly clean up resources and signal the end
+ * of the tracing span when the scope is no longer needed.
+ */
@Override
public void close() {
scope.close();
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
index 4a8f0c8cc..7a7458924 100644
--- 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
@@ -1,6 +1,17 @@
/*
* Copyright 2023 Amazon.com, Inc. or its affiliates.
- * Licensed under the Apache License, Version 2.0
+ * 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;
@@ -27,6 +38,50 @@
import software.amazon.lambda.powertools.tracing.opentelemetry.context.TraceContextPropagationMode;
import software.amazon.lambda.powertools.tracing.opentelemetry.provider.OpenTelemetryProvider;
+/**
+ * TracingOpenTelemetryAspect is an AspectJ aspect that facilitates tracing for methods annotated
+ * with the {@link Tracing} annotation. It integrates with OpenTelemetry to automatically create and
+ * manage spans for annotated methods, capturing execution context, responses, and errors.
+ *
+ *
Functional Overview:
+ * - Creates and manages OpenTelemetry spans around methods annotated with {@link Tracing}.
+ * - Supports both handler and internal method spans.
+ * - Extracts contextual information if available to enrich spans.
+ * - Captures response data and errors based on configurable capture modes.
+ * - Flushes telemetry data upon span completion.
+ *
+ *
Key Methods:
+ *
+ *
configure - Sets a custom {@link TracingOpenTelemetry} instance to be used.
+ *
callAt - Defines the pointcut for methods annotated with {@link Tracing}.
+ *
around - Core functionality that wraps the target method execution with a span.
+ *
+ *
+ *
Trace Context Handling:
+ * - Extracts trace context for handler methods for more seamless propagation.
+ * - Supports linking spans or connecting to existing parent spans based on configuration.
+ *
+ *
Capture Modes:
+ * - The capture modes ({@link Tracing.CaptureMode}) dictate whether and how responses and errors are
+ * recorded in spans:
+ * - RESPONSE_AND_ERROR: Captures both responses and errors.
+ * - RESPONSE: Captures only responses.
+ * - ERROR: Captures only errors.
+ * - DISABLED: Disables any capture.
+ * - ENVIRONMENT_VAR: Determines capture based on environment variables.
+ *
+ *
Span Creation:
+ * - Handler spans include additional AWS Lambda-related metadata if applicable.
+ * - Internal method spans are marked with a default {@link SpanKind#INTERNAL}.
+ *
+ *
Error Handling:
+ * - Ensures exceptions are propagated while recording them in the span if enabled.
+ *
+ *
Thread Safety:
+ * - The class ensures thread safety for span management in concurrent environments.
+ *
+ * Note: This class requires OpenTelemetry to be properly configured in the application context.
+ */
@Aspect
public final class TracingOpenTelemetryAspect {
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
index ffaf79399..056c881f3 100644
--- 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
@@ -1,7 +1,17 @@
/*
* 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.
+ * 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.provider;
@@ -30,6 +40,29 @@
import software.amazon.lambda.powertools.tracing.opentelemetry.context.TraceContextPropagationMode;
import software.amazon.lambda.powertools.tracing.opentelemetry.internal.LambdaResource;
+/**
+ * Provides a managed OpenTelemetry instance tailored for AWS Lambda Powertools.
+ * This class enables easy integration of tracing capabilities using OpenTelemetry
+ * for AWS Lambda function monitoring.
+ *
+ * It supports automatic instrumentation configuration through environment variables
+ * and enables customized configurations for propagators, tracing mode, exporter,
+ * and tracer provider.
+ *
+ * OpenTelemetryProvider ensures compatibility with the ADOT Lambda layer and javaagent,
+ * using the configured global OpenTelemetry instance when available. If no global
+ * configuration exists, it creates and uses a Lambda-optimized configuration.
+ *
+ * Key functionalities include:
+ * - Access to a pre-configured {@code Tracer}.
+ * - Support for multiple propagation formats (e.g., W3C Trace Context, AWS X-Ray).
+ * - Batch span processing with configurable export batch size, queue size, and timeouts.
+ * - Lambda-optimized default resource configuration.
+ * - Parsing environment variables for OTLP configuration (e.g., protocol, endpoint).
+ *
+ * This class cannot be instantiated directly and provides its functionalities
+ * through static methods.
+ */
public final class OpenTelemetryProvider {
private static final String INSTRUMENTATION_NAME = "aws-lambda-powertools";
@@ -59,36 +92,76 @@ public final class OpenTelemetryProvider {
private OpenTelemetryProvider() {
}
+ /**
+ * Provides a pre-configured singleton instance of {@link ObjectMapper}.
+ *
+ * This method is intended for consistent JSON processing across various
+ * components by returning an {@code ObjectMapper} instance that is shared
+ * across the application.
+ *
+ * @return A shared instance of {@link ObjectMapper}.
+ */
public static ObjectMapper objectMapper() {
return OBJECT_MAPPER;
}
+ /**
+ * Retrieves the current trace context propagation mode for OpenTelemetry tracing.
+ *
+ * The trace context propagation mode determines how trace context is propagated
+ * between spans, such as whether it uses a parent-child relationship or establishes
+ * links between related spans.
+ *
+ * @return The current {@link TraceContextPropagationMode}, which may be either
+ * {@code PARENT} or {@code LINK}, indicating the selected trace context
+ * propagation strategy.
+ */
public static TraceContextPropagationMode traceContextPropagationMode() {
return TRACE_CONTEXT_PROPAGATION_MODE;
}
+ /**
+ * Retrieves a pre-configured instance of {@link Tracer} from the OpenTelemetry SDK.
+ *
+ * The returned {@link Tracer} is associated with the specified instrumentation name,
+ * enabling tracing for specific operations and contexts within the application.
+ * This method leverages the global OpenTelemetry configuration, making it suitable
+ * for use in environments where consistent instrumentation is required.
+ *
+ * @return A {@link Tracer} instance for instrumenting and generating trace data.
+ */
public static Tracer tracer() {
return OPEN_TELEMETRY.getTracer(INSTRUMENTATION_NAME);
}
+ /**
+ * Retrieves a pre-configured instance of {@link TextMapPropagator}.
+ *
+ * The returned {@link TextMapPropagator} is configured to propagate
+ * tracing context information across process boundaries. This is
+ * used to encode and decode trace context in a key-value format,
+ * enabling distributed tracing in various systems.
+ *
+ * @return A pre-configured {@link TextMapPropagator} instance for trace context propagation.
+ */
public static TextMapPropagator propagator() {
return PROPAGATOR;
}
+ /**
+ * Provides a static {@link TextMapGetter} instance for extracting trace context
+ * information from a {@link Map} containing string key-value pairs.
+ *
+ * The returned {@link TextMapGetter} is used to interpret trace propagation
+ * attributes from a map structure, enabling distributed tracing functionality.
+ *
+ * @return A {@link TextMapGetter} instance that facilitates extracting trace
+ * context data from a {@link Map} of string keys and values.
+ */
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() {
if (GlobalOpenTelemetry.isSet()) {
@@ -98,6 +171,19 @@ private static OpenTelemetry initializeOpenTelemetry() {
return createDefaultOpenTelemetry();
}
+ /**
+ * Forces all pending telemetry data to be processed and exported, ensuring that
+ * any remaining spans or related information are handled by the OpenTelemetry
+ * SDK or the globally configured OpenTelemetry instance.
+ *
+ * If a global OpenTelemetry instance is available, the operation will immediately
+ * succeed. Otherwise, it delegates the flush operation to the SDK's tracer provider.
+ *
+ * @return A {@link CompletableResultCode} indicating the success or failure of the
+ * flush operation. It may represent an immediate success if the global
+ * OpenTelemetry instance is set, or the result of flushing managed by the
+ * SDK tracer provider otherwise.
+ */
public static CompletableResultCode forceFlush() {
if (GlobalOpenTelemetry.isSet()) {
return CompletableResultCode.ofSuccess();
From 4c22bc4c66a4de055030c1498acb0f2bdd8c5059 Mon Sep 17 00:00:00 2001
From: David-DAM <82216301+David-DAM@users.noreply.github.com>
Date: Tue, 22 Sep 2026 12:35:45 +0200
Subject: [PATCH 18/21] Add comprehensive test coverage for OpenTelemetry
context extractors, including API Gateway, DynamoDB, Kinesis, and SNS; update
`pom.xml` to include Mockito dependencies.
---
powertools-tracing-opentelemetry/pom.xml | 5 +
.../provider/OpenTelemetryProvider.java | 38 ++-
.../ApiGatewayTraceContextExtractorTest.java | 293 ++++++++++++++++--
.../DynamoDbTraceContextExtractorTest.java | 175 +++++++++++
.../KinesisTraceContextExtractorTest.java | 230 ++++++++++++++
.../context/S3TraceContextExtractorTest.java | 184 +++++++++++
.../context/SnsTraceContextExtractorTest.java | 258 ++++++++++++---
.../context/SqsTraceContextExtractorTest.java | 212 +++++++++++++
.../provider/OpenTelemetryProviderTest.java | 51 +++
9 files changed, 1367 insertions(+), 79 deletions(-)
create mode 100644 powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/DynamoDbTraceContextExtractorTest.java
create mode 100644 powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/KinesisTraceContextExtractorTest.java
create mode 100644 powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/S3TraceContextExtractorTest.java
create mode 100644 powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/SqsTraceContextExtractorTest.java
create mode 100644 powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/provider/OpenTelemetryProviderTest.java
diff --git a/powertools-tracing-opentelemetry/pom.xml b/powertools-tracing-opentelemetry/pom.xml
index 1878c964e..976d8a723 100644
--- a/powertools-tracing-opentelemetry/pom.xml
+++ b/powertools-tracing-opentelemetry/pom.xml
@@ -90,6 +90,11 @@
mockito-coretest
+
+ org.mockito
+ mockito-junit-jupiter
+ test
+ org.junit.jupiterjunit-jupiter-api
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
index 056c881f3..b9b5b3903 100644
--- 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
@@ -26,7 +26,9 @@
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.http.trace.OtlpHttpSpanExporterBuilder;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
+import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.common.CompletableResultCode;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
@@ -265,27 +267,39 @@ private static SpanExporter createExporter() {
Map headerMap = parseHeaders(headers);
if (protocol == null || protocol.isBlank()) {
- return OtlpGrpcSpanExporter.builder()
+ OtlpGrpcSpanExporterBuilder builder = OtlpGrpcSpanExporter.builder()
.setTimeout(EXPORT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
- .setEndpoint(endpoint)
- .setHeaders(() -> headerMap)
- .build();
+ .setHeaders(() -> headerMap);
+
+ if (endpoint != null && !endpoint.isBlank()) {
+ builder.setEndpoint(endpoint);
+ }
+
+ return builder.build();
}
switch (protocol.trim().toLowerCase()) {
case "grpc":
- return OtlpGrpcSpanExporter.builder()
+ OtlpGrpcSpanExporterBuilder grpcBuilder = OtlpGrpcSpanExporter.builder()
.setTimeout(EXPORT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
- .setEndpoint(endpoint)
- .setHeaders(() -> headerMap)
- .build();
+ .setHeaders(() -> headerMap);
+
+ if (endpoint != null && !endpoint.isBlank()) {
+ grpcBuilder.setEndpoint(endpoint);
+ }
+
+ return grpcBuilder.build();
case "http/protobuf":
- return OtlpHttpSpanExporter.builder()
+ OtlpHttpSpanExporterBuilder httpBuilder = OtlpHttpSpanExporter.builder()
.setTimeout(EXPORT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
- .setEndpoint(endpoint)
- .setHeaders(() -> headerMap)
- .build();
+ .setHeaders(() -> headerMap);
+
+ if (endpoint != null && !endpoint.isBlank()) {
+ httpBuilder.setEndpoint(endpoint);
+ }
+
+ return httpBuilder.build();
default:
throw new IllegalArgumentException(
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
index f0534e626..ecbe392ab 100644
--- 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
@@ -1,37 +1,270 @@
package software.amazon.lambda.powertools.tracing.opentelemetry.context;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.propagation.TextMapPropagator;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
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);
-// }
+ @Mock
+ private Span span;
+
+ @Mock
+ private TextMapPropagator propagator;
+
+ @InjectMocks
+ private ApiGatewayTraceContextExtractor extractor;
+
+ @Test
+ void shouldSupportApiGatewayProxyRequestEvent() {
+ APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent();
+
+ boolean result = extractor.supports(event);
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ void shouldNotSupportNonApiGatewayEvent() {
+ assertThat(extractor.supports("Some non-API Gateway event")).isFalse();
+ assertThat(extractor.supports(new Object())).isFalse();
+ assertThat(extractor.supports(null)).isFalse();
+ }
+
+ @Test
+ void shouldExtractTraceContextWithServerSpanKind() {
+ APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent()
+ .withHeaders(Map.of("traceparent", "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"));
+ Context parentContext = Context.current();
+
+ ExtractedTraceContext extractedContext =
+ extractor.extract(event, parentContext, W3CTraceContextPropagator.getInstance());
+
+ assertThat(extractedContext).isNotNull();
+ assertThat(extractedContext.spanContexts()).isEmpty();
+ assertThat(extractedContext.spanKind()).isEqualTo(SpanKind.SERVER);
+ }
+
+ @Test
+ void shouldExtractTraceContextWhenHeadersAreEmpty() {
+ APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent()
+ .withHeaders(Map.of());
+ Context parentContext = Context.current();
+
+ ExtractedTraceContext extractedContext = extractor.extract(event, parentContext, propagator);
+
+ assertThat(extractedContext).isNotNull();
+ assertThat(extractedContext.context()).isEqualTo(parentContext);
+ assertThat(extractedContext.spanContexts()).isEmpty();
+ assertThat(extractedContext.spanKind()).isEqualTo(SpanKind.SERVER);
+ }
+
+ @Test
+ void shouldExtractTraceContextWhenHeadersAreNull() {
+ APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent()
+ .withHeaders(null);
+ Context parentContext = Context.current();
+
+ ExtractedTraceContext extractedContext = extractor.extract(event, parentContext, propagator);
+
+ assertThat(extractedContext).isNotNull();
+ assertThat(extractedContext.context()).isEqualTo(parentContext);
+ assertThat(extractedContext.spanContexts()).isEmpty();
+ assertThat(extractedContext.spanKind()).isEqualTo(SpanKind.SERVER);
+ }
+
+ @Test
+ void shouldEnrichSpanWithHttpMethod() {
+ APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent()
+ .withHttpMethod("POST");
+
+ extractor.enrichSpan(event, span);
+
+ verify(span).setAttribute("http.request.method", "POST");
+ }
+
+ @Test
+ void shouldEnrichSpanWithPath() {
+ APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent()
+ .withPath("/api/users");
+
+ extractor.enrichSpan(event, span);
+
+ verify(span).setAttribute("url.path", "/api/users");
+ }
+
+ @Test
+ void shouldEnrichSpanWithQueryString() {
+ APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent()
+ .withQueryStringParameters(Map.of("name", "test", "page", "1"));
+
+ extractor.enrichSpan(event, span);
+
+ verify(span).setAttribute(eq("url.query"), anyString());
+ }
+
+ @Test
+ void shouldEnrichSpanWithUserAgent() {
+ APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent()
+ .withHeaders(Map.of("user-agent", "Mozilla/5.0"));
+
+ extractor.enrichSpan(event, span);
+
+ verify(span).setAttribute("user_agent.original", "Mozilla/5.0");
+ }
+
+ @Test
+ void shouldEnrichSpanWithUserAgentCaseInsensitive() {
+ APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent()
+ .withHeaders(Map.of("User-Agent", "PostmanRuntime/7.26.8"));
+
+ extractor.enrichSpan(event, span);
+
+ verify(span).setAttribute("user_agent.original", "PostmanRuntime/7.26.8");
+ }
+
+ @Test
+ void shouldEnrichSpanWithRequestContext() {
+ APIGatewayProxyRequestEvent.ProxyRequestContext requestContext =
+ new APIGatewayProxyRequestEvent.ProxyRequestContext();
+ requestContext.setRequestId("request-123");
+ requestContext.setStage("prod");
+ requestContext.setResourceId("resource-456");
+ requestContext.setResourcePath("/users/{id}");
+
+ APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent()
+ .withRequestContext(requestContext);
+
+ extractor.enrichSpan(event, span);
+
+ verify(span).setAttribute("aws.request_id", "request-123");
+ verify(span).setAttribute("aws.apigateway.stage", "prod");
+ verify(span).setAttribute("aws.apigateway.resource_id", "resource-456");
+ verify(span).setAttribute("aws.apigateway.resource_path", "/users/{id}");
+ }
+
+ @Test
+ void shouldEnrichSpanWithAllAttributes() {
+ APIGatewayProxyRequestEvent.ProxyRequestContext requestContext =
+ new APIGatewayProxyRequestEvent.ProxyRequestContext();
+ requestContext.setRequestId("request-789");
+ requestContext.setStage("dev");
+ requestContext.setResourceId("resource-101");
+ requestContext.setResourcePath("/api/products");
+
+ APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent()
+ .withHttpMethod("GET")
+ .withPath("/api/products")
+ .withQueryStringParameters(Map.of("category", "electronics"))
+ .withHeaders(Map.of("user-agent", "PostmanRuntime/7.26.8"))
+ .withRequestContext(requestContext);
+
+ extractor.enrichSpan(event, span);
+
+ verify(span).setAttribute("http.request.method", "GET");
+ verify(span).setAttribute("url.path", "/api/products");
+ verify(span).setAttribute(eq("url.query"), anyString());
+ verify(span).setAttribute("user_agent.original", "PostmanRuntime/7.26.8");
+ verify(span).setAttribute("aws.request_id", "request-789");
+ verify(span).setAttribute("aws.apigateway.stage", "dev");
+ verify(span).setAttribute("aws.apigateway.resource_id", "resource-101");
+ verify(span).setAttribute("aws.apigateway.resource_path", "/api/products");
+ }
+
+ @Test
+ void shouldNotEnrichSpanWhenHttpMethodIsNull() {
+ APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent()
+ .withHttpMethod(null);
+
+ extractor.enrichSpan(event, span);
+
+ verify(span, never()).setAttribute(eq("http.request.method"), anyString());
+ }
+
+ @Test
+ void shouldNotEnrichSpanWhenPathIsNull() {
+ APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent()
+ .withPath(null);
+
+ extractor.enrichSpan(event, span);
+
+ verify(span, never()).setAttribute(eq("url.path"), anyString());
+ }
+
+ @Test
+ void shouldNotEnrichSpanWhenQueryStringParametersIsNull() {
+ APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent()
+ .withQueryStringParameters(null);
+
+ extractor.enrichSpan(event, span);
+
+ verify(span, never()).setAttribute(eq("url.query"), anyString());
+ }
+
+ @Test
+ void shouldNotEnrichSpanWithUserAgentWhenHeadersIsNull() {
+ APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent()
+ .withHeaders(null);
+
+ extractor.enrichSpan(event, span);
+
+ verify(span, never()).setAttribute(eq("user_agent.original"), anyString());
+ }
+
+ @Test
+ void shouldNotEnrichSpanWhenRequestContextIsNull() {
+ APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent()
+ .withRequestContext(null);
+
+ extractor.enrichSpan(event, span);
+
+ verify(span, never()).setAttribute(eq("aws.request_id"), anyString());
+ verify(span, never()).setAttribute(eq("aws.apigateway.stage"), anyString());
+ verify(span, never()).setAttribute(eq("aws.apigateway.resource_id"), anyString());
+ verify(span, never()).setAttribute(eq("aws.apigateway.resource_path"), anyString());
+ }
+
+ @Test
+ void shouldNotEnrichSpanWithRequestIdWhenRequestIdIsNull() {
+ APIGatewayProxyRequestEvent.ProxyRequestContext requestContext =
+ new APIGatewayProxyRequestEvent.ProxyRequestContext();
+ requestContext.setRequestId(null);
+
+ APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent()
+ .withRequestContext(requestContext);
+
+ extractor.enrichSpan(event, span);
+
+ verify(span, never()).setAttribute(eq("aws.request_id"), anyString());
+ }
+
+ @Test
+ void shouldNotEnrichSpanWithStageWhenStageIsNull() {
+ APIGatewayProxyRequestEvent.ProxyRequestContext requestContext =
+ new APIGatewayProxyRequestEvent.ProxyRequestContext();
+ requestContext.setStage(null);
+
+ APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent()
+ .withRequestContext(requestContext);
+ extractor.enrichSpan(event, span);
+ verify(span, never()).setAttribute(eq("aws.apigateway.stage"), anyString());
+ }
}
\ No newline at end of file
diff --git a/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/DynamoDbTraceContextExtractorTest.java b/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/DynamoDbTraceContextExtractorTest.java
new file mode 100644
index 000000000..73b98c7ff
--- /dev/null
+++ b/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/DynamoDbTraceContextExtractorTest.java
@@ -0,0 +1,175 @@
+/*
+ * 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.context;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+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.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class DynamoDbTraceContextExtractorTest {
+
+ @Mock
+ private Span span;
+
+ @Mock
+ private TextMapPropagator propagator;
+
+ @InjectMocks
+ private DynamoDbTraceContextExtractor extractor;
+
+ @Test
+ void shouldSupportDynamodbEvent() {
+ DynamodbEvent dynamodbEvent = new DynamodbEvent();
+
+ boolean result = extractor.supports(dynamodbEvent);
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ void shouldNotSupportNonDynamodbEvent() {
+ assertThat(extractor.supports("Some non-Dynamodb event")).isFalse();
+ assertThat(extractor.supports(new Object())).isFalse();
+ assertThat(extractor.supports(null)).isFalse();
+ }
+
+ @Test
+ void shouldExtractTraceContextWithConsumerSpanKindAndNoSpanContexts() {
+ DynamodbEvent event = new DynamodbEvent();
+ Context parentContext = Context.current();
+
+ ExtractedTraceContext extractedContext = extractor.extract(event, parentContext, propagator);
+
+ assertThat(extractedContext).isNotNull();
+ assertThat(extractedContext.context()).isEqualTo(parentContext);
+ assertThat(extractedContext.spanContexts()).isEmpty();
+ assertThat(extractedContext.spanKind()).isEqualTo(SpanKind.CONSUMER);
+ }
+
+ @Test
+ void shouldNotEnrichSpanWhenRecordsIsNull() {
+ DynamodbEvent event = new DynamodbEvent();
+ event.setRecords(null);
+
+ extractor.enrichSpan(event, span);
+
+ verifyNoInteractions(span);
+ }
+
+ @Test
+ void shouldNotEnrichSpanWhenRecordsIsEmpty() {
+ DynamodbEvent event = new DynamodbEvent();
+ event.setRecords(Collections.emptyList());
+
+ extractor.enrichSpan(event, span);
+
+ verifyNoInteractions(span);
+ }
+
+ @Test
+ void shouldNotEnrichSpanWhenAllRecordsAreNull() {
+ DynamodbEvent event = new DynamodbEvent();
+ event.setRecords(Collections.singletonList(null));
+
+ extractor.enrichSpan(event, span);
+
+ verifyNoInteractions(span);
+ }
+
+ @Test
+ void shouldEnrichSpanWithDynamoDbMetadataAndStreamNameFromArn() {
+ DynamodbEvent.DynamodbStreamRecord record = new DynamodbEvent.DynamodbStreamRecord();
+ record.setEventSourceARN(
+ "arn:aws:dynamodb:us-east-1:123456789012:table/TestTable/stream/2024-01-01T00:00:00.000");
+
+ DynamodbEvent event = new DynamodbEvent();
+ event.setRecords(List.of(record));
+
+ extractor.enrichSpan(event, span);
+
+ verify(span).setAttribute("messaging.system", "aws.dynamodb");
+ verify(span).setAttribute("messaging.batch.message_count", 1);
+ verify(span).setAttribute("messaging.destination.name", "2024-01-01T00:00:00.000");
+ }
+
+ @Test
+ void shouldEnrichSpanWhenArnHasNoSlashSeparator() {
+ DynamodbEvent.DynamodbStreamRecord record = new DynamodbEvent.DynamodbStreamRecord();
+ record.setEventSourceARN("stream-name-without-separator");
+
+ DynamodbEvent event = new DynamodbEvent();
+ event.setRecords(List.of(record));
+
+ extractor.enrichSpan(event, span);
+
+ verify(span).setAttribute("messaging.system", "aws.dynamodb");
+ verify(span).setAttribute("messaging.batch.message_count", 1);
+ verify(span).setAttribute("messaging.destination.name", "stream-name-without-separator");
+ }
+
+ @Test
+ void shouldEnrichSpanWithoutDestinationNameWhenEventSourceArnIsNull() {
+ DynamodbEvent.DynamodbStreamRecord record = new DynamodbEvent.DynamodbStreamRecord();
+ record.setEventSourceARN(null);
+
+ DynamodbEvent event = new DynamodbEvent();
+ event.setRecords(List.of(record));
+
+ extractor.enrichSpan(event, span);
+
+ verify(span).setAttribute("messaging.system", "aws.dynamodb");
+ verify(span).setAttribute("messaging.batch.message_count", 1);
+ verify(span, never()).setAttribute(eq("messaging.destination.name"), anyString());
+ }
+
+ @Test
+ void shouldEnrichSpanWithBatchCountAndFirstNonNullRecordMetadata() {
+ DynamodbEvent.DynamodbStreamRecord firstRecord = null;
+
+ DynamodbEvent.DynamodbStreamRecord secondRecord = new DynamodbEvent.DynamodbStreamRecord();
+ secondRecord.setEventSourceARN(
+ "arn:aws:dynamodb:eu-west-1:123456789012:table/Orders/stream/orders-stream-2024");
+
+ DynamodbEvent.DynamodbStreamRecord thirdRecord = new DynamodbEvent.DynamodbStreamRecord();
+ thirdRecord.setEventSourceARN("arn:aws:dynamodb:eu-west-1:123456789012:table/Orders/stream/another-stream");
+
+ DynamodbEvent event = new DynamodbEvent();
+ event.setRecords(Arrays.asList(firstRecord, secondRecord, thirdRecord));
+
+ extractor.enrichSpan(event, span);
+
+ verify(span).setAttribute("messaging.system", "aws.dynamodb");
+ verify(span).setAttribute("messaging.batch.message_count", 3);
+ verify(span).setAttribute("messaging.destination.name", "orders-stream-2024");
+ }
+}
\ No newline at end of file
diff --git a/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/KinesisTraceContextExtractorTest.java b/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/KinesisTraceContextExtractorTest.java
new file mode 100644
index 000000000..6f86e7936
--- /dev/null
+++ b/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/KinesisTraceContextExtractorTest.java
@@ -0,0 +1,230 @@
+/*
+ * 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.context;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+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.Collections;
+import java.util.Date;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class KinesisTraceContextExtractorTest {
+
+ @Mock
+ private Span span;
+
+ @Mock
+ private TextMapPropagator propagator;
+
+ @InjectMocks
+ private KinesisTraceContextExtractor extractor;
+
+ @Test
+ void shouldSupportKinesisEvent() {
+ KinesisEvent kinesisEvent = new KinesisEvent();
+
+ boolean result = extractor.supports(kinesisEvent);
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ void shouldNotSupportNonKinesisEvent() {
+ assertThat(extractor.supports("Some non-Kinesis event")).isFalse();
+ assertThat(extractor.supports(new Object())).isFalse();
+ assertThat(extractor.supports(null)).isFalse();
+ }
+
+ @Test
+ void shouldExtractTraceContextWithConsumerSpanKindAndNoSpanContexts() {
+ KinesisEvent event = new KinesisEvent();
+ Context parentContext = Context.current();
+
+ ExtractedTraceContext extractedContext = extractor.extract(event, parentContext, propagator);
+
+ assertThat(extractedContext).isNotNull();
+ assertThat(extractedContext.context()).isEqualTo(parentContext);
+ assertThat(extractedContext.spanContexts()).isEmpty();
+ assertThat(extractedContext.spanKind()).isEqualTo(SpanKind.CONSUMER);
+ }
+
+ @Test
+ void shouldNotEnrichSpanWhenRecordsIsNull() {
+ KinesisEvent event = new KinesisEvent();
+ event.setRecords(null);
+
+ extractor.enrichSpan(event, span);
+
+ verifyNoInteractions(span);
+ }
+
+ @Test
+ void shouldNotEnrichSpanWhenRecordsIsEmpty() {
+ KinesisEvent event = new KinesisEvent();
+ event.setRecords(Collections.emptyList());
+
+ extractor.enrichSpan(event, span);
+
+ verifyNoInteractions(span);
+ }
+
+ @Test
+ void shouldNotEnrichSpanWhenFirstRecordIsNull() {
+ KinesisEvent event = new KinesisEvent();
+ event.setRecords(Collections.singletonList(null));
+
+ extractor.enrichSpan(event, span);
+
+ verifyNoInteractions(span);
+ }
+
+ @Test
+ void shouldNotEnrichSpanWhenKinesisIsNull() {
+ KinesisEvent.KinesisEventRecord record = new KinesisEvent.KinesisEventRecord();
+ record.setKinesis(null);
+
+ KinesisEvent event = new KinesisEvent();
+ event.setRecords(List.of(record));
+
+ extractor.enrichSpan(event, span);
+
+ verifyNoInteractions(span);
+ }
+
+ @Test
+ void shouldEnrichSpanWithKinesisMetadata() {
+ KinesisEvent.Record kinesis = new KinesisEvent.Record();
+ kinesis.setPartitionKey("partition-1");
+ kinesis.setSequenceNumber("12345678901234567890");
+ kinesis.setApproximateArrivalTimestamp(new Date(1609459200000L));
+
+ KinesisEvent.KinesisEventRecord record = new KinesisEvent.KinesisEventRecord();
+ record.setKinesis(kinesis);
+ record.setEventSourceARN("arn:aws:kinesis:us-east-1:123456789012:stream/test-stream");
+
+ KinesisEvent event = new KinesisEvent();
+ event.setRecords(List.of(record));
+
+ extractor.enrichSpan(event, span);
+
+ verify(span).setAttribute("messaging.system", "aws.kinesis");
+ verify(span).setAttribute("messaging.partition_key", "partition-1");
+ verify(span).setAttribute("messaging.message.id", "12345678901234567890");
+ verify(span).setAttribute("messaging.message.receive.timestamp", 1609459200000L);
+ verify(span).setAttribute("messaging.destination.name", "test-stream");
+ }
+
+ @Test
+ void shouldEnrichSpanWithStreamNameFromArnWithoutSlash() {
+ KinesisEvent.Record kinesis = new KinesisEvent.Record();
+
+ KinesisEvent.KinesisEventRecord record = new KinesisEvent.KinesisEventRecord();
+ record.setKinesis(kinesis);
+ record.setEventSourceARN("stream-name-without-separator");
+
+ KinesisEvent event = new KinesisEvent();
+ event.setRecords(List.of(record));
+
+ extractor.enrichSpan(event, span);
+
+ verify(span).setAttribute("messaging.system", "aws.kinesis");
+ verify(span).setAttribute("messaging.destination.name", "stream-name-without-separator");
+ }
+
+ @Test
+ void shouldNotEnrichSpanWithPartitionKeyWhenNull() {
+ KinesisEvent.Record kinesis = new KinesisEvent.Record();
+ kinesis.setPartitionKey(null);
+
+ KinesisEvent.KinesisEventRecord record = new KinesisEvent.KinesisEventRecord();
+ record.setKinesis(kinesis);
+
+ KinesisEvent event = new KinesisEvent();
+ event.setRecords(List.of(record));
+
+ extractor.enrichSpan(event, span);
+
+ verify(span).setAttribute("messaging.system", "aws.kinesis");
+ verify(span, never()).setAttribute(eq("messaging.partition_key"), anyString());
+ }
+
+ @Test
+ void shouldNotEnrichSpanWithSequenceNumberWhenNull() {
+ KinesisEvent.Record kinesis = new KinesisEvent.Record();
+ kinesis.setSequenceNumber(null);
+
+ KinesisEvent.KinesisEventRecord record = new KinesisEvent.KinesisEventRecord();
+ record.setKinesis(kinesis);
+
+ KinesisEvent event = new KinesisEvent();
+ event.setRecords(List.of(record));
+
+ extractor.enrichSpan(event, span);
+
+ verify(span).setAttribute("messaging.system", "aws.kinesis");
+ verify(span, never()).setAttribute(eq("messaging.message.id"), anyString());
+ }
+
+ @Test
+ void shouldNotEnrichSpanWithTimestampWhenNull() {
+ KinesisEvent.Record kinesis = new KinesisEvent.Record();
+ kinesis.setApproximateArrivalTimestamp(null);
+
+ KinesisEvent.KinesisEventRecord record = new KinesisEvent.KinesisEventRecord();
+ record.setKinesis(kinesis);
+
+ KinesisEvent event = new KinesisEvent();
+ event.setRecords(List.of(record));
+
+ extractor.enrichSpan(event, span);
+
+ verify(span).setAttribute("messaging.system", "aws.kinesis");
+ verify(span, never()).setAttribute(eq("messaging.message.receive.timestamp"), anyLong());
+ }
+
+ @Test
+ void shouldNotEnrichSpanWithDestinationNameWhenEventSourceArnIsNull() {
+ KinesisEvent.Record kinesis = new KinesisEvent.Record();
+
+ KinesisEvent.KinesisEventRecord record = new KinesisEvent.KinesisEventRecord();
+ record.setKinesis(kinesis);
+ record.setEventSourceARN(null);
+
+ KinesisEvent event = new KinesisEvent();
+ event.setRecords(List.of(record));
+
+ extractor.enrichSpan(event, span);
+
+ verify(span).setAttribute("messaging.system", "aws.kinesis");
+ verify(span, never()).setAttribute(eq("messaging.destination.name"), anyString());
+ }
+}
\ No newline at end of file
diff --git a/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/S3TraceContextExtractorTest.java b/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/S3TraceContextExtractorTest.java
new file mode 100644
index 000000000..4a3f20e22
--- /dev/null
+++ b/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/S3TraceContextExtractorTest.java
@@ -0,0 +1,184 @@
+/*
+ * 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.context;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+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.Collections;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class S3TraceContextExtractorTest {
+
+ @Mock
+ private Span span;
+
+ @Mock
+ private TextMapPropagator propagator;
+
+ @InjectMocks
+ private S3TraceContextExtractor extractor;
+
+ @Test
+ void shouldSupportS3Event() {
+ S3Event s3Event = new S3Event(Collections.emptyList());
+
+ boolean result = extractor.supports(s3Event);
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ void shouldNotSupportNonS3Event() {
+ assertThat(extractor.supports("Some non-S3 event")).isFalse();
+ assertThat(extractor.supports(new Object())).isFalse();
+ assertThat(extractor.supports(null)).isFalse();
+ }
+
+ @Test
+ void shouldExtractTraceContextWithConsumerSpanKindAndNoSpanContexts() {
+ S3Event event = new S3Event(Collections.emptyList());
+ Context parentContext = Context.current();
+
+ ExtractedTraceContext extractedContext = extractor.extract(event, parentContext, propagator);
+
+ assertThat(extractedContext).isNotNull();
+ assertThat(extractedContext.context()).isEqualTo(parentContext);
+ assertThat(extractedContext.spanContexts()).isEmpty();
+ assertThat(extractedContext.spanKind()).isEqualTo(SpanKind.CONSUMER);
+ }
+
+ @Test
+ void shouldNotEnrichSpanWhenRecordsIsNull() {
+ S3Event event = new S3Event(null);
+
+ extractor.enrichSpan(event, span);
+
+ verifyNoInteractions(span);
+ }
+
+ @Test
+ void shouldNotEnrichSpanWhenRecordsIsEmpty() {
+ S3Event event = new S3Event(Collections.emptyList());
+
+ extractor.enrichSpan(event, span);
+
+ verifyNoInteractions(span);
+ }
+
+ @Test
+ void shouldNotEnrichSpanWhenAllRecordsAreNull() {
+ S3Event event = new S3Event(Collections.singletonList(null));
+
+ extractor.enrichSpan(event, span);
+
+ verify(span).setAttribute("messaging.system", "aws.s3");
+ verify(span).setAttribute("messaging.batch.message_count", 1);
+ verify(span, never()).setAttribute(eq("messaging.destination.name"), anyString());
+ verify(span, never()).setAttribute(eq("messaging.event.type"), anyString());
+ }
+
+ @Test
+ void shouldEnrichSpanWithS3Metadata() {
+ S3EventNotification.S3BucketEntity bucket = new S3EventNotification.S3BucketEntity(
+ "test-bucket", null, null);
+ S3EventNotification.S3ObjectEntity object = new S3EventNotification.S3ObjectEntity(
+ "test-key", 1024L, null, null, null);
+ S3EventNotification.S3Entity s3 = new S3EventNotification.S3Entity(
+ null, bucket, object, null);
+
+ S3EventNotification.S3EventNotificationRecord record = new S3EventNotification.S3EventNotificationRecord(
+ null, "ObjectCreated:Put", null, null, null, null, null, s3, null);
+
+ S3Event event = new S3Event(List.of(record));
+
+ extractor.enrichSpan(event, span);
+
+ verify(span).setAttribute("messaging.system", "aws.s3");
+ verify(span).setAttribute("messaging.batch.message_count", 1);
+ verify(span).setAttribute("messaging.destination.name", "test-bucket");
+ verify(span).setAttribute("messaging.event.type", "ObjectCreated:Put");
+ }
+
+ @Test
+ void shouldEnrichSpanWithBatchCountOnly() {
+ S3EventNotification.S3EventNotificationRecord record1 = new S3EventNotification.S3EventNotificationRecord(
+ null, null, null, null, null, null, null, null, null);
+ S3EventNotification.S3EventNotificationRecord record2 = new S3EventNotification.S3EventNotificationRecord(
+ null, null, null, null, null, null, null, null, null);
+
+ S3Event event = new S3Event(List.of(record1, record2));
+
+ extractor.enrichSpan(event, span);
+
+ verify(span).setAttribute("messaging.system", "aws.s3");
+ verify(span).setAttribute("messaging.batch.message_count", 2);
+ verify(span, never()).setAttribute(eq("messaging.destination.name"), anyString());
+ }
+
+ @Test
+ void shouldNotEnrichSpanWithBucketNameWhenBucketIsNull() {
+ S3EventNotification.S3Entity s3 = new S3EventNotification.S3Entity(
+ null, null, null, null);
+
+ S3EventNotification.S3EventNotificationRecord record = new S3EventNotification.S3EventNotificationRecord(
+ null, "ObjectCreated:Put", null, null, null, null, null, s3, null);
+
+ S3Event event = new S3Event(List.of(record));
+
+ extractor.enrichSpan(event, span);
+
+ verify(span).setAttribute("messaging.system", "aws.s3");
+ verify(span).setAttribute("messaging.batch.message_count", 1);
+ verify(span, never()).setAttribute(eq("messaging.destination.name"), anyString());
+ verify(span).setAttribute("messaging.event.type", "ObjectCreated:Put");
+ }
+
+ @Test
+ void shouldNotEnrichSpanWithEventTypeWhenEventNameIsNull() {
+ S3EventNotification.S3BucketEntity bucket = new S3EventNotification.S3BucketEntity(
+ "test-bucket", null, null);
+ S3EventNotification.S3Entity s3 = new S3EventNotification.S3Entity(
+ null, bucket, null, null);
+
+ S3EventNotification.S3EventNotificationRecord record = new S3EventNotification.S3EventNotificationRecord(
+ null, null, null, null, null, null, null, s3, null);
+
+ S3Event event = new S3Event(List.of(record));
+
+ extractor.enrichSpan(event, span);
+
+ verify(span).setAttribute("messaging.system", "aws.s3");
+ verify(span).setAttribute("messaging.batch.message_count", 1);
+ verify(span).setAttribute("messaging.destination.name", "test-bucket");
+ verify(span, never()).setAttribute(eq("messaging.event.type"), anyString());
+ }
+}
\ 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
index 55a0f57b8..e372265dc 100644
--- 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
@@ -1,43 +1,227 @@
+/*
+ * 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.context;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+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.api.trace.propagation.W3CTraceContextPropagator;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.propagation.TextMapPropagator;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
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);
-// }
+ @Mock
+ private Span span;
+
+ @Mock
+ private TextMapPropagator propagator;
+
+ @InjectMocks
+ private SnsTraceContextExtractor extractor;
+
+ @Test
+ void shouldSupportSnsEvent() {
+ SNSEvent snsEvent = new SNSEvent();
+
+ boolean result = extractor.supports(snsEvent);
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ void shouldNotSupportNonSnsEvent() {
+ assertThat(extractor.supports("Some non-SNS event")).isFalse();
+ assertThat(extractor.supports(new Object())).isFalse();
+ assertThat(extractor.supports(null)).isFalse();
+ }
+
+ @Test
+ void shouldExtractTraceContextWithConsumerSpanKindWhenRecordsIsNull() {
+ SNSEvent event = new SNSEvent();
+ event.setRecords(null);
+ Context parentContext = Context.current();
+
+ ExtractedTraceContext extractedContext = extractor.extract(event, parentContext, propagator);
+
+ assertThat(extractedContext).isNotNull();
+ assertThat(extractedContext.context()).isEqualTo(parentContext);
+ assertThat(extractedContext.spanContexts()).isEmpty();
+ assertThat(extractedContext.spanKind()).isEqualTo(SpanKind.CONSUMER);
+ }
+
+ @Test
+ void shouldExtractTraceContextWithConsumerSpanKindWhenRecordsIsEmpty() {
+ SNSEvent event = new SNSEvent();
+ event.setRecords(Collections.emptyList());
+ Context parentContext = Context.current();
+
+ ExtractedTraceContext extractedContext = extractor.extract(event, parentContext, propagator);
+
+ assertThat(extractedContext).isNotNull();
+ assertThat(extractedContext.context()).isEqualTo(parentContext);
+ assertThat(extractedContext.spanContexts()).isEmpty();
+ assertThat(extractedContext.spanKind()).isEqualTo(SpanKind.CONSUMER);
+ }
+
+ @Test
+ void shouldExtractTraceContextFromSnsEvent() {
+ String traceId = "4bf92f3577b34da6a3ce929d0e0e4736";
+ String spanId = "00f067aa0ba902b7";
+
+ SNSEvent.MessageAttribute traceparent = new SNSEvent.MessageAttribute();
+ traceparent.setType("String");
+ traceparent.setValue("00-" + traceId + "-" + spanId + "-01");
+
+ Map messageAttributes = new HashMap<>();
+ messageAttributes.put("traceparent", traceparent);
+
+ SNSEvent.SNS sns = new SNSEvent.SNS();
+ sns.setMessageAttributes(messageAttributes);
+
+ SNSEvent.SNSRecord record = new SNSEvent.SNSRecord();
+ record.setSns(sns);
+
+ SNSEvent event = new SNSEvent();
+ event.setRecords(List.of(record));
+
+ ExtractedTraceContext extractedContext = extractor.extract(
+ event,
+ Context.current(),
+ W3CTraceContextPropagator.getInstance()
+ );
+
+ assertThat(extractedContext).isNotNull();
+ assertThat(extractedContext.spanContexts()).hasSize(1);
+ assertThat(extractedContext.spanKind()).isEqualTo(SpanKind.CONSUMER);
+
+ SpanContext spanContext = extractedContext.spanContexts().get(0);
+ assertThat(spanContext.isValid()).isTrue();
+ assertThat(spanContext.getTraceId()).isEqualTo(traceId);
+ assertThat(spanContext.getSpanId()).isEqualTo(spanId);
+ }
+
+ @Test
+ void shouldNotEnrichSpanWhenRecordsIsNull() {
+ SNSEvent event = new SNSEvent();
+ event.setRecords(null);
+
+ extractor.enrichSpan(event, span);
+
+ verifyNoInteractions(span);
+ }
+
+ @Test
+ void shouldNotEnrichSpanWhenRecordsIsEmpty() {
+ SNSEvent event = new SNSEvent();
+ event.setRecords(Collections.emptyList());
+
+ extractor.enrichSpan(event, span);
+
+ verifyNoInteractions(span);
+ }
+
+ @Test
+ void shouldNotEnrichSpanWhenAllRecordsAreNull() {
+ SNSEvent event = new SNSEvent();
+ event.setRecords(Collections.singletonList(null));
+
+ extractor.enrichSpan(event, span);
+
+ verifyNoInteractions(span);
+ }
+
+ @Test
+ void shouldNotEnrichSpanWhenSnsIsNull() {
+ SNSEvent.SNSRecord record = new SNSEvent.SNSRecord();
+ record.setSns(null);
+
+ SNSEvent event = new SNSEvent();
+ event.setRecords(List.of(record));
+
+ extractor.enrichSpan(event, span);
+
+ verifyNoInteractions(span);
+ }
+
+ @Test
+ void shouldEnrichSpanWithSnsMetadata() {
+ SNSEvent.SNS sns = new SNSEvent.SNS();
+ sns.setTopicArn("arn:aws:sns:us-east-1:123456789012:test-topic");
+
+ SNSEvent.SNSRecord record = new SNSEvent.SNSRecord();
+ record.setSns(sns);
+
+ SNSEvent event = new SNSEvent();
+ event.setRecords(List.of(record));
+
+ extractor.enrichSpan(event, span);
+
+ verify(span).setAttribute("messaging.system", "aws.sns");
+ verify(span).setAttribute("messaging.destination.name", "test-topic");
+ }
+
+ @Test
+ void shouldEnrichSpanWithTopicNameFromArnWithoutColon() {
+ SNSEvent.SNS sns = new SNSEvent.SNS();
+ sns.setTopicArn("topic-name-without-separator");
+
+ SNSEvent.SNSRecord record = new SNSEvent.SNSRecord();
+ record.setSns(sns);
+
+ SNSEvent event = new SNSEvent();
+ event.setRecords(List.of(record));
+
+ extractor.enrichSpan(event, span);
+
+ verify(span).setAttribute("messaging.system", "aws.sns");
+ verify(span).setAttribute("messaging.destination.name", "topic-name-without-separator");
+ }
+
+ @Test
+ void shouldNotEnrichSpanWithDestinationNameWhenTopicArnIsNull() {
+ SNSEvent.SNS sns = new SNSEvent.SNS();
+ sns.setTopicArn(null);
+
+ SNSEvent.SNSRecord record = new SNSEvent.SNSRecord();
+ record.setSns(sns);
+
+ SNSEvent event = new SNSEvent();
+ event.setRecords(List.of(record));
+
+ extractor.enrichSpan(event, span);
+ verify(span).setAttribute("messaging.system", "aws.sns");
+ verify(span, never()).setAttribute(eq("messaging.destination.name"), anyString());
+ }
}
\ No newline at end of file
diff --git a/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/SqsTraceContextExtractorTest.java b/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/SqsTraceContextExtractorTest.java
new file mode 100644
index 000000000..b26a36ca6
--- /dev/null
+++ b/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/SqsTraceContextExtractorTest.java
@@ -0,0 +1,212 @@
+/*
+ * 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.context;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+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.api.trace.propagation.W3CTraceContextPropagator;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.propagation.TextMapPropagator;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class SqsTraceContextExtractorTest {
+
+ @Mock
+ private Span span;
+
+ @Mock
+ private TextMapPropagator propagator;
+
+ @InjectMocks
+ private SqsTraceContextExtractor extractor;
+
+ @Test
+ void shouldSupportSqsEvent() {
+ SQSEvent sqsEvent = new SQSEvent();
+
+ boolean result = extractor.supports(sqsEvent);
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ void shouldNotSupportNonSqsEvent() {
+ assertThat(extractor.supports("Some non-SQS event")).isFalse();
+ assertThat(extractor.supports(new Object())).isFalse();
+ assertThat(extractor.supports(null)).isFalse();
+ }
+
+ @Test
+ void shouldExtractTraceContextWithConsumerSpanKindWhenRecordsIsNull() {
+ SQSEvent event = new SQSEvent();
+ event.setRecords(null);
+ Context parentContext = Context.current();
+
+ ExtractedTraceContext extractedContext = extractor.extract(event, parentContext, propagator);
+
+ assertThat(extractedContext).isNotNull();
+ assertThat(extractedContext.context()).isEqualTo(parentContext);
+ assertThat(extractedContext.spanContexts()).isEmpty();
+ assertThat(extractedContext.spanKind()).isEqualTo(SpanKind.CONSUMER);
+ }
+
+ @Test
+ void shouldExtractTraceContextWithConsumerSpanKindWhenRecordsIsEmpty() {
+ SQSEvent event = new SQSEvent();
+ event.setRecords(Collections.emptyList());
+ Context parentContext = Context.current();
+
+ ExtractedTraceContext extractedContext = extractor.extract(event, parentContext, propagator);
+
+ assertThat(extractedContext).isNotNull();
+ assertThat(extractedContext.context()).isEqualTo(parentContext);
+ assertThat(extractedContext.spanContexts()).isEmpty();
+ assertThat(extractedContext.spanKind()).isEqualTo(SpanKind.CONSUMER);
+ }
+
+ @Test
+ void shouldExtractTraceContextFromSqsEvent() {
+ String traceId = "4bf92f3577b34da6a3ce929d0e0e4736";
+ String spanId = "00f067aa0ba902b7";
+
+ SQSEvent.MessageAttribute traceparent = new SQSEvent.MessageAttribute();
+ traceparent.setStringValue("00-" + traceId + "-" + spanId + "-01");
+
+ Map messageAttributes = new HashMap<>();
+ messageAttributes.put("traceparent", traceparent);
+
+ SQSEvent.SQSMessage message = new SQSEvent.SQSMessage();
+ message.setMessageAttributes(messageAttributes);
+
+ SQSEvent event = new SQSEvent();
+ event.setRecords(List.of(message));
+
+ ExtractedTraceContext extractedContext = extractor.extract(
+ event,
+ Context.current(),
+ W3CTraceContextPropagator.getInstance()
+ );
+
+ assertThat(extractedContext).isNotNull();
+ assertThat(extractedContext.spanContexts()).hasSize(1);
+ assertThat(extractedContext.spanKind()).isEqualTo(SpanKind.CONSUMER);
+
+ SpanContext spanContext = extractedContext.spanContexts().get(0);
+ assertThat(spanContext.isValid()).isTrue();
+ assertThat(spanContext.getTraceId()).isEqualTo(traceId);
+ assertThat(spanContext.getSpanId()).isEqualTo(spanId);
+ }
+
+ @Test
+ void shouldNotEnrichSpanWhenRecordsIsNull() {
+ SQSEvent event = new SQSEvent();
+ event.setRecords(null);
+
+ extractor.enrichSpan(event, span);
+
+ verifyNoInteractions(span);
+ }
+
+ @Test
+ void shouldNotEnrichSpanWhenRecordsIsEmpty() {
+ SQSEvent event = new SQSEvent();
+ event.setRecords(Collections.emptyList());
+
+ extractor.enrichSpan(event, span);
+
+ verifyNoInteractions(span);
+ }
+
+ @Test
+ void shouldEnrichSpanWithSqsMetadata() {
+ SQSEvent.SQSMessage message = new SQSEvent.SQSMessage();
+ message.setEventSourceArn("arn:aws:sqs:us-east-1:123456789012:test-queue");
+
+ SQSEvent event = new SQSEvent();
+ event.setRecords(List.of(message));
+
+ extractor.enrichSpan(event, span);
+
+ verify(span).setAttribute("messaging.system", "aws.sqs");
+ verify(span).setAttribute("messaging.batch.message_count", 1);
+ verify(span).setAttribute("messaging.destination.name", "test-queue");
+ }
+
+ @Test
+ void shouldEnrichSpanWithQueueNameFromArnWithoutColon() {
+ SQSEvent.SQSMessage message = new SQSEvent.SQSMessage();
+ message.setEventSourceArn("queue-name-without-separator");
+
+ SQSEvent event = new SQSEvent();
+ event.setRecords(List.of(message));
+
+ extractor.enrichSpan(event, span);
+
+ verify(span).setAttribute("messaging.system", "aws.sqs");
+ verify(span).setAttribute("messaging.batch.message_count", 1);
+ verify(span).setAttribute("messaging.destination.name", "queue-name-without-separator");
+ }
+
+ @Test
+ void shouldNotEnrichSpanWithDestinationNameWhenEventSourceArnIsNull() {
+ SQSEvent.SQSMessage message = new SQSEvent.SQSMessage();
+ message.setEventSourceArn(null);
+
+ SQSEvent event = new SQSEvent();
+ event.setRecords(List.of(message));
+
+ extractor.enrichSpan(event, span);
+
+ verify(span).setAttribute("messaging.system", "aws.sqs");
+ verify(span).setAttribute("messaging.batch.message_count", 1);
+ verify(span, never()).setAttribute(eq("messaging.destination.name"), anyString());
+ }
+
+ @Test
+ void shouldEnrichSpanWithBatchCountForMultipleMessages() {
+ SQSEvent.SQSMessage message1 = new SQSEvent.SQSMessage();
+ message1.setEventSourceArn("arn:aws:sqs:us-east-1:123456789012:test-queue");
+
+ SQSEvent.SQSMessage message2 = new SQSEvent.SQSMessage();
+ message2.setEventSourceArn("arn:aws:sqs:us-east-1:123456789012:test-queue");
+
+ SQSEvent event = new SQSEvent();
+ event.setRecords(List.of(message1, message2));
+
+ extractor.enrichSpan(event, span);
+
+ verify(span).setAttribute("messaging.system", "aws.sqs");
+ verify(span).setAttribute("messaging.batch.message_count", 2);
+ verify(span).setAttribute("messaging.destination.name", "test-queue");
+ }
+}
\ No newline at end of file
diff --git a/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/provider/OpenTelemetryProviderTest.java b/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/provider/OpenTelemetryProviderTest.java
new file mode 100644
index 000000000..014355adf
--- /dev/null
+++ b/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/provider/OpenTelemetryProviderTest.java
@@ -0,0 +1,51 @@
+package software.amazon.lambda.powertools.tracing.opentelemetry.provider;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.opentelemetry.context.propagation.TextMapGetter;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import software.amazon.lambda.powertools.tracing.opentelemetry.context.TraceContextPropagationMode;
+
+class OpenTelemetryProviderTest {
+
+ @Test
+ void shouldProvideTextMapGetter() {
+ TextMapGetter> getter = OpenTelemetryProvider.textMapGetter();
+
+ assertThat(getter).isNotNull();
+
+ Map carrier = Map.of("key1", "value1", "key2", "value2");
+ assertThat(getter.get(carrier, "key1")).isEqualTo("value1");
+ assertThat(getter.get(carrier, "unknown")).isNull();
+ assertThat(getter.get(null, "key1")).isNull();
+ assertThat(getter.keys(carrier)).containsExactlyInAnyOrder("key1", "key2");
+ assertThat(getter.keys(null)).isEmpty();
+ }
+
+ @Test
+ void shouldProvidePropagator() {
+ assertThat(OpenTelemetryProvider.propagator()).isNotNull();
+ }
+
+ @Test
+ void shouldProvideTracer() {
+ assertThat(OpenTelemetryProvider.tracer()).isNotNull();
+ }
+
+ @Test
+ void shouldProvideObjectMapper() {
+ assertThat(OpenTelemetryProvider.objectMapper()).isNotNull();
+ }
+
+ @Test
+ void shouldProvideTraceContextPropagationMode() {
+ assertThat(OpenTelemetryProvider.traceContextPropagationMode())
+ .isEqualTo(TraceContextPropagationMode.PARENT);
+ }
+
+ @Test
+ void shouldForceFlush() {
+ assertThat(OpenTelemetryProvider.forceFlush()).isNotNull();
+ }
+}
From cb72298be911d81cd34a7f156b1c703d1766ed5f Mon Sep 17 00:00:00 2001
From: David-DAM <82216301+David-DAM@users.noreply.github.com>
Date: Tue, 22 Sep 2026 14:05:05 +0200
Subject: [PATCH 19/21] Expand test coverage for `SpanScope`,
`LambdaEventContextExtractorResolver`, and `OpenTelemetryProvider`, including
method chaining, fluent APIs, and various propagator scenarios.
---
.../TracingOpenTelemetryTest.java | 682 +++++++++++++-----
.../ApiGatewayTraceContextExtractorTest.java | 14 +
...mbdaEventContextExtractorResolverTest.java | 420 +++++++++++
.../internal/LambdaResourceTest.java | 298 ++++++++
.../opentelemetry/internal/SpanScopeTest.java | 180 ++++-
.../TracingOpenTelemetryAspectTest.java | 560 ++++++++++++--
.../provider/OpenTelemetryProviderTest.java | 326 ++++++++-
7 files changed, 2186 insertions(+), 294 deletions(-)
create mode 100644 powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/LambdaEventContextExtractorResolverTest.java
create mode 100644 powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/LambdaResourceTest.java
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
index 9b526c1de..0a4380a76 100644
--- 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
@@ -1,3 +1,4 @@
+
/*
* Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
@@ -14,11 +15,38 @@
package software.amazon.lambda.powertools.tracing.opentelemetry;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.trace.Span;
+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 io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter;
+import io.opentelemetry.sdk.trace.SdkTracerProvider;
+import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import software.amazon.lambda.powertools.tracing.opentelemetry.internal.SpanScope;
class TracingOpenTelemetryTest {
+ private InMemorySpanExporter exporter;
+ private SdkTracerProvider tracerProvider;
+ private Tracer tracer;
+
public static final TextMapGetter> MAP_GETTER = new TextMapGetter<>() {
@Override
public Iterable keys(Map carrier) {
@@ -26,210 +54,464 @@ public Iterable keys(Map carrier) {
}
@Override
- public String get(
- Map carrier,
- String key) {
+ 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();
-// }
+ public static final TextMapSetter> MAP_SETTER = Map::put;
+
+ @BeforeEach
+ void setUp() {
+ exporter = InMemorySpanExporter.create();
+ tracerProvider = SdkTracerProvider.builder()
+ .addSpanProcessor(SimpleSpanProcessor.create(exporter))
+ .build();
+ tracer = tracerProvider.get("test-tracer");
+ }
+
+ @AfterEach
+ void tearDown() {
+ tracerProvider.close();
+ }
+
+ @Test
+ void testDefaultConstructor() {
+
+ TracingOpenTelemetry tracing = new TracingOpenTelemetry();
+
+ assertThat(tracing).isNotNull();
+ assertThat(tracing.tracer()).isNotNull();
+ assertThat(tracing.propagator()).isNotNull();
+ assertThat(tracing.eventContextExtractorResolver()).isNotNull();
+ }
+
+ @Test
+ void testConstructorWithTracer() {
+
+ TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer);
+
+ assertThat(tracing.tracer()).isEqualTo(tracer);
+ assertThat(tracing.propagator()).isNotNull();
+ assertThat(tracing.eventContextExtractorResolver()).isNotNull();
+ }
+
+ @Test
+ void testCreate() {
+
+ TracingOpenTelemetry tracing = TracingOpenTelemetry.create();
+
+ assertThat(tracing).isNotNull();
+ assertThat(tracing.tracer()).isNotNull();
+ }
+
+ @Test
+ void testBuilder() {
+
+ TracingOpenTelemetry tracing = TracingOpenTelemetry.builder()
+ .tracer(tracer)
+ .build();
+
+ assertThat(tracing).isNotNull();
+ assertThat(tracing.tracer()).isEqualTo(tracer);
+ }
+
+ @Test
+ void testAddSpanWithName() {
+
+ TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer);
+
+ try (SpanScope scope = tracing.addSpan("test-span")) {
+
+ assertThat(scope.span().getSpanContext().isValid()).isTrue();
+ assertThat(Span.current()).isEqualTo(scope.span());
+ }
+
+ assertThat(exporter.getFinishedSpanItems()).hasSize(1);
+ assertThat(exporter.getFinishedSpanItems().get(0).getName()).isEqualTo("test-span");
+ assertThat(exporter.getFinishedSpanItems().get(0).getKind()).isEqualTo(SpanKind.INTERNAL);
+ }
+
+ @Test
+ void testAddSpanWithNameAndKind() {
+
+ TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer);
+
+ try (SpanScope scope = tracing.addSpan("client-span", SpanKind.CLIENT)) {
+
+ assertThat(scope.span().getSpanContext().isValid()).isTrue();
+ }
+
+ assertThat(exporter.getFinishedSpanItems()).hasSize(1);
+ assertThat(exporter.getFinishedSpanItems().get(0).getKind()).isEqualTo(SpanKind.CLIENT);
+ }
+
+ @Test
+ void testAddSpanWithAttributes() {
+
+ TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer);
+ Attributes attributes = Attributes.builder()
+ .put("key1", "value1")
+ .put("key2", 123L)
+ .build();
+
+ try (SpanScope scope = tracing.addSpan("span-with-attrs", SpanKind.INTERNAL, attributes)) {
+ assertThat(scope.span()).isNotNull();
+ }
+
+ assertThat(exporter.getFinishedSpanItems()).hasSize(1);
+ assertThat(exporter.getFinishedSpanItems().get(0).getAttributes().get(AttributeKey.stringKey("key1")))
+ .isEqualTo("value1");
+ assertThat(exporter.getFinishedSpanItems().get(0).getAttributes().get(AttributeKey.longKey("key2")))
+ .isEqualTo(123L);
+ }
+
+ @Test
+ void testAddSpanEndsWhenScopeIsClosed() {
+
+ TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer);
+
+ try (SpanScope ignored = tracing.addSpan("closing-span")) {
+ assertThat(exporter.getFinishedSpanItems()).isEmpty();
+ }
+
+ assertThat(exporter.getFinishedSpanItems()).hasSize(1);
+ assertThat(exporter.getFinishedSpanItems().get(0).getName()).isEqualTo("closing-span");
+ }
+
+ @Test
+ void testAddSpanRestoresPreviousSpan() {
+
+ 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());
+ }
+
+ assertThat(exporter.getFinishedSpanItems()).hasSize(2);
+ }
+
+ @Test
+ void testAddSpanWithSpanContextLinks() {
+
+ TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer);
+
+ SpanContext linkContext1 = SpanContext.createFromRemoteParent(
+ "00000000000000000000000000000001",
+ "0000000000000001",
+ io.opentelemetry.api.trace.TraceFlags.getSampled(),
+ io.opentelemetry.api.trace.TraceState.getDefault()
+ );
+
+ SpanContext linkContext2 = SpanContext.createFromRemoteParent(
+ "00000000000000000000000000000002",
+ "0000000000000002",
+ io.opentelemetry.api.trace.TraceFlags.getSampled(),
+ io.opentelemetry.api.trace.TraceState.getDefault()
+ );
+
+ try (SpanScope scope = tracing.addSpan(
+ "span-with-links",
+ SpanKind.INTERNAL,
+ Attributes.empty(),
+ Context.current(),
+ Arrays.asList(linkContext1, linkContext2)
+ )) {
+ assertThat(scope.span()).isNotNull();
+ }
+
+ assertThat(exporter.getFinishedSpanItems()).hasSize(1);
+ assertThat(exporter.getFinishedSpanItems().get(0).getLinks()).hasSize(2);
+ }
+
+ @Test
+ void testWithSpan() throws Exception {
+
+ TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer);
+
+ String result = tracing.withSpan("operation-span", span -> {
+ assertThat(span).isNotNull();
+ return "success";
+ });
+
+ assertThat(result).isEqualTo("success");
+ assertThat(exporter.getFinishedSpanItems()).hasSize(1);
+ assertThat(exporter.getFinishedSpanItems().get(0).getName()).isEqualTo("operation-span");
+ }
+
+ @Test
+ void testWithSpanRecordsException() {
+
+ TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer);
+ RuntimeException exception = new RuntimeException("boom");
+
+ assertThatThrownBy(() ->
+ tracing.withSpan("failing-span", 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);
+ }
+
+ @Test
+ void testWithSpanWithKindAndAttributes() throws Exception {
+
+ TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer);
+ Attributes attributes = Attributes.builder()
+ .put("custom", "attribute")
+ .build();
+
+ Integer result = tracing.withSpan("custom-span", SpanKind.SERVER, attributes, span -> 42);
+
+ assertThat(result).isEqualTo(42);
+ assertThat(exporter.getFinishedSpanItems()).hasSize(1);
+ assertThat(exporter.getFinishedSpanItems().get(0).getKind()).isEqualTo(SpanKind.SERVER);
+ }
+
+ @Test
+ void testCurrentSpan() {
+
+ TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer);
+
+ try (SpanScope scope = tracing.addSpan("current-test")) {
+ Span current = tracing.currentSpan();
+
+ assertThat(current).isEqualTo(scope.span());
+ }
+ }
+
+ @Test
+ void testExtractContext() {
+
+ TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer);
+ 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 testExtractContextWithParentContext() {
+
+ TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer);
+ Map headers = new HashMap<>();
+ headers.put("traceparent", "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01");
+
+ Context parentContext = Context.current();
+
+ Context context = tracing.extractContext(parentContext, headers, MAP_GETTER);
+ SpanContext spanContext = Span.fromContext(context).getSpanContext();
+
+ assertThat(spanContext.isValid()).isTrue();
+ }
+
+ @Test
+ void testExtractContextWithMissingTraceparent() {
+
+ TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer);
+ Map headers = new HashMap<>();
+
+ Context context = tracing.extractContext(headers, MAP_GETTER);
+
+ assertThat(Span.fromContext(context).getSpanContext().isValid()).isFalse();
+ }
+
+ @Test
+ void testInjectContext() {
+
+ TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer);
+ Map carrier = new HashMap<>();
+
+ try (SpanScope scope = tracing.addSpan("inject-test")) {
+ tracing.injectContext(carrier, MAP_SETTER);
+ }
+
+ assertThat(carrier).containsKey("traceparent");
+ }
+
+ @Test
+ void testInjectContextWithSpecificContext() {
+
+ TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer);
+ Map carrier = new HashMap<>();
+
+ try (SpanScope scope = tracing.addSpan("inject-test")) {
+ Context context = Context.current();
+ tracing.injectContext(context, carrier, MAP_SETTER);
+ }
+
+ assertThat(carrier).containsKey("traceparent");
+ }
+
+ @Test
+ void testFlush() {
+
+ TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer);
+
+ try (SpanScope ignored = tracing.addSpan("flush-test")) {
+ // span created
+ }
+
+ tracing.flush();
+
+ assertThat(exporter.getFinishedSpanItems()).hasSize(1);
+ }
+
+ @Test
+ void testFlushWithTimeout() {
+
+ TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer);
+
+ try (SpanScope ignored = tracing.addSpan("flush-timeout-test")) {
+ // span created
+ }
+
+ tracing.flush(10, TimeUnit.SECONDS);
+
+ assertThat(exporter.getFinishedSpanItems()).hasSize(1);
+ }
+
+ @Test
+ void testBuilderWithCustomPropagator() {
+
+ TextMapPropagator customPropagator = TextMapPropagator.noop();
+
+ TracingOpenTelemetry tracing = TracingOpenTelemetry.builder()
+ .tracer(tracer)
+ .propagator(customPropagator)
+ .build();
+
+ assertThat(tracing.propagator()).isEqualTo(customPropagator);
+ }
+
+ @Test
+ void testAddSpanThrowsNullPointerExceptionForNullName() {
+
+ TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer);
+
+ assertThatThrownBy(() ->
+ tracing.addSpan(null, SpanKind.INTERNAL, Attributes.empty(), Context.current(), Collections.emptyList())
+ ).isInstanceOf(NullPointerException.class)
+ .hasMessageContaining("name must not be null");
+ }
+
+ @Test
+ void testAddSpanThrowsNullPointerExceptionForNullKind() {
+
+ TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer);
+
+ assertThatThrownBy(() ->
+ tracing.addSpan("test", null, Attributes.empty(), Context.current(), Collections.emptyList())
+ ).isInstanceOf(NullPointerException.class)
+ .hasMessageContaining("kind must not be null");
+ }
+
+ @Test
+ void testAddSpanThrowsNullPointerExceptionForNullAttributes() {
+
+ TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer);
+
+ assertThatThrownBy(() ->
+ tracing.addSpan("test", SpanKind.INTERNAL, null, Context.current(), Collections.emptyList())
+ ).isInstanceOf(NullPointerException.class)
+ .hasMessageContaining("attributes must not be null");
+ }
+
+ @Test
+ void testAddSpanThrowsNullPointerExceptionForNullParentContext() {
+
+ TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer);
+
+ assertThatThrownBy(() ->
+ tracing.addSpan("test", SpanKind.INTERNAL, Attributes.empty(), null, Collections.emptyList())
+ ).isInstanceOf(NullPointerException.class)
+ .hasMessageContaining("parentContext must not be null");
+ }
+
+ @Test
+ void testAddSpanThrowsNullPointerExceptionForNullSpanContexts() {
+
+ TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer);
+
+ assertThatThrownBy(() ->
+ tracing.addSpan("test", SpanKind.INTERNAL, Attributes.empty(), Context.current(), null)
+ ).isInstanceOf(NullPointerException.class)
+ .hasMessageContaining("spanContexts must not be null");
+ }
+
+ @Test
+ void testWithSpanThrowsNullPointerExceptionForNullOperation() {
+
+ TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer);
+
+ assertThatThrownBy(() ->
+ tracing.withSpan("test", SpanKind.INTERNAL, Attributes.empty(), null)
+ ).isInstanceOf(NullPointerException.class)
+ .hasMessageContaining("operation must not be null");
+ }
+
+ @Test
+ void testExtractContextThrowsNullPointerExceptionForNullCarrier() {
+
+ TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer);
+
+ assertThatThrownBy(() ->
+ tracing.extractContext(Context.current(), null, MAP_GETTER)
+ ).isInstanceOf(NullPointerException.class)
+ .hasMessageContaining("carrier must not be null");
+ }
+
+ @Test
+ void testExtractContextThrowsNullPointerExceptionForNullGetter() {
+
+ TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer);
+ Map carrier = new HashMap<>();
+
+ assertThatThrownBy(() ->
+ tracing.extractContext(Context.current(), carrier, null)
+ ).isInstanceOf(NullPointerException.class)
+ .hasMessageContaining("getter must not be null");
+ }
+
+ @Test
+ void testInjectContextThrowsNullPointerExceptionForNullCarrier() {
+
+ TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer);
+
+ assertThatThrownBy(() ->
+ tracing.injectContext(Context.current(), null, MAP_SETTER)
+ ).isInstanceOf(NullPointerException.class)
+ .hasMessageContaining("carrier must not be null");
+ }
+
+ @Test
+ void testInjectContextThrowsNullPointerExceptionForNullSetter() {
+ TracingOpenTelemetry tracing = new TracingOpenTelemetry(tracer);
+ Map carrier = new HashMap<>();
+ assertThatThrownBy(() ->
+ tracing.injectContext(Context.current(), carrier, null)
+ ).isInstanceOf(NullPointerException.class)
+ .hasMessageContaining("setter must not be null");
+ }
}
\ 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
index ecbe392ab..27736c480 100644
--- 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
@@ -1,3 +1,17 @@
+/*
+ * 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.context;
import static org.assertj.core.api.Assertions.assertThat;
diff --git a/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/LambdaEventContextExtractorResolverTest.java b/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/LambdaEventContextExtractorResolverTest.java
new file mode 100644
index 000000000..5cd6c5bf5
--- /dev/null
+++ b/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/context/LambdaEventContextExtractorResolverTest.java
@@ -0,0 +1,420 @@
+package software.amazon.lambda.powertools.tracing.opentelemetry.context;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
+import com.amazonaws.services.lambda.runtime.events.DynamodbEvent;
+import com.amazonaws.services.lambda.runtime.events.KinesisEvent;
+import com.amazonaws.services.lambda.runtime.events.S3Event;
+import com.amazonaws.services.lambda.runtime.events.SNSEvent;
+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 io.opentelemetry.sdk.trace.SdkTracerProvider;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class LambdaEventContextExtractorResolverTest {
+
+ @Mock
+ private LambdaEventContextExtractor mockExtractor1;
+
+ @Mock
+ private LambdaEventContextExtractor mockExtractor2;
+
+ @Mock
+ private TextMapPropagator propagator;
+
+ private Context parentContext;
+ private SdkTracerProvider tracerProvider;
+ private Span testSpan;
+
+ @BeforeEach
+ void setUp() {
+ tracerProvider = SdkTracerProvider.builder().build();
+ testSpan = tracerProvider.get("test").spanBuilder("test-span").startSpan();
+ parentContext = Context.current();
+ }
+
+ @Test
+ void testCreate() {
+
+ LambdaEventContextExtractorResolver resolver = LambdaEventContextExtractorResolver.create();
+
+ assertThat(resolver).isNotNull();
+ }
+
+ @Test
+ void testExtractWithSupportedExtractor() {
+
+ Object event = new APIGatewayProxyRequestEvent();
+
+ SpanContext spanContext = SpanContext.createFromRemoteParent(
+ "00000000000000000000000000000001",
+ "0000000000000001",
+ io.opentelemetry.api.trace.TraceFlags.getSampled(),
+ io.opentelemetry.api.trace.TraceState.getDefault()
+ );
+
+ ExtractedTraceContext expectedContext = new ExtractedTraceContext(
+ parentContext,
+ List.of(spanContext),
+ SpanKind.SERVER
+ );
+
+ when(mockExtractor1.supports(event)).thenReturn(true);
+ when(mockExtractor1.extract(event, parentContext, propagator)).thenReturn(expectedContext);
+
+ LambdaEventContextExtractorResolver resolver = new LambdaEventContextExtractorResolver(
+ List.of(mockExtractor1, mockExtractor2)
+ );
+
+ ExtractedTraceContext result = resolver.extract(event, parentContext, propagator);
+
+ assertThat(result).isEqualTo(expectedContext);
+ assertThat(result.context()).isEqualTo(parentContext);
+ assertThat(result.spanContexts()).hasSize(1);
+ assertThat(result.spanKind()).isEqualTo(SpanKind.SERVER);
+ verify(mockExtractor1).supports(event);
+ verify(mockExtractor1).extract(event, parentContext, propagator);
+ verify(mockExtractor2, never()).supports(any());
+ }
+
+ @Test
+ void testExtractWithNoSupportingExtractor() {
+
+ Object event = new Object();
+
+ when(mockExtractor1.supports(event)).thenReturn(false);
+ when(mockExtractor2.supports(event)).thenReturn(false);
+
+ LambdaEventContextExtractorResolver resolver = new LambdaEventContextExtractorResolver(
+ List.of(mockExtractor1, mockExtractor2)
+ );
+
+ ExtractedTraceContext result = resolver.extract(event, parentContext, propagator);
+
+ assertThat(result).isNotNull();
+ assertThat(result.context()).isEqualTo(parentContext);
+ assertThat(result.spanContexts()).isEmpty();
+ assertThat(result.spanKind()).isEqualTo(SpanKind.SERVER);
+ verify(mockExtractor1).supports(event);
+ verify(mockExtractor2).supports(event);
+ verify(mockExtractor1, never()).extract(any(), any(), any());
+ verify(mockExtractor2, never()).extract(any(), any(), any());
+ }
+
+ @Test
+ void testExtractWithMultipleExtractorsFirstMatch() {
+
+ Object event = new SQSEvent();
+
+ ExtractedTraceContext expectedContext = new ExtractedTraceContext(
+ parentContext,
+ Collections.emptyList()
+ );
+
+ when(mockExtractor1.supports(event)).thenReturn(true);
+ when(mockExtractor1.extract(event, parentContext, propagator)).thenReturn(expectedContext);
+
+ LambdaEventContextExtractorResolver resolver = new LambdaEventContextExtractorResolver(
+ List.of(mockExtractor1, mockExtractor2)
+ );
+
+ ExtractedTraceContext result = resolver.extract(event, parentContext, propagator);
+
+ assertThat(result).isEqualTo(expectedContext);
+ verify(mockExtractor1).supports(event);
+ verify(mockExtractor1).extract(event, parentContext, propagator);
+ verify(mockExtractor2, never()).supports(any());
+ verify(mockExtractor2, never()).extract(any(), any(), any());
+ }
+
+ @Test
+ void testEnrichSpanWithSupportedExtractor() {
+
+ Object event = new SNSEvent();
+
+ when(mockExtractor1.supports(event)).thenReturn(true);
+
+ LambdaEventContextExtractorResolver resolver = new LambdaEventContextExtractorResolver(
+ List.of(mockExtractor1, mockExtractor2)
+ );
+
+ resolver.enrichSpan(event, testSpan);
+
+ verify(mockExtractor1).supports(event);
+ verify(mockExtractor1).enrichSpan(event, testSpan);
+ verify(mockExtractor2, never()).supports(any());
+ verify(mockExtractor2, never()).enrichSpan(any(), any());
+ }
+
+ @Test
+ void testEnrichSpanWithNoSupportingExtractor() {
+
+ Object event = new Object();
+
+ when(mockExtractor1.supports(event)).thenReturn(false);
+ when(mockExtractor2.supports(event)).thenReturn(false);
+
+ LambdaEventContextExtractorResolver resolver = new LambdaEventContextExtractorResolver(
+ List.of(mockExtractor1, mockExtractor2)
+ );
+
+ resolver.enrichSpan(event, testSpan);
+
+ verify(mockExtractor1).supports(event);
+ verify(mockExtractor2).supports(event);
+ verify(mockExtractor1, never()).enrichSpan(any(), any());
+ verify(mockExtractor2, never()).enrichSpan(any(), any());
+ }
+
+ @Test
+ void testEnrichSpanWithMultipleExtractorsFirstMatch() {
+
+ Object event = new KinesisEvent();
+
+ when(mockExtractor1.supports(event)).thenReturn(true);
+
+ LambdaEventContextExtractorResolver resolver = new LambdaEventContextExtractorResolver(
+ List.of(mockExtractor1, mockExtractor2)
+ );
+
+ resolver.enrichSpan(event, testSpan);
+
+ verify(mockExtractor1).supports(event);
+ verify(mockExtractor1).enrichSpan(event, testSpan);
+ verify(mockExtractor2, never()).supports(any());
+ verify(mockExtractor2, never()).enrichSpan(any(), any());
+ }
+
+ @Test
+ void testCreateWithAllDefaultExtractors() {
+
+ LambdaEventContextExtractorResolver resolver = LambdaEventContextExtractorResolver.create();
+
+ APIGatewayProxyRequestEvent apiGatewayEvent = new APIGatewayProxyRequestEvent();
+ apiGatewayEvent.setHeaders(new HashMap<>());
+
+ SQSEvent sqsEvent = new SQSEvent();
+ sqsEvent.setRecords(new ArrayList<>());
+
+ SNSEvent snsEvent = new SNSEvent();
+ snsEvent.setRecords(new ArrayList<>());
+
+ KinesisEvent kinesisEvent = new KinesisEvent();
+ kinesisEvent.setRecords(new ArrayList<>());
+
+ DynamodbEvent dynamoDbEvent = new DynamodbEvent();
+ dynamoDbEvent.setRecords(new ArrayList<>());
+
+ S3Event s3Event = new S3Event(new ArrayList<>());
+
+ ExtractedTraceContext apiResult = resolver.extract(apiGatewayEvent, parentContext, propagator);
+ assertThat(apiResult).isNotNull();
+
+ ExtractedTraceContext sqsResult = resolver.extract(sqsEvent, parentContext, propagator);
+ assertThat(sqsResult).isNotNull();
+
+ ExtractedTraceContext snsResult = resolver.extract(snsEvent, parentContext, propagator);
+ assertThat(snsResult).isNotNull();
+
+ ExtractedTraceContext kinesisResult = resolver.extract(kinesisEvent, parentContext, propagator);
+ assertThat(kinesisResult).isNotNull();
+
+ ExtractedTraceContext dynamoDbResult = resolver.extract(dynamoDbEvent, parentContext, propagator);
+ assertThat(dynamoDbResult).isNotNull();
+
+ ExtractedTraceContext s3Result = resolver.extract(s3Event, parentContext, propagator);
+ assertThat(s3Result).isNotNull();
+ }
+
+ @Test
+ void testConstructorCreatesImmutableCopy() {
+
+ List originalList = new ArrayList<>();
+ originalList.add(mockExtractor1);
+
+ LambdaEventContextExtractorResolver resolver = new LambdaEventContextExtractorResolver(originalList);
+
+ originalList.add(mockExtractor2);
+
+ Object event = new Object();
+ when(mockExtractor1.supports(event)).thenReturn(false);
+
+ resolver.extract(event, parentContext, propagator);
+
+ verify(mockExtractor1).supports(event);
+ verify(mockExtractor2, never()).supports(any());
+ }
+
+ @Test
+ void testExtractWithEmptyExtractorList() {
+
+ LambdaEventContextExtractorResolver resolver = new LambdaEventContextExtractorResolver(
+ Collections.emptyList()
+ );
+ Object event = new Object();
+
+ ExtractedTraceContext result = resolver.extract(event, parentContext, propagator);
+
+ assertThat(result).isNotNull();
+ assertThat(result.context()).isEqualTo(parentContext);
+ assertThat(result.spanContexts()).isEmpty();
+ }
+
+ @Test
+ void testEnrichSpanWithEmptyExtractorList() {
+
+ LambdaEventContextExtractorResolver resolver = new LambdaEventContextExtractorResolver(
+ Collections.emptyList()
+ );
+ Object event = new Object();
+
+ resolver.enrichSpan(event, testSpan);
+ }
+
+ @Test
+ void testExtractWithNullEvent() {
+
+ when(mockExtractor1.supports(null)).thenReturn(false);
+
+ LambdaEventContextExtractorResolver resolver = new LambdaEventContextExtractorResolver(
+ List.of(mockExtractor1)
+ );
+
+ ExtractedTraceContext result = resolver.extract(null, parentContext, propagator);
+
+ assertThat(result).isNotNull();
+ assertThat(result.spanContexts()).isEmpty();
+ verify(mockExtractor1).supports(null);
+ }
+
+ @Test
+ void testEnrichSpanWithNullEvent() {
+
+ when(mockExtractor1.supports(null)).thenReturn(false);
+
+ LambdaEventContextExtractorResolver resolver = new LambdaEventContextExtractorResolver(
+ List.of(mockExtractor1)
+ );
+
+ resolver.enrichSpan(null, testSpan);
+ verify(mockExtractor1).supports(null);
+ }
+
+ @Test
+ void testExtractReturnsContextWithMultipleSpanContexts() {
+
+ Object event = new DynamodbEvent();
+
+ SpanContext spanContext1 = SpanContext.createFromRemoteParent(
+ "00000000000000000000000000000001",
+ "0000000000000001",
+ io.opentelemetry.api.trace.TraceFlags.getSampled(),
+ io.opentelemetry.api.trace.TraceState.getDefault()
+ );
+
+ SpanContext spanContext2 = SpanContext.createFromRemoteParent(
+ "00000000000000000000000000000002",
+ "0000000000000002",
+ io.opentelemetry.api.trace.TraceFlags.getSampled(),
+ io.opentelemetry.api.trace.TraceState.getDefault()
+ );
+
+ ExtractedTraceContext expectedContext = new ExtractedTraceContext(
+ parentContext,
+ Arrays.asList(spanContext1, spanContext2),
+ SpanKind.CONSUMER
+ );
+
+ when(mockExtractor1.supports(event)).thenReturn(true);
+ when(mockExtractor1.extract(event, parentContext, propagator)).thenReturn(expectedContext);
+
+ LambdaEventContextExtractorResolver resolver = new LambdaEventContextExtractorResolver(
+ List.of(mockExtractor1)
+ );
+
+
+ ExtractedTraceContext result = resolver.extract(event, parentContext, propagator);
+
+
+ assertThat(result.spanContexts()).hasSize(2);
+ assertThat(result.spanKind()).isEqualTo(SpanKind.CONSUMER);
+ }
+
+ @Test
+ void testExtractWithDifferentSpanKinds() {
+
+ Object event = new Object();
+
+ ExtractedTraceContext clientContext = new ExtractedTraceContext(
+ parentContext,
+ Collections.emptyList(),
+ SpanKind.CLIENT
+ );
+
+ when(mockExtractor1.supports(event)).thenReturn(true);
+ when(mockExtractor1.extract(event, parentContext, propagator)).thenReturn(clientContext);
+
+ LambdaEventContextExtractorResolver resolver = new LambdaEventContextExtractorResolver(
+ List.of(mockExtractor1)
+ );
+
+ ExtractedTraceContext result = resolver.extract(event, parentContext, propagator);
+
+ assertThat(result.spanKind()).isEqualTo(SpanKind.CLIENT);
+ }
+
+ @Test
+ void testExtractorOrderMatters() {
+
+ Object event = new Object();
+
+ ExtractedTraceContext context1 = new ExtractedTraceContext(
+ parentContext,
+ Collections.emptyList(),
+ SpanKind.SERVER
+ );
+
+ ExtractedTraceContext context2 = new ExtractedTraceContext(
+ parentContext,
+ Collections.emptyList(),
+ SpanKind.CLIENT
+ );
+
+ when(mockExtractor1.supports(event)).thenReturn(true);
+ when(mockExtractor1.extract(event, parentContext, propagator)).thenReturn(context1);
+ when(mockExtractor2.supports(event)).thenReturn(true);
+ when(mockExtractor2.extract(event, parentContext, propagator)).thenReturn(context2);
+
+ LambdaEventContextExtractorResolver resolver1 = new LambdaEventContextExtractorResolver(
+ List.of(mockExtractor1, mockExtractor2)
+ );
+ ExtractedTraceContext result1 = resolver1.extract(event, parentContext, propagator);
+
+ LambdaEventContextExtractorResolver resolver2 = new LambdaEventContextExtractorResolver(
+ List.of(mockExtractor2, mockExtractor1)
+ );
+ ExtractedTraceContext result2 = resolver2.extract(event, parentContext, propagator);
+
+ assertThat(result1.spanKind()).isEqualTo(SpanKind.SERVER);
+ assertThat(result2.spanKind()).isEqualTo(SpanKind.CLIENT);
+ }
+}
\ No newline at end of file
diff --git a/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/LambdaResourceTest.java b/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/LambdaResourceTest.java
new file mode 100644
index 000000000..73d099117
--- /dev/null
+++ b/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/LambdaResourceTest.java
@@ -0,0 +1,298 @@
+/*
+ * 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 static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.sdk.resources.Resource;
+import org.junit.jupiter.api.Test;
+import org.junitpioneer.jupiter.SetEnvironmentVariable;
+
+class LambdaResourceTest {
+
+ @Test
+ @SetEnvironmentVariable(key = "AWS_LAMBDA_FUNCTION_NAME", value = "my-function")
+ @SetEnvironmentVariable(key = "AWS_LAMBDA_FUNCTION_VERSION", value = "1")
+ @SetEnvironmentVariable(key = "AWS_LAMBDA_FUNCTION_MEMORY_SIZE", value = "512")
+ @SetEnvironmentVariable(key = "AWS_LAMBDA_LOG_STREAM_NAME", value = "2023/01/01/[$LATEST]abcd1234")
+ @SetEnvironmentVariable(key = "AWS_REGION", value = "us-east-1")
+ @SetEnvironmentVariable(key = "AWS_LAMBDA_FUNCTION_ARN", value = "arn:aws:lambda:us-east-1:123456789012:function" +
+ ":my-function")
+ void testCreateWithAllEnvironmentVariables() {
+
+ Resource resource = LambdaResource.create();
+ Attributes attributes = resource.getAttributes();
+
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("cloud.provider")))
+ .isEqualTo("aws");
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("cloud.region")))
+ .isEqualTo("us-east-1");
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("cloud.account.id")))
+ .isEqualTo("123456789012");
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("service.name")))
+ .isEqualTo("my-function");
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("service.version")))
+ .isEqualTo("1");
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("faas.name")))
+ .isEqualTo("my-function");
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("faas.version")))
+ .isEqualTo("1");
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("faas.instance")))
+ .isEqualTo("2023/01/01/[$LATEST]abcd1234");
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.longKey("faas.max_memory")))
+ .isEqualTo(512L);
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("telemetry.sdk.name")))
+ .isEqualTo("opentelemetry");
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("telemetry.distro.name")))
+ .isEqualTo("powertools-for-aws-lambda");
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("telemetry.sdk.language")))
+ .isEqualTo("java");
+ }
+
+ @Test
+ void testCreateWithNoEnvironmentVariables() {
+
+ Resource resource = LambdaResource.create();
+ Attributes attributes = resource.getAttributes();
+
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("cloud.provider")))
+ .isEqualTo("aws");
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("telemetry.sdk.name")))
+ .isEqualTo("opentelemetry");
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("telemetry.distro.name")))
+ .isEqualTo("powertools-for-aws-lambda");
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("telemetry.sdk.language")))
+ .isEqualTo("java");
+
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("cloud.region"))).isNull();
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("cloud.account.id"))).isNull();
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("service.name"))).isNull();
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("service.version"))).isNull();
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("faas.name"))).isNull();
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("faas.version"))).isNull();
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("faas.instance"))).isNull();
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.longKey("faas.max_memory"))).isNull();
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "AWS_LAMBDA_FUNCTION_NAME", value = "")
+ @SetEnvironmentVariable(key = "AWS_REGION", value = " ")
+ void testCreateWithEmptyAndBlankEnvironmentVariables() {
+
+ Resource resource = LambdaResource.create();
+ Attributes attributes = resource.getAttributes();
+
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("service.name"))).isNull();
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("faas.name"))).isNull();
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("cloud.region"))).isNull();
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "AWS_LAMBDA_FUNCTION_MEMORY_SIZE", value = "1024")
+ void testCreateWithMemorySize() {
+
+ Resource resource = LambdaResource.create();
+ Attributes attributes = resource.getAttributes();
+
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.longKey("faas.max_memory")))
+ .isEqualTo(1024L);
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "AWS_LAMBDA_FUNCTION_MEMORY_SIZE", value = "invalid")
+ void testCreateWithInvalidMemorySize() {
+ assertThatThrownBy(() -> LambdaResource.create())
+ .isInstanceOf(NumberFormatException.class);
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "AWS_LAMBDA_FUNCTION_ARN", value = "arn:aws:lambda:us-east-1:123456789012:function" +
+ ":my-function")
+ void testExtractAccountIdFromValidArn() {
+
+ Resource resource = LambdaResource.create();
+ Attributes attributes = resource.getAttributes();
+
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("cloud.account.id")))
+ .isEqualTo("123456789012");
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "AWS_LAMBDA_FUNCTION_ARN", value = "arn:aws:lambda:us-west-2:999888777666:function" +
+ ":another-function:1")
+ void testExtractAccountIdFromArnWithVersion() {
+ Resource resource = LambdaResource.create();
+ Attributes attributes = resource.getAttributes();
+
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("cloud.account.id")))
+ .isEqualTo("999888777666");
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "AWS_LAMBDA_FUNCTION_ARN", value = "arn:aws:lambda:eu-central-1:111222333444" +
+ ":function:test-function:$LATEST")
+ void testExtractAccountIdFromArnWithLatestAlias() {
+
+ Resource resource = LambdaResource.create();
+ Attributes attributes = resource.getAttributes();
+
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("cloud.account.id")))
+ .isEqualTo("111222333444");
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "AWS_LAMBDA_FUNCTION_ARN", value = "arn:aws:lambda:us-east-1")
+ void testExtractAccountIdFromIncompleteArn() {
+
+ Resource resource = LambdaResource.create();
+ Attributes attributes = resource.getAttributes();
+
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("cloud.account.id")))
+ .isNull();
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "AWS_LAMBDA_FUNCTION_ARN", value = "arn:aws:lambda")
+ void testExtractAccountIdFromVeryShortArn() {
+
+ Resource resource = LambdaResource.create();
+ Attributes attributes = resource.getAttributes();
+
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("cloud.account.id")))
+ .isNull();
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "AWS_LAMBDA_FUNCTION_ARN", value = "invalid-arn")
+ void testExtractAccountIdFromInvalidArn() {
+
+ Resource resource = LambdaResource.create();
+ Attributes attributes = resource.getAttributes();
+
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("cloud.account.id")))
+ .isNull();
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "AWS_LAMBDA_FUNCTION_ARN", value = "")
+ void testExtractAccountIdFromEmptyArn() {
+
+ Resource resource = LambdaResource.create();
+ Attributes attributes = resource.getAttributes();
+
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("cloud.account.id")))
+ .isNull();
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "AWS_LAMBDA_FUNCTION_NAME", value = "test-function")
+ @SetEnvironmentVariable(key = "AWS_LAMBDA_FUNCTION_VERSION", value = "$LATEST")
+ void testCreateWithLatestVersion() {
+
+ Resource resource = LambdaResource.create();
+ Attributes attributes = resource.getAttributes();
+
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("service.version")))
+ .isEqualTo("$LATEST");
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("faas.version")))
+ .isEqualTo("$LATEST");
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "AWS_LAMBDA_LOG_STREAM_NAME", value = "2023/12/31/[$LATEST]xyz789")
+ void testCreateWithLogStreamName() {
+
+ Resource resource = LambdaResource.create();
+ Attributes attributes = resource.getAttributes();
+
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("faas.instance")))
+ .isEqualTo("2023/12/31/[$LATEST]xyz789");
+ }
+
+ @Test
+ void testCreateReturnsResourceWithAttributes() {
+
+ Resource resource = LambdaResource.create();
+
+ assertThat(resource).isNotNull();
+ assertThat(resource.getAttributes()).isNotNull();
+ assertThat(resource.getAttributes().size()).isGreaterThan(0);
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "AWS_REGION", value = "ap-southeast-1")
+ void testCreateWithDifferentRegion() {
+
+ Resource resource = LambdaResource.create();
+ Attributes attributes = resource.getAttributes();
+
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("cloud.region")))
+ .isEqualTo("ap-southeast-1");
+ }
+
+ @Test
+ void testTelemetryAttributesAreAlwaysPresent() {
+
+ Resource resource = LambdaResource.create();
+ Attributes attributes = resource.getAttributes();
+
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("cloud.provider")))
+ .isNotNull()
+ .isEqualTo("aws");
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("telemetry.sdk.name")))
+ .isNotNull()
+ .isEqualTo("opentelemetry");
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("telemetry.distro.name")))
+ .isNotNull()
+ .isEqualTo("powertools-for-aws-lambda");
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("telemetry.sdk.language")))
+ .isNotNull()
+ .isEqualTo("java");
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "AWS_LAMBDA_FUNCTION_ARN", value = "arn:aws:lambda:us-east-1:123456789012:function" +
+ ":my-function:alias-name")
+ void testCreateWithArnContainingAlias() {
+
+ Resource resource = LambdaResource.create();
+ Attributes attributes = resource.getAttributes();
+
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.stringKey("cloud.account.id")))
+ .isEqualTo("123456789012");
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "AWS_LAMBDA_FUNCTION_MEMORY_SIZE", value = "128")
+ void testCreateWithMinimumMemorySize() {
+ Resource resource = LambdaResource.create();
+ Attributes attributes = resource.getAttributes();
+
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.longKey("faas.max_memory")))
+ .isEqualTo(128L);
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "AWS_LAMBDA_FUNCTION_MEMORY_SIZE", value = "10240")
+ void testCreateWithMaximumMemorySize() {
+ Resource resource = LambdaResource.create();
+ Attributes attributes = resource.getAttributes();
+
+ assertThat(attributes.get(io.opentelemetry.api.common.AttributeKey.longKey("faas.max_memory")))
+ .isEqualTo(10240L);
+ }
+}
\ 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
index e0710f765..80db83c30 100644
--- 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
@@ -1,45 +1,156 @@
+/*
+ * 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 static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.mockito.Mockito.mock;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.context.Scope;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+@ExtendWith(MockitoExtension.class)
class SpanScopeTest {
+ @Mock
+ private Span mockSpan;
+
+ @Mock
+ private Scope mockScope;
+
+ @BeforeEach
+ void setUp() {
+ when(mockSpan.makeCurrent()).thenReturn(mockScope);
+ }
+
@Test
- void span_returnsCurrentSpan() {
+ void span_shouldReturnCurrentSpan() {
- 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.");
+ assertThat(result).isEqualTo(mockSpan);
+ }
+
+ @Test
+ void constructor_shouldMakeSpanCurrent() {
+ new SpanScope(mockSpan);
+
+ verify(mockSpan).makeCurrent();
+ }
+
+ @Test
+ void setStatus_shouldSetSpanStatus() {
+ SpanScope spanScope = new SpanScope(mockSpan);
+ StatusCode status = StatusCode.OK;
+
+ SpanScope result = spanScope.setStatus(status);
+
+ verify(mockSpan).setStatus(status);
+ assertThat(result).isSameAs(spanScope);
+ }
+
+ @Test
+ void setStatus_shouldAllowMethodChaining() {
+ SpanScope spanScope = new SpanScope(mockSpan);
+
+ SpanScope result = spanScope
+ .setStatus(StatusCode.OK)
+ .setStatus(StatusCode.ERROR);
+
+ assertThat(result).isSameAs(spanScope);
+ verify(mockSpan).setStatus(StatusCode.OK);
+ verify(mockSpan).setStatus(StatusCode.ERROR);
+ }
+
+ @Test
+ void addEvent_shouldAddEventWithName() {
+ SpanScope spanScope = new SpanScope(mockSpan);
+ String eventName = "test-event";
+
+ SpanScope result = spanScope.addEvent(eventName);
+
+ verify(mockSpan).addEvent(eventName);
+ assertThat(result).isSameAs(spanScope);
}
@Test
- void recordException_recordsThrowableAndSetsErrorStatus() {
- Span mockSpan = mock(Span.class);
+ void addEvent_shouldAddEventWithNameAndAttributes() {
+
+ SpanScope spanScope = new SpanScope(mockSpan);
+ String eventName = "test-event";
+ Attributes attributes = Attributes.builder()
+ .put("key1", "value1")
+ .put("key2", 42L)
+ .build();
+
+ SpanScope result = spanScope.addEvent(eventName, attributes);
+
+ verify(mockSpan).addEvent(eventName, attributes);
+ assertThat(result).isSameAs(spanScope);
+ }
+
+ @Test
+ void addEvent_shouldAllowMethodChaining() {
+
+ SpanScope spanScope = new SpanScope(mockSpan);
+ Attributes attrs = Attributes.empty();
+
+ SpanScope result = spanScope
+ .addEvent("event1")
+ .addEvent("event2", attrs);
+
+ assertThat(result).isSameAs(spanScope);
+ verify(mockSpan).addEvent("event1");
+ verify(mockSpan).addEvent("event2", attrs);
+ }
+
+ @Test
+ void recordException_shouldRecordExceptionAndSetErrorStatus() {
+
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);
+ verify(mockSpan).setStatus(StatusCode.ERROR);
}
@Test
- void close_closesScopeAndEndsSpan() {
+ void recordException_shouldHandleDifferentExceptionTypes() {
- Span mockSpan = mock(Span.class);
- Scope mockScope = mock(Scope.class);
- when(mockSpan.makeCurrent()).thenReturn(mockScope);
+ SpanScope spanScope = new SpanScope(mockSpan);
+ IllegalArgumentException exception = new IllegalArgumentException("Invalid argument");
+
+ spanScope.recordException(exception);
+
+ verify(mockSpan).recordException(exception);
+ verify(mockSpan).setStatus(StatusCode.ERROR);
+ }
+
+ @Test
+ void close_shouldCloseScopeAndEndSpan() {
SpanScope spanScope = new SpanScope(mockSpan);
@@ -48,4 +159,49 @@ void close_closesScopeAndEndsSpan() {
verify(mockScope).close();
verify(mockSpan).end();
}
+
+ @Test
+ void spanScope_shouldWorkWithTryWithResources() {
+
+ try (SpanScope spanScope = new SpanScope(mockSpan)) {
+ spanScope.addEvent("test-event");
+ }
+
+ verify(mockSpan).addEvent("test-event");
+ verify(mockScope).close();
+ verify(mockSpan).end();
+ }
+
+ @Test
+ void spanScope_shouldSupportFluentAPI() {
+
+ SpanScope spanScope = new SpanScope(mockSpan);
+ Attributes attrs = Attributes.builder()
+ .put("error.type", "validation")
+ .build();
+
+ spanScope
+ .addEvent("validation-started")
+ .setStatus(StatusCode.ERROR)
+ .addEvent("validation-failed", attrs);
+
+ verify(mockSpan).addEvent("validation-started");
+ verify(mockSpan).setStatus(StatusCode.ERROR);
+ verify(mockSpan).addEvent("validation-failed", attrs);
+ }
+
+ @Test
+ void spanScope_shouldAllowExceptionRecordingInTryWithResources() {
+
+ RuntimeException exception = new RuntimeException("Test failure");
+
+ try (SpanScope spanScope = new SpanScope(mockSpan)) {
+ spanScope.recordException(exception);
+ }
+
+ verify(mockSpan).recordException(exception);
+ verify(mockSpan).setStatus(StatusCode.ERROR);
+ 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
index 6414833aa..76364400b 100644
--- 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
@@ -1,105 +1,517 @@
+/*
+ * 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 static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.RequestHandler;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanContext;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.context.propagation.TextMapPropagator;
+import java.util.Collections;
+import java.util.List;
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 org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.junit.jupiter.MockitoExtension;
+import software.amazon.lambda.powertools.tracing.opentelemetry.CaptureMode;
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.LambdaEventContextExtractorResolver;
+import software.amazon.lambda.powertools.tracing.opentelemetry.context.TraceContextPropagationMode;
+import software.amazon.lambda.powertools.tracing.opentelemetry.provider.OpenTelemetryProvider;
+@ExtendWith(MockitoExtension.class)
class TracingOpenTelemetryAspectTest {
+ @Mock
private ProceedingJoinPoint pjp;
+
+ @Mock
private Tracing tracing;
+
+ @Mock
private TracingOpenTelemetry tracingOpenTelemetry;
+
+ @Mock
private SpanScope spanScope;
+
+ @Mock
+ private Span span;
+
+ @Mock
private Signature signature;
+
+ @Mock
+ private Context lambdaContext;
+
+ @Mock
+ private LambdaEventContextExtractorResolver extractorResolver;
+
+ @Mock
+ private TextMapPropagator propagator;
+
+ private TracingOpenTelemetryAspect aspect;
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);
+ aspect = new TracingOpenTelemetryAspect();
originalTracing = (TracingOpenTelemetry) FieldUtils
- .readStaticField(TracingOpenTelemetryAspect.class, "tracing", true);
+ .readStaticField(TracingOpenTelemetryAspect.class, "tracingOtel", true);
- FieldUtils.writeStaticField(TracingOpenTelemetryAspect.class, "tracing", tracingOpenTelemetry, true);
+ FieldUtils.writeStaticField(TracingOpenTelemetryAspect.class, "tracingOtel", tracingOpenTelemetry, true);
+
+ lenient().when(tracingOpenTelemetry.eventContextExtractorResolver()).thenReturn(extractorResolver);
+ lenient().when(tracingOpenTelemetry.propagator()).thenReturn(propagator);
+ lenient().when(spanScope.span()).thenReturn(span);
}
@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");
-// }
+ FieldUtils.writeStaticField(TracingOpenTelemetryAspect.class, "tracingOtel", originalTracing, true);
+ }
+
+ @Test
+ void shouldTraceHandlerMethodSuccessfully() throws Throwable {
+
+ setupHandlerMethod();
+ Object event = new Object();
+ Object[] args = {event, lambdaContext};
+ Object expectedResult = "result";
+
+ when(pjp.getArgs()).thenReturn(args);
+ when(tracing.spanName()).thenReturn("testHandler");
+ when(tracing.captureMode()).thenReturn(CaptureMode.DISABLED);
+
+ ExtractedTraceContext extractedContext = new ExtractedTraceContext(
+ io.opentelemetry.context.Context.current(),
+ Collections.emptyList(),
+ SpanKind.SERVER
+ );
+
+ when(extractorResolver.extract(any(), any(), any())).thenReturn(extractedContext);
+ when(tracingOpenTelemetry.addSpan(anyString(), any(SpanKind.class), any(Attributes.class),
+ any(io.opentelemetry.context.Context.class)))
+ .thenReturn(spanScope);
+ when(pjp.proceed(args)).thenReturn(expectedResult);
+
+ Object result = aspect.around(pjp, tracing);
+
+ assertThat(result).isEqualTo(expectedResult);
+ verify(tracingOpenTelemetry).addSpan(eq("testHandler"), eq(SpanKind.SERVER), any(Attributes.class),
+ any(io.opentelemetry.context.Context.class));
+ verify(extractorResolver).enrichSpan(event, span);
+ verify(tracingOpenTelemetry).flush();
+ }
+
+ @Test
+ void shouldUseMethodNameWhenSpanNameIsEmpty() throws Throwable {
+
+ setupHandlerMethod();
+ Object[] args = {new Object(), lambdaContext};
+ when(pjp.getArgs()).thenReturn(args);
+ when(tracing.spanName()).thenReturn("");
+ when(tracing.captureMode()).thenReturn(CaptureMode.DISABLED);
+
+ ExtractedTraceContext extractedContext = new ExtractedTraceContext(
+ io.opentelemetry.context.Context.current(),
+ Collections.emptyList(),
+ SpanKind.SERVER
+ );
+
+ when(extractorResolver.extract(any(), any(), any())).thenReturn(extractedContext);
+ when(tracingOpenTelemetry.addSpan(anyString(), any(SpanKind.class), any(Attributes.class),
+ any(io.opentelemetry.context.Context.class)))
+ .thenReturn(spanScope);
+ when(pjp.proceed(args)).thenReturn("result");
+
+ aspect.around(pjp, tracing);
+
+ verify(tracingOpenTelemetry).addSpan(eq("handleRequest"), any(SpanKind.class), any(Attributes.class),
+ any(io.opentelemetry.context.Context.class));
+ }
+
+ @Test
+ void shouldCaptureExceptionInHandler() throws Throwable {
+
+ setupHandlerMethod();
+ Object[] args = {new Object(), lambdaContext};
+ RuntimeException expectedException = new RuntimeException("Test exception");
+
+ when(pjp.getArgs()).thenReturn(args);
+ when(tracing.spanName()).thenReturn("testHandler");
+ when(tracing.captureMode()).thenReturn(CaptureMode.ERROR);
+
+ ExtractedTraceContext extractedContext = new ExtractedTraceContext(
+ io.opentelemetry.context.Context.current(),
+ Collections.emptyList(),
+ SpanKind.SERVER
+ );
+
+ when(extractorResolver.extract(any(), any(), any())).thenReturn(extractedContext);
+ when(tracingOpenTelemetry.addSpan(anyString(), any(SpanKind.class), any(Attributes.class),
+ any(io.opentelemetry.context.Context.class)))
+ .thenReturn(spanScope);
+ when(pjp.proceed(args)).thenThrow(expectedException);
+
+ assertThatThrownBy(() -> aspect.around(pjp, tracing))
+ .isEqualTo(expectedException);
+
+ verify(spanScope).recordException(expectedException);
+ verify(tracingOpenTelemetry).flush();
+ }
+
+ @Test
+ void shouldCaptureResponseWhenModeIsResponse() throws Throwable {
+
+ setupHandlerMethod();
+ Object[] args = {new Object(), lambdaContext};
+ String expectedResult = "test-response";
+
+ when(pjp.getArgs()).thenReturn(args);
+ when(tracing.spanName()).thenReturn("testHandler");
+ when(tracing.captureMode()).thenReturn(CaptureMode.RESPONSE);
+
+ ExtractedTraceContext extractedContext = new ExtractedTraceContext(
+ io.opentelemetry.context.Context.current(),
+ Collections.emptyList(),
+ SpanKind.SERVER
+ );
+
+ when(extractorResolver.extract(any(), any(), any())).thenReturn(extractedContext);
+ when(tracingOpenTelemetry.addSpan(anyString(), any(SpanKind.class), any(Attributes.class),
+ any(io.opentelemetry.context.Context.class)))
+ .thenReturn(spanScope);
+ when(pjp.proceed(args)).thenReturn(expectedResult);
+
+ aspect.around(pjp, tracing);
+
+ verify(span).setAttribute(eq(AttributesConstants.RESPONSE_ATTRIBUTE), anyString());
+ }
+
+ @Test
+ void shouldCaptureResponseAndErrorWhenModeIsResponseAndError() throws Throwable {
+
+ setupHandlerMethod();
+ Object[] args = {new Object(), lambdaContext};
+ RuntimeException exception = new RuntimeException("error");
+
+ when(pjp.getArgs()).thenReturn(args);
+ when(tracing.spanName()).thenReturn("testHandler");
+ when(tracing.captureMode()).thenReturn(CaptureMode.RESPONSE_AND_ERROR);
+
+ ExtractedTraceContext extractedContext = new ExtractedTraceContext(
+ io.opentelemetry.context.Context.current(),
+ Collections.emptyList(),
+ SpanKind.SERVER
+ );
+
+ when(extractorResolver.extract(any(), any(), any())).thenReturn(extractedContext);
+ when(tracingOpenTelemetry.addSpan(anyString(), any(SpanKind.class), any(Attributes.class),
+ any(io.opentelemetry.context.Context.class)))
+ .thenReturn(spanScope);
+ when(pjp.proceed(args)).thenThrow(exception);
+
+ assertThatThrownBy(() -> aspect.around(pjp, tracing))
+ .isEqualTo(exception);
+
+ verify(spanScope).recordException(exception);
+ }
+
+ @Test
+ void shouldNotCaptureWhenModeIsDisabled() throws Throwable {
+
+ setupHandlerMethod();
+ Object[] args = {new Object(), lambdaContext};
+ RuntimeException exception = new RuntimeException("error");
+
+ when(pjp.getArgs()).thenReturn(args);
+ when(tracing.spanName()).thenReturn("testHandler");
+ when(tracing.captureMode()).thenReturn(CaptureMode.DISABLED);
+
+ ExtractedTraceContext extractedContext = new ExtractedTraceContext(
+ io.opentelemetry.context.Context.current(),
+ Collections.emptyList(),
+ SpanKind.SERVER
+ );
+
+ when(extractorResolver.extract(any(), any(), any())).thenReturn(extractedContext);
+ when(tracingOpenTelemetry.addSpan(anyString(), any(SpanKind.class), any(Attributes.class),
+ any(io.opentelemetry.context.Context.class)))
+ .thenReturn(spanScope);
+ when(pjp.proceed(args)).thenThrow(exception);
+
+ assertThatThrownBy(() -> aspect.around(pjp, tracing))
+ .isEqualTo(exception);
+
+ verify(spanScope, never()).recordException(any());
+ verify(span, never()).setAttribute(eq(AttributesConstants.RESPONSE_ATTRIBUTE), anyString());
+ }
+
+ @Test
+ void shouldNotCaptureResponseWhenModeIsError() throws Throwable {
+
+ setupHandlerMethod();
+ Object[] args = {new Object(), lambdaContext};
+
+ when(pjp.getArgs()).thenReturn(args);
+ when(tracing.spanName()).thenReturn("testHandler");
+ when(tracing.captureMode()).thenReturn(CaptureMode.ERROR);
+
+ ExtractedTraceContext extractedContext = new ExtractedTraceContext(
+ io.opentelemetry.context.Context.current(),
+ Collections.emptyList(),
+ SpanKind.SERVER
+ );
+
+ when(extractorResolver.extract(any(), any(), any())).thenReturn(extractedContext);
+ when(tracingOpenTelemetry.addSpan(anyString(), any(SpanKind.class), any(Attributes.class),
+ any(io.opentelemetry.context.Context.class)))
+ .thenReturn(spanScope);
+ when(pjp.proceed(args)).thenReturn("result");
+
+ aspect.around(pjp, tracing);
+
+ verify(span, never()).setAttribute(eq(AttributesConstants.RESPONSE_ATTRIBUTE), anyString());
+ }
+
+ @Test
+ void shouldTraceNonHandlerMethodSuccessfully() throws Throwable {
+
+ setupNonHandlerMethod();
+ Object[] args = {"arg1", "arg2"};
+ Object expectedResult = "result";
+
+ when(pjp.getArgs()).thenReturn(args);
+ when(tracing.spanName()).thenReturn("testMethod");
+ when(tracing.captureMode()).thenReturn(CaptureMode.DISABLED);
+ when(tracingOpenTelemetry.addSpan(anyString(), eq(SpanKind.INTERNAL), any(Attributes.class),
+ any(io.opentelemetry.context.Context.class)))
+ .thenReturn(spanScope);
+ when(pjp.proceed(args)).thenReturn(expectedResult);
+
+ Object result = aspect.around(pjp, tracing);
+
+ assertThat(result).isEqualTo(expectedResult);
+ verify(tracingOpenTelemetry).addSpan(eq("testMethod"), eq(SpanKind.INTERNAL), any(Attributes.class),
+ any(io.opentelemetry.context.Context.class));
+ verify(extractorResolver, never()).enrichSpan(any(), any());
+ verify(tracingOpenTelemetry, never()).flush();
+ }
+
+ @Test
+ void shouldCaptureExceptionInNonHandlerMethod() throws Throwable {
+
+ setupNonHandlerMethod();
+ Object[] args = {"arg1"};
+ RuntimeException expectedException = new RuntimeException("Method exception");
+
+ when(pjp.getArgs()).thenReturn(args);
+ when(tracing.spanName()).thenReturn("testMethod");
+ when(tracing.captureMode()).thenReturn(CaptureMode.ERROR);
+ when(tracingOpenTelemetry.addSpan(anyString(), eq(SpanKind.INTERNAL), any(Attributes.class),
+ any(io.opentelemetry.context.Context.class)))
+ .thenReturn(spanScope);
+ when(pjp.proceed(args)).thenThrow(expectedException);
+
+ assertThatThrownBy(() -> aspect.around(pjp, tracing))
+ .isEqualTo(expectedException);
+
+ verify(spanScope).recordException(expectedException);
+ }
+
+ @Test
+ void shouldCaptureResponseInNonHandlerMethod() throws Throwable {
+
+ setupNonHandlerMethod();
+ Object[] args = {};
+ String expectedResult = "method-result";
+
+ when(pjp.getArgs()).thenReturn(args);
+ when(tracing.spanName()).thenReturn("testMethod");
+ when(tracing.captureMode()).thenReturn(CaptureMode.RESPONSE);
+ when(tracingOpenTelemetry.addSpan(anyString(), eq(SpanKind.INTERNAL), any(Attributes.class),
+ any(io.opentelemetry.context.Context.class)))
+ .thenReturn(spanScope);
+ when(pjp.proceed(args)).thenReturn(expectedResult);
+
+ aspect.around(pjp, tracing);
+
+ verify(span).setAttribute(eq(AttributesConstants.RESPONSE_ATTRIBUTE), anyString());
+ }
+
+ @Test
+ void shouldNotCaptureErrorInNonHandlerMethodWhenModeIsResponse() throws Throwable {
+
+ setupNonHandlerMethod();
+ Object[] args = {};
+ RuntimeException exception = new RuntimeException("error");
+
+ when(pjp.getArgs()).thenReturn(args);
+ when(tracing.spanName()).thenReturn("testMethod");
+ when(tracing.captureMode()).thenReturn(CaptureMode.RESPONSE);
+ when(tracingOpenTelemetry.addSpan(anyString(), eq(SpanKind.INTERNAL), any(Attributes.class),
+ any(io.opentelemetry.context.Context.class)))
+ .thenReturn(spanScope);
+ when(pjp.proceed(args)).thenThrow(exception);
+
+ assertThatThrownBy(() -> aspect.around(pjp, tracing))
+ .isEqualTo(exception);
+
+ verify(spanScope, never()).recordException(any());
+ }
+
+
+ @Test
+ void shouldConfigureCustomTracingOpenTelemetry() throws IllegalAccessException {
+
+ TracingOpenTelemetry customTracing = mock(TracingOpenTelemetry.class);
+
+ TracingOpenTelemetryAspect.configure(customTracing);
+
+ TracingOpenTelemetry configured = (TracingOpenTelemetry) FieldUtils
+ .readStaticField(TracingOpenTelemetryAspect.class, "tracingOtel", true);
+ assertThat(configured).isEqualTo(customTracing);
+ }
+
+ @Test
+ void shouldThrowNullPointerExceptionWhenConfiguringNull() {
+ assertThatThrownBy(() -> TracingOpenTelemetryAspect.configure(null))
+ .isInstanceOf(NullPointerException.class);
+ }
+
+
+ @Test
+ void shouldUseSpanLinksWhenPropagationModeIsLink() throws Throwable {
+
+ when(pjp.getSignature()).thenReturn(signature);
+ when(signature.getDeclaringType()).thenReturn(RequestHandler.class);
+
+ Object[] args = {new Object(), lambdaContext};
+ SpanContext spanContext = mock(SpanContext.class);
+ List spanContexts = List.of(spanContext);
+
+ when(pjp.getArgs()).thenReturn(args);
+ when(tracing.spanName()).thenReturn("testHandler");
+ when(tracing.captureMode()).thenReturn(CaptureMode.DISABLED);
+
+ ExtractedTraceContext extractedContext = new ExtractedTraceContext(
+ io.opentelemetry.context.Context.current(),
+ spanContexts,
+ SpanKind.SERVER
+ );
+
+ when(extractorResolver.extract(any(), any(), any())).thenReturn(extractedContext);
+
+ try (MockedStatic mockedProvider = mockStatic(OpenTelemetryProvider.class)) {
+ mockedProvider.when(OpenTelemetryProvider::traceContextPropagationMode)
+ .thenReturn(TraceContextPropagationMode.LINK);
+
+ when(tracingOpenTelemetry.addSpan(
+ anyString(),
+ any(SpanKind.class),
+ any(Attributes.class),
+ any(io.opentelemetry.context.Context.class),
+ any(List.class)))
+ .thenReturn(spanScope);
+
+ when(pjp.proceed(args)).thenReturn("result");
+
+ aspect.around(pjp, tracing);
+
+ verify(tracingOpenTelemetry, times(1)).addSpan(
+ eq("testHandler"),
+ eq(SpanKind.SERVER),
+ any(Attributes.class),
+ any(io.opentelemetry.context.Context.class),
+ eq(spanContexts)
+ );
+ }
+ }
+
+ @Test
+ void shouldUseParentContextWhenSpanContextsAreEmpty() throws Throwable {
+
+ setupHandlerMethod();
+ Object[] args = {new Object(), lambdaContext};
+
+ when(pjp.getArgs()).thenReturn(args);
+ when(tracing.spanName()).thenReturn("testHandler");
+ when(tracing.captureMode()).thenReturn(CaptureMode.DISABLED);
+
+ ExtractedTraceContext extractedContext = new ExtractedTraceContext(
+ io.opentelemetry.context.Context.current(),
+ Collections.emptyList(),
+ SpanKind.SERVER
+ );
+
+ when(extractorResolver.extract(any(), any(), any())).thenReturn(extractedContext);
+ when(tracingOpenTelemetry.addSpan(anyString(), any(SpanKind.class), any(Attributes.class),
+ any(io.opentelemetry.context.Context.class)))
+ .thenReturn(spanScope);
+ when(pjp.proceed(args)).thenReturn("result");
+
+ aspect.around(pjp, tracing);
+
+ verify(tracingOpenTelemetry).addSpan(
+ eq("testHandler"),
+ eq(SpanKind.SERVER),
+ any(Attributes.class),
+ any(io.opentelemetry.context.Context.class)
+ );
+ verify(tracingOpenTelemetry, never()).addSpan(
+ anyString(),
+ any(SpanKind.class),
+ any(Attributes.class),
+ any(io.opentelemetry.context.Context.class),
+ any(List.class)
+ );
+ }
+
+ private void setupHandlerMethod() {
+ lenient().when(pjp.getSignature()).thenReturn(signature);
+ lenient().when(signature.getDeclaringType()).thenReturn(RequestHandler.class);
+ lenient().when(signature.getName()).thenReturn("handleRequest");
+ }
+
+ private void setupNonHandlerMethod() {
+ lenient().when(pjp.getSignature()).thenReturn(signature);
+ lenient().when(signature.getDeclaringType()).thenReturn(TracingOpenTelemetryAspectTest.class);
+ lenient().when(signature.getName()).thenReturn("someMethod");
+ }
}
\ No newline at end of file
diff --git a/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/provider/OpenTelemetryProviderTest.java b/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/provider/OpenTelemetryProviderTest.java
index 014355adf..d13ba9c81 100644
--- a/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/provider/OpenTelemetryProviderTest.java
+++ b/powertools-tracing-opentelemetry/src/test/java/software/amazon/lambda/powertools/tracing/opentelemetry/provider/OpenTelemetryProviderTest.java
@@ -1,51 +1,361 @@
+/*
+ * 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.provider;
import static org.assertj.core.api.Assertions.assertThat;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.propagation.TextMapGetter;
+import io.opentelemetry.context.propagation.TextMapPropagator;
+import io.opentelemetry.sdk.common.CompletableResultCode;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.Map;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junitpioneer.jupiter.SetEnvironmentVariable;
import software.amazon.lambda.powertools.tracing.opentelemetry.context.TraceContextPropagationMode;
class OpenTelemetryProviderTest {
+ @BeforeEach
+ @AfterEach
+ void cleanup() {
+ System.clearProperty("otel.traces.exporter");
+ }
+
@Test
void shouldProvideTextMapGetter() {
TextMapGetter> getter = OpenTelemetryProvider.textMapGetter();
assertThat(getter).isNotNull();
+ }
+
+ @Test
+ void shouldGetValueFromCarrierWithTextMapGetter() {
+ TextMapGetter> getter = OpenTelemetryProvider.textMapGetter();
Map carrier = Map.of("key1", "value1", "key2", "value2");
+
assertThat(getter.get(carrier, "key1")).isEqualTo("value1");
+ assertThat(getter.get(carrier, "key2")).isEqualTo("value2");
+ }
+
+ @Test
+ void shouldReturnNullWhenKeyNotFoundInCarrier() {
+ TextMapGetter> getter = OpenTelemetryProvider.textMapGetter();
+
+ Map carrier = Map.of("key1", "value1");
+
assertThat(getter.get(carrier, "unknown")).isNull();
+ assertThat(getter.get(carrier, "")).isNull();
+ }
+
+ @Test
+ void shouldReturnNullWhenCarrierIsNull() {
+ TextMapGetter> getter = OpenTelemetryProvider.textMapGetter();
+
assertThat(getter.get(null, "key1")).isNull();
- assertThat(getter.keys(carrier)).containsExactlyInAnyOrder("key1", "key2");
+ assertThat(getter.get(null, "anyKey")).isNull();
+ }
+
+ @Test
+ void shouldReturnAllKeysFromCarrier() {
+ TextMapGetter> getter = OpenTelemetryProvider.textMapGetter();
+
+ Map carrier = Map.of("key1", "value1", "key2", "value2", "key3", "value3");
+
+ assertThat(getter.keys(carrier)).containsExactlyInAnyOrder("key1", "key2", "key3");
+ }
+
+ @Test
+ void shouldReturnEmptyKeysWhenCarrierIsNull() {
+ TextMapGetter> getter = OpenTelemetryProvider.textMapGetter();
+
assertThat(getter.keys(null)).isEmpty();
}
+ @Test
+ void shouldReturnEmptyKeysWhenCarrierIsEmpty() {
+ TextMapGetter> getter = OpenTelemetryProvider.textMapGetter();
+
+ assertThat(getter.keys(Collections.emptyMap())).isEmpty();
+ }
+
+ @Test
+ void shouldHandleMutableCarrier() {
+ TextMapGetter> getter = OpenTelemetryProvider.textMapGetter();
+
+ Map carrier = new HashMap<>();
+ carrier.put("key1", "value1");
+ carrier.put("key2", "value2");
+
+ assertThat(getter.get(carrier, "key1")).isEqualTo("value1");
+ assertThat(getter.keys(carrier)).containsExactlyInAnyOrder("key1", "key2");
+ }
+
@Test
void shouldProvidePropagator() {
- assertThat(OpenTelemetryProvider.propagator()).isNotNull();
+ TextMapPropagator propagator = OpenTelemetryProvider.propagator();
+
+ assertThat(propagator).isNotNull();
+ }
+
+ @Test
+ void shouldReturnSamePropagatorInstance() {
+ TextMapPropagator propagator1 = OpenTelemetryProvider.propagator();
+ TextMapPropagator propagator2 = OpenTelemetryProvider.propagator();
+
+ assertThat(propagator1).isSameAs(propagator2);
+ }
+
+ @Test
+ void shouldPropagatorHaveFields() {
+ TextMapPropagator propagator = OpenTelemetryProvider.propagator();
+
+ assertThat(propagator.fields()).isNotEmpty();
+ assertThat(propagator.fields()).contains("traceparent");
}
@Test
void shouldProvideTracer() {
- assertThat(OpenTelemetryProvider.tracer()).isNotNull();
+ Tracer tracer = OpenTelemetryProvider.tracer();
+
+ assertThat(tracer).isNotNull();
+ }
+
+ @Test
+ void shouldReturnSameTracerInstance() {
+ Tracer tracer1 = OpenTelemetryProvider.tracer();
+ Tracer tracer2 = OpenTelemetryProvider.tracer();
+
+ assertThat(tracer1).isSameAs(tracer2);
+ }
+
+ @Test
+ void shouldTracerCreateSpans() {
+ Tracer tracer = OpenTelemetryProvider.tracer();
+
+ assertThat(tracer.spanBuilder("test-span")).isNotNull();
}
@Test
void shouldProvideObjectMapper() {
- assertThat(OpenTelemetryProvider.objectMapper()).isNotNull();
+ ObjectMapper objectMapper = OpenTelemetryProvider.objectMapper();
+
+ assertThat(objectMapper).isNotNull();
}
@Test
- void shouldProvideTraceContextPropagationMode() {
- assertThat(OpenTelemetryProvider.traceContextPropagationMode())
- .isEqualTo(TraceContextPropagationMode.PARENT);
+ void shouldReturnSameObjectMapperInstance() {
+ ObjectMapper mapper1 = OpenTelemetryProvider.objectMapper();
+ ObjectMapper mapper2 = OpenTelemetryProvider.objectMapper();
+
+ assertThat(mapper1).isSameAs(mapper2);
+ }
+
+ @Test
+ void shouldObjectMapperBeUsable() throws Exception {
+ ObjectMapper mapper = OpenTelemetryProvider.objectMapper();
+
+ String json = mapper.writeValueAsString(Map.of("key", "value"));
+ assertThat(json).contains("key").contains("value");
+
+ Map parsed = mapper.readValue(json, Map.class);
+ assertThat(parsed).containsEntry("key", "value");
+ }
+
+ @Test
+ void shouldProvideDefaultTraceContextPropagationMode() {
+ TraceContextPropagationMode mode = OpenTelemetryProvider.traceContextPropagationMode();
+
+ assertThat(mode).isEqualTo(TraceContextPropagationMode.PARENT);
+ }
+
+ @Test
+ void shouldReturnSameTraceContextPropagationModeInstance() {
+ TraceContextPropagationMode mode1 = OpenTelemetryProvider.traceContextPropagationMode();
+ TraceContextPropagationMode mode2 = OpenTelemetryProvider.traceContextPropagationMode();
+
+ assertThat(mode1).isEqualTo(mode2);
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "POWERTOOLS_TRACE_CONTEXT_PROPAGATION_MODE", value = "LINK")
+ void shouldReadTraceContextPropagationModeFromEnvironment() {
+ assertThat(TraceContextPropagationMode.LINK).isNotNull();
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "POWERTOOLS_TRACE_CONTEXT_PROPAGATION_MODE", value = "PARENT")
+ void shouldHandleParentModeFromEnvironment() {
+ assertThat(TraceContextPropagationMode.PARENT).isNotNull();
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "POWERTOOLS_TRACE_CONTEXT_PROPAGATION_MODE", value = "invalid")
+ void shouldDefaultToParentOnInvalidEnvironmentValue() {
+ assertThat(TraceContextPropagationMode.PARENT).isNotNull();
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "POWERTOOLS_TRACE_CONTEXT_PROPAGATION_MODE", value = "")
+ void shouldDefaultToParentOnEmptyEnvironmentValue() {
+ assertThat(TraceContextPropagationMode.PARENT).isNotNull();
}
@Test
void shouldForceFlush() {
+ CompletableResultCode result = OpenTelemetryProvider.forceFlush();
+
+ assertThat(result).isNotNull();
+ }
+
+ @Test
+ void shouldForceFlushComplete() {
+ CompletableResultCode result = OpenTelemetryProvider.forceFlush();
+
+ assertThat(result.join(5000, java.util.concurrent.TimeUnit.MILLISECONDS).isSuccess()).isTrue();
+ }
+
+
+ @Test
+ @SetEnvironmentVariable(key = "OTEL_EXPORTER_OTLP_TRACES_HEADERS", value = "key1=value1,key2=value2")
+ void shouldParseHeadersCorrectly() {
+ assertThat(OpenTelemetryProvider.tracer()).isNotNull();
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "OTEL_EXPORTER_OTLP_TRACES_HEADERS", value = "")
+ void shouldHandleEmptyHeaders() {
+ assertThat(OpenTelemetryProvider.tracer()).isNotNull();
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", value = "grpc")
+ void shouldUseGrpcProtocol() {
+ assertThat(OpenTelemetryProvider.tracer()).isNotNull();
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", value = "http/protobuf")
+ void shouldUseHttpProtobufProtocol() {
+ assertThat(OpenTelemetryProvider.tracer()).isNotNull();
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", value = "")
+ void shouldDefaultToGrpcWhenProtocolIsEmpty() {
+ assertThat(OpenTelemetryProvider.tracer()).isNotNull();
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", value = "http://localhost:4317")
+ void shouldUseCustomEndpoint() {
+ assertThat(OpenTelemetryProvider.tracer()).isNotNull();
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", value = "")
+ void shouldHandleEmptyEndpoint() {
+ assertThat(OpenTelemetryProvider.tracer()).isNotNull();
+ }
+
+
+ @Test
+ void shouldAllComponentsWorkTogether() {
+ assertThat(OpenTelemetryProvider.tracer()).isNotNull();
+ assertThat(OpenTelemetryProvider.propagator()).isNotNull();
+ assertThat(OpenTelemetryProvider.textMapGetter()).isNotNull();
+ assertThat(OpenTelemetryProvider.objectMapper()).isNotNull();
+ assertThat(OpenTelemetryProvider.traceContextPropagationMode()).isNotNull();
assertThat(OpenTelemetryProvider.forceFlush()).isNotNull();
}
-}
+
+ @Test
+ void shouldTracerAndPropagatorBeCompatible() {
+ Tracer tracer = OpenTelemetryProvider.tracer();
+ TextMapPropagator propagator = OpenTelemetryProvider.propagator();
+
+ assertThat(tracer.spanBuilder("test").startSpan()).isNotNull();
+ assertThat(propagator.fields()).isNotEmpty();
+ }
+
+ @Test
+ void shouldTextMapGetterWorkWithEmptyMap() {
+ TextMapGetter> getter = OpenTelemetryProvider.textMapGetter();
+ Map emptyMap = Collections.emptyMap();
+
+ assertThat(getter.keys(emptyMap)).isEmpty();
+ assertThat(getter.get(emptyMap, "anyKey")).isNull();
+ }
+
+ @Test
+ void shouldTextMapGetterWorkWithSingleEntry() {
+ TextMapGetter> getter = OpenTelemetryProvider.textMapGetter();
+ Map singleEntry = Map.of("singleKey", "singleValue");
+
+ assertThat(getter.keys(singleEntry)).containsExactly("singleKey");
+ assertThat(getter.get(singleEntry, "singleKey")).isEqualTo("singleValue");
+ }
+
+ @Test
+ void shouldTextMapGetterHandleSpecialCharacters() {
+ TextMapGetter> getter = OpenTelemetryProvider.textMapGetter();
+ Map carrier = Map.of(
+ "key-with-dash", "value1",
+ "key_with_underscore", "value2",
+ "key.with.dot", "value3"
+ );
+
+ assertThat(getter.get(carrier, "key-with-dash")).isEqualTo("value1");
+ assertThat(getter.get(carrier, "key_with_underscore")).isEqualTo("value2");
+ assertThat(getter.get(carrier, "key.with.dot")).isEqualTo("value3");
+ }
+
+ @Test
+ void shouldHandleNullSafely() {
+ TextMapGetter> getter = OpenTelemetryProvider.textMapGetter();
+
+ assertThat(getter.get(null, null)).isNull();
+ assertThat(getter.get(null, "key")).isNull();
+ assertThat(getter.keys(null)).isEmpty();
+ }
+
+ @Test
+ void shouldObjectMapperHandleComplexObjects() throws Exception {
+ ObjectMapper mapper = OpenTelemetryProvider.objectMapper();
+
+ Map complex = Map.of(
+ "string", "value",
+ "number", 123,
+ "nested", Map.of("inner", "data")
+ );
+
+ String json = mapper.writeValueAsString(complex);
+ assertThat(json).isNotNull();
+
+ Map parsed = mapper.readValue(json, Map.class);
+ assertThat(parsed).containsKeys("string", "number", "nested");
+ }
+
+ @Test
+ void shouldTracerNameBeCorrect() {
+ Tracer tracer = OpenTelemetryProvider.tracer();
+ assertThat(tracer.spanBuilder("test").startSpan().getSpanContext()).isNotNull();
+ }
+}
\ No newline at end of file
From 7030be60ee491836127d80116cfebc6d212158ef Mon Sep 17 00:00:00 2001
From: David-DAM <82216301+David-DAM@users.noreply.github.com>
Date: Wed, 7 Oct 2026 15:11:19 +0200
Subject: [PATCH 20/21] Add `TracingUserAgentInterceptor` to configure AWS SDK
clients with a custom User-Agent when using Powertools Tracing OpenTelemetry;
add initial documentation and usage guide.
---
docs/core/tracing-opentelemetry.md | 498 ++++++++++++++++++
.../internal/TracingUserAgentInterceptor.java | 38 ++
.../execution.interceptors | 1 +
3 files changed, 537 insertions(+)
create mode 100644 docs/core/tracing-opentelemetry.md
create mode 100644 powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/TracingUserAgentInterceptor.java
create mode 100644 powertools-tracing-opentelemetry/src/main/resources/software.amazon.awssdk.global.handlers/execution.interceptors
diff --git a/docs/core/tracing-opentelemetry.md b/docs/core/tracing-opentelemetry.md
new file mode 100644
index 000000000..582249d13
--- /dev/null
+++ b/docs/core/tracing-opentelemetry.md
@@ -0,0 +1,498 @@
+---
+title: Tracing Opentelemetry
+description: Core utility
+---
+
+The Tracing Opentelemetry utility is an opinionated thin wrapper for [AWS X-Ray Java SDK](https://github.com/aws/aws-xray-sdk-java/)
+ provides functionality to reduce the overhead of performing common tracing tasks.
+
+
+
+ **Key Features**
+
+ * Capture cold start as annotation, and responses as well as full exceptions as metadata
+ * Helper methods to improve the developer experience of creating new X-Ray subsegments.
+ * Better developer experience when developing with multiple threads.
+ * Auto patch supported modules by AWS X-Ray
+ * GraalVM support
+
+## Install
+
+=== "Maven"
+
+ ```xml hl_lines="3-7 25-28"
+
+ ...
+
+ software.amazon.lambda
+ powertools-tracing-opentelemetry
+ {{ powertools.version }}
+
+ ...
+
+ ...
+
+
+
+
+ ...
+
+ dev.aspectj
+ aspectj-maven-plugin
+ 1.14
+
+ 11
+ 11
+ 11
+
+
+ software.amazon.lambda
+ powertools-tracing-opentelemetry
+
+
+
+
+
+ org.aspectj
+ aspectjtools
+
+ 1.9.22
+
+
+
+
+
+ compile
+
+
+
+
+ ...
+
+
+ ```
+
+=== "Gradle"
+
+ ```groovy hl_lines="3 11 12"
+ plugins {
+ id 'java'
+ id 'io.freefair.aspectj.post-compile-weaving' version '8.1.0' // Not needed when using the functional approach
+ }
+
+ repositories {
+ mavenCentral()
+ }
+
+ dependencies {
+ aspect 'software.amazon.lambda:powertools-tracing-opentelemetry:{{ powertools.version }}' // Not needed when using the functional approach
+ implementation 'software.amazon.lambda:powertools-tracing-opentelemetry:{{ powertools.version }}' // Use this instead of 'aspect' when using the functional approach
+ }
+
+ sourceCompatibility = 11 // or higher
+ targetCompatibility = 11 // or higher
+ ```
+
+## Initialization
+
+Before your use this utility, your AWS Lambda function [must have permissions](https://docs.aws.amazon.com/lambda/latest/dg/services-xray.html#services-xray-permissions) to send traces to AWS X-Ray.
+
+> Example using AWS Serverless Application Model (SAM)
+
+=== "template.yaml"
+
+ ```yaml hl_lines="8 11"
+ Resources:
+ HelloWorldFunction:
+ Type: AWS::Serverless::Function
+ Properties:
+ ...
+ Runtime: java11
+
+ Tracing: Active
+ Environment:
+ Variables:
+ POWERTOOLS_SERVICE_NAME: example
+ ```
+
+The Powertools for AWS Lambda (Java) service name is used as the X-Ray namespace. This can be set using the environment variable
+`POWERTOOLS_SERVICE_NAME`
+
+### Lambda handler
+
+You can enable tracing using either the `@Tracing` annotation or the functional API.
+
+**With the `@Tracing` annotation**, add it to your `handleRequest` method or any method to capture it as a separate subsegment automatically. You can optionally customize the segment name that appears in traces.
+
+**With the functional API**, use `TracingUtils.withSubsegment()` to manually create subsegments without AspectJ configuration.
+
+=== "@Tracing annotation"
+
+ ```java hl_lines="3 10 15"
+ public class App implements RequestHandler {
+
+ @Tracing
+ public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
+ businessLogic1();
+
+ businessLogic2();
+ }
+
+ @Tracing
+ public void businessLogic1(){
+
+ }
+
+ @Tracing
+ public void businessLogic2(){
+
+ }
+ }
+ ```
+
+=== "Functional API"
+
+ ```java hl_lines="1 6 7 8 10 11 12"
+ import software.amazon.lambda.powertools.tracing.opentelemetry.TracingUtils;
+
+ public class App implements RequestHandler {
+
+ public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
+ TracingUtils.withSubsegment("businessLogic1", subsegment -> {
+ // Business logic 1
+ });
+
+ TracingUtils.withSubsegment("businessLogic2", subsegment -> {
+ // Business logic 2
+ });
+ }
+ }
+ ```
+
+=== "Custom Segment names"
+
+ ```java hl_lines="3"
+ public class App implements RequestHandler {
+
+ @Tracing(segmentName="yourCustomName")
+ public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
+ ...
+ }
+ ```
+
+When using the `@Tracing` annotation, the utility performs these additional tasks to ease operations:
+
+ * Creates a `ColdStart` annotation to easily filter traces that have had an initialization overhead.
+ * Creates a `Service` annotation if service parameter or `POWERTOOLS_SERVICE_NAME` is set.
+ * Captures any response, or full exceptions generated by the handler, and include as tracing metadata.
+
+By default, the `@Tracing` annotation uses `captureMode=ENVIRONMENT_VAR`, which means it will only record method responses and exceptions if you set
+the environment variables `POWERTOOLS_TRACER_CAPTURE_RESPONSE` and `POWERTOOLS_TRACER_CAPTURE_ERROR` to `true`. You can override this behavior by
+specifying a different `captureMode` to always record response, exception, both, or neither.
+
+!!! note
+ When using the functional API with `TracingUtils.withSubsegment()`, response and exception capture is not automatic. You can manually add metadata using `TracingUtils.putMetadata()` as needed.
+
+!!! warning "Returning sensitive information from your Lambda handler or functions?"
+ When using the `@Tracing` annotation, you can disable it from capturing responses and exceptions as tracing metadata with **`captureMode=DISABLED`**
+ or globally by setting the environment variables **`POWERTOOLS_TRACER_CAPTURE_RESPONSE`** and **`POWERTOOLS_TRACER_CAPTURE_ERROR`** to **`false`**.
+ When using the functional API, you have full control over what metadata is captured.
+
+=== "@Tracing annotation - Disable on method"
+
+ ```java hl_lines="3"
+ public class App implements RequestHandler {
+
+ @Tracing(captureMode=CaptureMode.DISABLED)
+ public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
+ ...
+ }
+ ```
+
+=== "@Tracing annotation - Disable Globally"
+
+ ```yaml hl_lines="11 12"
+ Resources:
+ HelloWorldFunction:
+ Type: AWS::Serverless::Function
+ Properties:
+ ...
+ Runtime: java11
+
+ Tracing: Active
+ Environment:
+ Variables:
+ POWERTOOLS_TRACER_CAPTURE_RESPONSE: false
+ POWERTOOLS_TRACER_CAPTURE_ERROR: false
+ ```
+
+=== "Functional API"
+
+ ```java hl_lines="6 7 8"
+ import software.amazon.lambda.powertools.tracing.TracingUtils;
+
+ public class App implements RequestHandler {
+
+ public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
+ TracingUtils.withSubsegment("businessLogic", subsegment -> {
+ // With functional API, you control what metadata is captured
+ });
+ }
+ ```
+
+### Annotations & Metadata
+
+**Annotations** are key-values associated with traces and indexed by AWS X-Ray. You can use them to filter traces and to
+create [Trace Groups](https://aws.amazon.com/about-aws/whats-new/2018/11/aws-xray-adds-the-ability-to-group-traces/) to slice and dice your transactions.
+
+**Metadata** are key-values also associated with traces but not indexed by AWS X-Ray. You can use them to add additional
+context for an operation using any native object.
+
+=== "Annotations"
+
+ You can add annotations using `putAnnotation()` method from TracingUtils
+ ```java hl_lines="8"
+ import software.amazon.lambda.powertools.tracing.Tracing;
+ import software.amazon.lambda.powertools.tracing.TracingUtils;
+
+ public class App implements RequestHandler {
+
+ @Tracing
+ public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
+ TracingUtils.putAnnotation("annotation", "value");
+ }
+ }
+ ```
+
+=== "Metadata"
+
+ You can add metadata using `putMetadata()` method from TracingUtils
+ ```java hl_lines="8"
+ import software.amazon.lambda.powertools.tracing.Tracing;
+ import software.amazon.lambda.powertools.tracing.TracingUtils;
+
+ public class App implements RequestHandler {
+
+ @Tracing
+ public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
+ TracingUtils.putMetadata("content", "value");
+ }
+ }
+ ```
+
+## Override default object mapper
+
+You can optionally choose to override default object mapper which is used to serialize method response and exceptions when enabled. You might
+want to supply custom object mapper in order to control how serialisation is done, for example, when you want to log only
+specific fields from received event due to security.
+
+=== "App.java"
+
+ ```java hl_lines="10-14"
+ import software.amazon.lambda.powertools.tracing.Tracing;
+ import software.amazon.lambda.powertools.tracing.TracingUtils;
+ import static software.amazon.lambda.powertools.tracing.CaptureMode.RESPONSE;
+
+ /**
+ * Handler for requests to Lambda function.
+ */
+ public class App implements RequestHandler {
+ static {
+ ObjectMapper objectMapper = new ObjectMapper();
+ SimpleModule simpleModule = new SimpleModule();
+ objectMapper.registerModule(simpleModule);
+
+ TracingUtils.defaultObjectMapper(objectMapper);
+ }
+
+ @Tracing(captureMode = RESPONSE)
+ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
+ ...
+ }
+ }
+ ```
+
+## Advanced usage
+
+### Multi-threaded programming
+
+When working with multiple threads, you need to pass the trace entity to ensure proper trace context propagation.
+
+=== "Multi-threaded example"
+
+ ```java hl_lines="7 9 10 11"
+ import static software.amazon.lambda.powertools.tracing.TracingUtils.withEntitySubsegment;
+
+ public class App implements RequestHandler {
+
+ public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
+ // Extract existing trace data
+ Entity traceEntity = AWSXRay.getTraceEntity();
+
+ Thread anotherThread = new Thread(() -> withEntitySubsegment("inlineLog", traceEntity, subsegment -> {
+ // Business logic in separate thread
+ }));
+ }
+ }
+ ```
+
+## Instrumenting SDK clients and HTTP calls
+
+### AWS SDK for Java 2.x
+
+Powertools for AWS Lambda (Java) includes the `aws-xray-recorder-sdk-aws-sdk-v2-instrumentor` library, which **automatically instruments all AWS SDK v2 clients** when you add the `powertools-tracing` dependency to your project. This means downstream calls to AWS services are traced without any additional configuration.
+
+If you need more control over which clients are instrumented, you can manually add the `TracingInterceptor` to specific clients:
+
+=== "Manual instrumentation (optional)"
+
+ ```java hl_lines="1 2 3 8 9 10 11"
+ import com.amazonaws.xray.interceptors.TracingInterceptor;
+ import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
+ import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
+
+ public class LambdaHandler {
+ private DynamoDbClient client = DynamoDbClient.builder()
+ .region(Region.US_WEST_2)
+ .overrideConfiguration(ClientOverrideConfiguration.builder()
+ .addExecutionInterceptor(new TracingInterceptor())
+ .build()
+ )
+ .build();
+ // ...
+ }
+ ```
+
+For more details, refer to the [AWS X-Ray documentation on tracing AWS SDK calls](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-java-awssdkclients.html) and [outgoing HTTP calls](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-java-httpclients.html).
+
+## Testing your code
+
+When using `@Tracing` annotation, your Junit test cases needs to be configured to create parent Segment required by [AWS X-Ray SDK for Java](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-java.html).
+
+Below are two ways in which you can configure your tests.
+
+#### Configure environment variable on project level (Recommended)
+
+You can choose to configure environment variable on project level for your test cases run. This is recommended approach as it will avoid the need of configuring each test case specifically.
+
+Below are examples configuring your maven/gradle projects. You can choose to configure it differently as well as long as you are making sure that environment variable `LAMBDA_TASK_ROOT` is set. This variable is
+used internally via AWS X-Ray SDK to configure itself properly for lambda runtime.
+
+=== "Maven (pom.xml)"
+
+ ```xml
+
+ ...
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ handler
+
+
+
+
+
+
+ ```
+
+=== "Gradle (build.gradle)"
+
+ ```json
+ // Configures environment variable to avoid initialization of AWS X-Ray segments for each tests
+ test {
+ environment "LAMBDA_TASK_ROOT", "handler"
+ }
+ ```
+
+#### Configure test cases (Not Recommended)
+
+You can choose to configure each of your test case instead as well if you choose not to configure environment variable on project level.
+Below is an example configuration needed for each test case.
+
+=== "AppTest.java"
+
+ ```java hl_lines="10 11 12 17 18 19 20 21 22 23 24"
+ import com.amazonaws.xray.AWSXRay;
+ import org.junit.After;
+ import org.junit.Before;
+ import org.junit.Test;
+
+ public class AppTest {
+
+ @Before
+ public void setup() {
+ if(null == System.getenv("LAMBDA_TASK_ROOT")) {
+ AWSXRay.beginSegment("test");
+ }
+ }
+
+ @After
+ public void tearDown() {
+ // Needed when using sam build --use-container
+ if (AWSXRay.getCurrentSubsegmentOptional().isPresent()) {
+ AWSXRay.endSubsegment();
+ }
+
+ if(null == System.getenv("LAMBDA_TASK_ROOT")) {
+ AWSXRay.endSegment();
+ }
+ }
+
+ @Test
+ public void successfulResponse() {
+ // test logic
+ }
+ ```
+## Advanced
+
+### Lambda SnapStart priming
+
+The Tracing utility integrates with AWS Lambda SnapStart to improve restore durations. To make sure the SnapStart priming logic of this utility runs correctly, you need an explicit reference to `TracingUtils` in your code to allow the library to register before SnapStart takes a memory snapshot. Learn more about what priming is in this [blog post](https://aws.amazon.com/blogs/compute/optimizing-cold-start-performance-of-aws-lambda-using-advanced-priming-strategies-with-snapstart/){target="_blank"}.
+
+If you don't set a custom `TracingUtils` in your code yet, make sure to reference `TracingUtils` in your Lambda handler initialization code. This can be done by adding one of the following lines to your handler class:
+
+=== "Constructor"
+
+ ```java hl_lines="7"
+ import software.amazon.lambda.powertools.validation.Validation;
+ import software.amazon.lambda.powertools.validation.ValidationConfig;
+
+ public class MyFunctionHandler implements RequestHandler {
+
+ public MyFunctionHandler() {
+ TracingUtils.init(); // Ensure TracingUtils is loaded for SnapStart
+ }
+
+ @Override
+ @Validation(inboundSchema = "classpath:/schema_in.json", outboundSchema = "classpath:/schema_out.json")
+ public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
+ // ...
+ return something;
+ }
+ }
+ ```
+
+=== "Static Initializer"
+
+ ```java hl_lines="7"
+ import software.amazon.lambda.powertools.validation.Validation;
+ import software.amazon.lambda.powertools.validation.ValidationConfig;
+
+ public class MyFunctionHandler implements RequestHandler {
+
+ static {
+ TracingUtils.init(); // Ensure TracingUtils is loaded for SnapStart
+ }
+
+ @Override
+ @Validation(inboundSchema = "classpath:/schema_in.json", outboundSchema = "classpath:/schema_out.json")
+ public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
+ // ...
+ return something;
+ }
+ }
+ ```
diff --git a/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/TracingUserAgentInterceptor.java b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/TracingUserAgentInterceptor.java
new file mode 100644
index 000000000..b2f58a699
--- /dev/null
+++ b/powertools-tracing-opentelemetry/src/main/java/software/amazon/lambda/powertools/tracing/opentelemetry/internal/TracingUserAgentInterceptor.java
@@ -0,0 +1,38 @@
+/*
+ * 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 software.amazon.awssdk.core.SdkRequest;
+import software.amazon.awssdk.core.interceptor.Context;
+import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
+import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
+import software.amazon.lambda.powertools.common.internal.UserAgentConfigurator;
+
+/**
+ * Global interceptor that configures the User-Agent for all AWS SDK clients
+ * when the powertools-tracing module is on the classpath.
+ */
+public final class TracingUserAgentInterceptor implements ExecutionInterceptor {
+ static {
+ UserAgentConfigurator.configureUserAgent("tracing-opentelemetry");
+ }
+
+ @Override
+ public SdkRequest modifyRequest(Context.ModifyRequest context, ExecutionAttributes executionAttributes) {
+ // This is a no-op interceptor. We use this class to configure the PT User-Agent in the static block. It is
+ // loaded by AWS SDK Global Interceptors.
+ return context.request();
+ }
+}
diff --git a/powertools-tracing-opentelemetry/src/main/resources/software.amazon.awssdk.global.handlers/execution.interceptors b/powertools-tracing-opentelemetry/src/main/resources/software.amazon.awssdk.global.handlers/execution.interceptors
new file mode 100644
index 000000000..58f3df538
--- /dev/null
+++ b/powertools-tracing-opentelemetry/src/main/resources/software.amazon.awssdk.global.handlers/execution.interceptors
@@ -0,0 +1 @@
+software.amazon.lambda.powertools.tracing.opentelemetry.internal.TracingUserAgentInterceptor
From 66c5985ffe21d6ffa7d468d818bf0f7b9448ce94 Mon Sep 17 00:00:00 2001
From: David-DAM <82216301+David-DAM@users.noreply.github.com>
Date: Tue, 13 Oct 2026 17:59:16 +0200
Subject: [PATCH 21/21] Update OpenTelemetry documentation to reflect new
features, including enhanced annotation handling, programmatic API support,
updated initialization steps, and compatibility improvements with Java 17.
---
docs/core/tracing-opentelemetry.md | 444 ++++++++++++++---------------
1 file changed, 222 insertions(+), 222 deletions(-)
diff --git a/docs/core/tracing-opentelemetry.md b/docs/core/tracing-opentelemetry.md
index 582249d13..df6a302dd 100644
--- a/docs/core/tracing-opentelemetry.md
+++ b/docs/core/tracing-opentelemetry.md
@@ -1,20 +1,22 @@
---
-title: Tracing Opentelemetry
+title: Tracing OpenTelemetry
description: Core utility
---
-The Tracing Opentelemetry utility is an opinionated thin wrapper for [AWS X-Ray Java SDK](https://github.com/aws/aws-xray-sdk-java/)
- provides functionality to reduce the overhead of performing common tracing tasks.
+The Tracing OpenTelemetry utility provides an opinionated integration with [OpenTelemetry](https://opentelemetry.io/) for AWS Lambda. It reduces the overhead of performing distributed tracing tasks by automatically creating spans, extracting trace context from various AWS Lambda event sources, capturing cold starts, responses, and errors, and exporting traces to OpenTelemetry Protocol (OTLP) collectors or AWS Distro for OpenTelemetry (ADOT).

**Key Features**
- * Capture cold start as annotation, and responses as well as full exceptions as metadata
- * Helper methods to improve the developer experience of creating new X-Ray subsegments.
- * Better developer experience when developing with multiple threads.
- * Auto patch supported modules by AWS X-Ray
- * GraalVM support
+ * Automatic span lifecycle management for Lambda handlers and arbitrary methods using the `@Tracing` annotation or programmatic API.
+ * Capture cold start as attribute (`faas.coldstart`), Lambda invocation ID (`faas.invocation_id`), responses, and full exceptions.
+ * Built-in trace context extraction and span enrichment for major AWS Lambda event sources (API Gateway, SQS, SNS, Kinesis, DynamoDB Streams, S3).
+ * Flexible context propagation modes (`PARENT` and `LINK`) supporting W3C Trace Context and AWS X-Ray formats.
+ * Automatic configuration for OTLP exporters (gRPC and HTTP/Protobuf) and seamless compatibility with Global OpenTelemetry (e.g. ADOT Java agent / Lambda layer).
+ * Programmatic tracing API (`TracingOpenTelemetry`) for custom spans, scopes, and manual context injection/extraction.
+ * Automatic User-Agent tagging for AWS SDK for Java 2.x clients.
+ * GraalVM support.
## Install
@@ -32,7 +34,7 @@ The Tracing Opentelemetry utility is an opinionated thin wrapper for [AWS X-Ray
...
-
+
...
@@ -41,9 +43,9 @@ The Tracing Opentelemetry utility is an opinionated thin wrapper for [AWS X-Ray
aspectj-maven-plugin1.14
- 11
- 11
- 11
+ 17
+ 17
+ 17software.amazon.lambda
@@ -77,7 +79,7 @@ The Tracing Opentelemetry utility is an opinionated thin wrapper for [AWS X-Ray
```groovy hl_lines="3 11 12"
plugins {
id 'java'
- id 'io.freefair.aspectj.post-compile-weaving' version '8.1.0' // Not needed when using the functional approach
+ id 'io.freefair.aspectj.post-compile-weaving' version '8.1.0' // Not needed when using the programmatic approach
}
repositories {
@@ -85,46 +87,58 @@ The Tracing Opentelemetry utility is an opinionated thin wrapper for [AWS X-Ray
}
dependencies {
- aspect 'software.amazon.lambda:powertools-tracing-opentelemetry:{{ powertools.version }}' // Not needed when using the functional approach
- implementation 'software.amazon.lambda:powertools-tracing-opentelemetry:{{ powertools.version }}' // Use this instead of 'aspect' when using the functional approach
+ aspect 'software.amazon.lambda:powertools-tracing-opentelemetry:{{ powertools.version }}' // Not needed when using the programmatic approach
+ implementation 'software.amazon.lambda:powertools-tracing-opentelemetry:{{ powertools.version }}' // Use this instead of 'aspect' when using the programmatic approach
}
- sourceCompatibility = 11 // or higher
- targetCompatibility = 11 // or higher
+ sourceCompatibility = 17 // or higher
+ targetCompatibility = 17 // or higher
```
## Initialization
-Before your use this utility, your AWS Lambda function [must have permissions](https://docs.aws.amazon.com/lambda/latest/dg/services-xray.html#services-xray-permissions) to send traces to AWS X-Ray.
+Before using this utility, ensure your environment is configured to export OpenTelemetry traces (such as using an OpenTelemetry Collector, AWS Distro for OpenTelemetry (ADOT), or a generic OTLP backend).
> Example using AWS Serverless Application Model (SAM)
=== "template.yaml"
- ```yaml hl_lines="8 11"
+ ```yaml hl_lines="8 10-14"
Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
...
- Runtime: java11
+ Runtime: java17
Tracing: Active
Environment:
Variables:
POWERTOOLS_SERVICE_NAME: example
+ POWERTOOLS_TRACE_CONTEXT_PROPAGATION_MODE: PARENT
+ OTEL_EXPORTER_OTLP_TRACES_PROTOCOL: grpc
+ OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: http://localhost:4317
```
-The Powertools for AWS Lambda (Java) service name is used as the X-Ray namespace. This can be set using the environment variable
-`POWERTOOLS_SERVICE_NAME`
+The following environment variables can be configured:
+
+| Environment Variable | Description | Default |
+| --- | --- | --- |
+| **`POWERTOOLS_SERVICE_NAME`** | Service name associated with the Lambda function resource | `""` |
+| **`POWERTOOLS_TRACE_CONTEXT_PROPAGATION_MODE`** | Propagation strategy: `PARENT` or `LINK` | `PARENT` |
+| **`POWERTOOLS_TRACER_CAPTURE_RESPONSE`** | Capture method response in span attribute | `false` |
+| **`POWERTOOLS_TRACER_CAPTURE_ERROR`** | Capture thrown exception in span | `false` |
+| **`OTEL_EXPORTER_OTLP_TRACES_PROTOCOL`** | Exporter protocol: `grpc` or `http/protobuf` | `grpc` |
+| **`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`** | Target OTLP endpoint (e.g., `http://localhost:4317`) | Collector default |
+| **`OTEL_EXPORTER_OTLP_TRACES_HEADERS`** | Comma-separated list of headers (`key=value,key2=value2`) | `""` |
### Lambda handler
-You can enable tracing using either the `@Tracing` annotation or the functional API.
+You can enable tracing using either the `@Tracing` annotation or the programmatic API.
-**With the `@Tracing` annotation**, add it to your `handleRequest` method or any method to capture it as a separate subsegment automatically. You can optionally customize the segment name that appears in traces.
+**With the `@Tracing` annotation**, add it to your `handleRequest` method or any internal method to capture it as a separate span automatically. You can optionally customize the span name and namespace.
-**With the functional API**, use `TracingUtils.withSubsegment()` to manually create subsegments without AspectJ configuration.
+**With the programmatic API**, use `TracingOpenTelemetry.create()` to manually manage spans without AspectJ configuration.
=== "@Tracing annotation"
@@ -150,60 +164,67 @@ You can enable tracing using either the `@Tracing` annotation or the functional
}
```
-=== "Functional API"
+=== "Programmatic API"
```java hl_lines="1 6 7 8 10 11 12"
- import software.amazon.lambda.powertools.tracing.opentelemetry.TracingUtils;
+ import software.amazon.lambda.powertools.tracing.opentelemetry.TracingOpenTelemetry;
public class App implements RequestHandler {
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
- TracingUtils.withSubsegment("businessLogic1", subsegment -> {
+ TracingOpenTelemetry.create().withSpan("businessLogic1", span -> {
// Business logic 1
+ return null;
});
- TracingUtils.withSubsegment("businessLogic2", subsegment -> {
+ TracingOpenTelemetry.create().withSpan("businessLogic2", span -> {
// Business logic 2
+ return null;
});
}
}
```
-=== "Custom Segment names"
+=== "Custom Span names"
```java hl_lines="3"
public class App implements RequestHandler {
- @Tracing(segmentName="yourCustomName")
+ @Tracing(spanName="yourCustomSpanName")
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
...
}
```
-When using the `@Tracing` annotation, the utility performs these additional tasks to ease operations:
+When using the `@Tracing` annotation on a Lambda handler, the utility performs these additional tasks:
+
+ * Creates a `faas.coldstart` attribute to easily identify cold starts.
+ * Adds the `faas.invocation_id` attribute containing the AWS Request ID.
+ * Automatically extracts incoming trace context from supported Lambda event sources (API Gateway, SQS, SNS, Kinesis, DynamoDB, S3) and enriches the span with event details.
+ * Captures responses and full exceptions based on the configured capture mode.
+ * Automatically flushes telemetry data when handler execution finishes.
- * Creates a `ColdStart` annotation to easily filter traces that have had an initialization overhead.
- * Creates a `Service` annotation if service parameter or `POWERTOOLS_SERVICE_NAME` is set.
- * Captures any response, or full exceptions generated by the handler, and include as tracing metadata.
+By default, the `@Tracing` annotation uses `captureMode = CaptureMode.ENVIRONMENT_VAR`, which means it will record method responses and exceptions only if you set the environment variables `POWERTOOLS_TRACER_CAPTURE_RESPONSE` and `POWERTOOLS_TRACER_CAPTURE_ERROR` to `true`. You can override this behavior by specifying a different `captureMode`:
-By default, the `@Tracing` annotation uses `captureMode=ENVIRONMENT_VAR`, which means it will only record method responses and exceptions if you set
-the environment variables `POWERTOOLS_TRACER_CAPTURE_RESPONSE` and `POWERTOOLS_TRACER_CAPTURE_ERROR` to `true`. You can override this behavior by
-specifying a different `captureMode` to always record response, exception, both, or neither.
+* `CaptureMode.ENVIRONMENT_VAR` (default): Uses environment variables.
+* `CaptureMode.RESPONSE`: Always captures the response.
+* `CaptureMode.ERROR`: Always captures exceptions.
+* `CaptureMode.RESPONSE_AND_ERROR`: Captures both response and exceptions.
+* `CaptureMode.DISABLED`: Disables response and exception capture.
!!! note
- When using the functional API with `TracingUtils.withSubsegment()`, response and exception capture is not automatic. You can manually add metadata using `TracingUtils.putMetadata()` as needed.
+ When using the programmatic API with `TracingOpenTelemetry`, response capture is manual, while `withSpan` automatically records thrown exceptions on the span before propagating them.
!!! warning "Returning sensitive information from your Lambda handler or functions?"
- When using the `@Tracing` annotation, you can disable it from capturing responses and exceptions as tracing metadata with **`captureMode=DISABLED`**
+ When using the `@Tracing` annotation, you can disable response and exception capture with **`captureMode = CaptureMode.DISABLED`**
or globally by setting the environment variables **`POWERTOOLS_TRACER_CAPTURE_RESPONSE`** and **`POWERTOOLS_TRACER_CAPTURE_ERROR`** to **`false`**.
- When using the functional API, you have full control over what metadata is captured.
=== "@Tracing annotation - Disable on method"
```java hl_lines="3"
public class App implements RequestHandler {
- @Tracing(captureMode=CaptureMode.DISABLED)
+ @Tracing(captureMode = CaptureMode.DISABLED)
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
...
}
@@ -217,7 +238,7 @@ specifying a different `captureMode` to always record response, exception, both,
Type: AWS::Serverless::Function
Properties:
...
- Runtime: java11
+ Runtime: java17
Tracing: Active
Environment:
@@ -226,112 +247,180 @@ specifying a different `captureMode` to always record response, exception, both,
POWERTOOLS_TRACER_CAPTURE_ERROR: false
```
-=== "Functional API"
+=== "Programmatic API"
```java hl_lines="6 7 8"
- import software.amazon.lambda.powertools.tracing.TracingUtils;
+ import software.amazon.lambda.powertools.tracing.opentelemetry.TracingOpenTelemetry;
public class App implements RequestHandler {
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
- TracingUtils.withSubsegment("businessLogic", subsegment -> {
- // With functional API, you control what metadata is captured
+ TracingOpenTelemetry.create().withSpan("businessLogic", span -> {
+ // You have full control over what attributes are set on the span
+ return null;
});
}
```
-### Annotations & Metadata
+### Attributes & Events
-**Annotations** are key-values associated with traces and indexed by AWS X-Ray. You can use them to filter traces and to
-create [Trace Groups](https://aws.amazon.com/about-aws/whats-new/2018/11/aws-xray-adds-the-ability-to-group-traces/) to slice and dice your transactions.
+In OpenTelemetry, **Attributes** are key-value pairs associated with spans used for querying and filtering traces. **Events** are timestamped annotations within a span representing specific moments during execution.
-**Metadata** are key-values also associated with traces but not indexed by AWS X-Ray. You can use them to add additional
-context for an operation using any native object.
+=== "Attributes"
-=== "Annotations"
-
- You can add annotations using `putAnnotation()` method from TracingUtils
- ```java hl_lines="8"
- import software.amazon.lambda.powertools.tracing.Tracing;
- import software.amazon.lambda.powertools.tracing.TracingUtils;
+ You can add attributes directly to the active span using OpenTelemetry API:
+ ```java hl_lines="2 8"
+ import io.opentelemetry.api.trace.Span;
+ import software.amazon.lambda.powertools.tracing.opentelemetry.Tracing;
public class App implements RequestHandler {
@Tracing
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
- TracingUtils.putAnnotation("annotation", "value");
+ Span.current().setAttribute("customer.id", "12345");
+ Span.current().setAttribute("order.amount", 99.99);
}
}
```
-=== "Metadata"
+=== "Events"
- You can add metadata using `putMetadata()` method from TracingUtils
- ```java hl_lines="8"
- import software.amazon.lambda.powertools.tracing.Tracing;
- import software.amazon.lambda.powertools.tracing.TracingUtils;
+ You can record events on the active span using `addEvent()`:
+ ```java hl_lines="2 8 9"
+ import io.opentelemetry.api.trace.Span;
+ import software.amazon.lambda.powertools.tracing.opentelemetry.Tracing;
public class App implements RequestHandler {
@Tracing
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
- TracingUtils.putMetadata("content", "value");
+ Span.current().addEvent("processing_started");
+ // Perform operations...
+ Span.current().addEvent("processing_finished");
}
}
```
-## Override default object mapper
+## Event Source Context Extraction
-You can optionally choose to override default object mapper which is used to serialize method response and exceptions when enabled. You might
-want to supply custom object mapper in order to control how serialisation is done, for example, when you want to log only
-specific fields from received event due to security.
+Powertools for AWS Lambda OpenTelemetry Tracing automatically inspects incoming Lambda events and extracts trace context across distributed boundaries using W3C Trace Context and AWS X-Ray headers.
-=== "App.java"
+Supported event sources include:
- ```java hl_lines="10-14"
- import software.amazon.lambda.powertools.tracing.Tracing;
- import software.amazon.lambda.powertools.tracing.TracingUtils;
- import static software.amazon.lambda.powertools.tracing.CaptureMode.RESPONSE;
+* **API Gateway** (`APIGatewayProxyRequestEvent`, `APIGatewayV2HTTPEvent`): Extracts `traceparent`, `tracestate`, or `X-Amzn-Trace-Id` headers and enriches span with HTTP method, path, and status code.
+* **Amazon SQS** (`SQSEvent`): Extracts trace headers from message attributes and system attributes.
+* **Amazon SNS** (`SNSEvent`): Extracts trace headers from message attributes.
+* **Amazon Kinesis** (`KinesisEvent`): Extracts trace context from record data.
+* **Amazon DynamoDB Streams** (`DynamodbEvent`): Extracts trace context from stream records.
+* **Amazon S3** (`S3Event`): Extracts and enriches span with bucket and object metadata.
- /**
- * Handler for requests to Lambda function.
- */
- public class App implements RequestHandler {
- static {
- ObjectMapper objectMapper = new ObjectMapper();
- SimpleModule simpleModule = new SimpleModule();
- objectMapper.registerModule(simpleModule);
+### Propagation Modes
- TracingUtils.defaultObjectMapper(objectMapper);
- }
-
- @Tracing(captureMode = RESPONSE)
- public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
- ...
+You can control how trace contexts are connected using the `POWERTOOLS_TRACE_CONTEXT_PROPAGATION_MODE` environment variable:
+
+* **`PARENT` (Default)**: Creates the handler span as a child of the extracted trace context.
+* **`LINK`**: Creates the handler span in the current context and links it to extracted trace contexts. This is particularly useful for batch event sources (such as SQS or Kinesis) where a single batch contains messages from multiple independent traces.
+
+=== "PARENT Mode"
+
+ ```yaml hl_lines="4"
+ Environment:
+ Variables:
+ POWERTOOLS_TRACE_CONTEXT_PROPAGATION_MODE: PARENT
+ ```
+
+=== "LINK Mode"
+
+ ```yaml hl_lines="4"
+ Environment:
+ Variables:
+ POWERTOOLS_TRACE_CONTEXT_PROPAGATION_MODE: LINK
+ ```
+
+## Advanced usage
+
+### Programmatic Tracing with TracingOpenTelemetry
+
+You can use `TracingOpenTelemetry` to manually create and manage spans with full control over span lifecycle, kinds, attributes, and context scoping.
+
+=== "Span with SpanScope"
+
+ ```java hl_lines="1 6 7 11"
+ import software.amazon.lambda.powertools.tracing.opentelemetry.TracingOpenTelemetry;
+ import software.amazon.lambda.powertools.tracing.opentelemetry.internal.SpanScope;
+
+ public class OrderService {
+ public void processOrder(String orderId) {
+ TracingOpenTelemetry tracing = TracingOpenTelemetry.create();
+ try (SpanScope scope = tracing.addSpan("processOrder")) {
+ scope.span().setAttribute("order.id", orderId);
+ scope.addEvent("validating_order");
+ // Business logic
+ }
}
}
```
-## Advanced usage
+=== "Custom Builder Configuration"
+
+ ```java hl_lines="1 4-8"
+ import software.amazon.lambda.powertools.tracing.opentelemetry.TracingOpenTelemetry;
+
+ TracingOpenTelemetry customTracing = TracingOpenTelemetry.builder()
+ .tracer(customTracer)
+ .propagator(customPropagator)
+ .eventContextExtractorResolver(customResolver)
+ .build();
+ ```
+
+### Context Injection & Extraction
+
+When communicating across distributed systems or custom protocols, you can inject and extract trace context directly:
+
+=== "Context Propagation"
+
+ ```java hl_lines="1 2 7 13"
+ import io.opentelemetry.context.propagation.TextMapSetter;
+ import software.amazon.lambda.powertools.tracing.opentelemetry.TracingOpenTelemetry;
+
+ public class MessageSender {
+ public void sendMessage(Map carrier) {
+ TracingOpenTelemetry tracing = TracingOpenTelemetry.create();
+ tracing.injectContext(carrier, (map, key, value) -> map.put(key, value));
+ }
+
+ public void receiveMessage(Map carrier) {
+ TracingOpenTelemetry tracing = TracingOpenTelemetry.create();
+ io.opentelemetry.context.Context context = tracing.extractContext(carrier, (map, key) -> map.get(key));
+ }
+ }
+ ```
### Multi-threaded programming
-When working with multiple threads, you need to pass the trace entity to ensure proper trace context propagation.
+When spawning multiple threads, ensure the OpenTelemetry context is propagated to the new thread using `Context.makeCurrent()`:
=== "Multi-threaded example"
- ```java hl_lines="7 9 10 11"
- import static software.amazon.lambda.powertools.tracing.TracingUtils.withEntitySubsegment;
+ ```java hl_lines="1 2 8 10 11"
+ import io.opentelemetry.context.Context;
+ import io.opentelemetry.context.Scope;
+ import software.amazon.lambda.powertools.tracing.opentelemetry.TracingOpenTelemetry;
public class App implements RequestHandler {
- public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
- // Extract existing trace data
- Entity traceEntity = AWSXRay.getTraceEntity();
+ public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, com.amazonaws.services.lambda.runtime.Context lambdaContext) {
+ Context parentContext = Context.current();
- Thread anotherThread = new Thread(() -> withEntitySubsegment("inlineLog", traceEntity, subsegment -> {
- // Business logic in separate thread
- }));
+ Thread anotherThread = new Thread(() -> {
+ try (Scope scope = parentContext.makeCurrent()) {
+ TracingOpenTelemetry.create().withSpan("asyncTask", span -> {
+ // Business logic in separate thread with preserved context
+ return null;
+ });
+ }
+ });
+ anotherThread.start();
}
}
```
@@ -340,159 +429,70 @@ When working with multiple threads, you need to pass the trace entity to ensure
### AWS SDK for Java 2.x
-Powertools for AWS Lambda (Java) includes the `aws-xray-recorder-sdk-aws-sdk-v2-instrumentor` library, which **automatically instruments all AWS SDK v2 clients** when you add the `powertools-tracing` dependency to your project. This means downstream calls to AWS services are traced without any additional configuration.
-
-If you need more control over which clients are instrumented, you can manually add the `TracingInterceptor` to specific clients:
+Powertools for AWS Lambda (Java) OpenTelemetry module includes `TracingUserAgentInterceptor`, which automatically tags AWS SDK v2 client requests with the Powertools user-agent header without any additional code required.
-=== "Manual instrumentation (optional)"
+For tracing outgoing AWS SDK calls, you can use the OpenTelemetry AWS SDK Instrumentation library or the [AWS Distro for OpenTelemetry (ADOT)](https://aws-otel.github.io/) Java agent / Lambda layer, which automatically instruments AWS SDK clients and outgoing HTTP calls.
- ```java hl_lines="1 2 3 8 9 10 11"
- import com.amazonaws.xray.interceptors.TracingInterceptor;
- import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
- import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
-
- public class LambdaHandler {
- private DynamoDbClient client = DynamoDbClient.builder()
- .region(Region.US_WEST_2)
- .overrideConfiguration(ClientOverrideConfiguration.builder()
- .addExecutionInterceptor(new TracingInterceptor())
- .build()
- )
- .build();
- // ...
- }
- ```
+## Testing your code
-For more details, refer to the [AWS X-Ray documentation on tracing AWS SDK calls](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-java-awssdkclients.html) and [outgoing HTTP calls](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-java-httpclients.html).
+When testing code instrumented with `@Tracing` or `TracingOpenTelemetry`, you can configure unit tests using OpenTelemetry testing utilities like `InMemorySpanExporter` or configure custom `TracingOpenTelemetry` instances.
-## Testing your code
+=== "JUnit 5 Test (InMemorySpanExporter)"
-When using `@Tracing` annotation, your Junit test cases needs to be configured to create parent Segment required by [AWS X-Ray SDK for Java](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-java.html).
+ ```java hl_lines="1 2 11 12 13 14 15 22 23"
+ import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter;
+ import io.opentelemetry.sdk.trace.SdkTracerProvider;
+ import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
+ import org.junit.jupiter.api.AfterEach;
+ import org.junit.jupiter.api.BeforeEach;
+ import org.junit.jupiter.api.Test;
-Below are two ways in which you can configure your tests.
+ public class AppTest {
+ private InMemorySpanExporter exporter;
+ private SdkTracerProvider tracerProvider;
-#### Configure environment variable on project level (Recommended)
+ @BeforeEach
+ public void setup() {
+ exporter = InMemorySpanExporter.create();
+ tracerProvider = SdkTracerProvider.builder()
+ .addSpanProcessor(SimpleSpanProcessor.create(exporter))
+ .build();
+ }
-You can choose to configure environment variable on project level for your test cases run. This is recommended approach as it will avoid the need of configuring each test case specifically.
+ @AfterEach
+ public void tearDown() {
+ tracerProvider.close();
+ }
-Below are examples configuring your maven/gradle projects. You can choose to configure it differently as well as long as you are making sure that environment variable `LAMBDA_TASK_ROOT` is set. This variable is
-used internally via AWS X-Ray SDK to configure itself properly for lambda runtime.
+ @Test
+ public void successfulResponse() {
+ // Execute handler and assert captured spans from exporter.getFinishedSpanItems()
+ }
+ }
+ ```
=== "Maven (pom.xml)"
```xml
- ...
-
org.apache.maven.pluginsmaven-surefire-plugin
- handler
+ on-demand
-
```
=== "Gradle (build.gradle)"
- ```json
- // Configures environment variable to avoid initialization of AWS X-Ray segments for each tests
+ ```groovy
test {
- environment "LAMBDA_TASK_ROOT", "handler"
- }
- ```
-
-#### Configure test cases (Not Recommended)
-
-You can choose to configure each of your test case instead as well if you choose not to configure environment variable on project level.
-Below is an example configuration needed for each test case.
-
-=== "AppTest.java"
-
- ```java hl_lines="10 11 12 17 18 19 20 21 22 23 24"
- import com.amazonaws.xray.AWSXRay;
- import org.junit.After;
- import org.junit.Before;
- import org.junit.Test;
-
- public class AppTest {
-
- @Before
- public void setup() {
- if(null == System.getenv("LAMBDA_TASK_ROOT")) {
- AWSXRay.beginSegment("test");
- }
- }
-
- @After
- public void tearDown() {
- // Needed when using sam build --use-container
- if (AWSXRay.getCurrentSubsegmentOptional().isPresent()) {
- AWSXRay.endSubsegment();
- }
-
- if(null == System.getenv("LAMBDA_TASK_ROOT")) {
- AWSXRay.endSegment();
- }
- }
-
- @Test
- public void successfulResponse() {
- // test logic
- }
- ```
-## Advanced
-
-### Lambda SnapStart priming
-
-The Tracing utility integrates with AWS Lambda SnapStart to improve restore durations. To make sure the SnapStart priming logic of this utility runs correctly, you need an explicit reference to `TracingUtils` in your code to allow the library to register before SnapStart takes a memory snapshot. Learn more about what priming is in this [blog post](https://aws.amazon.com/blogs/compute/optimizing-cold-start-performance-of-aws-lambda-using-advanced-priming-strategies-with-snapstart/){target="_blank"}.
-
-If you don't set a custom `TracingUtils` in your code yet, make sure to reference `TracingUtils` in your Lambda handler initialization code. This can be done by adding one of the following lines to your handler class:
-
-=== "Constructor"
-
- ```java hl_lines="7"
- import software.amazon.lambda.powertools.validation.Validation;
- import software.amazon.lambda.powertools.validation.ValidationConfig;
-
- public class MyFunctionHandler implements RequestHandler {
-
- public MyFunctionHandler() {
- TracingUtils.init(); // Ensure TracingUtils is loaded for SnapStart
- }
-
- @Override
- @Validation(inboundSchema = "classpath:/schema_in.json", outboundSchema = "classpath:/schema_out.json")
- public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
- // ...
- return something;
- }
- }
- ```
-
-=== "Static Initializer"
-
- ```java hl_lines="7"
- import software.amazon.lambda.powertools.validation.Validation;
- import software.amazon.lambda.powertools.validation.ValidationConfig;
-
- public class MyFunctionHandler implements RequestHandler {
-
- static {
- TracingUtils.init(); // Ensure TracingUtils is loaded for SnapStart
- }
-
- @Override
- @Validation(inboundSchema = "classpath:/schema_in.json", outboundSchema = "classpath:/schema_out.json")
- public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
- // ...
- return something;
- }
+ environment "AWS_LAMBDA_INITIALIZATION_TYPE", "on-demand"
}
```