diff --git a/docs/mp/telemetry.adoc b/docs/mp/telemetry.adoc index 0fe7bb9d58a..580924f4241 100644 --- a/docs/mp/telemetry.adoc +++ b/docs/mp/telemetry.adoc @@ -69,7 +69,7 @@ For Automatic Instrumentation, OpenTelemetry provides a JavaAgent. The Tracing A For Manual Instrumentation, there is a set of annotations and access to OpenTelemetry API. -`@WithSpan` - By adding this annotation to a method in any Jakarta CDI aware bean, a new Span will be created and any necessary connections to the current Trace context will be established. Additionally, the `SpanAttribute` annotation can be used to mark method parameters that should be included in the Trace. +`@WithSpan` - By adding this annotation to a method in any Jakarta CDI aware bean, a new span will be created and any necessary connections to the current Trace context will be established. Additionally, the `SpanAttribute` annotation can be used to mark method parameters that should be included in the Trace. Helidon provides full access to OpenTelemetry Tracing API: @@ -100,7 +100,10 @@ class HelidonBean { <1> Simple `@WithSpan` annotation usage. <2> Additional attributes can be set on a method. -You can also inject OpenTelemetry `Tracer` using the regular `@Inject` annotation and use `SpanBuilder` to manually create, star, and stop Spans. + +=== Working With Tracers + +You can inject OpenTelemetry `Tracer` using the regular `@Inject` annotation and use `SpanBuilder` to manually create, star and stop spans. .SpanBuilder usage [source, java] @@ -128,6 +131,66 @@ public class HelidonEndpoint { <1> Inject `Tracer`. <2> Use `Tracer.spanBuilder` to create and start new `Span`. +Helidon Microprofile Telemetry is integrated with xref:tracing.adoc[Helidon Tracing API]. This means that both APIs can be mixed, and all parent hierarchies will be kept. In the case below, `@WithSpan` annotated method is mixed with manually created `io.helidon.tracing.Span`: + +.Inject Helidon Tracer +[source, java] +---- +private io.helidon.tracing.Tracer helidonTracerInjected; + +@Inject +GreetResource(io.helidon.tracing.Tracer helidonTracerInjected) { + this.helidonTracerInjected = helidonTracerInjected; <1> +} + +@GET +@Path("mixed_injected") +@Produces(MediaType.APPLICATION_JSON) +@WithSpan("mixed_parent_injected") +public GreetingMessage mixedSpanInjected() { + io.helidon.tracing.Span mixedSpan = helidonTracerInjected.spanBuilder("mixed_injected") <2> + .kind(io.helidon.tracing.Span.Kind.SERVER) + .tag("attribute", "value") + .start(); + mixedSpan.end(); + + return new GreetingMessage("Mixed Span Injected" + span); +} +---- +<1> Inject `io.helidon.tracing.Tracer`. +<2> Use the injected tracer to create `io.helidon.tracing.Span` using the `spanBuilder()` method. + +The span is then started and ended manually. Span parent relations will be preserved. This means that span named "mixed_injected" with have parent span named "mixed_parent_injected", which will have parent span named "mixed_injected". + +Another option is to use the Global Tracer: + +.Obtain the Global tracer +[source, java] +---- +@GET +@Path("mixed") +@Produces(MediaType.APPLICATION_JSON) +@WithSpan("mixed_parent") +public GreetingMessage mixedSpan() { + + io.helidon.tracing.Tracer helidonTracer = io.helidon.tracing.Tracer.global(); <1> + io.helidon.tracing.Span mixedSpan = helidonTracer.spanBuilder("mixed") <2> + .kind(io.helidon.tracing.Span.Kind.SERVER) + .tag("attribute", "value") + .start(); + mixedSpan.end(); + + return new GreetingMessage("Mixed Span" + span); +} +---- +<1> Obtain tracer using the `io.helidon.tracing.Tracer.global()` method; +<2> Use the created tracer to create a span. + +The span is then started and ended manually. Span parent relations will be preserved. + + +=== Working With Spans + To obtain the current span, it can be injected by CDI. The current span can also be obtained using the static method `Span.current()`. .Inject the current span @@ -156,6 +219,8 @@ public class HelidonEndpoint { <2> Use the injected span. <3> Use `Span.current()` to access the current span. +=== Working With Baggage + The same functionality is available for the `Baggage` API: .Inject the current baggage @@ -205,7 +270,7 @@ The OpenTelemetry Java Agent may influence the work of MicroProfile Telemetry, o `otel.agent.present=true` -This way, Helidon will explicitly get all the configuration and objects from the Agent, thus allowing correct Span hierarchy settings. +This way, Helidon will explicitly get all the configuration and objects from the Agent, thus allowing correct span hierarchy settings. == Examples @@ -335,7 +400,7 @@ public JsonObject useCustomSpan(){ } ---- <1> Inject Opentelemetry `Tracer`. -<2> Create Span around the method `useCustomSpan()`. +<2> Create a span around the method `useCustomSpan()`. <3> Create a custom `INTERNAL` span and start it. <4> End the custom span. @@ -391,7 +456,7 @@ curl localhost:8080/greet/outbound Secondary ---- -The `greeting-service` call `secondary-service`. Each service will create Spans with corresponding names, and a service class hierarchy will be created. +The `greeting-service` call `secondary-service`. Each service will create spans with corresponding names, and a service class hierarchy will be created. Launch the Jaeger UI at link:http://localhost:16686/[] to see the expected output (shown below). diff --git a/examples/microprofile/telemetry/greeting/src/main/java/io/helidon/examples/microprofile/telemetry/GreetResource.java b/examples/microprofile/telemetry/greeting/src/main/java/io/helidon/examples/microprofile/telemetry/GreetResource.java index 4b218cc7e9e..48ddfae3968 100644 --- a/examples/microprofile/telemetry/greeting/src/main/java/io/helidon/examples/microprofile/telemetry/GreetResource.java +++ b/examples/microprofile/telemetry/greeting/src/main/java/io/helidon/examples/microprofile/telemetry/GreetResource.java @@ -28,7 +28,6 @@ import jakarta.ws.rs.core.MediaType; import org.glassfish.jersey.server.Uri; - /** * A simple JAX-RS resource to greet you. Examples: * @@ -40,6 +39,8 @@ * * Call secondary service: * curl -X GET http://localhost:8080/greet/outbound + * + * Explore traces in Jaeger UI. */ @Path("/greet") public class GreetResource { @@ -85,6 +86,7 @@ public GreetingMessage useCustomSpan() { return new GreetingMessage("Custom Span" + span); } + /** * Get Span info. * diff --git a/examples/microprofile/telemetry/greeting/src/main/java/io/helidon/examples/microprofile/telemetry/MixedTelemetryGreetResource.java b/examples/microprofile/telemetry/greeting/src/main/java/io/helidon/examples/microprofile/telemetry/MixedTelemetryGreetResource.java new file mode 100644 index 00000000000..eae2c42ae05 --- /dev/null +++ b/examples/microprofile/telemetry/greeting/src/main/java/io/helidon/examples/microprofile/telemetry/MixedTelemetryGreetResource.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.microprofile.telemetry; + +import io.opentelemetry.instrumentation.annotations.WithSpan; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +/** + * A simple JAX-RS resource to use `io.opentelemetry` and `io.helidon.api` in mixed mode. Examples: + * + * Get mixed traces with Global tracer: + * curl -X GET http://localhost:8080/mixed + * + * Get mixed traces with an injected Helidon tracer: + * curl -X GET http://localhost:8080/mixed/injected + * + * Explore traces in Jaeger UI. + */ +@Path("/mixed") +public class MixedTelemetryGreetResource { + + private io.helidon.tracing.Tracer helidonTracerInjected; + + @Inject + MixedTelemetryGreetResource(io.helidon.tracing.Tracer helidonTracerInjected) { + this.helidonTracerInjected = helidonTracerInjected; + } + + + /** + * Create a helidon mixed span using Helidon Global tracer. + * @return {@link GreetingMessage} + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + @WithSpan("mixed_parent") + public GreetingMessage mixedSpan() { + + io.helidon.tracing.Tracer helidonTracer = io.helidon.tracing.Tracer.global(); + io.helidon.tracing.Span mixedSpan = helidonTracer.spanBuilder("mixed_inner") + .kind(io.helidon.tracing.Span.Kind.SERVER) + .tag("attribute", "value") + .start(); + mixedSpan.end(); + + return new GreetingMessage("Mixed Span"); + } + + /** + * Create a helidon mixed span using injected Helidon Tracer. + * @return {@link GreetingMessage} + */ + @GET + @Path("injected") + @Produces(MediaType.APPLICATION_JSON) + @WithSpan("mixed_parent_injected") + public GreetingMessage mixedSpanInjected() { + io.helidon.tracing.Span mixedSpan = helidonTracerInjected.spanBuilder("mixed_injected_inner") + .kind(io.helidon.tracing.Span.Kind.SERVER) + .tag("attribute", "value") + .start(); + mixedSpan.end(); + + return new GreetingMessage("Mixed Span Injected"); + } +} diff --git a/examples/microprofile/telemetry/greeting/src/main/resources/META-INF/microprofile-config.properties b/examples/microprofile/telemetry/greeting/src/main/resources/META-INF/microprofile-config.properties index a2579922b94..018e8845a2c 100644 --- a/examples/microprofile/telemetry/greeting/src/main/resources/META-INF/microprofile-config.properties +++ b/examples/microprofile/telemetry/greeting/src/main/resources/META-INF/microprofile-config.properties @@ -25,3 +25,5 @@ metrics.rest-request.enabled=true otel.sdk.disabled=false otel.traces.exporter=jaeger otel.service.name=greeting-service + +#telemetry.span.full.url=true diff --git a/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/HelidonTelemetryContainerFilter.java b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/HelidonTelemetryContainerFilter.java index 5c299938104..407457f43d3 100644 --- a/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/HelidonTelemetryContainerFilter.java +++ b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/HelidonTelemetryContainerFilter.java @@ -51,6 +51,10 @@ class HelidonTelemetryContainerFilter implements ContainerRequestFilter, Contain private static final String SPAN_SCOPE = "otel.span.server.scope"; private static final String HTTP_TARGET = "http.target"; + private static final String SPAN_NAME_FULL_URL = "telemetry.span.full.url"; + + private static boolean spanNameFullUrl = false; + // Extract OpenTelemetry Parent Context from Request headers. private static final TextMapGetter CONTEXT_HEADER_INJECTOR; @@ -78,9 +82,11 @@ public Iterable keys(ContainerRequestContext containerRequestContext) { private ResourceInfo resourceInfo; @Inject - HelidonTelemetryContainerFilter(Tracer tracer, OpenTelemetry openTelemetry) { + HelidonTelemetryContainerFilter(Tracer tracer, OpenTelemetry openTelemetry, org.eclipse.microprofile.config.Config mpConfig) { this.tracer = tracer; this.openTelemetry = openTelemetry; + + mpConfig.getOptionalValue(SPAN_NAME_FULL_URL, Boolean.class).ifPresent(e -> spanNameFullUrl = e); } @Override @@ -100,10 +106,15 @@ public void filter(ContainerRequestContext requestContext) { parentContext = extractedContext; } - String annotatedPath = requestContext.getUriInfo().getPath(); - Path pathAnnotation = resourceInfo.getResourceMethod().getAnnotation(Path.class); - if (pathAnnotation != null) { - annotatedPath = pathAnnotation.value(); + String annotatedPath; + if (spanNameFullUrl) { + annotatedPath = requestContext.getUriInfo().getAbsolutePath().toString(); + } else { + annotatedPath = requestContext.getUriInfo().getPath(); + Path pathAnnotation = resourceInfo.getResourceMethod().getAnnotation(Path.class); + if (pathAnnotation != null) { + annotatedPath = pathAnnotation.value(); + } } //Start new span for container request. diff --git a/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/HelidonWithSpan.java b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/HelidonWithSpan.java index 92a0fe8a3f6..d98ec5b1bbe 100644 --- a/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/HelidonWithSpan.java +++ b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/HelidonWithSpan.java @@ -15,6 +15,7 @@ */ package io.helidon.microprofile.telemetry; +import java.io.Serial; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -37,6 +38,8 @@ // Literal to create HelidonWithSpan annotation. class Literal extends AnnotationLiteral implements HelidonWithSpan { static final Literal INSTANCE = new Literal(); + @Serial + private static final long serialVersionUID = 5910339603347723544L; private Literal() { } diff --git a/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/OpenTelemetryProducer.java b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/OpenTelemetryProducer.java index 1b464a675c6..b51a8e4228a 100644 --- a/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/OpenTelemetryProducer.java +++ b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/OpenTelemetryProducer.java @@ -19,9 +19,9 @@ import java.util.HashMap; import java.util.Map; -import io.helidon.common.LazyValue; import io.helidon.config.Config; import io.helidon.tracing.providers.opentelemetry.HelidonOpenTelemetry; +import io.helidon.tracing.providers.opentelemetry.OpenTelemetryTracerProvider; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.OpenTelemetry; @@ -54,11 +54,15 @@ class OpenTelemetryProducer { private static final String EXPORTER_NAME_PROPERTY = "otel.exporter.name"; private String exporterName = HELIDON_SERVICE_NAME; // Default if not set - private LazyValue openTelemetry; + private OpenTelemetry openTelemetry; private Map telemetryProperties; private final Config config; + private volatile Tracer tracer; + + private volatile io.helidon.tracing.Tracer helidonTracer; + private final org.eclipse.microprofile.config.Config mpConfig; @Inject @@ -67,6 +71,10 @@ class OpenTelemetryProducer { this.mpConfig = mpConfig; } + /** + * Construct OpenTelemetry. If the OTEL Agent is present, everything is configured and prepared. + * We just reuse objects from the Agent. + */ @PostConstruct private void init() { @@ -74,11 +82,14 @@ private void init() { mpConfig.getOptionalValue(EXPORTER_NAME_PROPERTY, String.class).ifPresent(e -> exporterName = e); - //Initialize OpenTelemetry in a lazy way. - if (!isTelemetryDisabled()) { - openTelemetry = LazyValue.create(() -> { + // If there is an OTEL Agent – delegate everything to it. + if (HelidonOpenTelemetry.AgentDetector.isAgentPresent(config)) { + openTelemetry = GlobalOpenTelemetry.get(); + } else { - OpenTelemetry openTelemetry = AutoConfiguredOpenTelemetrySdk.builder() + //Initialize OpenTelemetry + if (!isTelemetryDisabled()) { + openTelemetry = AutoConfiguredOpenTelemetrySdk.builder() .addPropertiesCustomizer(x -> telemetryProperties) .addResourceCustomizer(this::customizeResource) .setServiceClassLoader(Thread.currentThread().getContextClassLoader()) @@ -97,43 +108,48 @@ private void init() { } openTelemetry = OpenTelemetry.noop(); } - return openTelemetry; - }); - } else { - if (LOGGER.isLoggable(System.Logger.Level.TRACE)) { - LOGGER.log(System.Logger.Level.TRACE, "Telemetry Disabled by configuration"); + } else { + if (LOGGER.isLoggable(System.Logger.Level.TRACE)) { + LOGGER.log(System.Logger.Level.TRACE, "Telemetry Disabled by configuration"); + } + openTelemetry = OpenTelemetry.noop(); } - openTelemetry = LazyValue.create(OpenTelemetry::noop); } + + tracer = openTelemetry.getTracer(exporterName); + helidonTracer = HelidonOpenTelemetry.create(openTelemetry, tracer, Map.of()); + OpenTelemetryTracerProvider.globalTracer(helidonTracer); } /** - * Get OpenTelemetry. If the OTEL Agent is present, everything is configured and prepared. - * We just reuse objects from the Agent. + * Get OpenTelemetry. * * @return OpenTelemetry */ @ApplicationScoped @Produces OpenTelemetry openTelemetry() { - - if (HelidonOpenTelemetry.AgentDetector.isAgentPresent(config)) { - return GlobalOpenTelemetry.get(); - } - - return openTelemetry.get(); + return openTelemetry; } - /** * Provides an instance of the current OpenTelemetry Tracer. * - * @param openTelemetry instance of OpenTelemetry. * @return Tracer. */ @Produces - Tracer tracer(OpenTelemetry openTelemetry) { - return openTelemetry.getTracer(exporterName); + Tracer tracer() { + return tracer; + } + + /** + * Provides an instance of the current Helidon API Tracer. + * + * @return Tracer. + */ + @Produces + io.helidon.tracing.Tracer helidonTracer() { + return helidonTracer; } /**