Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add telemetry filter helper feature so developer code can influence automatic span creation #9552

Merged
merged 3 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/src/main/asciidoc/includes/attributes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 34 additions & 0 deletions docs/src/main/asciidoc/mp/telemetry.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
36 changes: 36 additions & 0 deletions docs/src/main/java/io/helidon/docs/mp/TelemetrySnippets.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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[]
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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.net.URI;

Expand All @@ -33,7 +33,7 @@
import org.eclipse.microprofile.metrics.annotation.Timed;
import org.eclipse.microprofile.rest.client.RestClientBuilder;

import static io.helidon.docs.mp.RestclientMetricsSnippets.Snippet1.GreetRestClient;
import static io.helidon.docs.mp.restclient.RestclientMetricsSnippets.Snippet1.GreetRestClient;

@SuppressWarnings("ALL")
class RestclientMetricsSnippets {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,19 @@

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;
import io.helidon.tracing.Span;

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;
Expand Down Expand Up @@ -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<HelidonTelemetryClientFilterHelper> helpers;

@Inject
HelidonTelemetryClientFilter(io.helidon.tracing.Tracer helidonTracer) {
HelidonTelemetryClientFilter(io.helidon.tracing.Tracer helidonTracer,
Instance<HelidonTelemetryClientFilterHelper> 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");
}
Expand Down Expand Up @@ -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");
}
Expand All @@ -128,6 +151,10 @@ public void filter(ClientRequestContext clientRequestContext, ClientResponseCont
clientRequestContext.removeProperty(SPAN_SCOPE);
}

private static List<HelidonTelemetryClientFilterHelper> helpers() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A quick comment without a lot of research behind it: I wonder if you shouldn't use the native Jersey features for this discovery operation. In CDI this would involve injecting an Instance<...Helper> at construction time. In "native" Jersey this would probably involve injecting a Providers to get hold of a ContextResolver<...Helper>. Rationale: Jersey already has a robust service discovery mechanism defined by spec. On the other hand maybe Helidon service loading has been deemed the architectural path forward here; I don't know.

Another related question I have here is that a helper that votes to not start a span is, notionally, just a filter that does nothing, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good questions.

  1. I did not think about that approach.

    It would certainly be an alternative and the filters that are already in place are certainly Jakarta REST elements so that would be thematically consistent.

    I guess my own thinking of late in general is to use the Helidon mechanism for ease of moving to a hypothetical future alternative. On the surface (I have not tried redoing the PR as you described) it seems as if there are fewer moving parts as written.

  2. I suppose, because the existing filter passes the ClientRequestContext or ContainerReqeustContext to each helper I suppose a helper could act as a filter by manipulating the context in the same way "real" filters can. That's certainly not the intent.

    I also considered having the filter pass our own implementation of those Jakarta REST XXXContext interfaces that reject the mutator methods and delegate to the real context otherwise. But that seemed excessive (to me) to guard developers from themselves.

    I did think briefly about--and decided against--having the filter pass a number of parameters to the helper - such as all the items available from the context using the various getXXX methods. But that would have been quite cumbersome and there would be no built-in way of allowing access to the properties in the context.

    I also thought about having developers provide their own Jakarta REST filter and communicate their desire to suppress the span creation by setting a property in the request context. That seems less transparent and would involve priorities for the developers' filters vs. ours so ours runs last; I didn't pursue this approach.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right; the general pattern here is, loosely, you want some Filter-Like-Entities to be able to veto other FLEs (you want those helpers to say to downstream filters of a particular kind "please don't act"). I'm just wondering if there's a more idiomatic way to do it rather than introducing new members into the overall ontology. I'm probably just overthinking it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am leaning toward using the Instance approach and CDI.

From what I've found, the Jakarta @Provider approach would only support a single instance of the helper type being made available to the filters; users might have multiple helpers all of which need to participate in each vote.

It would be simpler for developers to annotate their helper implementation classes with a bean-defining annotation (such as @ApplicationScoped) (as in the CDI approach) than to create the Java service loader file under META-INF/services (as in the Java service loading approach). And as you pointed out earlier, using Instance allows the filter to retrieve all the implementations.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only gotcha here is: for every contextual reference you get via Instance#get() (or next()) you should arrange to destroy it if and only if it is in @Dependent scope.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(from an off-line conversation about the preceding comment)

Conclusion: The filters which find the helpers are not created per-request, so caching the helpers via the Instance in the filter constructor seems OK here.

Reasoning: The filters last for the life of the application and so therefore any dependent-scoped helpers a developer might write--and which the filters might bring into existence via the Instance--should have the same lifetime as the filter. Because that's the lifetime of the app, the filter does not really have to explicitly destroy any dependent-scoped helpers and there should not be any memory leak.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right; the only real-world side effect of not arranging for dependent instances to be destroyed (one way or the other) is that any @PreDestroy methods they declare will not be executed. This is rarely a problem and I doubt it will crop up in this use case.

return HelidonServiceLoader.create(ServiceLoader.load(HelidonTelemetryClientFilterHelper.class)).asList();
}

private static class RequestContextHeaderInjector implements HeaderConsumer {

private final MultivaluedMap<String, Object> requestHeaders;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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";

Expand All @@ -81,12 +85,15 @@ class HelidonTelemetryContainerFilter implements ContainerRequestFilter, Contain
*/
private final boolean restSpanNameIncludesMethod;

private final List<HelidonTelemetryContainerFilterHelper> 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<HelidonTelemetryContainerFilterHelper> helpersInstance) {
this.helidonTracer = helidonTracer;
isAgentPresent = HelidonOpenTelemetry.AgentDetector.isAgentPresent(MpConfig.toHelidonConfig(mpConfig));

Expand All @@ -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
Expand All @@ -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");
}
Expand Down Expand Up @@ -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");
}
Expand Down Expand Up @@ -179,6 +203,10 @@ public void filter(final ContainerRequestContext request, final ContainerRespons
}
}

// private static List<HelidonTelemetryContainerFilterHelper> 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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@
*/
public class TelemetryAutoDiscoverable implements AutoDiscoverable {

/**
* For service loading.
*/
public TelemetryAutoDiscoverable() {
}

/**
* Used to register {@code HelidonTelemetryContainerFilter} and {@code HelidonTelemetryClientFilter}
* filters.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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);
}
Loading