+ * 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