Skip to content

Commit

Permalink
4.1.5: Add telemetry filter helper feature so developer code can infl…
Browse files Browse the repository at this point in the history
…uence 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
  • Loading branch information
tjquinno authored Dec 10, 2024
1 parent 6e5ef21 commit 493359f
Show file tree
Hide file tree
Showing 23 changed files with 816 additions and 9 deletions.
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
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
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
@@ -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> 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[]
}

}
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() {
return HelidonServiceLoader.create(ServiceLoader.load(HelidonTelemetryClientFilterHelper.class)).asList();
}

private static class RequestContextHeaderInjector implements HeaderConsumer {

private final MultivaluedMap<String, Object> requestHeaders;
Expand Down
Loading

0 comments on commit 493359f

Please sign in to comment.