From 493359f06465a7aef8c7bab7760fe326543238fb Mon Sep 17 00:00:00 2001 From: Tim Quinn Date: Tue, 10 Dec 2024 17:59:59 -0600 Subject: [PATCH] 4.1.5: Add telemetry filter helper feature so developer code can influence automatic span creation (#9581) * Add telemetry filter helper feature so developer code can influence whether incoming or outgoing spans are automatically created * Add doc describing filter helpers; also fix some Javadoc warnings in related files * Use CDI instead of Java service loading to locate helpers --- .../main/asciidoc/includes/attributes.adoc | 1 + docs/src/main/asciidoc/mp/telemetry.adoc | 34 +++++ .../helidon/docs/mp/RestclientSnippets.java | 2 +- .../io/helidon/docs/mp/TelemetrySnippets.java | 36 +++++ .../restclient/RestclientMetricsSnippets.java | 129 ++++++++++++++++++ .../HelidonTelemetryClientFilter.java | 33 ++++- .../HelidonTelemetryContainerFilter.java | 30 +++- .../telemetry/TelemetryAutoDiscoverable.java | 6 + .../telemetry/TelemetryCdiExtension.java | 6 + .../HelidonTelemetryClientFilterHelper.java | 33 +++++ ...HelidonTelemetryContainerFilterHelper.java | 33 +++++ .../telemetry/spi/package-info.java | 19 +++ .../telemetry/src/main/java/module-info.java | 10 +- tests/integration/mp-telemetry/pom.xml | 110 +++++++++++++++ ...ectorSuppressPersonalizedGreetingSpan.java | 31 +++++ .../mp/filterselectivity/GreetResource.java | 43 ++++++ .../src/main/resources/META-INF/beans.xml | 25 ++++ .../META-INF/microprofile-config.properties | 20 +++ .../filterselectivity/TestSpanExporter.java | 95 +++++++++++++ .../TestSpanExporterProvider.java | 38 ++++++ .../TestSpanSelectivity.java | 73 ++++++++++ ...pi.traces.ConfigurableSpanExporterProvider | 17 +++ tests/integration/pom.xml | 1 + 23 files changed, 816 insertions(+), 9 deletions(-) create mode 100644 docs/src/main/java/io/helidon/docs/mp/restclient/RestclientMetricsSnippets.java create mode 100644 microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/spi/HelidonTelemetryClientFilterHelper.java create mode 100644 microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/spi/HelidonTelemetryContainerFilterHelper.java create mode 100644 microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/spi/package-info.java create mode 100644 tests/integration/mp-telemetry/pom.xml create mode 100644 tests/integration/mp-telemetry/src/main/java/io/helidon/tests/integration/telemetry/mp/filterselectivity/FilterSelectorSuppressPersonalizedGreetingSpan.java create mode 100644 tests/integration/mp-telemetry/src/main/java/io/helidon/tests/integration/telemetry/mp/filterselectivity/GreetResource.java create mode 100644 tests/integration/mp-telemetry/src/main/resources/META-INF/beans.xml create mode 100644 tests/integration/mp-telemetry/src/main/resources/META-INF/microprofile-config.properties create mode 100644 tests/integration/mp-telemetry/src/test/java/io/helidon/tests/integration/telemetry/mp/filterselectivity/TestSpanExporter.java create mode 100644 tests/integration/mp-telemetry/src/test/java/io/helidon/tests/integration/telemetry/mp/filterselectivity/TestSpanExporterProvider.java create mode 100644 tests/integration/mp-telemetry/src/test/java/io/helidon/tests/integration/telemetry/mp/filterselectivity/TestSpanSelectivity.java create mode 100644 tests/integration/mp-telemetry/src/test/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider diff --git a/docs/src/main/asciidoc/includes/attributes.adoc b/docs/src/main/asciidoc/includes/attributes.adoc index 94a39db9ba3..9b72abfe2f8 100644 --- a/docs/src/main/asciidoc/includes/attributes.adoc +++ b/docs/src/main/asciidoc/includes/attributes.adoc @@ -225,6 +225,7 @@ endif::[] :scheduling-javadoc-base-url: {javadoc-base-url}/io.helidon.microprofile.scheduling :security-integration-jersey-base-url: {javadoc-base-url}/io.helidon.security.integration.jersey :security-integration-webserver-base-url: {javadoc-base-url}/io.helidon.webserver.security +:telemetry-javadoc-base-url: {javadoc-base-url}/io.helidon.microprofile.telemetry :tracing-javadoc-base-url: {javadoc-base-url}/io.helidon.tracing :webclient-javadoc-base-url: {javadoc-base-url}/io.helidon.webclient diff --git a/docs/src/main/asciidoc/mp/telemetry.adoc b/docs/src/main/asciidoc/mp/telemetry.adoc index 7251e88c06e..02981c59d69 100644 --- a/docs/src/main/asciidoc/mp/telemetry.adoc +++ b/docs/src/main/asciidoc/mp/telemetry.adoc @@ -204,6 +204,40 @@ include::{sourcedir}/mp/TelemetrySnippets.java[tag=snippet_6, indent=0] include::{rootdir}/includes/tracing/common-callbacks.adoc[tags=defs;detailed,leveloffset=+1] +=== Controlling Automatic Span Creation +By default, Helidon MP Telemetry creates a new child span for each incoming REST request and for each outgoing REST client request. You can selectively control if Helidon creates these automatic spans on a request-by-request basis by adding a very small amount of code to your project. + +==== Controlling Automatic Spans for Incoming REST Requests +To selectively suppress child span creation for incoming REST requests implement the link:{telemetry-javadoc-base-url}/io/helidon/microprofile/telemetry/spi/HelidonTelemetryContainerFilterHelper.html[HelidonTelemetryContainerFilterHelper interface]. + +When Helidon receives an incoming REST request it invokes the `shouldStartSpan` method on each such implementation, passing the link:{jakarta-jaxrs-javadoc-url}/jakarta.ws.rs/jakarta/ws/rs/container/containerrequestcontext[Jakarta REST container request context] for the request. If at least one implementation returns `false` then Helidon suppresses the automatic child span. If all implementations return `true` then Helidon creates the automatic child span. + +The following example shows how to allow automatic spans in the Helidon greet example app for requests for the default greeting but not for the personalized greeting or the `PUT` request to change the greeting message (because the update path ends with `greeting` not `greet`). + +Your implementation of `HelidonTelemetryContainerFilterHelper` must have a CDI bean-defining annotation. The example shows `@ApplicationScoped`. + +.Example container helper for the Helidon MP Greeting app +[source,java] +---- +include::{sourcedir}/mp/TelemetrySnippets.java[tag=snippet_11, indent=0] +---- + + +==== Controlling Automatic Spans for Outgoing REST Client Requests +To selectively suppress child span creation for outgoing REST client requests implement the link:{telemetry-javadoc-base-url}/io/helidon/microprofile/telemetry/spi/HelidonTelemetryClientFilterHelper.html[HelidonTelemetryClientFilterHelper interface]. + +When your application sends an outgoing REST client request Helidon invokes the `shouldStartSpan` method on each such implementation, passing the link:{jakarta-jaxrs-javadoc-url}/jakarta.ws.rs/jakarta/ws/rs/client/clientrequestcontext[Jakarta REST client request context] for the request. If at least one implementation returns `false` then Helidon suppresses the automatic child span. If all implementations return `true` then Helidon creates the automatic child span. + +The following example shows how to allow automatic spans in an app that invokes the Helidon greet example app. The example permits automatic child spans for outgoing requests for the default greeting but not for the personalized greeting or the `PUT` request to change the greeting message (because the update path ends with `greeting` not `greet`). + +Your implementation of `HelidonTelemetryClientFilterHelper` must have a CDI bean-defining annotation. The example shows `@ApplicationScoped`. + +.Example Client Helper for the Helidon MP Greeting App +[source,java] +---- +include::{sourcedir}/mp/TelemetrySnippets.java[tag=snippet_12, indent=0] +---- + == Configuration IMPORTANT: MicroProfile Telemetry is not activated by default. To activate this feature, you need to specify the configuration `otel.sdk.disabled=false` in one of the MicroProfile Config or other config sources. diff --git a/docs/src/main/java/io/helidon/docs/mp/RestclientSnippets.java b/docs/src/main/java/io/helidon/docs/mp/RestclientSnippets.java index 28522d2ab67..856cc5a5dbf 100644 --- a/docs/src/main/java/io/helidon/docs/mp/RestclientSnippets.java +++ b/docs/src/main/java/io/helidon/docs/mp/RestclientSnippets.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.helidon.docs.mp; +package io.helidon.docs.mp.restclient; import java.io.IOException; import java.net.URI; diff --git a/docs/src/main/java/io/helidon/docs/mp/TelemetrySnippets.java b/docs/src/main/java/io/helidon/docs/mp/TelemetrySnippets.java index f7bb37a4472..3de3c94c0e0 100644 --- a/docs/src/main/java/io/helidon/docs/mp/TelemetrySnippets.java +++ b/docs/src/main/java/io/helidon/docs/mp/TelemetrySnippets.java @@ -15,6 +15,9 @@ */ package io.helidon.docs.mp; +import io.helidon.microprofile.telemetry.spi.HelidonTelemetryClientFilterHelper; +import io.helidon.microprofile.telemetry.spi.HelidonTelemetryContainerFilterHelper; + import io.opentelemetry.api.baggage.Baggage; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanKind; @@ -28,7 +31,9 @@ import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.client.ClientRequestContext; import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.glassfish.jersey.server.Uri; @@ -225,4 +230,35 @@ public String getSecondaryMessage() { // end::snippet_10[] } + class FilterHelperSnippets_11_to_12 { + + // tag::snippet_11[] + @ApplicationScoped + public class CustomRestRequestFilterHelper implements HelidonTelemetryContainerFilterHelper { + + @Override + public boolean shouldStartSpan(ContainerRequestContext containerRequestContext) { + + // Allows automatic spans for incoming requests for the default greeting but not for + // personalized greetings or the PUT request to update the greeting message. + return containerRequestContext.getUriInfo().getPath().endsWith("greet"); + } + } + // end::snippet_11[] + + // tag::snippet_12[] + @ApplicationScoped + public class CustomRestClientRequestFilterHelper implements HelidonTelemetryClientFilterHelper { + + @Override + public boolean shouldStartSpan(ClientRequestContext clientRequestContext) { + + // Allows automatic spans for outgoing requests for the default greeting but not for + // personalized greetings or the PUT request to update the greeting message. + return clientRequestContext.getUri().getPath().endsWith("greet"); + } + } + // end::snippet_12[] + } + } diff --git a/docs/src/main/java/io/helidon/docs/mp/restclient/RestclientMetricsSnippets.java b/docs/src/main/java/io/helidon/docs/mp/restclient/RestclientMetricsSnippets.java new file mode 100644 index 00000000000..770516c11ed --- /dev/null +++ b/docs/src/main/java/io/helidon/docs/mp/restclient/RestclientMetricsSnippets.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2024 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.docs.mp.restclient; + +import java.net.URI; + +import io.helidon.common.LazyValue; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.metrics.annotation.Counted; +import org.eclipse.microprofile.metrics.annotation.Timed; +import org.eclipse.microprofile.rest.client.RestClientBuilder; + +import static io.helidon.docs.mp.restclient.RestclientMetricsSnippets.Snippet1.GreetRestClient; + +@SuppressWarnings("ALL") +class RestclientMetricsSnippets { + + // stub + static interface GreetingMessage { } + + + class Snippet1 { + + // tag::snippet_1[] + @Path("/greet") + @Timed(name = "timedGreet", absolute = true) // <1> + public interface GreetRestClient { + + @Counted // <2> + @GET + @Produces(MediaType.APPLICATION_JSON) + GreetingMessage getDefaultMessage(); + + @Path("/{name}") + @GET + @Produces(MediaType.APPLICATION_JSON) + GreetingMessage getMessage(@PathParam("name") String name); + + @Path("/greeting") + @PUT + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + Response updateGreeting(GreetingMessage message); + } + // end::snippet_1[] + } + + class Snippet2 { + + // tag::snippet_2[] + @Path("/delegate") + public class DelegatingResource { + + private static LazyValue greetRestClient = LazyValue.create(DelegatingResource::prepareClient); // <1> + + /** + * Return a worldly greeting message. + * + * @return {@link GreetingMessage} + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + public GreetingMessage getDefaultMessage() { + return greetRestClient.get().getDefaultMessage(); // <2> + } + + /** + * Return a greeting message using the name that was provided. + * + * @param name the name to greet + * @return {@link GreetingMessage} + */ + @Path("/{name}") + @GET + @Produces(MediaType.APPLICATION_JSON) + public GreetingMessage getMessage(@PathParam("name") String name) { + return greetRestClient.get().getMessage(name); + } + + /** + * Set the greeting to use in future messages. + * + * @param message JSON containing the new greeting + * @return {@link jakarta.ws.rs.core.Response} + */ + @Path("/greeting") + @PUT + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Response updateGreeting(GreetingMessage message) { + return greetRestClient.get().updateGreeting(message); + } + + private static GreetRestClient prepareClient() { // <3> + Config config = ConfigProvider.getConfig(); + String serverHost = config.getOptionalValue("server.host", String.class).orElse("localhost"); + String serverPort = config.getOptionalValue("server.port", String.class).orElse("8080"); + return RestClientBuilder.newBuilder() + .baseUri(URI.create("http://" + serverHost + ":" + serverPort)) + .build(GreetRestClient.class); + } + } + // end::snippet_2[] + } + +} diff --git a/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/HelidonTelemetryClientFilter.java b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/HelidonTelemetryClientFilter.java index f506661cccd..96f77a2ee65 100644 --- a/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/HelidonTelemetryClientFilter.java +++ b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/HelidonTelemetryClientFilter.java @@ -17,8 +17,11 @@ import java.util.List; import java.util.Optional; +import java.util.ServiceLoader; import java.util.Set; +import io.helidon.common.HelidonServiceLoader; +import io.helidon.microprofile.telemetry.spi.HelidonTelemetryClientFilterHelper; import io.helidon.tracing.HeaderConsumer; import io.helidon.tracing.HeaderProvider; import io.helidon.tracing.Scope; @@ -26,6 +29,7 @@ import io.opentelemetry.api.baggage.Baggage; import io.opentelemetry.context.Context; +import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; import jakarta.ws.rs.client.ClientRequestContext; import jakarta.ws.rs.client.ClientRequestFilter; @@ -55,17 +59,32 @@ class HelidonTelemetryClientFilter implements ClientRequestFilter, ClientRespons Response.Status.Family.CLIENT_ERROR, Response.Status.Family.SERVER_ERROR); + private static final String HELPER_START_SPAN_PROPERTY = HelidonTelemetryClientFilterHelper.class.getName() + ".startSpan"; + private final io.helidon.tracing.Tracer helidonTracer; + private final List helpers; + @Inject - HelidonTelemetryClientFilter(io.helidon.tracing.Tracer helidonTracer) { + HelidonTelemetryClientFilter(io.helidon.tracing.Tracer helidonTracer, + Instance helpersInstance) { this.helidonTracer = helidonTracer; + helpers = helpersInstance.stream().toList(); } - @Override public void filter(ClientRequestContext clientRequestContext) { + boolean startSpan = helpers.stream().allMatch(h -> h.shouldStartSpan(clientRequestContext)); + clientRequestContext.setProperty(HELPER_START_SPAN_PROPERTY, startSpan); + if (!startSpan) { + if (LOGGER.isLoggable(System.Logger.Level.TRACE)) { + LOGGER.log(System.Logger.Level.TRACE, + "Client filter helper(s) voted to not start a span for " + clientRequestContext.getUri()); + } + return; + } + if (LOGGER.isLoggable(System.Logger.Level.TRACE)) { LOGGER.log(System.Logger.Level.TRACE, "Starting Span in a Client Request"); } @@ -99,10 +118,14 @@ public void filter(ClientRequestContext clientRequestContext) { new RequestContextHeaderInjector(clientRequestContext.getHeaders())); } - @Override public void filter(ClientRequestContext clientRequestContext, ClientResponseContext clientResponseContext) { + Boolean startSpanObj = (Boolean) clientRequestContext.getProperty(HELPER_START_SPAN_PROPERTY); + if (startSpanObj != null && !startSpanObj) { + return; + } + if (LOGGER.isLoggable(System.Logger.Level.TRACE)) { LOGGER.log(System.Logger.Level.TRACE, "Closing Span in a Client Response"); } @@ -128,6 +151,10 @@ public void filter(ClientRequestContext clientRequestContext, ClientResponseCont clientRequestContext.removeProperty(SPAN_SCOPE); } + private static List helpers() { + return HelidonServiceLoader.create(ServiceLoader.load(HelidonTelemetryClientFilterHelper.class)).asList(); + } + private static class RequestContextHeaderInjector implements HeaderConsumer { private final MultivaluedMap requestHeaders; 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 f361d54b84e..6548f912211 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 @@ -23,6 +23,7 @@ import io.helidon.common.context.Contexts; import io.helidon.config.mp.MpConfig; +import io.helidon.microprofile.telemetry.spi.HelidonTelemetryContainerFilterHelper; import io.helidon.tracing.Scope; import io.helidon.tracing.Span; import io.helidon.tracing.SpanContext; @@ -32,6 +33,7 @@ import io.opentelemetry.api.baggage.BaggageEntryMetadata; import io.opentelemetry.context.Context; import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; import jakarta.ws.rs.ApplicationPath; import jakarta.ws.rs.container.ContainerRequestContext; @@ -63,6 +65,8 @@ class HelidonTelemetryContainerFilter implements ContainerRequestFilter, Contain private static final String SPAN_NAME_FULL_URL = "telemetry.span.full.url"; + private static final String HELPER_START_SPAN_PROPERTY = HelidonTelemetryContainerFilterHelper.class + ".startSpan"; + @Deprecated(forRemoval = true, since = "4.1") static final String SPAN_NAME_INCLUDES_METHOD = "telemetry.span.name-includes-method"; @@ -81,12 +85,15 @@ class HelidonTelemetryContainerFilter implements ContainerRequestFilter, Contain */ private final boolean restSpanNameIncludesMethod; + private final List helpers; + @jakarta.ws.rs.core.Context private ResourceInfo resourceInfo; @Inject HelidonTelemetryContainerFilter(io.helidon.tracing.Tracer helidonTracer, - org.eclipse.microprofile.config.Config mpConfig) { + org.eclipse.microprofile.config.Config mpConfig, + Instance helpersInstance) { this.helidonTracer = helidonTracer; isAgentPresent = HelidonOpenTelemetry.AgentDetector.isAgentPresent(MpConfig.toHelidonConfig(mpConfig)); @@ -106,6 +113,8 @@ class HelidonTelemetryContainerFilter implements ContainerRequestFilter, Contain SPAN_NAME_INCLUDES_METHOD)); } // end of code to remove in 5.x. + + helpers = helpersInstance.stream().toList(); } @Override @@ -115,6 +124,16 @@ public void filter(ContainerRequestContext requestContext) { return; } + boolean startSpan = helpers.stream().allMatch(h -> h.shouldStartSpan(requestContext)); + requestContext.setProperty(HELPER_START_SPAN_PROPERTY, startSpan); + if (!startSpan) { + if (LOGGER.isLoggable(System.Logger.Level.TRACE)) { + LOGGER.log(System.Logger.Level.TRACE, + "Container filter helper(s) voted to not start a span for " + requestContext); + } + return; + } + if (LOGGER.isLoggable(System.Logger.Level.TRACE)) { LOGGER.log(System.Logger.Level.TRACE, "Starting Span in a Container Request"); } @@ -151,6 +170,11 @@ public void filter(final ContainerRequestContext request, final ContainerRespons return; } + Boolean startSpanObj = (Boolean) request.getProperty(HELPER_START_SPAN_PROPERTY); + if (startSpanObj != null && !startSpanObj) { + return; + } + if (LOGGER.isLoggable(System.Logger.Level.TRACE)) { LOGGER.log(System.Logger.Level.TRACE, "Closing Span in a Container Request"); } @@ -179,6 +203,10 @@ public void filter(final ContainerRequestContext request, final ContainerRespons } } +// private static List helpers() { +// return HelidonServiceLoader.create(ServiceLoader.load(HelidonTelemetryContainerFilterHelper.class)).asList(); +// } + private String spanName(ContainerRequestContext requestContext, String route) { // @Deprecated(forRemoval = true) In 5.x remove the option of excluding the HTTP method from the REST span name. // Starting in 5.x this method should be: diff --git a/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/TelemetryAutoDiscoverable.java b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/TelemetryAutoDiscoverable.java index 300df0e0109..129e4404dad 100644 --- a/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/TelemetryAutoDiscoverable.java +++ b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/TelemetryAutoDiscoverable.java @@ -22,6 +22,12 @@ */ public class TelemetryAutoDiscoverable implements AutoDiscoverable { + /** + * For service loading. + */ + public TelemetryAutoDiscoverable() { + } + /** * Used to register {@code HelidonTelemetryContainerFilter} and {@code HelidonTelemetryClientFilter} * filters. diff --git a/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/TelemetryCdiExtension.java b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/TelemetryCdiExtension.java index a4767bd01cd..2e587f08ce5 100644 --- a/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/TelemetryCdiExtension.java +++ b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/TelemetryCdiExtension.java @@ -36,6 +36,12 @@ public class TelemetryCdiExtension implements Extension { private static final System.Logger LOGGER = System.getLogger(TelemetryCdiExtension.class.getName()); + /** + * For service loading. + */ + public TelemetryCdiExtension() { + } + /** * Add {@code HelidonWithSpan} annotation with interceptor. * diff --git a/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/spi/HelidonTelemetryClientFilterHelper.java b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/spi/HelidonTelemetryClientFilterHelper.java new file mode 100644 index 00000000000..cebcea0ef07 --- /dev/null +++ b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/spi/HelidonTelemetryClientFilterHelper.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 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.microprofile.telemetry.spi; + +import jakarta.ws.rs.client.ClientRequestContext; + +/** + * Service-loaded type applied while the Helidon-provided client filter executes. + */ +public interface HelidonTelemetryClientFilterHelper { + + /** + * Invoked to see if this helper votes to create and start a new span for the outgoing client request reflected + * in the provided client request context. + * + * @param clientRequestContext the {@link jakarta.ws.rs.client.ClientRequestContext} passed to the filter + * @return true to vote to start a span; false to vote not to start a span + */ + boolean shouldStartSpan(ClientRequestContext clientRequestContext); +} diff --git a/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/spi/HelidonTelemetryContainerFilterHelper.java b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/spi/HelidonTelemetryContainerFilterHelper.java new file mode 100644 index 00000000000..449a5ac9372 --- /dev/null +++ b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/spi/HelidonTelemetryContainerFilterHelper.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 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.microprofile.telemetry.spi; + +import jakarta.ws.rs.container.ContainerRequestContext; + +/** + * Service-loaded type applied while the Helidon-provided container filter executes. + */ +public interface HelidonTelemetryContainerFilterHelper { + + /** + * Invoked to see if this helper votes to create and start a new span for the incoming + * request reflected in the provided container request context. + * + * @param containerRequestContext the {@link jakarta.ws.rs.container.ContainerRequestContext} passed to the filter + * @return true to vote to start a span; false to vote not to start a span + */ + boolean shouldStartSpan(ContainerRequestContext containerRequestContext); +} diff --git a/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/spi/package-info.java b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/spi/package-info.java new file mode 100644 index 00000000000..a4e243a17c4 --- /dev/null +++ b/microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/spi/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2024 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. + */ +/** + * SPI interfaces open to developer implementation as needed. + */ +package io.helidon.microprofile.telemetry.spi; diff --git a/microprofile/telemetry/src/main/java/module-info.java b/microprofile/telemetry/src/main/java/module-info.java index f56c961d88a..031f52f9033 100644 --- a/microprofile/telemetry/src/main/java/module-info.java +++ b/microprofile/telemetry/src/main/java/module-info.java @@ -17,8 +17,6 @@ import io.helidon.common.features.api.Feature; import io.helidon.common.features.api.HelidonFlavor; import io.helidon.common.features.api.Preview; -import io.helidon.microprofile.telemetry.TelemetryAutoDiscoverable; -import io.helidon.microprofile.telemetry.TelemetryCdiExtension; /** * MicroProfile Telemetry support for Helidon. @@ -57,11 +55,15 @@ requires transitive jersey.common; exports io.helidon.microprofile.telemetry; + exports io.helidon.microprofile.telemetry.spi; + + uses io.helidon.microprofile.telemetry.spi.HelidonTelemetryClientFilterHelper; + uses io.helidon.microprofile.telemetry.spi.HelidonTelemetryContainerFilterHelper; provides jakarta.enterprise.inject.spi.Extension - with TelemetryCdiExtension; + with io.helidon.microprofile.telemetry.TelemetryCdiExtension; provides org.glassfish.jersey.internal.spi.AutoDiscoverable - with TelemetryAutoDiscoverable; + with io.helidon.microprofile.telemetry.TelemetryAutoDiscoverable; } \ No newline at end of file diff --git a/tests/integration/mp-telemetry/pom.xml b/tests/integration/mp-telemetry/pom.xml new file mode 100644 index 00000000000..ea801c97911 --- /dev/null +++ b/tests/integration/mp-telemetry/pom.xml @@ -0,0 +1,110 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.4-SNAPSHOT + ../../../applications/mp/pom.xml + + io.helidon.tests.integration + helidon-tests-integration-mp-telemetry + Helidon Tests Integration MP Telemetry + + + + 100 + + + + + io.helidon.microprofile.bundles + helidon-microprofile-core + + + io.helidon.microprofile.telemetry + helidon-microprofile-telemetry + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.microprofile.testing + helidon-microprofile-testing-junit5 + test + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + io.opentelemetry + opentelemetry-exporter-otlp + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + default-test + + + ${otel.bsp.schedule.delay} + + + + + + + + + diff --git a/tests/integration/mp-telemetry/src/main/java/io/helidon/tests/integration/telemetry/mp/filterselectivity/FilterSelectorSuppressPersonalizedGreetingSpan.java b/tests/integration/mp-telemetry/src/main/java/io/helidon/tests/integration/telemetry/mp/filterselectivity/FilterSelectorSuppressPersonalizedGreetingSpan.java new file mode 100644 index 00000000000..7ac86be00f6 --- /dev/null +++ b/tests/integration/mp-telemetry/src/main/java/io/helidon/tests/integration/telemetry/mp/filterselectivity/FilterSelectorSuppressPersonalizedGreetingSpan.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 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.tests.integration.telemetry.mp.filterselectivity; + +import io.helidon.microprofile.telemetry.spi.HelidonTelemetryContainerFilterHelper; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.container.ContainerRequestContext; + +@ApplicationScoped +public class FilterSelectorSuppressPersonalizedGreetingSpan implements HelidonTelemetryContainerFilterHelper { + + @Override + public boolean shouldStartSpan(ContainerRequestContext containerRequestContext) { + // Suppress spans for the personalized greeting endpoint. + return containerRequestContext.getUriInfo().getPath().endsWith("greet"); + } +} diff --git a/tests/integration/mp-telemetry/src/main/java/io/helidon/tests/integration/telemetry/mp/filterselectivity/GreetResource.java b/tests/integration/mp-telemetry/src/main/java/io/helidon/tests/integration/telemetry/mp/filterselectivity/GreetResource.java new file mode 100644 index 00000000000..356f871957d --- /dev/null +++ b/tests/integration/mp-telemetry/src/main/java/io/helidon/tests/integration/telemetry/mp/filterselectivity/GreetResource.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 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.tests.integration.telemetry.mp.filterselectivity; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/greet") +public class GreetResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String getDefaultMessage() { + return createResponse("World"); + } + + @Path("/{name}") + @GET + @Produces(MediaType.TEXT_PLAIN) + public String getMessage(@PathParam("name") String name) { + return createResponse(name); + } + + private static String createResponse(String who) { + return "Hello " + who + "!"; + } +} diff --git a/tests/integration/mp-telemetry/src/main/resources/META-INF/beans.xml b/tests/integration/mp-telemetry/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000000..4ba37321310 --- /dev/null +++ b/tests/integration/mp-telemetry/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/tests/integration/mp-telemetry/src/main/resources/META-INF/microprofile-config.properties b/tests/integration/mp-telemetry/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 00000000000..b2e858082ce --- /dev/null +++ b/tests/integration/mp-telemetry/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,20 @@ +# +# Copyright (c) 2024 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. +# + +# Microprofile server properties +server.port=8080 +server.host=0.0.0.0 + diff --git a/tests/integration/mp-telemetry/src/test/java/io/helidon/tests/integration/telemetry/mp/filterselectivity/TestSpanExporter.java b/tests/integration/mp-telemetry/src/test/java/io/helidon/tests/integration/telemetry/mp/filterselectivity/TestSpanExporter.java new file mode 100644 index 00000000000..764ca12b8b8 --- /dev/null +++ b/tests/integration/mp-telemetry/src/test/java/io/helidon/tests/integration/telemetry/mp/filterselectivity/TestSpanExporter.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2024 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.tests.integration.telemetry.mp.filterselectivity; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import io.helidon.common.testing.junit5.MatcherWithRetry; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import jakarta.enterprise.context.ApplicationScoped; + +import static org.hamcrest.Matchers.iterableWithSize; + +// Partially inspired by the MP Telemetry TCK InMemorySpanExporter. +@ApplicationScoped +public class TestSpanExporter implements SpanExporter { + + private final List spanData = new CopyOnWriteArrayList<>(); + private final System.Logger LOGGER = System.getLogger(TestSpanExporter.class.getName()); + + private final int RETRY_COUNT = Integer.getInteger(TestSpanExporter.class.getName() + ".test.retryCount", 120); + private final int RETRY_DELAY_MS = Integer.getInteger(TestSpanExporter.class.getName() + ".test.retryDelayMs", 500); + + + private enum State {READY, STOPPED} + + private State state = State.READY; + + @Override + public CompletableResultCode export(Collection collection) { + if (state == State.STOPPED) { + return CompletableResultCode.ofFailure(); + } + spanData.addAll(collection); + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + state = State.STOPPED; + spanData.clear(); + return CompletableResultCode.ofSuccess(); + } + + List spanData(int expectedCount) { + long startTime = 0; + if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) { + startTime = System.currentTimeMillis(); + } + var result = MatcherWithRetry.assertThatWithRetry("Expected span count", + () -> new ArrayList<>(spanData), + iterableWithSize(expectedCount), + RETRY_COUNT, + RETRY_DELAY_MS); + if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) { + LOGGER.log(System.Logger.Level.DEBUG, "spanData waited " + + (System.currentTimeMillis() - startTime) + + " ms for expected spans to arrive."); + } + return result; + } + + List spanData(Duration delay) throws InterruptedException { + Thread.sleep(delay); + return new ArrayList<>(spanData); + } + + void clear() { + spanData.clear(); + } +} diff --git a/tests/integration/mp-telemetry/src/test/java/io/helidon/tests/integration/telemetry/mp/filterselectivity/TestSpanExporterProvider.java b/tests/integration/mp-telemetry/src/test/java/io/helidon/tests/integration/telemetry/mp/filterselectivity/TestSpanExporterProvider.java new file mode 100644 index 00000000000..2e8f21d0155 --- /dev/null +++ b/tests/integration/mp-telemetry/src/test/java/io/helidon/tests/integration/telemetry/mp/filterselectivity/TestSpanExporterProvider.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 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.tests.integration.telemetry.mp.filterselectivity; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import jakarta.enterprise.inject.spi.CDI; + +public class TestSpanExporterProvider implements ConfigurableSpanExporterProvider { + + public TestSpanExporterProvider() { + System.err.println("provider ctor"); + } + + @Override + public SpanExporter createExporter(ConfigProperties configProperties) { + return CDI.current().select(TestSpanExporter.class).get(); + } + + @Override + public String getName() { + return "in-memory"; + } +} diff --git a/tests/integration/mp-telemetry/src/test/java/io/helidon/tests/integration/telemetry/mp/filterselectivity/TestSpanSelectivity.java b/tests/integration/mp-telemetry/src/test/java/io/helidon/tests/integration/telemetry/mp/filterselectivity/TestSpanSelectivity.java new file mode 100644 index 00000000000..2d775842ddf --- /dev/null +++ b/tests/integration/mp-telemetry/src/test/java/io/helidon/tests/integration/telemetry/mp/filterselectivity/TestSpanSelectivity.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2024 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.tests.integration.telemetry.mp.filterselectivity; + +import java.time.Duration; +import java.util.List; + +import io.helidon.microprofile.testing.junit5.AddBean; +import io.helidon.microprofile.testing.junit5.AddConfig; +import io.helidon.microprofile.testing.junit5.AddConfigBlock; +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import io.opentelemetry.sdk.trace.data.SpanData; +import jakarta.inject.Inject; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; + +@HelidonTest +@AddBean(TestSpanExporter.class) +@AddConfig(key = "otel.sdk.disabled", value = "false") +@AddConfig(key = "otel.traces.exporter", value = "in-memory") + +class TestSpanSelectivity { + + @Inject + private WebTarget webTarget; + + @Inject + private TestSpanExporter testSpanExporter; + + @BeforeEach + void clearSpanData() { + testSpanExporter.clear(); + } + + @Test + void checkSpansForDefaultGreeting() { + Response response = webTarget.path("/greet").request(MediaType.TEXT_PLAIN).get(); + assertThat("Request status", response.getStatus(), is(200)); + + List spanData = testSpanExporter.spanData(2); // Automatic GET span plus the resource span + assertThat("Span data", spanData, hasSize(2)); + } + + @Test + void checkSpansForPersonalizedGreeting() throws InterruptedException { + Response response = webTarget.path("/greet/Joe").request(MediaType.TEXT_PLAIN).get(); + assertThat("Request status", response.getStatus(), is(200)); + + List spanData = testSpanExporter.spanData(Duration.ofSeconds(2)); + assertThat("Span data", spanData, hasSize(1)); + } +} diff --git a/tests/integration/mp-telemetry/src/test/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider b/tests/integration/mp-telemetry/src/test/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider new file mode 100644 index 00000000000..03282f6e36d --- /dev/null +++ b/tests/integration/mp-telemetry/src/test/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider @@ -0,0 +1,17 @@ +# +# Copyright (c) 2024 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. +# + +io.helidon.tests.integration.telemetry.mp.filterselectivity.TestSpanExporterProvider diff --git a/tests/integration/pom.xml b/tests/integration/pom.xml index 843518cbcd7..ee79cc0afd8 100644 --- a/tests/integration/pom.xml +++ b/tests/integration/pom.xml @@ -66,6 +66,7 @@ vault zipkin-mp-2.2 tls-revocation-config + mp-telemetry