From d0b463dd6f5760e05dc0220d10d1885f91450fb2 Mon Sep 17 00:00:00 2001 From: jack-berg <34418638+jack-berg@users.noreply.github.com> Date: Thu, 6 Jun 2024 09:42:01 -0500 Subject: [PATCH] Refactor ExtendedTracer, ExtendedSpanBuilder to reflect incubating API conventions (#6497) --- api/incubator/README.md | 174 +------------- .../incubator/trace/ExtendedSpanBuilder.java | 155 +------------ .../api/incubator/trace/ExtendedTracer.java | 44 ---- .../trace/ExtendedTraceApiUsageTest.java | 211 +++++++++++++++++ .../incubator/trace/ExtendedTracerTest.java | 214 ------------------ sdk/trace/build.gradle.kts | 2 + .../sdk/trace/SdkSpanBuilder.java | 67 +++++- 7 files changed, 295 insertions(+), 572 deletions(-) delete mode 100644 api/incubator/src/main/java/io/opentelemetry/api/incubator/trace/ExtendedTracer.java create mode 100644 api/incubator/src/test/java/io/opentelemetry/api/incubator/trace/ExtendedTraceApiUsageTest.java delete mode 100644 api/incubator/src/test/java/io/opentelemetry/api/incubator/trace/ExtendedTracerTest.java diff --git a/api/incubator/README.md b/api/incubator/README.md index 68769849e5c..068f54a3ca5 100644 --- a/api/incubator/README.md +++ b/api/incubator/README.md @@ -35,170 +35,10 @@ Features: See [ExtendedContextPropagatorsUsageTest](./src/test/java/io/opentelemetry/api/incubator/propagation/ExtendedContextPropagatorsUsageTest.java). -## ExtendedTracer - -Utility methods to make it easier to use the OpenTelemetry tracer. - -Here are some examples how the utility methods can help reduce boilerplate code. - -TODO: translate examples to test to ensure no java compilation issues. - -### Tracing a function - -Before: - - -```java -Span span = tracer.spanBuilder("reset_checkout").startSpan(); -String transactionId; -try (Scope scope = span.makeCurrent()) { - transactionId = resetCheckout(cartId); -} catch (Throwable e) { - span.setStatus(StatusCode.ERROR); - span.recordException(e); - throw e; // or throw new RuntimeException(e) - depending on your error handling strategy -} finally { - span.end(); -} -``` - - -After: - -```java -import io.opentelemetry.extension.incubator.trace.ExtendedTracer; - -ExtendedTracer extendedTracer = ExtendedTracer.create(tracer); -String transactionId = extendedTracer.spanBuilder("reset_checkout").startAndCall(() -> resetCheckout(cartId)); -``` - -If you want to set attributes on the span, you can use the `startAndCall` method on the span builder: - -```java -import io.opentelemetry.extension.incubator.trace.ExtendedTracer; - -ExtendedTracer extendedTracer = ExtendedTracer.create(tracer); -String transactionId = extendedTracer.spanBuilder("reset_checkout") - .setAttribute("foo", "bar") - .startAndCall(() -> resetCheckout(cartId)); -``` - -Note: - -- Use `startAndRun` instead of `startAndCall` if the function returns `void` (both on the tracer and span builder). -- Exceptions are re-thrown without modification - see [Exception handling](#exception-handling) - for more details. - -### Trace context propagation - -Before: - -```java -Map propagationHeaders = new HashMap<>(); -openTelemetry - .getPropagators() - .getTextMapPropagator() - .inject( - Context.current(), - propagationHeaders, - (map, key, value) -> { - if (map != null) { - map.put(key, value); - } - }); - -// add propagationHeaders to request headers and call checkout service -``` - - -```java -// in checkout service: get request headers into a Map requestHeaders -Map requestHeaders = new HashMap<>(); -String cartId = "cartId"; - -SpanBuilder spanBuilder = tracer.spanBuilder("checkout_cart"); - -TextMapGetter> TEXT_MAP_GETTER = - new TextMapGetter>() { - @Override - public Set keys(Map carrier) { - return carrier.keySet(); - } - - @Override - @Nullable - public String get(@Nullable Map carrier, String key) { - return carrier == null ? null : carrier.get(key); - } - }; - -Map normalizedTransport = - requestHeaders.entrySet().stream() - .collect( - Collectors.toMap( - entry -> entry.getKey().toLowerCase(Locale.ROOT), Map.Entry::getValue)); -Context newContext = openTelemetry - .getPropagators() - .getTextMapPropagator() - .extract(Context.current(), normalizedTransport, TEXT_MAP_GETTER); -String transactionId; -try (Scope ignore = newContext.makeCurrent()) { - Span span = spanBuilder.setSpanKind(SERVER).startSpan(); - try (Scope scope = span.makeCurrent()) { - transactionId = processCheckout(cartId); - } catch (Throwable e) { - span.setStatus(StatusCode.ERROR); - span.recordException(e); - throw e; // or throw new RuntimeException(e) - depending on your error handling strategy - } finally { - span.end(); - } -} -``` - - -After: - -```java -import io.opentelemetry.extension.incubator.propagation.ExtendedContextPropagators; - -Map propagationHeaders = - ExtendedContextPropagators.getTextMapPropagationContext(openTelemetry.getPropagators()); -// add propagationHeaders to request headers and call checkout service -``` - -```java -import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.extension.incubator.trace.ExtendedTracer; - -// in checkout service: get request headers into a Map requestHeaders -Map requestHeaders = new HashMap<>(); -String cartId = "cartId"; - -ExtendedTracer extendedTracer = ExtendedTracer.create(tracer); -String transactionId = extendedTracer.spanBuilder("checkout_cart") - .setSpanKind(SpanKind.SERVER) - .setParentFrom(openTelemetry.getPropagators(), requestHeaders) - .startAndCall(() -> processCheckout(cartId)); -``` - -### Exception handling - -`ExtendedTracer` re-throws exceptions without modification. This means you can -catch exceptions around `ExtendedTracer` calls and handle them as you would without `ExtendedTracer`. - -When an exception is encountered during an `ExtendedTracer` call, the span is marked as error and -the exception is recorded. - -If you want to customize this behaviour, e.g. to only record the exception, because you are -able to recover from the error, you can call the overloaded method of `startAndCall` or -`startAndRun` that takes an exception handler: - -```java -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.extension.incubator.trace.ExtendedTracer; - -ExtendedTracer extendedTracer = ExtendedTracer.create(tracer); -String transactionId = extendedTracer.spanBuilder("checkout_cart") - .startAndCall(() -> processCheckout(cartId), Span::recordException); -``` +## Extended Trace APIs + +Features: + +* Utility methods to reduce boilerplace using span API, including extracting context, and wrapping runnables / callables with spans + +See [ExtendedTraceApiUsageTest](./src/test/java/io/opentelemetry/api/incubator/trace/ExtendedTraceApiUsageTest.java). diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/trace/ExtendedSpanBuilder.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/trace/ExtendedSpanBuilder.java index 32f7a7a3d90..715b79f70c4 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/trace/ExtendedSpanBuilder.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/trace/ExtendedSpanBuilder.java @@ -6,106 +6,14 @@ package io.opentelemetry.api.incubator.trace; import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.incubator.propagation.ExtendedContextPropagators; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanBuilder; -import io.opentelemetry.api.trace.SpanContext; -import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.api.trace.StatusCode; -import io.opentelemetry.context.Context; -import io.opentelemetry.context.Scope; import io.opentelemetry.context.propagation.ContextPropagators; -import java.time.Instant; import java.util.Map; -import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; -public final class ExtendedSpanBuilder implements SpanBuilder { - private final SpanBuilder delegate; - - ExtendedSpanBuilder(SpanBuilder delegate) { - this.delegate = delegate; - } - - @Override - public ExtendedSpanBuilder setParent(Context context) { - delegate.setParent(context); - return this; - } - - @Override - public ExtendedSpanBuilder setNoParent() { - delegate.setNoParent(); - return this; - } - - @Override - public ExtendedSpanBuilder addLink(SpanContext spanContext) { - delegate.addLink(spanContext); - return this; - } - - @Override - public ExtendedSpanBuilder addLink(SpanContext spanContext, Attributes attributes) { - delegate.addLink(spanContext, attributes); - return this; - } - - @Override - public ExtendedSpanBuilder setAttribute(String key, String value) { - delegate.setAttribute(key, value); - return this; - } - - @Override - public ExtendedSpanBuilder setAttribute(String key, long value) { - delegate.setAttribute(key, value); - return this; - } - - @Override - public ExtendedSpanBuilder setAttribute(String key, double value) { - delegate.setAttribute(key, value); - return this; - } - - @Override - public ExtendedSpanBuilder setAttribute(String key, boolean value) { - delegate.setAttribute(key, value); - return this; - } - - @Override - public ExtendedSpanBuilder setAttribute(AttributeKey key, T value) { - delegate.setAttribute(key, value); - return this; - } - - @Override - public ExtendedSpanBuilder setAllAttributes(Attributes attributes) { - delegate.setAllAttributes(attributes); - return this; - } - - @Override - public ExtendedSpanBuilder setSpanKind(SpanKind spanKind) { - delegate.setSpanKind(spanKind); - return this; - } - - @Override - public ExtendedSpanBuilder setStartTimestamp(long startTimestamp, TimeUnit unit) { - delegate.setStartTimestamp(startTimestamp, unit); - return this; - } - - @Override - public ExtendedSpanBuilder setStartTimestamp(Instant startTimestamp) { - delegate.setStartTimestamp(startTimestamp); - return this; - } +/** Extended {@link SpanBuilder} with experimental APIs. */ +public interface ExtendedSpanBuilder extends SpanBuilder { /** * Extract a span context from the given carrier and set it as parent of the span for {@link @@ -124,16 +32,7 @@ public ExtendedSpanBuilder setStartTimestamp(Instant startTimestamp) { * @param propagators provide the propagators from {@link OpenTelemetry#getPropagators()} * @param carrier the string map where to extract the span context from */ - public ExtendedSpanBuilder setParentFrom( - ContextPropagators propagators, Map carrier) { - setParent(ExtendedContextPropagators.extractTextMapPropagationContext(carrier, propagators)); - return this; - } - - @Override - public Span startSpan() { - return delegate.startSpan(); - } + ExtendedSpanBuilder setParentFrom(ContextPropagators propagators, Map carrier); /** * Runs the given {@link SpanCallable} inside of the span created by the given {@link @@ -147,9 +46,7 @@ public Span startSpan() { * @param the type of the exception * @return the result of the {@link SpanCallable} */ - public T startAndCall(SpanCallable spanCallable) throws E { - return startAndCall(spanCallable, ExtendedSpanBuilder::setSpanError); - } + T startAndCall(SpanCallable spanCallable) throws E; /** * Runs the given {@link SpanCallable} inside of the span created by the given {@link @@ -165,20 +62,8 @@ public T startAndCall(SpanCallable spanCallable) * @param the type of the exception * @return the result of the {@link SpanCallable} */ - public T startAndCall( - SpanCallable spanCallable, BiConsumer handleException) throws E { - Span span = startSpan(); - - //noinspection unused - try (Scope unused = span.makeCurrent()) { - return spanCallable.callInSpan(); - } catch (Throwable e) { - handleException.accept(span, e); - throw e; - } finally { - span.end(); - } - } + T startAndCall( + SpanCallable spanCallable, BiConsumer handleException) throws E; /** * Runs the given {@link SpanRunnable} inside of the span created by the given {@link @@ -190,10 +75,7 @@ public T startAndCall( * @param runnable the {@link SpanRunnable} to run * @param the type of the exception */ - @SuppressWarnings("NullAway") - public void startAndRun(SpanRunnable runnable) throws E { - startAndRun(runnable, ExtendedSpanBuilder::setSpanError); - } + void startAndRun(SpanRunnable runnable) throws E; /** * Runs the given {@link SpanRunnable} inside of the span created by the given {@link @@ -206,25 +88,6 @@ public void startAndRun(SpanRunnable runnable) throws E * @param runnable the {@link SpanRunnable} to run * @param the type of the exception */ - @SuppressWarnings("NullAway") - public void startAndRun( - SpanRunnable runnable, BiConsumer handleException) throws E { - startAndCall( - () -> { - runnable.runInSpan(); - return null; - }, - handleException); - } - - /** - * Marks a span as error. This is the default exception handler. - * - * @param span the span - * @param exception the exception that caused the error - */ - private static void setSpanError(Span span, Throwable exception) { - span.setStatus(StatusCode.ERROR); - span.recordException(exception); - } + void startAndRun( + SpanRunnable runnable, BiConsumer handleException) throws E; } diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/trace/ExtendedTracer.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/trace/ExtendedTracer.java deleted file mode 100644 index 86ed57df1df..00000000000 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/trace/ExtendedTracer.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.api.incubator.trace; - -import io.opentelemetry.api.trace.Tracer; - -/** - * Utility class to simplify tracing. - * - *

The README - * explains the use cases in more detail. - */ -public final class ExtendedTracer implements Tracer { - - private final Tracer delegate; - - private ExtendedTracer(Tracer delegate) { - this.delegate = delegate; - } - - /** - * Creates a new instance of {@link ExtendedTracer}. - * - * @param delegate the {@link Tracer} to use - */ - public static ExtendedTracer create(Tracer delegate) { - return new ExtendedTracer(delegate); - } - - /** - * Creates a new {@link ExtendedSpanBuilder} with the given span name. - * - * @param spanName the name of the span - * @return the {@link ExtendedSpanBuilder} - */ - @Override - public ExtendedSpanBuilder spanBuilder(String spanName) { - return new ExtendedSpanBuilder(delegate.spanBuilder(spanName)); - } -} diff --git a/api/incubator/src/test/java/io/opentelemetry/api/incubator/trace/ExtendedTraceApiUsageTest.java b/api/incubator/src/test/java/io/opentelemetry/api/incubator/trace/ExtendedTraceApiUsageTest.java new file mode 100644 index 00000000000..18bc1ec78a8 --- /dev/null +++ b/api/incubator/src/test/java/io/opentelemetry/api/incubator/trace/ExtendedTraceApiUsageTest.java @@ -0,0 +1,211 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.incubator.trace; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.IdGenerator; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiConsumer; +import org.junit.jupiter.api.Test; + +/** Demonstrating usage of extended Trace API. */ +class ExtendedTraceApiUsageTest { + + /** Demonstrates {@link ExtendedSpanBuilder#setParentFrom(ContextPropagators, Map)}. */ + @Test + void setParentFrom() { + // Setup SdkTracerProvider + InMemorySpanExporter spanExporter = InMemorySpanExporter.create(); + SdkTracerProvider tracerProvider = + SdkTracerProvider.builder() + // Default resource used for demonstration purposes + .setResource(Resource.getDefault()) + // SimpleSpanProcessor with InMemorySpanExporter used for demonstration purposes + .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) + .build(); + + // Setup ContextPropagators + ContextPropagators contextPropagators = + ContextPropagators.create( + TextMapPropagator.composite(W3CTraceContextPropagator.getInstance())); + + // Get a Tracer for a scope + Tracer tracer = tracerProvider.get("org.foo.my-scope"); + + // Populate a map with W3C trace context + Map contextCarrier = new HashMap<>(); + SpanContext remoteParentContext = + SpanContext.createFromRemoteParent( + IdGenerator.random().generateTraceId(), + IdGenerator.random().generateSpanId(), + TraceFlags.getSampled(), + TraceState.getDefault()); + W3CTraceContextPropagator.getInstance() + .inject( + Context.current().with(Span.wrap(remoteParentContext)), + contextCarrier, + (carrier, key, value) -> { + if (carrier != null) { + carrier.put(key, value); + } + }); + + // Set parent from the Map context carrier + ((ExtendedSpanBuilder) tracer.spanBuilder("local_root")) + .setParentFrom(contextPropagators, contextCarrier) + .startSpan() + .end(); + + // Verify the span has the correct parent context + assertThat(spanExporter.getFinishedSpanItems()) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("local_root") + .hasParentSpanId(remoteParentContext.getSpanId()) + .hasTraceId(remoteParentContext.getTraceId())); + } + + /** + * Demonstrates {@link ExtendedSpanBuilder#startAndCall(SpanCallable)}, {@link + * ExtendedSpanBuilder#startAndCall(SpanCallable, BiConsumer)}, {@link + * ExtendedSpanBuilder#startAndRun(SpanRunnable)}, {@link + * ExtendedSpanBuilder#startAndRun(SpanRunnable, BiConsumer)}. + */ + @Test + void startAndCallOrRun() { + // Setup SdkTracerProvider + InMemorySpanExporter spanExporter = InMemorySpanExporter.create(); + SdkTracerProvider tracerProvider = + SdkTracerProvider.builder() + // Default resource used for demonstration purposes + .setResource(Resource.getDefault()) + // SimpleSpanProcessor with InMemorySpanExporter used for demonstration purposes + .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) + .build(); + + // Get a Tracer for a scope + Tracer tracer = tracerProvider.get("org.foo.my-scope"); + + // Wrap the resetCheckout method in a span + String cartId = + ((ExtendedSpanBuilder) tracer.spanBuilder("reset_checkout_and_return")) + .startAndCall(() -> resetCheckoutAndReturn("abc123", /* throwException= */ false)); + assertThat(cartId).isEqualTo("abc123"); + // ...or runnable variation + ((ExtendedSpanBuilder) tracer.spanBuilder("reset_checkout")) + .startAndRun(() -> resetCheckout("abc123", /* throwException= */ false)); + + // Wrap the resetCheckout method in a span; resetCheckout throws an exception + try { + ((ExtendedSpanBuilder) tracer.spanBuilder("reset_checkout_and_return")) + .startAndCall(() -> resetCheckoutAndReturn("def456", /* throwException= */ true)); + } catch (Throwable e) { + // Ignore expected exception + } + // ...or runnable variation + try { + ((ExtendedSpanBuilder) tracer.spanBuilder("reset_checkout")) + .startAndRun(() -> resetCheckout("def456", /* throwException= */ true)); + } catch (Throwable e) { + // Ignore expected exception + } + + // Wrap the resetCheckout method in a span; resetCheckout throws an exception; use custom error + // handler + try { + ((ExtendedSpanBuilder) tracer.spanBuilder("reset_checkout_and_return")) + .startAndCall( + () -> resetCheckoutAndReturn("ghi789", /* throwException= */ true), + (span, throwable) -> span.setAttribute("my-attribute", "error")); + } catch (Throwable e) { + // Ignore expected exception + } + // ...or runnable variation + try { + ((ExtendedSpanBuilder) tracer.spanBuilder("reset_checkout")) + .startAndRun( + () -> resetCheckout("ghi789", /* throwException= */ true), + (span, throwable) -> span.setAttribute("my-attribute", "error")); + } catch (Throwable e) { + // Ignore expected exception + } + + // Verify the spans are as expected + assertThat(spanExporter.getFinishedSpanItems()) + .satisfiesExactly( + span -> + assertThat(span) + .hasName("reset_checkout_and_return") + .hasAttribute(AttributeKey.stringKey("cartId"), "abc123") + .hasStatus(StatusData.unset()) + .hasTotalRecordedEvents(0), + span -> + assertThat(span) + .hasName("reset_checkout") + .hasAttribute(AttributeKey.stringKey("cartId"), "abc123") + .hasStatus(StatusData.unset()) + .hasTotalRecordedEvents(0), + span -> + assertThat(span) + .hasName("reset_checkout_and_return") + .hasAttribute(AttributeKey.stringKey("cartId"), "def456") + .hasStatus(StatusData.error()) + .hasEventsSatisfyingExactly(event -> event.hasName("exception")), + span -> + assertThat(span) + .hasName("reset_checkout") + .hasAttribute(AttributeKey.stringKey("cartId"), "def456") + .hasStatus(StatusData.error()) + .hasEventsSatisfyingExactly(event -> event.hasName("exception")), + span -> + assertThat(span) + .hasName("reset_checkout_and_return") + .hasAttribute(AttributeKey.stringKey("cartId"), "ghi789") + .hasAttribute(AttributeKey.stringKey("my-attribute"), "error") + .hasStatus(StatusData.unset()) + .hasTotalRecordedEvents(0), + span -> + assertThat(span) + .hasName("reset_checkout") + .hasAttribute(AttributeKey.stringKey("cartId"), "ghi789") + .hasAttribute(AttributeKey.stringKey("my-attribute"), "error") + .hasStatus(StatusData.unset()) + .hasTotalRecordedEvents(0)); + } + + private static String resetCheckoutAndReturn(String cartId, boolean throwException) { + Span.current().setAttribute("cartId", cartId); + if (throwException) { + throw new RuntimeException("Error!"); + } + return cartId; + } + + private static void resetCheckout(String cartId, boolean throwException) { + Span.current().setAttribute("cartId", cartId); + if (throwException) { + throw new RuntimeException("Error!"); + } + } +} diff --git a/api/incubator/src/test/java/io/opentelemetry/api/incubator/trace/ExtendedTracerTest.java b/api/incubator/src/test/java/io/opentelemetry/api/incubator/trace/ExtendedTracerTest.java deleted file mode 100644 index 20ad50ca0be..00000000000 --- a/api/incubator/src/test/java/io/opentelemetry/api/incubator/trace/ExtendedTracerTest.java +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.api.incubator.trace; - -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatException; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.junit.jupiter.api.Named.named; - -import com.google.errorprone.annotations.Keep; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.incubator.propagation.ExtendedContextPropagators; -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.context.Context; -import io.opentelemetry.context.Scope; -import io.opentelemetry.context.propagation.ContextPropagators; -import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; -import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; -import io.opentelemetry.sdk.trace.data.StatusData; -import io.opentelemetry.semconv.ClientAttributes; -import io.opentelemetry.semconv.ExceptionAttributes; -import java.time.Instant; -import java.util.Collections; -import java.util.Map; -import java.util.function.BiConsumer; -import java.util.stream.Stream; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -class ExtendedTracerTest { - - interface ThrowingBiConsumer { - void accept(T t, U u) throws Throwable; - } - - @RegisterExtension - static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); - - private final ExtendedTracer extendedTracer = - ExtendedTracer.create(otelTesting.getOpenTelemetry().getTracer("test")); - - @Test - void wrapInSpan() { - assertThatIllegalStateException() - .isThrownBy( - () -> - extendedTracer - .spanBuilder("test") - .startAndRun( - () -> { - // runs in span - throw new IllegalStateException("ex"); - })); - - String result = - extendedTracer - .spanBuilder("another test") - .startAndCall( - () -> { - // runs in span - return "result"; - }); - assertThat(result).isEqualTo("result"); - - otelTesting - .assertTraces() - .hasTracesSatisfyingExactly( - trace -> - trace.hasSpansSatisfyingExactly( - span -> - span.hasName("test") - .hasStatus(StatusData.error()) - .hasEventsSatisfyingExactly( - event -> - event - .hasName("exception") - .hasAttributesSatisfyingExactly( - equalTo( - ExceptionAttributes.EXCEPTION_TYPE, - "java.lang.IllegalStateException"), - satisfies( - ExceptionAttributes.EXCEPTION_STACKTRACE, - string -> - string.contains( - "java.lang.IllegalStateException: ex")), - equalTo(ExceptionAttributes.EXCEPTION_MESSAGE, "ex")))), - trace -> trace.hasSpansSatisfyingExactly(a -> a.hasName("another test"))); - } - - @Test - void propagation() { - extendedTracer - .spanBuilder("parent") - .startAndRun( - () -> { - ContextPropagators propagators = otelTesting.getOpenTelemetry().getPropagators(); - Map propagationHeaders = - ExtendedContextPropagators.getTextMapPropagationContext(propagators); - assertThat(propagationHeaders).hasSize(1).containsKey("traceparent"); - - // make sure the parent span is not stored in a thread local anymore - Span invalid = Span.getInvalid(); - //noinspection unused - try (Scope unused = invalid.makeCurrent()) { - extendedTracer - .spanBuilder("child") - .setSpanKind(SpanKind.SERVER) - .setParent(Context.current()) - .setNoParent() - .setParentFrom(propagators, propagationHeaders) - .setAttribute( - "key", - "value") // any method can be called here on the span (and we increase the - // test coverage) - .setAttribute("key2", 0) - .setAttribute("key3", 0.0) - .setAttribute("key4", false) - .setAttribute(ClientAttributes.CLIENT_PORT, 1234L) - .addLink(invalid.getSpanContext()) - .addLink(invalid.getSpanContext(), Attributes.empty()) - .setAllAttributes(Attributes.empty()) - .setStartTimestamp(0, java.util.concurrent.TimeUnit.NANOSECONDS) - .setStartTimestamp(Instant.MIN) - .startAndRun(() -> {}); - } - }); - - otelTesting - .assertTraces() - .hasTracesSatisfyingExactly( - trace -> - trace.hasSpansSatisfyingExactly( - SpanDataAssert::hasNoParent, span -> span.hasParent(trace.getSpan(0)))); - } - - private static class ExtractAndRunParameter { - private final ThrowingBiConsumer> extractAndRun; - private final SpanKind wantKind; - private final StatusData wantStatus; - - private ExtractAndRunParameter( - ThrowingBiConsumer> extractAndRun, - SpanKind wantKind, - StatusData wantStatus) { - this.extractAndRun = extractAndRun; - this.wantKind = wantKind; - this.wantStatus = wantStatus; - } - } - - @Keep - private static Stream extractAndRun() { - BiConsumer ignoreException = - (span, throwable) -> { - // ignore - }; - return Stream.of( - Arguments.of( - named( - "server", - new ExtractAndRunParameter( - (t, c) -> - t.spanBuilder("span") - .setSpanKind(SpanKind.SERVER) - .setParentFrom( - otelTesting.getOpenTelemetry().getPropagators(), - Collections.emptyMap()) - .startAndCall(c), - SpanKind.SERVER, - StatusData.error()))), - Arguments.of( - named( - "server - ignore exception", - new ExtractAndRunParameter( - (t, c) -> - t.spanBuilder("span") - .setSpanKind(SpanKind.SERVER) - .setParentFrom( - otelTesting.getOpenTelemetry().getPropagators(), - Collections.emptyMap()) - .startAndCall(c, ignoreException), - SpanKind.SERVER, - StatusData.unset())))); - } - - @ParameterizedTest - @MethodSource - void extractAndRun(ExtractAndRunParameter parameter) { - assertThatException() - .isThrownBy( - () -> - parameter.extractAndRun.accept( - extendedTracer, - () -> { - throw new RuntimeException("ex"); - })); - - otelTesting - .assertTraces() - .hasTracesSatisfyingExactly( - trace -> - trace.hasSpansSatisfyingExactly( - span -> span.hasKind(parameter.wantKind).hasStatus(parameter.wantStatus))); - } -} diff --git a/sdk/trace/build.gradle.kts b/sdk/trace/build.gradle.kts index ae87071507a..34690eaeeca 100644 --- a/sdk/trace/build.gradle.kts +++ b/sdk/trace/build.gradle.kts @@ -22,6 +22,8 @@ dependencies { api(project(":api:all")) api(project(":sdk:common")) + implementation(project(":api:incubator")) + compileOnly(project(":sdk:trace-shaded-deps")) annotationProcessor("com.google.auto.value:auto-value") diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpanBuilder.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpanBuilder.java index fa823fa179a..1a9a946ae5b 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpanBuilder.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpanBuilder.java @@ -12,14 +12,21 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.incubator.propagation.ExtendedContextPropagators; +import io.opentelemetry.api.incubator.trace.ExtendedSpanBuilder; +import io.opentelemetry.api.incubator.trace.SpanCallable; +import io.opentelemetry.api.incubator.trace.SpanRunnable; import io.opentelemetry.api.internal.ImmutableSpanContext; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanBuilder; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; import io.opentelemetry.api.trace.TraceFlags; import io.opentelemetry.api.trace.TraceState; import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.context.propagation.ContextPropagators; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.internal.AttributeUtil; import io.opentelemetry.sdk.internal.AttributesMap; @@ -29,11 +36,13 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; import javax.annotation.Nullable; /** {@link SdkSpanBuilder} is SDK implementation of {@link SpanBuilder}. */ -final class SdkSpanBuilder implements SpanBuilder { +final class SdkSpanBuilder implements ExtendedSpanBuilder { private final String spanName; private final InstrumentationScopeInfo instrumentationScopeInfo; @@ -163,6 +172,13 @@ public SpanBuilder setStartTimestamp(long startTimestamp, TimeUnit unit) { return this; } + @Override + public ExtendedSpanBuilder setParentFrom( + ContextPropagators propagators, Map carrier) { + setParent(ExtendedContextPropagators.extractTextMapPropagationContext(carrier, propagators)); + return this; + } + @Override @SuppressWarnings({"unchecked", "rawtypes"}) public Span startSpan() { @@ -234,6 +250,44 @@ public Span startSpan() { startEpochNanos); } + @Override + public T startAndCall(SpanCallable spanCallable) throws E { + return startAndCall(spanCallable, SdkSpanBuilder::setSpanError); + } + + @Override + public T startAndCall( + SpanCallable spanCallable, BiConsumer handleException) throws E { + Span span = startSpan(); + + //noinspection unused + try (Scope unused = span.makeCurrent()) { + return spanCallable.callInSpan(); + } catch (Throwable e) { + handleException.accept(span, e); + throw e; + } finally { + span.end(); + } + } + + @Override + public void startAndRun(SpanRunnable runnable) throws E { + startAndRun(runnable, SdkSpanBuilder::setSpanError); + } + + @SuppressWarnings("NullAway") + @Override + public void startAndRun( + SpanRunnable runnable, BiConsumer handleException) throws E { + startAndCall( + () -> { + runnable.runInSpan(); + return null; + }, + handleException); + } + private AttributesMap attributes() { AttributesMap attributes = this.attributes; if (attributes == null) { @@ -255,4 +309,15 @@ static boolean isRecording(SamplingDecision decision) { static boolean isSampled(SamplingDecision decision) { return SamplingDecision.RECORD_AND_SAMPLE.equals(decision); } + + /** + * Marks a span as error. This is the default exception handler. + * + * @param span the span + * @param exception the exception that caused the error + */ + private static void setSpanError(Span span, Throwable exception) { + span.setStatus(StatusCode.ERROR); + span.recordException(exception); + } }