diff --git a/extensions/incubator/README.md b/extensions/incubator/README.md new file mode 100644 index 0000000000..64264d0168 --- /dev/null +++ b/extensions/incubator/README.md @@ -0,0 +1,167 @@ +# ExtendedTracer + +Utility methods to make it easier to use the OpenTelemetry tracer. + +## Usage Examples + +Here are some examples how the utility methods can help reduce boilerplate code. + +### 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); +``` diff --git a/extensions/incubator/build.gradle.kts b/extensions/incubator/build.gradle.kts index 6e79f9361f..5812f6462a 100644 --- a/extensions/incubator/build.gradle.kts +++ b/extensions/incubator/build.gradle.kts @@ -14,5 +14,6 @@ dependencies { annotationProcessor("com.google.auto.value:auto-value") + testImplementation("io.opentelemetry.semconv:opentelemetry-semconv:1.21.0-alpha") testImplementation(project(":sdk:testing")) } diff --git a/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/propagation/CaseInsensitiveMap.java b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/propagation/CaseInsensitiveMap.java new file mode 100644 index 0000000000..5c6d7840a5 --- /dev/null +++ b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/propagation/CaseInsensitiveMap.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.incubator.propagation; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import javax.annotation.Nullable; + +class CaseInsensitiveMap extends HashMap { + + private static final long serialVersionUID = -4202518750189126871L; + + CaseInsensitiveMap(Map carrier) { + super(carrier); + } + + @Override + public String put(String key, String value) { + return super.put(key.toLowerCase(Locale.ROOT), value); + } + + @Override + @Nullable + public String get(Object key) { + return super.get(((String) key).toLowerCase(Locale.ROOT)); + } +} diff --git a/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/propagation/ExtendedContextPropagators.java b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/propagation/ExtendedContextPropagators.java new file mode 100644 index 0000000000..8095d81b69 --- /dev/null +++ b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/propagation/ExtendedContextPropagators.java @@ -0,0 +1,81 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.incubator.propagation; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapGetter; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import javax.annotation.Nullable; + +/** + * Utility class to simplify context propagation. + * + *

The README + * explains the use cases in more detail. + */ +public final class ExtendedContextPropagators { + + private ExtendedContextPropagators() {} + + private static final 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); + } + }; + + /** + * Injects the current context into a string map, which can then be added to HTTP headers or the + * metadata of a message. + * + * @param propagators provide the propagators from {@link OpenTelemetry#getPropagators()} + */ + public static Map getTextMapPropagationContext(ContextPropagators propagators) { + Map carrier = new HashMap<>(); + propagators + .getTextMapPropagator() + .inject( + Context.current(), + carrier, + (map, key, value) -> { + if (map != null) { + map.put(key, value); + } + }); + + return Collections.unmodifiableMap(carrier); + } + + /** + * Extract the context from a string map, which you get from HTTP headers of the metadata of a + * message you're processing. + * + * @param carrier the string map + * @param propagators provide the propagators from {@link OpenTelemetry#getPropagators()} + */ + public static Context extractTextMapPropagationContext( + Map carrier, ContextPropagators propagators) { + Context current = Context.current(); + if (carrier == null) { + return current; + } + CaseInsensitiveMap caseInsensitiveMap = new CaseInsensitiveMap(carrier); + return propagators.getTextMapPropagator().extract(current, caseInsensitiveMap, TEXT_MAP_GETTER); + } +} diff --git a/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/trace/ExtendedSpanBuilder.java b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/trace/ExtendedSpanBuilder.java new file mode 100644 index 0000000000..f28b75dceb --- /dev/null +++ b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/trace/ExtendedSpanBuilder.java @@ -0,0 +1,230 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.incubator.trace; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.extension.incubator.propagation.ExtendedContextPropagators; +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; + } + + /** + * Extract a span context from the given carrier and set it as parent of the span for {@link + * #startAndCall(SpanCallable)} and {@link #startAndRun(SpanRunnable)}. + * + *

The span context will be extracted from the carrier, which you usually get from + * HTTP headers of the metadata of a message you're processing. + * + *

A typical usage would be: + * ExtendedTracer.create(tracer) + * .setSpanKind(SpanKind.SERVER) + * .setParentFrom(propagators, carrier) + * .run("my-span", () -> { ... }); + * + * + * @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(); + } + + /** + * Runs the given {@link SpanCallable} inside of the span created by the given {@link + * SpanBuilder}. The span will be ended at the end of the {@link SpanCallable}. + * + *

If an exception is thrown by the {@link SpanCallable}, the span will be marked as error, and + * the exception will be recorded. + * + * @param spanCallable the {@link SpanCallable} to call + * @param the type of the result + * @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); + } + + /** + * Runs the given {@link SpanCallable} inside of the span created by the given {@link + * SpanBuilder}. The span will be ended at the end of the {@link SpanCallable}. + * + *

If an exception is thrown by the {@link SpanCallable}, the handleException + * consumer will be called, giving you the opportunity to handle the exception and span in a + * custom way, e.g. not marking the span as error. + * + * @param spanCallable the {@link SpanCallable} to call + * @param handleException the consumer to call when an exception is thrown + * @param the type of the result + * @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(); + } + } + + /** + * Runs the given {@link SpanRunnable} inside of the span created by the given {@link + * SpanBuilder}. The span will be ended at the end of the {@link SpanRunnable}. + * + *

If an exception is thrown by the {@link SpanRunnable}, the span will be marked as error, and + * the exception will be recorded. + * + * @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); + } + + /** + * Runs the given {@link SpanRunnable} inside of the span created by the given {@link + * SpanBuilder}. The span will be ended at the end of the {@link SpanRunnable}. + * + *

If an exception is thrown by the {@link SpanRunnable}, the handleException + * consumer will be called, giving you the opportunity to handle the exception and span in a + * custom way, e.g. not marking the span as error. + * + * @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); + } +} diff --git a/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/trace/ExtendedTracer.java b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/trace/ExtendedTracer.java index 1be6bc899f..a564063e49 100644 --- a/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/trace/ExtendedTracer.java +++ b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/trace/ExtendedTracer.java @@ -5,54 +5,40 @@ package io.opentelemetry.extension.incubator.trace; -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.api.trace.SpanBuilder; import io.opentelemetry.api.trace.Tracer; -import io.opentelemetry.context.Scope; -import java.util.concurrent.Callable; -/** Provides easy mechanisms for wrapping standard Java constructs with an OpenTelemetry Span. */ +/** + * Utility class to simplify tracing. + * + *

The README + * explains the use cases in more detail. + */ public final class ExtendedTracer implements Tracer { private final Tracer delegate; - /** Create a new {@link ExtendedTracer} that wraps the provided Tracer. */ - public static ExtendedTracer create(Tracer delegate) { - return new ExtendedTracer(delegate); - } - private ExtendedTracer(Tracer delegate) { this.delegate = delegate; } - /** Run the provided {@link Runnable} and wrap with a {@link Span} with the provided name. */ - public void run(String spanName, Runnable runnable) { - Span span = delegate.spanBuilder(spanName).startSpan(); - try (Scope scope = span.makeCurrent()) { - runnable.run(); - } catch (Throwable e) { - span.recordException(e); - throw e; - } finally { - span.end(); - } - } - - /** Call the provided {@link Callable} and wrap with a {@link Span} with the provided name. */ - public T call(String spanName, Callable callable) throws Exception { - Span span = delegate.spanBuilder(spanName).startSpan(); - try (Scope scope = span.makeCurrent()) { - return callable.call(); - } catch (Throwable e) { - span.recordException(e); - throw e; - } finally { - span.end(); - } + /** + * 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 SpanBuilder spanBuilder(String spanName) { - return delegate.spanBuilder(spanName); + public ExtendedSpanBuilder spanBuilder(String spanName) { + return new ExtendedSpanBuilder(delegate.spanBuilder(spanName)); } } diff --git a/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/trace/SpanCallable.java b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/trace/SpanCallable.java new file mode 100644 index 0000000000..eb87683f04 --- /dev/null +++ b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/trace/SpanCallable.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.incubator.trace; + +/** + * An interface for creating a lambda that is wrapped in a span, returns a value, and that may + * throw. + * + * @param Thrown exception type. + */ +@FunctionalInterface +public interface SpanCallable { + T callInSpan() throws E; +} diff --git a/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/trace/SpanRunnable.java b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/trace/SpanRunnable.java new file mode 100644 index 0000000000..508df5a6c3 --- /dev/null +++ b/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/trace/SpanRunnable.java @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.incubator.trace; + +/** + * An interface for creating a lambda that is wrapped in a span and that may throw. + * + * @param Thrown exception type. + */ +@FunctionalInterface +public interface SpanRunnable { + void runInSpan() throws E; +} diff --git a/extensions/incubator/src/test/java/io/opentelemetry/extension/incubator/trace/ExtendedTracerTest.java b/extensions/incubator/src/test/java/io/opentelemetry/extension/incubator/trace/ExtendedTracerTest.java index 537fb8aa44..2268d6e59a 100644 --- a/extensions/incubator/src/test/java/io/opentelemetry/extension/incubator/trace/ExtendedTracerTest.java +++ b/extensions/incubator/src/test/java/io/opentelemetry/extension/incubator/trace/ExtendedTracerTest.java @@ -5,113 +5,209 @@ package io.opentelemetry.extension.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.assertThatThrownBy; +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 io.opentelemetry.api.common.AttributeKey; +import com.google.errorprone.annotations.Keep; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.Span; -import io.opentelemetry.api.trace.Tracer; +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.extension.incubator.propagation.ExtendedContextPropagators; +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.SemanticAttributes; +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 Tracer tracer = otelTesting.getOpenTelemetry().getTracer("test"); + private final ExtendedTracer extendedTracer = + ExtendedTracer.create(otelTesting.getOpenTelemetry().getTracer("test")); @Test - void runRunnable() { - ExtendedTracer.create(tracer).run("testSpan", () -> Span.current().setAttribute("one", 1)); - - otelTesting - .assertTraces() - .hasTracesSatisfyingExactly( - traceAssert -> - traceAssert.hasSpansSatisfyingExactly( - spanDataAssert -> - spanDataAssert - .hasName("testSpan") - .hasAttributes(Attributes.of(AttributeKey.longKey("one"), 1L)))); - } - - @Test - void runRunnable_throws() { - assertThatThrownBy( + void wrapInSpan() { + assertThatIllegalStateException() + .isThrownBy( () -> - ExtendedTracer.create(tracer) - .run( - "throwingRunnable", + extendedTracer + .spanBuilder("test") + .startAndRun( () -> { - Span.current().setAttribute("one", 1); - throw new RuntimeException("failed"); - })) - .isInstanceOf(RuntimeException.class); + // 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( - traceAssert -> - traceAssert.hasSpansSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( span -> - span.hasName("throwingRunnable") - .hasAttributes(Attributes.of(AttributeKey.longKey("one"), 1L)) - .hasEventsSatisfying( - (events) -> - assertThat(events) - .singleElement() - .satisfies( - eventData -> - assertThat(eventData.getName()) - .isEqualTo("exception"))))); + span.hasName("test") + .hasStatus(StatusData.error()) + .hasEventsSatisfyingExactly( + event -> + event + .hasName("exception") + .hasAttributesSatisfyingExactly( + equalTo( + SemanticAttributes.EXCEPTION_TYPE, + "java.lang.IllegalStateException"), + satisfies( + SemanticAttributes.EXCEPTION_STACKTRACE, + string -> + string.contains( + "java.lang.IllegalStateException: ex")), + equalTo(SemanticAttributes.EXCEPTION_MESSAGE, "ex")))), + trace -> trace.hasSpansSatisfyingExactly(a -> a.hasName("another test"))); } @Test - void callCallable() throws Exception { - assertThat( - ExtendedTracer.create(tracer) - .call( - "spanCallable", - () -> { - Span.current().setAttribute("one", 1); - return "hello"; - })) - .isEqualTo("hello"); + 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(SemanticAttributes.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( - traceAssert -> - traceAssert.hasSpansSatisfyingExactly( - spanDataAssert -> - spanDataAssert - .hasName("spanCallable") - .hasAttributes(Attributes.of(AttributeKey.longKey("one"), 1L)))); + trace -> + trace.hasSpansSatisfyingExactly( + SpanDataAssert::hasNoParent, span -> span.hasParent(trace.getSpan(0)))); } - @Test - void callCallable_throws() { - assertThatThrownBy( + 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( () -> - ExtendedTracer.create(tracer) - .call( - "throwingCallable", - () -> { - Span.current().setAttribute("one", 1); - throw new RuntimeException("failed"); - })) - .isInstanceOf(RuntimeException.class); + parameter.extractAndRun.accept( + extendedTracer, + () -> { + throw new RuntimeException("ex"); + })); otelTesting .assertTraces() .hasTracesSatisfyingExactly( - traceAssert -> - traceAssert.hasSpansSatisfyingExactly( - spanDataAssert -> - spanDataAssert - .hasName("throwingCallable") - .hasAttributes(Attributes.of(AttributeKey.longKey("one"), 1L)))); + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasKind(parameter.wantKind).hasStatus(parameter.wantStatus))); } }