diff --git a/.github/scripts/draft-change-log-entries.sh b/.github/scripts/draft-change-log-entries.sh index 55eb299b6..eff9b6e0a 100755 --- a/.github/scripts/draft-change-log-entries.sh +++ b/.github/scripts/draft-change-log-entries.sh @@ -36,6 +36,7 @@ component_names["maven-extension/"]="Maven extension" component_names["micrometer-meter-provider/"]="Micrometer MeterProvider" component_names["noop-api/"]="No-op API" component_names["processors/"]="Telemetry processors" +component_names["extended-tracer/"]="Extended Tracer" component_names["prometheus-client-bridge/"]="Prometheus client bridge" component_names["runtime-attach/"]="Runtime attach" component_names["resource-providers/"]="Resource providers" diff --git a/extended-tracer/README.md b/extended-tracer/README.md new file mode 100644 index 000000000..78f20f455 --- /dev/null +++ b/extended-tracer/README.md @@ -0,0 +1,168 @@ +# extended-tracer + +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 +Tracing tracing = new Tracing(openTelemetry, "service"); +String transactionId = tracing.call("reset_checkout", () -> resetCheckout(cartId)); +``` + +Note: + +- Use `run` instead of `call` if the function returns `void`. +- 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"); +String transactionId; + +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) { + //noinspection ConstantConditions + 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); +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 +Tracing tracing = new Tracing(openTelemetry, "service"); +Map propagationHeaders = tracing.getTextMapPropagationContext(); +// 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"; + +Tracing tracing = new Tracing(openTelemetry, "service"); +String transactionId = tracing.traceServerSpan(requestHeaders, + tracer.spanBuilder("checkout_cart"), () -> processCheckout(cartId)); +``` + +Note: + +- You can use `traceConsumerSpan` if you want to trace a consumer (e.g. from a message queue) + instead of a server. +- Exceptions are re-thrown without modification - see [Exception handling](#exception-handling) + for more details. + +### Setting baggage entries + +Before: + +```java +BaggageBuilder builder = Baggage.current().toBuilder(); +builder.put("key", "value"); +Context context = builder.build().storeInContext(Context.current()); +try (Scope ignore = context.makeCurrent()) { + String value = Baggage.current().getEntryValue("key"); +} +``` + +After: + +```java +Tracing tracing = new Tracing(openTelemetry, "service"); +String value = Tracing.callWithBaggage( + Collections.singletonMap("key", "value"), + () -> Baggage.current().getEntryValue("key")) +``` + +## Exception handling + +`Tracing` re-throws exceptions without modification. This means you can catch exceptions around +`Tracing` calls and handle them as you would without `Tracing`. + +Note that the `Tracing` methods do not declare any checked exceptions +(the idea is taken from [@SneakyThrows](https://projectlombok.org/features/SneakyThrows)). +Declaring a checked exception would force callers to handle it, which would create a lot of +boilerplate code. Instead, `Tracing` re-throws checked exceptions as unchecked exceptions. + +## Component owners + +- [Gregor Zeitlinger](https://github.com/zeitlinger), Grafana Labs + +Learn more about component owners in [component_owners.yml](../.github/component_owners.yml). diff --git a/extended-tracer/build.gradle.kts b/extended-tracer/build.gradle.kts new file mode 100644 index 000000000..af1ac258c --- /dev/null +++ b/extended-tracer/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + id("otel.java-conventions") +} + +description = "Extended Tracer" +otelJava.moduleName.set("io.opentelemetry.contrib.extended-tracer") + +dependencies { + api("io.opentelemetry:opentelemetry-api") + testImplementation("io.opentelemetry:opentelemetry-sdk-testing") + testImplementation("io.opentelemetry.semconv:opentelemetry-semconv") +} diff --git a/extended-tracer/src/main/java/io/opentelemetry/contrib/tracer/Propagation.java b/extended-tracer/src/main/java/io/opentelemetry/contrib/tracer/Propagation.java new file mode 100644 index 000000000..66c9f84be --- /dev/null +++ b/extended-tracer/src/main/java/io/opentelemetry/contrib/tracer/Propagation.java @@ -0,0 +1,91 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.tracer; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.Nullable; + +/** + * Utility class to simplify context propagation. + * + *

The README + * explains the use cases in more detail. + */ +public final class Propagation { + + private Propagation() {} + + 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) { + //noinspection ConstantConditions + 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 an event. + */ + public static Map getTextMapPropagationContext(OpenTelemetry openTelemetry) { + Map carrier = new HashMap<>(); + //noinspection ConstantConditions + openTelemetry + .getPropagators() + .getTextMapPropagator() + .inject( + Context.current(), + carrier, + (map, key, value) -> { + if (map != null) { + map.put(key, value); + } + }); + + return carrier; + } + + /** + * Extract the context from a string map, which you get from HTTP headers of the metadata of an + * event you're processing. + * + * @param carrier the string map + */ + static Context extractTextMapPropagationContext( + OpenTelemetry openTelemetry, Map carrier) { + Context current = Context.current(); + //noinspection ConstantConditions + if (carrier == null) { + return current; + } + // HTTP headers are case-insensitive. As we're using Map, which is case-sensitive, we need to + // normalize all the keys + Map normalizedCarrier = + carrier.entrySet().stream() + .collect( + Collectors.toMap( + entry -> entry.getKey().toLowerCase(Locale.ROOT), Map.Entry::getValue)); + return openTelemetry + .getPropagators() + .getTextMapPropagator() + .extract(current, normalizedCarrier, TEXT_MAP_GETTER); + } +} diff --git a/extended-tracer/src/main/java/io/opentelemetry/contrib/tracer/SpanCallback.java b/extended-tracer/src/main/java/io/opentelemetry/contrib/tracer/SpanCallback.java new file mode 100644 index 000000000..d39e9e631 --- /dev/null +++ b/extended-tracer/src/main/java/io/opentelemetry/contrib/tracer/SpanCallback.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.tracer; + +/** + * An interface for creating a lambda that is wrapped in a span, returns a value, and that may + * throw, similar to TransactionCallback. + * + * @param Thrown exception type. + */ +@FunctionalInterface +public interface SpanCallback { + T doInSpan() throws E; +} diff --git a/extended-tracer/src/main/java/io/opentelemetry/contrib/tracer/SpanRunnable.java b/extended-tracer/src/main/java/io/opentelemetry/contrib/tracer/SpanRunnable.java new file mode 100644 index 000000000..0e440726d --- /dev/null +++ b/extended-tracer/src/main/java/io/opentelemetry/contrib/tracer/SpanRunnable.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.tracer; + +/** + * An interface for creating a lambda that is wrapped in a span and that may throw, similar to TransactionCallback. + * + * @param Thrown exception type. + */ +@FunctionalInterface +public interface SpanRunnable { + void doInSpan() throws E; +} diff --git a/extended-tracer/src/main/java/io/opentelemetry/contrib/tracer/Tracing.java b/extended-tracer/src/main/java/io/opentelemetry/contrib/tracer/Tracing.java new file mode 100644 index 000000000..d868900d4 --- /dev/null +++ b/extended-tracer/src/main/java/io/opentelemetry/contrib/tracer/Tracing.java @@ -0,0 +1,319 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.tracer; + +import static io.opentelemetry.api.trace.SpanKind.CONSUMER; +import static io.opentelemetry.api.trace.SpanKind.SERVER; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.baggage.Baggage; +import io.opentelemetry.api.baggage.BaggageBuilder; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.util.Map; +import java.util.function.BiConsumer; + +/** + * Utility class to simplify tracing. + * + *

The README + * explains the use cases in more detail. + */ +public final class Tracing { + + private final OpenTelemetry openTelemetry; + + private final Tracer tracer; + + /** + * Creates a new instance of {@link Tracing}. + * + * @param openTelemetry the {@link OpenTelemetry} instance + * @param instrumentationScopeName the name of the tracer to use + */ + public Tracing(OpenTelemetry openTelemetry, String instrumentationScopeName) { + this(openTelemetry, openTelemetry.getTracer(instrumentationScopeName)); + } + + /** + * Creates a new instance of {@link Tracing}. + * + * @param openTelemetry the {@link OpenTelemetry} instance + * @param tracer the {@link Tracer} to use + */ + public Tracing(OpenTelemetry openTelemetry, Tracer tracer) { + this.openTelemetry = openTelemetry; + this.tracer = tracer; + } + + /** + * Creates a new {@link SpanBuilder} with the given span name. + * + * @param spanName the name of the span + * @return the {@link SpanBuilder} + */ + public SpanBuilder spanBuilder(String spanName) { + return tracer.spanBuilder(spanName); + } + + /** + * Runs the given {@link SpanRunnable} in a new span with the given name and with kind INTERNAL. + * + *

If an exception is thrown by the {@link SpanRunnable}, the span will be marked as error, and + * the exception will be recorded. + * + * @param spanName the name of the span + * @param runnable the {@link SpanRunnable} to run + */ + public void run(String spanName, SpanRunnable runnable) throws E { + run(tracer.spanBuilder(spanName), runnable); + } + + /** + * 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 spanBuilder the {@link SpanBuilder} to use + * @param runnable the {@link SpanRunnable} to run + */ + @SuppressWarnings("NullAway") + public void run(SpanBuilder spanBuilder, SpanRunnable runnable) + throws E { + call( + spanBuilder, + () -> { + runnable.doInSpan(); + return null; + }); + } + + /** + * Runs the given {@link SpanCallback} inside a new span with the given name. + * + *

If an exception is thrown by the {@link SpanCallback}, the span will be marked as error, and + * the exception will be recorded. + * + * @param spanName the name of the span + * @param spanCallback the {@link SpanCallback} to call + * @param the type of the result + * @param the type of the exception + * @return the result of the {@link SpanCallback} + */ + public T call(String spanName, SpanCallback spanCallback) + throws E { + return call(tracer.spanBuilder(spanName), spanCallback); + } + + /** + * Runs the given {@link SpanCallback} inside of the span created by the given {@link + * SpanBuilder}. The span will be ended at the end of the {@link SpanCallback}. + * + *

If an exception is thrown by the {@link SpanCallback}, the span will be marked as error, and + * the exception will be recorded. + * + * @param spanBuilder the {@link SpanBuilder} to use + * @param spanCallback the {@link SpanCallback} to call + * @param the type of the result + * @param the type of the exception + * @return the result of the {@link SpanCallback} + */ + public T call(SpanBuilder spanBuilder, SpanCallback spanCallback) + throws E { + return call(spanBuilder, spanCallback, Tracing::setSpanError); + } + + /** + * Runs the given {@link SpanCallback} inside of the span created by the given {@link + * SpanBuilder}. The span will be ended at the end of the {@link SpanCallback}. + * + *

If an exception is thrown by the {@link SpanCallback}, 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 spanBuilder the {@link SpanBuilder} to use + * @param spanCallback the {@link SpanCallback} 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 SpanCallback} + */ + @SuppressWarnings("NullAway") + public T call( + SpanBuilder spanBuilder, + SpanCallback spanCallback, + BiConsumer handleException) + throws E { + Span span = spanBuilder.startSpan(); + //noinspection unused + try (Scope unused = span.makeCurrent()) { + return spanCallback.doInSpan(); + } catch (Throwable e) { + handleException.accept(span, e); + throw e; + } finally { + span.end(); + } + } + + /** + * Marks a span as error. + * + * @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); + } + + /** + * Injects the current context into a string map, which can then be added to HTTP headers or the + * metadata of an event. + */ + public Map getTextMapPropagationContext() { + return Propagation.getTextMapPropagationContext(openTelemetry); + } + + /** + * Set baggage items inside the given {@link SpanCallback}. + * + * @param baggage the baggage items to set + * @param spanCallback the {@link SpanCallback} to call + * @param the type of the result + * @param the type of the exception + * @return the result of the {@link SpanCallback} + */ + @SuppressWarnings("NullAway") + public static T callWithBaggage( + Map baggage, SpanCallback spanCallback) throws E { + BaggageBuilder builder = Baggage.current().toBuilder(); + baggage.forEach(builder::put); + Context context = builder.build().storeInContext(Context.current()); + try (Scope ignore = context.makeCurrent()) { + return spanCallback.doInSpan(); + } + } + + /** + * Run the given {@link SpanCallback} inside a server span. + * + *

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

If an exception is thrown by the {@link SpanCallback}, the span will be marked as error, and + * the exception will be recorded. + * + * @param carrier the string map where to extract the span context from + * @param spanBuilder the {@link SpanBuilder} to use + * @param spanCallback the {@link SpanCallback} to call + * @param the type of the result + * @param the type of the exception + * @return the result of the {@link SpanCallback} + */ + public T traceServerSpan( + Map carrier, SpanBuilder spanBuilder, SpanCallback spanCallback) + throws E { + return extractAndRun(SERVER, carrier, spanBuilder, spanCallback, Tracing::setSpanError); + } + + /** + * Run the given {@link SpanCallback} inside a server span. + * + *

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

If an exception is thrown by the {@link SpanCallback}, 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 carrier the string map where to extract the span context from + * @param spanBuilder the {@link SpanBuilder} to use + * @param spanCallback the {@link SpanCallback} 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 SpanCallback} + */ + public T traceServerSpan( + Map carrier, + SpanBuilder spanBuilder, + SpanCallback spanCallback, + BiConsumer handleException) + throws E { + return extractAndRun(SERVER, carrier, spanBuilder, spanCallback, handleException); + } + + /** + * Run the given {@link SpanCallback} inside a server span. + * + *

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

If an exception is thrown by the {@link SpanCallback}, the span will be marked as error, and + * the exception will be recorded. + * + * @param carrier the string map where to extract the span context from + * @param spanBuilder the {@link SpanBuilder} to use + * @param spanCallback the {@link SpanCallback} to call + * @param the type of the result + * @param the type of the exception + * @return the result of the {@link SpanCallback} + */ + public T traceConsumerSpan( + Map carrier, SpanBuilder spanBuilder, SpanCallback spanCallback) + throws E { + return extractAndRun(CONSUMER, carrier, spanBuilder, spanCallback, Tracing::setSpanError); + } + + /** + * Run the given {@link SpanRunnable} inside a consumer span. + * + *

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

If an exception is thrown by the {@link SpanCallback}, 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 carrier the string map where to extract the span context from + * @param spanBuilder the {@link SpanBuilder} to use + * @param spanCallback the {@link SpanCallback} 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 SpanCallback} + */ + public T traceConsumerSpan( + Map carrier, + SpanBuilder spanBuilder, + SpanCallback spanCallback, + BiConsumer handleException) + throws E { + return extractAndRun(CONSUMER, carrier, spanBuilder, spanCallback, handleException); + } + + private T extractAndRun( + SpanKind spanKind, + Map carrier, + SpanBuilder spanBuilder, + SpanCallback spanCallback, + BiConsumer handleException) + throws E { + try (Scope ignore = + Propagation.extractTextMapPropagationContext(openTelemetry, carrier).makeCurrent()) { + return call(spanBuilder.setSpanKind(spanKind), spanCallback, handleException); + } + } +} diff --git a/extended-tracer/src/test/java/io/opentelemetry/contrib/tracer/TracingTest.java b/extended-tracer/src/test/java/io/opentelemetry/contrib/tracer/TracingTest.java new file mode 100644 index 000000000..582582b36 --- /dev/null +++ b/extended-tracer/src/test/java/io/opentelemetry/contrib/tracer/TracingTest.java @@ -0,0 +1,200 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.tracer; + +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.baggage.Baggage; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions; +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.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; + +public class TracingTest { + + interface ThrowingBiConsumer { + void accept(T t, U u) throws Throwable; + } + + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private final Tracing tracing = new Tracing(otelTesting.getOpenTelemetry(), "test"); + + @Test + void wrapInSpan() { + assertThatIllegalStateException() + .isThrownBy( + () -> + tracing.run( + "test", + () -> { + // runs in span + throw new IllegalStateException("ex"); + })); + + String result = + tracing.call( + "another test", + () -> { + // runs in span + return "result"; + }); + assertThat(result).isEqualTo("result"); + + otelTesting + .assertTraces() + .hasSize(2) + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("test") + .hasStatus(StatusData.error()) + .hasEventsSatisfyingExactly( + event -> + event + .hasName("exception") + .hasAttributesSatisfyingExactly( + OpenTelemetryAssertions.equalTo( + SemanticAttributes.EXCEPTION_TYPE, + "java.lang.IllegalStateException"), + OpenTelemetryAssertions.satisfies( + SemanticAttributes.EXCEPTION_STACKTRACE, + string -> + string.contains( + "java.lang.IllegalStateException: ex")), + OpenTelemetryAssertions.equalTo( + SemanticAttributes.EXCEPTION_MESSAGE, "ex")))), + trace -> trace.hasSpansSatisfyingExactly(a -> a.hasName("another test"))); + } + + @Test + void propagation() { + tracing.run( + "parent", + () -> { + Map propagationHeaders = tracing.getTextMapPropagationContext(); + assertThat(propagationHeaders).hasSize(1).containsKey("traceparent"); + + tracing.traceServerSpan(propagationHeaders, tracing.spanBuilder("child"), () -> null); + }); + + otelTesting + .assertTraces() + .hasSize(1) + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + SpanDataAssert::hasNoParent, span -> span.hasParent(trace.getSpan(0)))); + } + + @Test + void callWithBaggage() { + String value = + tracing.call( + "parent", + () -> + Tracing.callWithBaggage( + Collections.singletonMap("key", "value"), + () -> Baggage.current().getEntryValue("key"))); + + assertThat(value).isEqualTo("value"); + } + + 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.traceServerSpan(Collections.emptyMap(), t.spanBuilder("span"), c), + io.opentelemetry.api.trace.SpanKind.SERVER, + io.opentelemetry.sdk.trace.data.StatusData.error()))), + Arguments.of( + named( + "server - ignore exception", + new ExtractAndRunParameter( + (t, c) -> + t.traceServerSpan( + Collections.emptyMap(), t.spanBuilder("span"), c, ignoreException), + io.opentelemetry.api.trace.SpanKind.SERVER, + io.opentelemetry.sdk.trace.data.StatusData.unset()))), + Arguments.of( + named( + "consumer", + new ExtractAndRunParameter( + (t, c) -> t.traceConsumerSpan(Collections.emptyMap(), t.spanBuilder("span"), c), + io.opentelemetry.api.trace.SpanKind.CONSUMER, + io.opentelemetry.sdk.trace.data.StatusData.error()))), + Arguments.of( + named( + "consumer - ignore exception", + new ExtractAndRunParameter( + (t, c) -> + t.traceConsumerSpan( + Collections.emptyMap(), t.spanBuilder("span"), c, ignoreException), + io.opentelemetry.api.trace.SpanKind.CONSUMER, + io.opentelemetry.sdk.trace.data.StatusData.unset())))); + } + + @ParameterizedTest + @MethodSource + void extractAndRun(ExtractAndRunParameter parameter) { + assertThatException() + .isThrownBy( + () -> + parameter.extractAndRun.accept( + tracing, + () -> { + throw new RuntimeException("ex"); + })); + + otelTesting + .assertTraces() + .hasSize(1) + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasKind(parameter.wantKind).hasStatus(parameter.wantStatus))); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index dcd8914b5..4cc198498 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -71,6 +71,7 @@ include(":jfr-connection") include(":jmx-metrics") include(":maven-extension") include(":micrometer-meter-provider") +include(":extended-tracer") include(":noop-api") include(":processors") include(":prometheus-client-bridge")