diff --git a/.github/scripts/draft-change-log-entries.sh b/.github/scripts/draft-change-log-entries.sh index b31e51970..45be1ebc6 100755 --- a/.github/scripts/draft-change-log-entries.sh +++ b/.github/scripts/draft-change-log-entries.sh @@ -34,6 +34,7 @@ component_names["jmx-metrics/"]="JMX metrics" component_names["maven-extension/"]="Maven extension" component_names["micrometer-meter-provider/"]="Micrometer MeterProvider" component_names["noop-api/"]="No-op API" +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/build.gradle.kts b/extended-tracer/build.gradle.kts new file mode 100644 index 000000000..6179cc03e --- /dev/null +++ b/extended-tracer/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("otel.java-conventions") + id("otel.publish-conventions") +} + +description = "Tracing Utilities" +otelJava.moduleName.set("io.opentelemetry.contrib.extended-tracer") + +dependencies { + api("io.opentelemetry:opentelemetry-api") +} diff --git a/extended-tracer/src/main/java/io/opentelemetry/contrib/tracer/ExtendedSpanBuilder.java b/extended-tracer/src/main/java/io/opentelemetry/contrib/tracer/ExtendedSpanBuilder.java new file mode 100644 index 000000000..db05614f6 --- /dev/null +++ b/extended-tracer/src/main/java/io/opentelemetry/contrib/tracer/ExtendedSpanBuilder.java @@ -0,0 +1,126 @@ +package io.opentelemetry.contrib.tracer;/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +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.context.Context; +import java.time.Instant; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + +public class ExtendedSpanBuilder implements SpanBuilder { + + private final SpanBuilder delegate; + + ExtendedSpanBuilder(SpanBuilder delegate) { + this.delegate = delegate; + } + + /** Run the provided {@link Runnable} and wrap with a {@link Span} with the provided name. */ + public void run(Runnable runnable) { + Tracing.run(startSpan(), runnable); + } + + /** Call the provided {@link Callable} and wrap with a {@link Span} with the provided name. */ + public T call(Callable callable) { + return Tracing.call(startSpan(), callable); + } + + /** + * Trace a block of code using a server span. + * + *

The span context will be extracted from the transport, which you usually get + * from HTTP headers of the metadata of an event you're processing. + */ + public T traceServerSpan(Map transport, Callable callable) { + return Tracing.traceServerSpan(transport, this, callable); + } + + /** + * Trace a block of code using a consumer span. + * + *

The span context will be extracted from the transport, which you usually get + * from HTTP headers of the metadata of an event you're processing. + */ + public T traceConsumerSpan( + Map transport, SpanBuilder spanBuilder, Callable callable) { + return Tracing.traceConsumerSpan(transport, this, callable); + } + + @Override + public SpanBuilder setParent(Context context) { + return delegate.setParent(context); + } + + @Override + public SpanBuilder setNoParent() { + return delegate.setNoParent(); + } + + @Override + public SpanBuilder addLink(SpanContext spanContext) { + return delegate.addLink(spanContext); + } + + @Override + public SpanBuilder addLink(SpanContext spanContext, Attributes attributes) { + return delegate.addLink(spanContext, attributes); + } + + @Override + public SpanBuilder setAttribute(String key, String value) { + return delegate.setAttribute(key, value); + } + + @Override + public SpanBuilder setAttribute(String key, long value) { + return delegate.setAttribute(key, value); + } + + @Override + public SpanBuilder setAttribute(String key, double value) { + return delegate.setAttribute(key, value); + } + + @Override + public SpanBuilder setAttribute(String key, boolean value) { + return delegate.setAttribute(key, value); + } + + @Override + public SpanBuilder setAttribute(AttributeKey key, T value) { + return delegate.setAttribute(key, value); + } + + @Override + public SpanBuilder setAllAttributes(Attributes attributes) { + return delegate.setAllAttributes(attributes); + } + + @Override + public SpanBuilder setSpanKind(SpanKind spanKind) { + return delegate.setSpanKind(spanKind); + } + + @Override + public SpanBuilder setStartTimestamp(long startTimestamp, TimeUnit unit) { + return delegate.setStartTimestamp(startTimestamp, unit); + } + + @Override + public SpanBuilder setStartTimestamp(Instant startTimestamp) { + return delegate.setStartTimestamp(startTimestamp); + } + + @Override + public Span startSpan() { + return delegate.startSpan(); + } +} diff --git a/extended-tracer/src/main/java/io/opentelemetry/contrib/tracer/ExtendedTracer.java b/extended-tracer/src/main/java/io/opentelemetry/contrib/tracer/ExtendedTracer.java new file mode 100644 index 000000000..e799a1a70 --- /dev/null +++ b/extended-tracer/src/main/java/io/opentelemetry/contrib/tracer/ExtendedTracer.java @@ -0,0 +1,61 @@ +package io.opentelemetry.contrib.tracer;/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import java.util.Map; +import java.util.concurrent.Callable; + +/** Provides easy mechanisms for wrapping standard Java constructs with an OpenTelemetry Span. */ +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) { + spanBuilder(spanName).run(runnable); + } + + /** Call the provided {@link Callable} and wrap with a {@link Span} with the provided name. */ + public T call(String spanName, Callable callable) { + return spanBuilder(spanName).call(callable); + } + + /** + * Injects the current context into a string map, which can then be added to HTTP headers or the + * metadata of an event. + */ + public Map injectContext() { + return Tracing.injectContext(); + } + + /** + * Extract the context from a string map, which you get from HTTP headers of the metadata of an + * event you're processing. + */ + public static Context extractContext(Map transport) { + return Tracing.extractContext(transport); + } + + /** Sets baggage items which are active in given block. */ + public static T setBaggage(Map baggage, Callable callable) { + return Tracing.setBaggage(baggage, callable); + } + + @Override + public ExtendedSpanBuilder spanBuilder(String spanName) { + return new ExtendedSpanBuilder(delegate.spanBuilder(spanName)); + } +} 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..e38c1eaf3 --- /dev/null +++ b/extended-tracer/src/main/java/io/opentelemetry/contrib/tracer/Tracing.java @@ -0,0 +1,271 @@ +package io.opentelemetry.contrib.tracer;/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CONSUMER; +import static io.opentelemetry.api.trace.SpanKind.SERVER; + +import io.opentelemetry.api.GlobalOpenTelemetry; +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.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.context.propagation.TextMapGetter; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; + +public class Tracing { + + private static final TextMapGetter> TEXT_MAP_GETTER = + new TextMapGetter>() { + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Override + public String get(Map carrier, String key) { + //noinspection ConstantConditions + return carrier.get(key); + } + }; + + private Tracing() {} + + public static void run(String spanName, Runnable runnable) { + serviceTracer().run(spanName, runnable); + } + + public static void run(Span span, Runnable runnable) { + call( + span, + () -> { + runnable.run(); + return null; + }); + } + + /** + * Runs a block of code with a new span - ending the span at the end and recording any exception. + * + * @param spanName name of the new span + */ + public static T call(String spanName, Callable callable) { + return serviceTracer().call(spanName, callable); + } + + public static T call(Span span, Callable callable) { + return call(span, callable, Tracing::setSpanError); + } + + public static T call( + Span span, Callable callable, BiConsumer handleException) { + //noinspection unused + try (Scope scope = span.makeCurrent()) { + return callable.call(); + } catch (Exception e) { + handleException.accept(span, e); + sneakyThrow(e); + return null; + } finally { + span.end(); + } + } + + /** + * @return the tracer to be used in an service + */ + public static ExtendedTracer serviceTracer() { + return ExtendedTracer.create(GlobalOpenTelemetry.getTracer("service")); + } + + public static ExtendedSpanBuilder withSpan(String spanName) { + return serviceTracer().spanBuilder(spanName); + } + + /** + * Marks the current span as error. + * + * @param description what went wrong + */ + public static void setSpanError(String description) { + setSpanError(Span.current(), description); + } + + /** + * Marks the current span as error. + * + * @param exception the exception that caused the error + */ + public static void setSpanError(Throwable exception) { + setSpanError(Span.current(), exception); + } + + /** + * Marks the current span as error. + * + * @param description what went wrong + * @param exception the exception that caused the error + */ + public static void setSpanError(String description, Throwable exception) { + setSpanError(Span.current(), description, exception); + } + + /** + * Marks a span as error. + * + * @param span the span + * @param description what went wrong + */ + public static void setSpanError(Span span, String description) { + span.setStatus(StatusCode.ERROR, description); + } + + /** + * Marks a span as error. + * + * @param span the span + * @param exception the exception that caused the error + */ + public static void setSpanError(Span span, Throwable exception) { + span.setStatus(StatusCode.ERROR); + span.recordException(exception); + } + + /** + * Marks a span as error. + * + * @param span the span + * @param description what went wrong + * @param exception the exception that caused the error + */ + public static void setSpanError(Span span, String description, Throwable exception) { + span.setStatus(StatusCode.ERROR, description); + 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 static Map injectContext() { + Map transport = new HashMap<>(); + //noinspection ConstantConditions + GlobalOpenTelemetry.get() + .getPropagators() + .getTextMapPropagator() + .inject(Context.current(), transport, Map::put); + return transport; + } + + /** + * Extract the context from a string map, which you get from HTTP headers of the metadata of an + * event you're processing. + */ + public static Context extractContext(Map transport) { + Context current = Context.current(); + //noinspection ConstantConditions + if (transport == 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 normalizedTransport = + transport.entrySet().stream() + .collect( + Collectors.toMap( + entry -> entry.getKey().toLowerCase(Locale.ROOT), Map.Entry::getValue)); + return GlobalOpenTelemetry.get() + .getPropagators() + .getTextMapPropagator() + .extract(current, normalizedTransport, TEXT_MAP_GETTER); + } + + /** Sets baggage items which are active in given block. */ + public static T setBaggage(Map baggage, Callable callable) { + BaggageBuilder builder = Baggage.current().toBuilder(); + baggage.forEach(builder::put); + Context context = builder.build().storeInContext(Context.current()); + try (Scope ignore = context.makeCurrent()) { + return callable.call(); + } catch (Throwable e) { + sneakyThrow(e); + return null; + } + } + + /** + * Trace a block of code using a server span. + * + *

The span context will be extracted from the transport, which you usually get + * from HTTP headers of the metadata of an event you're processing. + */ + public static T traceServerSpan( + Map transport, SpanBuilder spanBuilder, Callable callable) { + return extractAndRun(SERVER, transport, spanBuilder, callable, Tracing::setSpanError); + } + + /** + * Trace a block of code using a server span. + * + *

The span context will be extracted from the transport, which you usually get + * from HTTP headers of the metadata of an event you're processing. + */ + public static T traceServerSpan( + Map transport, + SpanBuilder spanBuilder, + Callable callable, + BiConsumer handleException) { + return extractAndRun(SERVER, transport, spanBuilder, callable, handleException); + } + + /** + * Trace a block of code using a consumer span. + * + *

The span context will be extracted from the transport, which you usually get + * from HTTP headers of the metadata of an event you're processing. + */ + public static T traceConsumerSpan( + Map transport, SpanBuilder spanBuilder, Callable callable) { + return extractAndRun(CONSUMER, transport, spanBuilder, callable, Tracing::setSpanError); + } + + /** + * Trace a block of code using a consumer span. + * + *

The span context will be extracted from the transport, which you usually get + * from HTTP headers of the metadata of an event you're processing. + */ + public static T traceConsumerSpan( + Map transport, + SpanBuilder spanBuilder, + Callable callable, + BiConsumer handleException) { + return extractAndRun(CONSUMER, transport, spanBuilder, callable, handleException); + } + + private static T extractAndRun( + SpanKind spanKind, + Map transport, + SpanBuilder spanBuilder, + Callable callable, + BiConsumer handleException) { + try (Scope ignore = extractContext(transport).makeCurrent()) { + return call(spanBuilder.setSpanKind(spanKind).startSpan(), callable, handleException); + } + } + + @SuppressWarnings("unchecked") + private static void sneakyThrow(Throwable e) throws E { + throw (E) e; + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index dc04fe183..e8d9216e9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -49,6 +49,7 @@ include(":jfr-streaming") include(":jmx-metrics") include(":maven-extension") include(":micrometer-meter-provider") +include(":extended-tracer") include(":noop-api") include(":prometheus-client-bridge") include(":resource-providers")