Skip to content

Commit 647d0c7

Browse files
authored
Add telemetry filter helper feature so developer code can influence automatic span creation (helidon-io#9552)
* 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
1 parent 4591886 commit 647d0c7

23 files changed

Lines changed: 689 additions & 11 deletions

File tree

docs/src/main/asciidoc/includes/attributes.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ endif::[]
225225
:scheduling-javadoc-base-url: {javadoc-base-url}/io.helidon.microprofile.scheduling
226226
:security-integration-jersey-base-url: {javadoc-base-url}/io.helidon.security.integration.jersey
227227
:security-integration-webserver-base-url: {javadoc-base-url}/io.helidon.webserver.security
228+
:telemetry-javadoc-base-url: {javadoc-base-url}/io.helidon.microprofile.telemetry
228229
:tracing-javadoc-base-url: {javadoc-base-url}/io.helidon.tracing
229230
230231
:webclient-javadoc-base-url: {javadoc-base-url}/io.helidon.webclient

docs/src/main/asciidoc/mp/telemetry.adoc

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,40 @@ include::{sourcedir}/mp/TelemetrySnippets.java[tag=snippet_6, indent=0]
204204
205205
include::{rootdir}/includes/tracing/common-callbacks.adoc[tags=defs;detailed,leveloffset=+1]
206206
207+
=== Controlling Automatic Span Creation
208+
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.
209+
210+
==== Controlling Automatic Spans for Incoming REST Requests
211+
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].
212+
213+
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.
214+
215+
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`).
216+
217+
Your implementation of `HelidonTelemetryContainerFilterHelper` must have a CDI bean-defining annotation. The example shows `@ApplicationScoped`.
218+
219+
.Example container helper for the Helidon MP Greeting app
220+
[source,java]
221+
----
222+
include::{sourcedir}/mp/TelemetrySnippets.java[tag=snippet_11, indent=0]
223+
----
224+
225+
226+
==== Controlling Automatic Spans for Outgoing REST Client Requests
227+
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].
228+
229+
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.
230+
231+
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`).
232+
233+
Your implementation of `HelidonTelemetryClientFilterHelper` must have a CDI bean-defining annotation. The example shows `@ApplicationScoped`.
234+
235+
.Example Client Helper for the Helidon MP Greeting App
236+
[source,java]
237+
----
238+
include::{sourcedir}/mp/TelemetrySnippets.java[tag=snippet_12, indent=0]
239+
----
240+
207241
== Configuration
208242
209243
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.

docs/src/main/java/io/helidon/docs/mp/TelemetrySnippets.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
*/
1616
package io.helidon.docs.mp;
1717

18+
import io.helidon.microprofile.telemetry.spi.HelidonTelemetryClientFilterHelper;
19+
import io.helidon.microprofile.telemetry.spi.HelidonTelemetryContainerFilterHelper;
20+
1821
import io.opentelemetry.api.baggage.Baggage;
1922
import io.opentelemetry.api.trace.Span;
2023
import io.opentelemetry.api.trace.SpanKind;
@@ -28,7 +31,9 @@
2831
import jakarta.ws.rs.GET;
2932
import jakarta.ws.rs.Path;
3033
import jakarta.ws.rs.Produces;
34+
import jakarta.ws.rs.client.ClientRequestContext;
3135
import jakarta.ws.rs.client.WebTarget;
36+
import jakarta.ws.rs.container.ContainerRequestContext;
3237
import jakarta.ws.rs.core.MediaType;
3338
import jakarta.ws.rs.core.Response;
3439
import org.glassfish.jersey.server.Uri;
@@ -225,4 +230,35 @@ public String getSecondaryMessage() {
225230
// end::snippet_10[]
226231
}
227232

233+
class FilterHelperSnippets_11_to_12 {
234+
235+
// tag::snippet_11[]
236+
@ApplicationScoped
237+
public class CustomRestRequestFilterHelper implements HelidonTelemetryContainerFilterHelper {
238+
239+
@Override
240+
public boolean shouldStartSpan(ContainerRequestContext containerRequestContext) {
241+
242+
// Allows automatic spans for incoming requests for the default greeting but not for
243+
// personalized greetings or the PUT request to update the greeting message.
244+
return containerRequestContext.getUriInfo().getPath().endsWith("greet");
245+
}
246+
}
247+
// end::snippet_11[]
248+
249+
// tag::snippet_12[]
250+
@ApplicationScoped
251+
public class CustomRestClientRequestFilterHelper implements HelidonTelemetryClientFilterHelper {
252+
253+
@Override
254+
public boolean shouldStartSpan(ClientRequestContext clientRequestContext) {
255+
256+
// Allows automatic spans for outgoing requests for the default greeting but not for
257+
// personalized greetings or the PUT request to update the greeting message.
258+
return clientRequestContext.getUri().getPath().endsWith("greet");
259+
}
260+
}
261+
// end::snippet_12[]
262+
}
263+
228264
}

docs/src/main/java/io/helidon/docs/mp/restclient/RestclientMetricsSnippets.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package io.helidon.docs.mp;
16+
package io.helidon.docs.mp.restclient;
1717

1818
import java.net.URI;
1919

@@ -33,7 +33,7 @@
3333
import org.eclipse.microprofile.metrics.annotation.Timed;
3434
import org.eclipse.microprofile.rest.client.RestClientBuilder;
3535

36-
import static io.helidon.docs.mp.RestclientMetricsSnippets.Snippet1.GreetRestClient;
36+
import static io.helidon.docs.mp.restclient.RestclientMetricsSnippets.Snippet1.GreetRestClient;
3737

3838
@SuppressWarnings("ALL")
3939
class RestclientMetricsSnippets {

docs/src/main/java/io/helidon/docs/mp/restclient/RestclientSnippets.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package io.helidon.docs.mp;
16+
package io.helidon.docs.mp.restclient;
1717

1818
import java.io.IOException;
1919
import java.net.URI;

microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/HelidonTelemetryClientFilter.java

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,19 @@
1717

1818
import java.util.List;
1919
import java.util.Optional;
20+
import java.util.ServiceLoader;
2021
import java.util.Set;
2122

23+
import io.helidon.common.HelidonServiceLoader;
24+
import io.helidon.microprofile.telemetry.spi.HelidonTelemetryClientFilterHelper;
2225
import io.helidon.tracing.HeaderConsumer;
2326
import io.helidon.tracing.HeaderProvider;
2427
import io.helidon.tracing.Scope;
2528
import io.helidon.tracing.Span;
2629

2730
import io.opentelemetry.api.baggage.Baggage;
2831
import io.opentelemetry.context.Context;
32+
import jakarta.enterprise.inject.Instance;
2933
import jakarta.inject.Inject;
3034
import jakarta.ws.rs.client.ClientRequestContext;
3135
import jakarta.ws.rs.client.ClientRequestFilter;
@@ -55,17 +59,32 @@ class HelidonTelemetryClientFilter implements ClientRequestFilter, ClientRespons
5559
Response.Status.Family.CLIENT_ERROR,
5660
Response.Status.Family.SERVER_ERROR);
5761

62+
private static final String HELPER_START_SPAN_PROPERTY = HelidonTelemetryClientFilterHelper.class.getName() + ".startSpan";
63+
5864
private final io.helidon.tracing.Tracer helidonTracer;
5965

66+
private final List<HelidonTelemetryClientFilterHelper> helpers;
67+
6068
@Inject
61-
HelidonTelemetryClientFilter(io.helidon.tracing.Tracer helidonTracer) {
69+
HelidonTelemetryClientFilter(io.helidon.tracing.Tracer helidonTracer,
70+
Instance<HelidonTelemetryClientFilterHelper> helpersInstance) {
6271
this.helidonTracer = helidonTracer;
72+
helpers = helpersInstance.stream().toList();
6373
}
6474

65-
6675
@Override
6776
public void filter(ClientRequestContext clientRequestContext) {
6877

78+
boolean startSpan = helpers.stream().allMatch(h -> h.shouldStartSpan(clientRequestContext));
79+
clientRequestContext.setProperty(HELPER_START_SPAN_PROPERTY, startSpan);
80+
if (!startSpan) {
81+
if (LOGGER.isLoggable(System.Logger.Level.TRACE)) {
82+
LOGGER.log(System.Logger.Level.TRACE,
83+
"Client filter helper(s) voted to not start a span for " + clientRequestContext.getUri());
84+
}
85+
return;
86+
}
87+
6988
if (LOGGER.isLoggable(System.Logger.Level.TRACE)) {
7089
LOGGER.log(System.Logger.Level.TRACE, "Starting Span in a Client Request");
7190
}
@@ -99,10 +118,14 @@ public void filter(ClientRequestContext clientRequestContext) {
99118
new RequestContextHeaderInjector(clientRequestContext.getHeaders()));
100119
}
101120

102-
103121
@Override
104122
public void filter(ClientRequestContext clientRequestContext, ClientResponseContext clientResponseContext) {
105123

124+
Boolean startSpanObj = (Boolean) clientRequestContext.getProperty(HELPER_START_SPAN_PROPERTY);
125+
if (startSpanObj != null && !startSpanObj) {
126+
return;
127+
}
128+
106129
if (LOGGER.isLoggable(System.Logger.Level.TRACE)) {
107130
LOGGER.log(System.Logger.Level.TRACE, "Closing Span in a Client Response");
108131
}
@@ -128,6 +151,10 @@ public void filter(ClientRequestContext clientRequestContext, ClientResponseCont
128151
clientRequestContext.removeProperty(SPAN_SCOPE);
129152
}
130153

154+
private static List<HelidonTelemetryClientFilterHelper> helpers() {
155+
return HelidonServiceLoader.create(ServiceLoader.load(HelidonTelemetryClientFilterHelper.class)).asList();
156+
}
157+
131158
private static class RequestContextHeaderInjector implements HeaderConsumer {
132159

133160
private final MultivaluedMap<String, Object> requestHeaders;

microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/HelidonTelemetryContainerFilter.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import io.helidon.common.context.Contexts;
2525
import io.helidon.config.mp.MpConfig;
26+
import io.helidon.microprofile.telemetry.spi.HelidonTelemetryContainerFilterHelper;
2627
import io.helidon.tracing.Scope;
2728
import io.helidon.tracing.Span;
2829
import io.helidon.tracing.SpanContext;
@@ -32,6 +33,7 @@
3233
import io.opentelemetry.api.baggage.BaggageEntryMetadata;
3334
import io.opentelemetry.context.Context;
3435
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
36+
import jakarta.enterprise.inject.Instance;
3537
import jakarta.inject.Inject;
3638
import jakarta.ws.rs.ApplicationPath;
3739
import jakarta.ws.rs.container.ContainerRequestContext;
@@ -63,6 +65,8 @@ class HelidonTelemetryContainerFilter implements ContainerRequestFilter, Contain
6365

6466
private static final String SPAN_NAME_FULL_URL = "telemetry.span.full.url";
6567

68+
private static final String HELPER_START_SPAN_PROPERTY = HelidonTelemetryContainerFilterHelper.class + ".startSpan";
69+
6670
@Deprecated(forRemoval = true, since = "4.1")
6771
static final String SPAN_NAME_INCLUDES_METHOD = "telemetry.span.name-includes-method";
6872

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

88+
private final List<HelidonTelemetryContainerFilterHelper> helpers;
89+
8490
@jakarta.ws.rs.core.Context
8591
private ResourceInfo resourceInfo;
8692

8793
@Inject
8894
HelidonTelemetryContainerFilter(io.helidon.tracing.Tracer helidonTracer,
89-
org.eclipse.microprofile.config.Config mpConfig) {
95+
org.eclipse.microprofile.config.Config mpConfig,
96+
Instance<HelidonTelemetryContainerFilterHelper> helpersInstance) {
9097
this.helidonTracer = helidonTracer;
9198
isAgentPresent = HelidonOpenTelemetry.AgentDetector.isAgentPresent(MpConfig.toHelidonConfig(mpConfig));
9299

@@ -106,6 +113,8 @@ class HelidonTelemetryContainerFilter implements ContainerRequestFilter, Contain
106113
SPAN_NAME_INCLUDES_METHOD));
107114
}
108115
// end of code to remove in 5.x.
116+
117+
helpers = helpersInstance.stream().toList();
109118
}
110119

111120
@Override
@@ -115,6 +124,16 @@ public void filter(ContainerRequestContext requestContext) {
115124
return;
116125
}
117126

127+
boolean startSpan = helpers.stream().allMatch(h -> h.shouldStartSpan(requestContext));
128+
requestContext.setProperty(HELPER_START_SPAN_PROPERTY, startSpan);
129+
if (!startSpan) {
130+
if (LOGGER.isLoggable(System.Logger.Level.TRACE)) {
131+
LOGGER.log(System.Logger.Level.TRACE,
132+
"Container filter helper(s) voted to not start a span for " + requestContext);
133+
}
134+
return;
135+
}
136+
118137
if (LOGGER.isLoggable(System.Logger.Level.TRACE)) {
119138
LOGGER.log(System.Logger.Level.TRACE, "Starting Span in a Container Request");
120139
}
@@ -151,6 +170,11 @@ public void filter(final ContainerRequestContext request, final ContainerRespons
151170
return;
152171
}
153172

173+
Boolean startSpanObj = (Boolean) request.getProperty(HELPER_START_SPAN_PROPERTY);
174+
if (startSpanObj != null && !startSpanObj) {
175+
return;
176+
}
177+
154178
if (LOGGER.isLoggable(System.Logger.Level.TRACE)) {
155179
LOGGER.log(System.Logger.Level.TRACE, "Closing Span in a Container Request");
156180
}
@@ -179,6 +203,10 @@ public void filter(final ContainerRequestContext request, final ContainerRespons
179203
}
180204
}
181205

206+
// private static List<HelidonTelemetryContainerFilterHelper> helpers() {
207+
// return HelidonServiceLoader.create(ServiceLoader.load(HelidonTelemetryContainerFilterHelper.class)).asList();
208+
// }
209+
182210
private String spanName(ContainerRequestContext requestContext, String route) {
183211
// @Deprecated(forRemoval = true) In 5.x remove the option of excluding the HTTP method from the REST span name.
184212
// Starting in 5.x this method should be:

microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/TelemetryAutoDiscoverable.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@
2222
*/
2323
public class TelemetryAutoDiscoverable implements AutoDiscoverable {
2424

25+
/**
26+
* For service loading.
27+
*/
28+
public TelemetryAutoDiscoverable() {
29+
}
30+
2531
/**
2632
* Used to register {@code HelidonTelemetryContainerFilter} and {@code HelidonTelemetryClientFilter}
2733
* filters.

microprofile/telemetry/src/main/java/io/helidon/microprofile/telemetry/TelemetryCdiExtension.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ public class TelemetryCdiExtension implements Extension {
3636

3737
private static final System.Logger LOGGER = System.getLogger(TelemetryCdiExtension.class.getName());
3838

39+
/**
40+
* For service loading.
41+
*/
42+
public TelemetryCdiExtension() {
43+
}
44+
3945
/**
4046
* Add {@code HelidonWithSpan} annotation with interceptor.
4147
*
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright (c) 2024 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.helidon.microprofile.telemetry.spi;
17+
18+
import jakarta.ws.rs.client.ClientRequestContext;
19+
20+
/**
21+
* Service-loaded type applied while the Helidon-provided client filter executes.
22+
*/
23+
public interface HelidonTelemetryClientFilterHelper {
24+
25+
/**
26+
* Invoked to see if this helper votes to create and start a new span for the outgoing client request reflected
27+
* in the provided client request context.
28+
*
29+
* @param clientRequestContext the {@link jakarta.ws.rs.client.ClientRequestContext} passed to the filter
30+
* @return true to vote to start a span; false to vote not to start a span
31+
*/
32+
boolean shouldStartSpan(ClientRequestContext clientRequestContext);
33+
}

0 commit comments

Comments
 (0)