diff --git a/brave-bom/pom.xml b/brave-bom/pom.xml index 7b133ef10a..b71d4f9c6e 100644 --- a/brave-bom/pom.xml +++ b/brave-bom/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-bom - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT Brave BOM Bill Of Materials POM for all Brave artifacts pom @@ -157,6 +157,11 @@ brave-instrumentation-jersey-server ${project.version} + + ${project.groupId} + brave-instrumentation-jersey-server-jakarta + ${project.version} + ${project.groupId} brave-instrumentation-jms diff --git a/brave-tests/pom.xml b/brave-tests/pom.xml index 3a3d64a76b..f3fd6e2ade 100644 --- a/brave-tests/pom.xml +++ b/brave-tests/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/brave/pom.xml b/brave/pom.xml index a9d871bde4..32978917c6 100644 --- a/brave/pom.xml +++ b/brave/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT brave diff --git a/context/jfr/pom.xml b/context/jfr/pom.xml index 6b0bdbd482..7f6a83ae98 100644 --- a/context/jfr/pom.xml +++ b/context/jfr/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-context-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/context/log4j12/pom.xml b/context/log4j12/pom.xml index d13f2ce1a9..a1133ed9bf 100644 --- a/context/log4j12/pom.xml +++ b/context/log4j12/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-context-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/context/log4j2/pom.xml b/context/log4j2/pom.xml index 5edc3fc5ba..11cb7e2b7b 100644 --- a/context/log4j2/pom.xml +++ b/context/log4j2/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-context-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/context/pom.xml b/context/pom.xml index df07c036d6..75cbb46dd3 100644 --- a/context/pom.xml +++ b/context/pom.xml @@ -11,7 +11,7 @@ io.zipkin.brave brave-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT brave-context-parent diff --git a/context/slf4j/pom.xml b/context/slf4j/pom.xml index f95138bdb2..60d3aae77e 100644 --- a/context/slf4j/pom.xml +++ b/context/slf4j/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-context-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/benchmarks/pom.xml b/instrumentation/benchmarks/pom.xml index 3a233dfda5..e54c02603c 100644 --- a/instrumentation/benchmarks/pom.xml +++ b/instrumentation/benchmarks/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT brave-instrumentation-benchmarks diff --git a/instrumentation/dubbo/pom.xml b/instrumentation/dubbo/pom.xml index dd40ca4971..46f926aaa5 100644 --- a/instrumentation/dubbo/pom.xml +++ b/instrumentation/dubbo/pom.xml @@ -8,7 +8,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/grpc/pom.xml b/instrumentation/grpc/pom.xml index 1be3fa95e4..0eb1b3e58c 100644 --- a/instrumentation/grpc/pom.xml +++ b/instrumentation/grpc/pom.xml @@ -8,7 +8,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/http-tests-jakarta/pom.xml b/instrumentation/http-tests-jakarta/pom.xml index 40f3cdb595..24c6e56d9e 100644 --- a/instrumentation/http-tests-jakarta/pom.xml +++ b/instrumentation/http-tests-jakarta/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/http-tests/pom.xml b/instrumentation/http-tests/pom.xml index 83fc7b8291..60d2143648 100644 --- a/instrumentation/http-tests/pom.xml +++ b/instrumentation/http-tests/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/http/pom.xml b/instrumentation/http/pom.xml index b0fb746f04..22b1c0f798 100644 --- a/instrumentation/http/pom.xml +++ b/instrumentation/http/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/httpasyncclient/pom.xml b/instrumentation/httpasyncclient/pom.xml index d7ef474510..110afe0ade 100644 --- a/instrumentation/httpasyncclient/pom.xml +++ b/instrumentation/httpasyncclient/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/httpclient/pom.xml b/instrumentation/httpclient/pom.xml index c8a62a141e..47bc7c1cb8 100644 --- a/instrumentation/httpclient/pom.xml +++ b/instrumentation/httpclient/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/httpclient5/pom.xml b/instrumentation/httpclient5/pom.xml index 7affa5056c..ddf66351f9 100644 --- a/instrumentation/httpclient5/pom.xml +++ b/instrumentation/httpclient5/pom.xml @@ -11,7 +11,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT brave-instrumentation-httpclient5 diff --git a/instrumentation/jakarta-jms/pom.xml b/instrumentation/jakarta-jms/pom.xml index be7f91ade0..c9ffba3793 100644 --- a/instrumentation/jakarta-jms/pom.xml +++ b/instrumentation/jakarta-jms/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/jaxrs2/pom.xml b/instrumentation/jaxrs2/pom.xml index 0cf77476a8..0fe6aae300 100644 --- a/instrumentation/jaxrs2/pom.xml +++ b/instrumentation/jaxrs2/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/jersey-server-jakarta/README.md b/instrumentation/jersey-server-jakarta/README.md new file mode 100644 index 0000000000..593ca0c833 --- /dev/null +++ b/instrumentation/jersey-server-jakarta/README.md @@ -0,0 +1,70 @@ +# brave-instrumentation-jersey-server-jakarta +This module contains application event listeners for [Jersey Server 3.x](https://eclipse-ee4j.github.io/jersey.github.io/documentation/latest/monitoring_tracing.html). + +These instrumentation is an alternative to the [jaxrs2](../jaxrs2) container +instrumentation. Do *not* use both. + +`TracingApplicationEventListener` extracts trace state from incoming +requests. Then, it reports Zipkin how long each request takes, along +with relevant tags like the http url and the resource. + +`SpanCustomizingApplicationEventListener` layers over [servlet](../servlet-jakarta), +adding resource tags and route information to servlet-originated spans. + +When in a servlet environment, use `SpanCustomizingApplicationEventListener`. +When not, use `TracingApplicationEventListener`. Don't use both! + +`TracingApplicationEventListener` extracts trace state from incoming +requests. Then, it reports Zipkin how long each request takes, along +with relevant tags like the http url. + +To enable tracing, you need to register the `TracingApplicationEventListener`. + +## Configuration + +### Normal configuration (`TracingApplicationEventListener`) + +The `TracingApplicationEventListener` requires an instance of +`HttpTracing` to operate. With that in mind, use [standard means](https://eclipse-ee4j.github.io/jersey.github.io/apidocs/3.0.17/jersey/org/glassfish/jersey/server/monitoring/ApplicationEventListener.html) +to register the listener. + +For example, you could wire up like this: +```java +public class MyApplication extends Application { + + public Set getSingletons() { + HttpTracing httpTracing = // configure me! + return new LinkedHashSet<>(Arrays.asList( + TracingApplicationEventListener.create(httpTracing), + new MyResource() + )); + } +} +``` + +### Servlet-based configuration (`SpanCustomizingApplicationEventListener`) + +When using `jersey-container-servlet`, setup [servlet tracing](../servlet), +an register `SpanCustomizingContainerFilter`. + +```java +public class MyApplication extends Application { + public Set getSingletons() { + HttpTracing httpTracing = // configure me! + return new LinkedHashSet<>(Arrays.asList( + SpanCustomizingApplicationEventListener.create(), + new MyResource() + )); + } +``` + + +## Customizing Span data based on resources +`EventParser` decides which resource-specific (data beyond normal +http tags) end up on the span. You can override this to change what's +parsed, or use `NOOP` to disable controller-specific data. + +Ex. If you want less tags, you can disable the JAX-RS resource ones. +```java +SpanCustomizingApplicationEventListener.create(EventParser.NOOP); +``` diff --git a/instrumentation/jersey-server-jakarta/bnd.bnd b/instrumentation/jersey-server-jakarta/bnd.bnd new file mode 100644 index 0000000000..083b9ca313 --- /dev/null +++ b/instrumentation/jersey-server-jakarta/bnd.bnd @@ -0,0 +1,6 @@ +# We use brave.internal.Nullable, but it is not used at runtime. +Import-Package: \ + !brave.internal*,\ + * +Export-Package: \ + brave.jakarta.jersey.server diff --git a/instrumentation/jersey-server-jakarta/pom.xml b/instrumentation/jersey-server-jakarta/pom.xml new file mode 100644 index 0000000000..8518ca233f --- /dev/null +++ b/instrumentation/jersey-server-jakarta/pom.xml @@ -0,0 +1,79 @@ + + + + + io.zipkin.brave + brave-instrumentation-parent + 6.2.0-SNAPSHOT + + 4.0.0 + + brave-instrumentation-jersey-server-jakarta + Brave Instrumentation: Jersey Server 3.x (Jakarta) + + + + brave.jakarta.jersey.server + + ${project.basedir}/../.. + + + + + org.glassfish.jersey.core + jersey-server + ${jersey3.version} + provided + + + ${project.groupId} + brave-instrumentation-http + ${project.version} + + + + ${project.groupId} + brave-instrumentation-http-tests-jakarta + ${project.version} + test + + + ${project.groupId} + brave-instrumentation-servlet-jakarta + ${project.version} + test + + + org.eclipse.jetty + jetty-servlet + ${jetty11.version} + test + + + org.glassfish.jersey.containers + jersey-container-servlet + ${jersey3.version} + test + + + + org.glassfish.jersey.inject + jersey-hk2 + ${jersey3.version} + test + + + + + com.google.inject + guice + ${guice6.version} + test + + + diff --git a/instrumentation/jersey-server-jakarta/src/main/java/brave/jakarta/jersey/server/EventParser.java b/instrumentation/jersey-server-jakarta/src/main/java/brave/jakarta/jersey/server/EventParser.java new file mode 100644 index 0000000000..f892a26852 --- /dev/null +++ b/instrumentation/jersey-server-jakarta/src/main/java/brave/jakarta/jersey/server/EventParser.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenZipkin Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package brave.jakarta.jersey.server; + +import brave.SpanCustomizer; +import brave.http.HttpTracing; +import org.glassfish.jersey.server.model.Invocable; +import org.glassfish.jersey.server.model.ResourceMethod; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEventListener; + +/** + * Jersey specific type used to customize traced requests based on the JAX-RS resource. + * + * Note: This should not duplicate data added by {@link HttpTracing}. For example, this should + * not add the tag "http.url". + */ +// named event parser, not request event parser, in case we want to later support application event. +public class EventParser { + /** Adds no data to the request */ + public static final EventParser NOOP = new EventParser() { + @Override protected void requestMatched(RequestEvent event, SpanCustomizer customizer) { + } + }; + + /** Simple class name that processed the request. ex BookResource */ + public static final String RESOURCE_CLASS = "jaxrs.resource.class"; + /** Method name that processed the request. ex listOfBooks */ + public static final String RESOURCE_METHOD = "jaxrs.resource.method"; + + /** + * Invoked prior to request invocation during {@link RequestEventListener#onEvent(RequestEvent)} + * where the event type is {@link RequestEvent.Type#REQUEST_MATCHED} + * + * Adds the tags {@link #RESOURCE_CLASS} and {@link #RESOURCE_METHOD}. Override or use {@link + * #NOOP} to change this behavior. + */ + protected void requestMatched(RequestEvent event, SpanCustomizer customizer) { + ResourceMethod method = event.getContainerRequest().getUriInfo().getMatchedResourceMethod(); + if (method == null) return; // This case is extremely odd as this is called on REQUEST_MATCHED! + Invocable i = method.getInvocable(); + customizer.tag(RESOURCE_CLASS, i.getHandler().getHandlerClass().getSimpleName()); + customizer.tag(RESOURCE_METHOD, i.getHandlingMethod().getName()); + } + + public EventParser() { // intentionally public for @Inject to work without explicit binding + } +} diff --git a/instrumentation/jersey-server-jakarta/src/main/java/brave/jakarta/jersey/server/SpanCustomizingApplicationEventListener.java b/instrumentation/jersey-server-jakarta/src/main/java/brave/jakarta/jersey/server/SpanCustomizingApplicationEventListener.java new file mode 100644 index 0000000000..aa5ce39ed4 --- /dev/null +++ b/instrumentation/jersey-server-jakarta/src/main/java/brave/jakarta/jersey/server/SpanCustomizingApplicationEventListener.java @@ -0,0 +1,120 @@ +/* + * Copyright The OpenZipkin Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package brave.jakarta.jersey.server; + +import brave.SpanCustomizer; +import brave.internal.Nullable; +import jakarta.inject.Inject; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.ext.Provider; +import java.util.List; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ExtendedUriInfo; +import org.glassfish.jersey.server.internal.process.MappableException; +import org.glassfish.jersey.server.monitoring.ApplicationEvent; +import org.glassfish.jersey.server.monitoring.ApplicationEventListener; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEventListener; +import org.glassfish.jersey.uri.UriTemplate; + +import static org.glassfish.jersey.server.monitoring.RequestEvent.Type.FINISHED; + +/** + * Adds application-tier data to an existing http span via {@link EventParser}. This also sets the + * request property "http.route" so that it can be used in naming the http span. + * + * Use this instead of {@link TracingApplicationEventListener} when you start traces at the + * servlet level via {@code brave.servlet.TracingFilter}. + */ +@Provider +public class SpanCustomizingApplicationEventListener + implements ApplicationEventListener, RequestEventListener { + public static SpanCustomizingApplicationEventListener create() { + return new SpanCustomizingApplicationEventListener(new EventParser()); + } + + public static SpanCustomizingApplicationEventListener create(EventParser parser) { + return new SpanCustomizingApplicationEventListener(parser); + } + + final EventParser parser; + + @Inject + SpanCustomizingApplicationEventListener(EventParser parser) { + if (parser == null) throw new NullPointerException("parser == null"); + this.parser = parser; + } + + @Override public void onEvent(ApplicationEvent event) { + // only onRequest is used + } + + @Override public RequestEventListener onRequest(RequestEvent requestEvent) { + if (requestEvent.getType() == RequestEvent.Type.START) return this; + return null; + } + + @Override public void onEvent(RequestEvent event) { + // Note: until REQUEST_MATCHED, we don't know metadata such as if the request is async or not + if (event.getType() != FINISHED) return; + ContainerRequest request = event.getContainerRequest(); + Object maybeSpan = request.getProperty(SpanCustomizer.class.getName()); + if (!(maybeSpan instanceof SpanCustomizer)) return; + + // Set the HTTP route attribute so that TracingFilter can see it + request.setProperty("http.route", route(request)); + + Throwable error = unwrapError(event); + // Set the error attribute so that TracingFilter can see it + if (error != null && request.getProperty("error") == null) request.setProperty("error", error); + + parser.requestMatched(event, (SpanCustomizer) maybeSpan); + } + + @Nullable static Throwable unwrapError(RequestEvent event) { + Throwable error = event.getException(); + // For example, if thrown in an async controller + if (error instanceof MappableException && error.getCause() != null) { + error = error.getCause(); + } + // Don't create error messages for normal HTTP status codes. + if (error instanceof WebApplicationException) return error.getCause(); + return error; + } + + /** + * This returns the matched template as defined by a base URL and path expressions. + * + * Matched templates are pairs of (resource path, method path) added with + * {@link org.glassfish.jersey.server.internal.routing.RoutingContext#pushTemplates(UriTemplate, + * UriTemplate)}. This code skips redundant slashes from either source caused by Path("/") or + * Path(""). + */ + @Nullable static String route(ContainerRequest request) { + ExtendedUriInfo uriInfo = request.getUriInfo(); + List templates = uriInfo.getMatchedTemplates(); + int templateCount = templates.size(); + if (templateCount == 0) return ""; + StringBuilder builder = null; // don't allocate unless you need it! + String basePath = uriInfo.getBaseUri().getPath(); + String result = null; + if (!"/" .equals(basePath)) { // skip empty base paths + result = basePath; + } + for (int i = templateCount - 1; i >= 0; i--) { + String template = templates.get(i).getTemplate(); + if ("/" .equals(template)) continue; // skip allocation + if (builder != null) { + builder.append(template); + } else if (result != null) { + builder = new StringBuilder(result).append(template); + result = null; + } else { + result = template; + } + } + return result != null ? result : builder != null ? builder.toString() : ""; + } +} diff --git a/instrumentation/jersey-server-jakarta/src/main/java/brave/jakarta/jersey/server/TracingApplicationEventListener.java b/instrumentation/jersey-server-jakarta/src/main/java/brave/jakarta/jersey/server/TracingApplicationEventListener.java new file mode 100644 index 0000000000..646deb7502 --- /dev/null +++ b/instrumentation/jersey-server-jakarta/src/main/java/brave/jakarta/jersey/server/TracingApplicationEventListener.java @@ -0,0 +1,190 @@ +/* + * Copyright The OpenZipkin Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package brave.jakarta.jersey.server; + +import brave.Span; +import brave.http.HttpServerHandler; +import brave.http.HttpServerRequest; +import brave.http.HttpServerResponse; +import brave.http.HttpTracing; +import brave.internal.Nullable; +import brave.propagation.CurrentTraceContext; +import brave.propagation.CurrentTraceContext.Scope; +import jakarta.inject.Inject; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.container.Suspended; +import jakarta.ws.rs.ext.Provider; +import java.util.concurrent.atomic.AtomicReference; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.ManagedAsync; +import org.glassfish.jersey.server.internal.process.MappableException; +import org.glassfish.jersey.server.monitoring.ApplicationEvent; +import org.glassfish.jersey.server.monitoring.ApplicationEventListener; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEventListener; + +@Provider +public final class TracingApplicationEventListener implements ApplicationEventListener { + public static ApplicationEventListener create(HttpTracing httpTracing) { + return new TracingApplicationEventListener(httpTracing, new EventParser()); + } + + final CurrentTraceContext currentTraceContext; + final HttpServerHandler handler; + final EventParser parser; + + @Inject + TracingApplicationEventListener(HttpTracing httpTracing, EventParser parser) { + currentTraceContext = httpTracing.tracing().currentTraceContext(); + handler = HttpServerHandler.create(httpTracing); + this.parser = parser; + } + + @Override public void onEvent(ApplicationEvent event) { + // only onRequest is used + } + + @Override public RequestEventListener onRequest(RequestEvent event) { + if (event.getType() != RequestEvent.Type.START) return null; + Span span = handler.handleReceive(new ContainerRequestWrapper(event.getContainerRequest())); + return new TracingRequestEventListener(span, currentTraceContext.newScope(span.context())); + } + + // Scope reference invalidated when an asynchronous method is in use + class TracingRequestEventListener extends AtomicReference implements RequestEventListener { + final Span span; + volatile boolean async; + + TracingRequestEventListener(Span span, Scope scope) { + super(scope); + this.span = span; + } + + /** + * This keeps the span in scope as long as possible. In synchronous methods, the span remains in + * scope for the whole request/response lifecycle. {@linkplain ManagedAsync} and {@linkplain + * Suspended} requests are the worst case: the span is only visible until request filters + * complete. + */ + @Override + public void onEvent(RequestEvent event) { + Scope maybeScope; + switch (event.getType()) { + // Note: until REQUEST_MATCHED, we don't know metadata such as if the request is async or not + case REQUEST_MATCHED: + parser.requestMatched(event, span); + async = async(event); + break; + case REQUEST_FILTERED: + case RESOURCE_METHOD_FINISHED: + // If we scoped above, we have to close that to avoid leaks. + // Jersey-specific @ManagedAsync stays on the request thread until REQUEST_FILTERED + // Normal async methods sometimes stay on a thread until RESOURCE_METHOD_FINISHED, but + // this is not reliable. So, we eagerly close the scope from request filters, and re-apply + // it later when the resource method starts. + if (!async || (maybeScope = getAndSet(null)) == null) break; + maybeScope.close(); + break; + case RESOURCE_METHOD_START: + // If we are async, we have to re-scope the span as the resource method invocation is + // is likely on a different thread than the request filtering. + if (!async || get() != null) break; + set(currentTraceContext.newScope(span.context())); + break; + case FINISHED: + handler.handleSend(new RequestEventWrapper(event), span); + // In async FINISHED can happen before RESOURCE_METHOD_FINISHED, and on different threads! + // Don't close the scope unless it is a synchronous method. + if (!async && (maybeScope = getAndSet(null)) != null) { + maybeScope.close(); + } + break; + default: + } + } + } + + static boolean async(RequestEvent event) { + return event.getUriInfo().getMatchedResourceMethod().isManagedAsyncDeclared() + || event.getUriInfo().getMatchedResourceMethod().isSuspendDeclared(); + } + + static final class ContainerRequestWrapper extends HttpServerRequest { + final ContainerRequest delegate; + + ContainerRequestWrapper(ContainerRequest delegate) { + this.delegate = delegate; + } + + @Override public String route() { + return SpanCustomizingApplicationEventListener.route(delegate); + } + + @Override public Object unwrap() { + return delegate; + } + + @Override public String method() { + return delegate.getMethod(); + } + + @Override public String path() { + String result = delegate.getPath(false); + return result.indexOf('/') == 0 ? result : "/" + result; + } + + @Override public String url() { + return delegate.getUriInfo().getRequestUri().toString(); + } + + @Override public String header(String name) { + return delegate.getHeaderString(name); + } + + // NOTE: this currently lacks remote socket parsing even though some platforms might work. For + // example, org.glassfish.grizzly.http.server.Request.getRemoteAddr or + // HttpServletRequest.getRemoteAddr + } + + static final class RequestEventWrapper extends HttpServerResponse { + final RequestEvent event; + @Nullable final Throwable error; + ContainerRequestWrapper request; + + RequestEventWrapper(RequestEvent event) { + this.event = event; + this.error = SpanCustomizingApplicationEventListener.unwrapError(event); + } + + @Override public Object unwrap() { + return event; + } + + @Override public ContainerRequestWrapper request() { + if (request == null) request = new ContainerRequestWrapper(event.getContainerRequest()); + return request; + } + + @Override public Throwable error() { + return error; + } + + @Override public int statusCode() { + ContainerResponse response = event.getContainerResponse(); + if (response != null) return response.getStatus(); + + Throwable error = event.getException(); + // For example, if thrown in an async controller + if (error instanceof MappableException && error.getCause() != null) { + error = error.getCause(); + } + if (error instanceof WebApplicationException) { + return ((WebApplicationException) error).getResponse().getStatus(); + } + return 0; + } + } +} diff --git a/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/ContainerRequestWrapperTest.java b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/ContainerRequestWrapperTest.java new file mode 100644 index 0000000000..dd7949107c --- /dev/null +++ b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/ContainerRequestWrapperTest.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenZipkin Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package brave.jakarta.jersey.server; + +import brave.jakarta.jersey.server.TracingApplicationEventListener.ContainerRequestWrapper; +import java.net.URI; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ExtendedUriInfo; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ContainerRequestWrapperTest { + ContainerRequest request = mock(ContainerRequest.class); + + @Test void path_prefixesSlashWhenMissing() { + when(request.getPath(false)).thenReturn("bar"); + + assertThat(new ContainerRequestWrapper(request).path()) + .isEqualTo("/bar"); + } + + @Test void url_derivedFromExtendedUriInfo() { + ExtendedUriInfo uriInfo = mock(ExtendedUriInfo.class); + when(request.getUriInfo()).thenReturn(uriInfo); + when(uriInfo.getRequestUri()).thenReturn(URI.create("http://foo:8080/bar?hello=world")); + + assertThat(new ContainerRequestWrapper(request).url()) + .isEqualTo("http://foo:8080/bar?hello=world"); + } +} diff --git a/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/EventParserTest.java b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/EventParserTest.java new file mode 100644 index 0000000000..a758ce77d5 --- /dev/null +++ b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/EventParserTest.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenZipkin Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package brave.jakarta.jersey.server; + +import brave.SpanCustomizer; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ExtendedUriInfo; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class EventParserTest { + @Mock RequestEvent event; + @Mock ContainerRequest request; + @Mock ExtendedUriInfo uriInfo; + @Mock SpanCustomizer customizer; + + EventParser eventParser = new EventParser(); + + @Test void requestMatched_missingResourceMethodOk() { + when(event.getContainerRequest()).thenReturn(request); + when(request.getUriInfo()).thenReturn(uriInfo); + + eventParser.requestMatched(event, customizer); + + verifyNoMoreInteractions(customizer); + } +} diff --git a/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/ITSpanCustomizingApplicationEventListener.java b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/ITSpanCustomizingApplicationEventListener.java new file mode 100644 index 0000000000..ce4dc40ac2 --- /dev/null +++ b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/ITSpanCustomizingApplicationEventListener.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenZipkin Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package brave.jakarta.jersey.server; + +import brave.Span; +import brave.jakarta.servlet.TracingFilter; +import brave.test.http.ITServletContainer; +import brave.test.jakarta.http.Jetty11ServerController; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.FilterRegistration; +import java.util.EnumSet; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.util.log.Log; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.servlet.ServletContainer; +import org.junit.AssumptionViolatedException; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class ITSpanCustomizingApplicationEventListener extends ITServletContainer { + + public ITSpanCustomizingApplicationEventListener() { + super(new Jetty11ServerController(), Log.getLogger("org.eclipse.jetty.util.log")); + } + + @Override @Test public void reportsClientAddress() { + throw new AssumptionViolatedException("TODO!"); + } + + @Test void tagsResource() throws Exception { + get("/foo"); + + assertThat(testSpanHandler.takeRemoteSpan(Span.Kind.SERVER).tags()) + .containsEntry("jaxrs.resource.class", "TestResource") + .containsEntry("jaxrs.resource.method", "foo"); + } + + /** Tests that the span propagates between under asynchronous callbacks managed by jersey. */ + @Disabled("TODO: investigate race condition") + @Test void managedAsync() throws Exception { + get("/managedAsync"); + + testSpanHandler.takeRemoteSpan(Span.Kind.SERVER); + } + + @Override public void init(ServletContextHandler handler) { + ResourceConfig config = new ResourceConfig(); + config.register(new TestResource(httpTracing)); + config.register(SpanCustomizingApplicationEventListener.create()); + handler.addServlet(new ServletHolder(new ServletContainer(config)), "/*"); + + // add the underlying servlet tracing filter which the event listener decorates with more tags + FilterRegistration.Dynamic filterRegistration = + handler.getServletContext().addFilter("tracingFilter", TracingFilter.create(httpTracing)); + filterRegistration.setAsyncSupported(true); + // isMatchAfter=true is required for async tests to pass! + filterRegistration.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, "/*"); + } +} diff --git a/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/ITTracingApplicationEventListener.java b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/ITTracingApplicationEventListener.java new file mode 100644 index 0000000000..86b7011d2d --- /dev/null +++ b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/ITTracingApplicationEventListener.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenZipkin Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package brave.jakarta.jersey.server; + +import brave.Span; +import brave.test.http.ITServletContainer; +import brave.test.jakarta.http.Jetty11ServerController; +import okhttp3.Response; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.util.log.Log; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.servlet.ServletContainer; +import org.junit.AssumptionViolatedException; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class ITTracingApplicationEventListener extends ITServletContainer { + public ITTracingApplicationEventListener() { + super(new Jetty11ServerController(), Log.getLogger("org.eclipse.jetty.util.log")); + } + + @Override @Test public void reportsClientAddress() { + throw new AssumptionViolatedException("TODO!"); + } + + @Test void tagsResource() throws Exception { + get("/foo"); + + assertThat(testSpanHandler.takeRemoteSpan(Span.Kind.SERVER).tags()) + .containsEntry("jaxrs.resource.class", "TestResource") + .containsEntry("jaxrs.resource.method", "foo"); + } + + /** Tests that the span propagates between under asynchronous callbacks managed by jersey. */ + @Test void managedAsync() throws Exception { + Response response = get("/managedAsync"); + assertThat(response.isSuccessful()).withFailMessage("not successful: " + response).isTrue(); + + testSpanHandler.takeRemoteSpan(Span.Kind.SERVER); + } + + @Override public void init(ServletContextHandler handler) { + ResourceConfig config = new ResourceConfig(); + config.register(new TestResource(httpTracing)); + config.register(TracingApplicationEventListener.create(httpTracing)); + ServletHolder servlet = new ServletHolder(new ServletContainer(config)); + servlet.setAsyncSupported(true); + handler.addServlet(servlet, "/*"); + } +} diff --git a/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/InjectionTest.java b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/InjectionTest.java new file mode 100644 index 0000000000..91d42c2c5f --- /dev/null +++ b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/InjectionTest.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenZipkin Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package brave.jakarta.jersey.server; + +import brave.Tracing; +import brave.http.HttpTracing; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** This ensures all filters can be injected, supplied with only {@linkplain HttpTracing}. */ +public class InjectionTest { + Tracing tracing = Tracing.newBuilder().build(); + + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(HttpTracing.class).toInstance(HttpTracing.create(tracing)); + } + }); + + @AfterEach void close() { + tracing.close(); + } + + @Test void spanCustomizingApplicationEventListener() { + SpanCustomizingApplicationEventListener filter = + injector.getInstance(SpanCustomizingApplicationEventListener.class); + + assertThat(filter.parser.getClass()) + .isSameAs(EventParser.class); + } + + @Test void spanCustomizingApplicationEventListener_resource() { + SpanCustomizingApplicationEventListener filter = + injector.createChildInjector(new AbstractModule() { + @Override protected void configure() { + bind(EventParser.class).toInstance(EventParser.NOOP); + } + }).getInstance(SpanCustomizingApplicationEventListener.class); + + assertThat(filter.parser) + .isSameAs(EventParser.NOOP); + } + + @Test void tracingApplicationEventListener() { + TracingApplicationEventListener filter = + injector.getInstance(TracingApplicationEventListener.class); + + assertThat(filter.parser.getClass()) + .isSameAs(EventParser.class); + } + + @Test void tracingApplicationEventListener_resource() { + TracingApplicationEventListener filter = injector.createChildInjector(new AbstractModule() { + @Override protected void configure() { + bind(EventParser.class).toInstance(EventParser.NOOP); + } + }).getInstance(TracingApplicationEventListener.class); + + assertThat(filter.parser) + .isSameAs(EventParser.NOOP); + } +} diff --git a/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/RequestEventWrapperTest.java b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/RequestEventWrapperTest.java new file mode 100644 index 0000000000..bb824db89a --- /dev/null +++ b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/RequestEventWrapperTest.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenZipkin Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package brave.jakarta.jersey.server; + +import brave.jakarta.jersey.server.TracingApplicationEventListener.RequestEventWrapper; +import jakarta.ws.rs.ClientErrorException; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.internal.process.MappableException; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class RequestEventWrapperTest { + @Mock ContainerRequest request; + @Mock RequestEvent event; + @Mock ContainerResponse response; + + @Test void method() { + when(event.getContainerRequest()).thenReturn(request); + when(request.getMethod()).thenReturn("GET"); + + assertThat(new RequestEventWrapper(event).method()) + .isEqualTo("GET"); + } + + @Test void request() { + when(event.getContainerRequest()).thenReturn(request); + + assertThat(new RequestEventWrapper(event).request().unwrap()) + .isSameAs(request); + } + + @Test void statusCode() { + when(event.getContainerResponse()).thenReturn(response); + when(response.getStatus()).thenReturn(200); + + assertThat(new RequestEventWrapper(event).statusCode()).isEqualTo(200); + } + + @Test void statusCode_exception() { + when(event.getException()).thenReturn(new ClientErrorException(400)); + + assertThat(new RequestEventWrapper(event).statusCode()).isEqualTo(400); + } + + @Test void statusCode_mappableException() { + when(event.getException()).thenReturn(new MappableException(new ClientErrorException(400))); + + assertThat(new RequestEventWrapper(event).statusCode()).isEqualTo(400); + } + + @Test void statusCode_zeroNoResponse() { + assertThat(new RequestEventWrapper(event).statusCode()).isZero(); + } +} diff --git a/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/SpanCustomizingApplicationEventListenerTest.java b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/SpanCustomizingApplicationEventListenerTest.java new file mode 100644 index 0000000000..4854784c67 --- /dev/null +++ b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/SpanCustomizingApplicationEventListenerTest.java @@ -0,0 +1,211 @@ +/* + * Copyright The OpenZipkin Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package brave.jakarta.jersey.server; + +import brave.SpanCustomizer; +import java.net.URI; +import java.util.Arrays; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ExtendedUriInfo; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.uri.PathTemplate; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) // TODO: hunt down these +public class SpanCustomizingApplicationEventListenerTest { + @Mock + EventParser parser; + @Mock RequestEvent requestEvent; + @Mock ContainerRequest request; + @Mock ExtendedUriInfo uriInfo; + @Mock SpanCustomizer span; + SpanCustomizingApplicationEventListener listener; + + @BeforeEach void setup() { + listener = SpanCustomizingApplicationEventListener.create(parser); + when(requestEvent.getContainerRequest()).thenReturn(request); + when(request.getUriInfo()).thenReturn(uriInfo); + } + + @Test void onEvent_processesFINISHED() { + setEventType(RequestEvent.Type.FINISHED); + setBaseUri("/"); + + when(request.getProperty(SpanCustomizer.class.getName())).thenReturn(span); + + listener.onEvent(requestEvent); + + verify(parser).requestMatched(requestEvent, span); + } + + @Test void onEvent_setsErrorWhenNotAlreadySet() { + setEventType(RequestEvent.Type.FINISHED); + setBaseUri("/"); + + when(request.getProperty(SpanCustomizer.class.getName())).thenReturn(span); + + Exception error = new Exception(); + when(requestEvent.getException()).thenReturn(error); + when(request.getProperty("error")).thenReturn(null); + + listener.onEvent(requestEvent); + + verify(request).setProperty("error", error); + } + + /** Don't clobber user-defined properties! */ + @Test void onEvent_skipsErrorWhenSet() { + setEventType(RequestEvent.Type.FINISHED); + setBaseUri("/"); + + when(request.getProperty(SpanCustomizer.class.getName())).thenReturn(span); + + Exception error = new Exception(); + when(requestEvent.getException()).thenReturn(error); + when(request.getProperty("error")).thenReturn("madness"); + + listener.onEvent(requestEvent); + + verify(request).getProperty(SpanCustomizer.class.getName()); + verify(request).getProperty("error"); + verify(request).getUriInfo(); + verify(request).setProperty("http.route", ""); // empty means no route found + verifyNoMoreInteractions(request); // no setting of error + } + + @Test void onEvent_toleratesMissingCustomizer() { + setEventType(RequestEvent.Type.FINISHED); + setBaseUri("/"); + + listener.onEvent(requestEvent); + + verifyNoMoreInteractions(parser); + } + + @Test void onEvent_toleratesBadCustomizer() { + setEventType(RequestEvent.Type.FINISHED); + setBaseUri("/"); + + when(request.getProperty(SpanCustomizer.class.getName())).thenReturn("eyeballs"); + + listener.onEvent(requestEvent); + + verifyNoMoreInteractions(parser); + } + + @Test void onEvent_ignoresNotFinished() { + for (RequestEvent.Type type : RequestEvent.Type.values()) { + if (type == RequestEvent.Type.FINISHED) return; + + setEventType(type); + + listener.onEvent(requestEvent); + + verifyNoMoreInteractions(span); + } + } + + @Test void ignoresEventsExceptFinish() { + setBaseUri("/"); + when(uriInfo.getMatchedTemplates()).thenReturn(Arrays.asList( + new PathTemplate("/"), + new PathTemplate("/items/{itemId}") + )); + + assertThat(SpanCustomizingApplicationEventListener.route(request)) + .isEqualTo("/items/{itemId}"); + } + + @Test void route() { + setBaseUri("/"); + when(uriInfo.getMatchedTemplates()).thenReturn(Arrays.asList( + new PathTemplate("/"), + new PathTemplate("/items/{itemId}") + )); + + assertThat(SpanCustomizingApplicationEventListener.route(request)) + .isEqualTo("/items/{itemId}"); + } + + @Test void route_noPath() { + setBaseUri("/"); + when(uriInfo.getMatchedTemplates()).thenReturn(Arrays.asList( + new PathTemplate("/eggs") + )); + + assertThat(SpanCustomizingApplicationEventListener.route(request)) + .isEqualTo("/eggs"); + } + + /** not sure it is even possible for a template to match "/" "/".. */ + @Test void route_invalid() { + setBaseUri("/"); + when(uriInfo.getMatchedTemplates()).thenReturn(Arrays.asList( + new PathTemplate("/"), + new PathTemplate("/") + )); + + assertThat(SpanCustomizingApplicationEventListener.route(request)) + .isEmpty(); + } + + @Test void route_basePath() { + setBaseUri("/base"); + when(uriInfo.getMatchedTemplates()).thenReturn(Arrays.asList( + new PathTemplate("/"), + new PathTemplate("/items/{itemId}") + )); + + assertThat(SpanCustomizingApplicationEventListener.route(request)) + .isEqualTo("/base/items/{itemId}"); + } + + @Test void route_nested() { + setBaseUri("/"); + when(uriInfo.getMatchedTemplates()).thenReturn(Arrays.asList( + new PathTemplate("/"), + new PathTemplate("/items/{itemId}"), + new PathTemplate("/"), + new PathTemplate("/nested") + )); + + assertThat(SpanCustomizingApplicationEventListener.route(request)) + .isEqualTo("/nested/items/{itemId}"); + } + + /** when the path expression is on the type not on the method */ + @Test void route_nested_reverse() { + setBaseUri("/"); + when(uriInfo.getMatchedTemplates()).thenReturn(Arrays.asList( + new PathTemplate("/items/{itemId}"), + new PathTemplate("/"), + new PathTemplate("/nested"), + new PathTemplate("/") + )); + + assertThat(SpanCustomizingApplicationEventListener.route(request)) + .isEqualTo("/nested/items/{itemId}"); + } + + void setBaseUri(String path) { + when(uriInfo.getBaseUri()).thenReturn(URI.create(path)); + } + + void setEventType(RequestEvent.Type type) { + when(requestEvent.getType()).thenReturn(type); + } +} diff --git a/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/TestResource.java b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/TestResource.java new file mode 100644 index 0000000000..60031aa5aa --- /dev/null +++ b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/TestResource.java @@ -0,0 +1,133 @@ +/* + * Copyright The OpenZipkin Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package brave.jakarta.jersey.server; + +import brave.Tracer; +import brave.http.HttpTracing; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.OPTIONS; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.container.AsyncResponse; +import jakarta.ws.rs.container.Suspended; +import jakarta.ws.rs.core.Response; +import org.glassfish.jersey.server.ManagedAsync; + +import static brave.test.ITRemote.BAGGAGE_FIELD; +import static brave.test.http.ITHttpServer.NOT_READY_ISE; + +@Path("") +public class TestResource { + final Tracer tracer; + + TestResource(HttpTracing httpTracing) { + this.tracer = httpTracing.tracing().tracer(); + } + + @OPTIONS // intentionally leave out the @Path annotation + public Response root() { + return Response.ok().build(); + } + + @GET + @Path("foo") + public Response foo() { + return Response.ok().build(); + } + + @GET + @Path("extra") + public Response extra() { + return Response.ok(BAGGAGE_FIELD.getValue()).build(); + } + + @GET + @Path("badrequest") + public Response badrequest() { + return Response.status(400).build(); + } + + @GET + @Path("child") + public Response child() { + tracer.nextSpan().name("child").start().finish(); + return Response.status(200).build(); + } + + @GET + @Path("async") + public void async(@Suspended AsyncResponse response) { + if (tracer.currentSpan() == null) { + response.resume(new IllegalStateException("couldn't read current span!")); + return; + } + blockOnAsyncResult("foo", response); + } + + @GET + @Path("managedAsync") + @ManagedAsync + public void managedAsync(@Suspended AsyncResponse response) { + if (tracer.currentSpan() == null) { + response.resume(new IllegalStateException("couldn't read current span!")); + return; + } + response.resume("foo"); + } + + @GET + @Path("items/{itemId}") + public String item(@PathParam("itemId") String itemId) { + return itemId; + } + + @GET + @Path("async_items/{itemId}") + public void asyncItem(@PathParam("itemId") String itemId, @Suspended AsyncResponse response) { + blockOnAsyncResult(itemId, response); + } + + static void blockOnAsyncResult(String body, AsyncResponse response) { + Thread thread = new Thread(() -> response.resume(body)); + thread.start(); + try { + thread.join(); + } catch (InterruptedException e) { + } + } + + @GET + @Path("exception") + public Response notReady() { + throw new WebApplicationException(NOT_READY_ISE, 503); + } + + @GET + @Path("exceptionAsync") + public void notReadyAsync(@Suspended AsyncResponse response) { + Thread thread = new Thread(() -> response.resume( + new WebApplicationException(NOT_READY_ISE, 503) + )); + thread.start(); + try { + thread.join(); + } catch (InterruptedException e) { + } + } + + public static class NestedResource { + @GET + @Path("items/{itemId}") + public String item(@PathParam("itemId") String itemId) { + return itemId; + } + } + + @Path("nested") + public NestedResource nestedResource() { + return new NestedResource(); + } +} diff --git a/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/TracingApplicationEventListenerInjectionTest.java b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/TracingApplicationEventListenerInjectionTest.java new file mode 100644 index 0000000000..08e47c6ab9 --- /dev/null +++ b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/TracingApplicationEventListenerInjectionTest.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenZipkin Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package brave.jakarta.jersey.server; + +import brave.Tracing; +import brave.http.HttpTracing; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TracingApplicationEventListenerInjectionTest { + Tracing tracing = Tracing.newBuilder().build(); + + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(HttpTracing.class).toInstance(HttpTracing.create(tracing)); + } + }); + + @AfterEach void close() { + tracing.close(); + } + + @Test void onlyRequiresHttpTracing() { + assertThat(injector.getInstance(TracingApplicationEventListener.class)) + .isNotNull(); + } +} diff --git a/instrumentation/jersey-server-jakarta/src/test/resources/log4j2.properties b/instrumentation/jersey-server-jakarta/src/test/resources/log4j2.properties new file mode 100755 index 0000000000..8162463e60 --- /dev/null +++ b/instrumentation/jersey-server-jakarta/src/test/resources/log4j2.properties @@ -0,0 +1,14 @@ +appenders=console +appender.console.type=Console +appender.console.name=STDOUT +appender.console.layout.type=PatternLayout +appender.console.layout.pattern=%d{ABSOLUTE} %-5p [%t] %C{2} (%F:%L) - %m%n +rootLogger.level=warn +rootLogger.appenderRefs=stdout +rootLogger.appenderRef.stdout.ref=STDOUT + +# mute logs that do not effect our tests +logger.wadl.name=org.glassfish.jersey.server.wadl.WadlFeature +logger.wadl.level=off +logger.model.name=org.glassfish.jersey.internal.inject.Providers +logger.model.level=off diff --git a/instrumentation/jersey-server/pom.xml b/instrumentation/jersey-server/pom.xml index c14740332a..5fcc1d9d45 100644 --- a/instrumentation/jersey-server/pom.xml +++ b/instrumentation/jersey-server/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/jms-jakarta/pom.xml b/instrumentation/jms-jakarta/pom.xml index 9f1a09bbf0..41e5f4d819 100644 --- a/instrumentation/jms-jakarta/pom.xml +++ b/instrumentation/jms-jakarta/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/jms/pom.xml b/instrumentation/jms/pom.xml index ad3bb46e8f..ff8dd6febd 100644 --- a/instrumentation/jms/pom.xml +++ b/instrumentation/jms/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/kafka-clients/pom.xml b/instrumentation/kafka-clients/pom.xml index d53fe340fa..a989a1c376 100644 --- a/instrumentation/kafka-clients/pom.xml +++ b/instrumentation/kafka-clients/pom.xml @@ -10,7 +10,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT brave-instrumentation-kafka-clients diff --git a/instrumentation/kafka-streams/pom.xml b/instrumentation/kafka-streams/pom.xml index 6c6359c9dd..162e202549 100644 --- a/instrumentation/kafka-streams/pom.xml +++ b/instrumentation/kafka-streams/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/messaging/pom.xml b/instrumentation/messaging/pom.xml index dfa6f09822..b582080e0f 100644 --- a/instrumentation/messaging/pom.xml +++ b/instrumentation/messaging/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/mongodb/pom.xml b/instrumentation/mongodb/pom.xml index 1dc048324d..557a0bb789 100644 --- a/instrumentation/mongodb/pom.xml +++ b/instrumentation/mongodb/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/mysql/pom.xml b/instrumentation/mysql/pom.xml index 319c1668a8..70bb0fd5eb 100644 --- a/instrumentation/mysql/pom.xml +++ b/instrumentation/mysql/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/mysql6/pom.xml b/instrumentation/mysql6/pom.xml index f72a45b817..69a5e4d182 100644 --- a/instrumentation/mysql6/pom.xml +++ b/instrumentation/mysql6/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/mysql8/pom.xml b/instrumentation/mysql8/pom.xml index 3a4644a58d..f602955f6c 100644 --- a/instrumentation/mysql8/pom.xml +++ b/instrumentation/mysql8/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/netty-codec-http/pom.xml b/instrumentation/netty-codec-http/pom.xml index 97d64aa588..c0ce1e9303 100644 --- a/instrumentation/netty-codec-http/pom.xml +++ b/instrumentation/netty-codec-http/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/okhttp3/pom.xml b/instrumentation/okhttp3/pom.xml index 64b972d2aa..4ac655cfd3 100644 --- a/instrumentation/okhttp3/pom.xml +++ b/instrumentation/okhttp3/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/pom.xml b/instrumentation/pom.xml index ae4a2bb41f..917fb32deb 100644 --- a/instrumentation/pom.xml +++ b/instrumentation/pom.xml @@ -11,7 +11,7 @@ io.zipkin.brave brave-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT brave-instrumentation-parent @@ -33,6 +33,7 @@ httpclient5 jaxrs2 jersey-server + jersey-server-jakarta jms jms-jakarta jakarta-jms diff --git a/instrumentation/rocketmq-client/pom.xml b/instrumentation/rocketmq-client/pom.xml index a19b3982a3..a6c9364aeb 100644 --- a/instrumentation/rocketmq-client/pom.xml +++ b/instrumentation/rocketmq-client/pom.xml @@ -10,7 +10,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT brave-instrumentation-rocketmq-client diff --git a/instrumentation/rpc/pom.xml b/instrumentation/rpc/pom.xml index a41cdfef81..562e6888e0 100644 --- a/instrumentation/rpc/pom.xml +++ b/instrumentation/rpc/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/servlet-jakarta/pom.xml b/instrumentation/servlet-jakarta/pom.xml index 468c9e114d..eae0a518fe 100644 --- a/instrumentation/servlet-jakarta/pom.xml +++ b/instrumentation/servlet-jakarta/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/servlet/pom.xml b/instrumentation/servlet/pom.xml index 6e00337f4d..0eae3dc758 100644 --- a/instrumentation/servlet/pom.xml +++ b/instrumentation/servlet/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/spring-rabbit/pom.xml b/instrumentation/spring-rabbit/pom.xml index b4a63e4eaf..364fbdf0f5 100644 --- a/instrumentation/spring-rabbit/pom.xml +++ b/instrumentation/spring-rabbit/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/spring-web/pom.xml b/instrumentation/spring-web/pom.xml index 4dab4cd955..01e8c1a38f 100644 --- a/instrumentation/spring-web/pom.xml +++ b/instrumentation/spring-web/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/spring-webmvc/pom.xml b/instrumentation/spring-webmvc/pom.xml index 4a0303cbe5..95a09443b5 100644 --- a/instrumentation/spring-webmvc/pom.xml +++ b/instrumentation/spring-webmvc/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/vertx-web/pom.xml b/instrumentation/vertx-web/pom.xml index 8e8bc2f9e1..6a5077fb68 100644 --- a/instrumentation/vertx-web/pom.xml +++ b/instrumentation/vertx-web/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/pom.xml b/pom.xml index 62281dda69..481f9e9cda 100755 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ io.zipkin.brave brave-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT pom Brave (parent) @@ -93,6 +93,8 @@ 11.0.24 + + 3.0.17 5.0.0 3.9.0 diff --git a/spring-beans/pom.xml b/spring-beans/pom.xml index c866ac3287..0227ebd902 100644 --- a/spring-beans/pom.xml +++ b/spring-beans/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0
Note: This should not duplicate data added by {@link HttpTracing}. For example, this should + * not add the tag "http.url". + */ +// named event parser, not request event parser, in case we want to later support application event. +public class EventParser { + /** Adds no data to the request */ + public static final EventParser NOOP = new EventParser() { + @Override protected void requestMatched(RequestEvent event, SpanCustomizer customizer) { + } + }; + + /** Simple class name that processed the request. ex BookResource */ + public static final String RESOURCE_CLASS = "jaxrs.resource.class"; + /** Method name that processed the request. ex listOfBooks */ + public static final String RESOURCE_METHOD = "jaxrs.resource.method"; + + /** + * Invoked prior to request invocation during {@link RequestEventListener#onEvent(RequestEvent)} + * where the event type is {@link RequestEvent.Type#REQUEST_MATCHED} + * + *
Adds the tags {@link #RESOURCE_CLASS} and {@link #RESOURCE_METHOD}. Override or use {@link + * #NOOP} to change this behavior. + */ + protected void requestMatched(RequestEvent event, SpanCustomizer customizer) { + ResourceMethod method = event.getContainerRequest().getUriInfo().getMatchedResourceMethod(); + if (method == null) return; // This case is extremely odd as this is called on REQUEST_MATCHED! + Invocable i = method.getInvocable(); + customizer.tag(RESOURCE_CLASS, i.getHandler().getHandlerClass().getSimpleName()); + customizer.tag(RESOURCE_METHOD, i.getHandlingMethod().getName()); + } + + public EventParser() { // intentionally public for @Inject to work without explicit binding + } +} diff --git a/instrumentation/jersey-server-jakarta/src/main/java/brave/jakarta/jersey/server/SpanCustomizingApplicationEventListener.java b/instrumentation/jersey-server-jakarta/src/main/java/brave/jakarta/jersey/server/SpanCustomizingApplicationEventListener.java new file mode 100644 index 0000000000..aa5ce39ed4 --- /dev/null +++ b/instrumentation/jersey-server-jakarta/src/main/java/brave/jakarta/jersey/server/SpanCustomizingApplicationEventListener.java @@ -0,0 +1,120 @@ +/* + * Copyright The OpenZipkin Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package brave.jakarta.jersey.server; + +import brave.SpanCustomizer; +import brave.internal.Nullable; +import jakarta.inject.Inject; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.ext.Provider; +import java.util.List; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ExtendedUriInfo; +import org.glassfish.jersey.server.internal.process.MappableException; +import org.glassfish.jersey.server.monitoring.ApplicationEvent; +import org.glassfish.jersey.server.monitoring.ApplicationEventListener; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEventListener; +import org.glassfish.jersey.uri.UriTemplate; + +import static org.glassfish.jersey.server.monitoring.RequestEvent.Type.FINISHED; + +/** + * Adds application-tier data to an existing http span via {@link EventParser}. This also sets the + * request property "http.route" so that it can be used in naming the http span. + * + *
Use this instead of {@link TracingApplicationEventListener} when you start traces at the + * servlet level via {@code brave.servlet.TracingFilter}. + */ +@Provider +public class SpanCustomizingApplicationEventListener + implements ApplicationEventListener, RequestEventListener { + public static SpanCustomizingApplicationEventListener create() { + return new SpanCustomizingApplicationEventListener(new EventParser()); + } + + public static SpanCustomizingApplicationEventListener create(EventParser parser) { + return new SpanCustomizingApplicationEventListener(parser); + } + + final EventParser parser; + + @Inject + SpanCustomizingApplicationEventListener(EventParser parser) { + if (parser == null) throw new NullPointerException("parser == null"); + this.parser = parser; + } + + @Override public void onEvent(ApplicationEvent event) { + // only onRequest is used + } + + @Override public RequestEventListener onRequest(RequestEvent requestEvent) { + if (requestEvent.getType() == RequestEvent.Type.START) return this; + return null; + } + + @Override public void onEvent(RequestEvent event) { + // Note: until REQUEST_MATCHED, we don't know metadata such as if the request is async or not + if (event.getType() != FINISHED) return; + ContainerRequest request = event.getContainerRequest(); + Object maybeSpan = request.getProperty(SpanCustomizer.class.getName()); + if (!(maybeSpan instanceof SpanCustomizer)) return; + + // Set the HTTP route attribute so that TracingFilter can see it + request.setProperty("http.route", route(request)); + + Throwable error = unwrapError(event); + // Set the error attribute so that TracingFilter can see it + if (error != null && request.getProperty("error") == null) request.setProperty("error", error); + + parser.requestMatched(event, (SpanCustomizer) maybeSpan); + } + + @Nullable static Throwable unwrapError(RequestEvent event) { + Throwable error = event.getException(); + // For example, if thrown in an async controller + if (error instanceof MappableException && error.getCause() != null) { + error = error.getCause(); + } + // Don't create error messages for normal HTTP status codes. + if (error instanceof WebApplicationException) return error.getCause(); + return error; + } + + /** + * This returns the matched template as defined by a base URL and path expressions. + * + *
Matched templates are pairs of (resource path, method path) added with + * {@link org.glassfish.jersey.server.internal.routing.RoutingContext#pushTemplates(UriTemplate, + * UriTemplate)}. This code skips redundant slashes from either source caused by Path("/") or + * Path(""). + */ + @Nullable static String route(ContainerRequest request) { + ExtendedUriInfo uriInfo = request.getUriInfo(); + List templates = uriInfo.getMatchedTemplates(); + int templateCount = templates.size(); + if (templateCount == 0) return ""; + StringBuilder builder = null; // don't allocate unless you need it! + String basePath = uriInfo.getBaseUri().getPath(); + String result = null; + if (!"/" .equals(basePath)) { // skip empty base paths + result = basePath; + } + for (int i = templateCount - 1; i >= 0; i--) { + String template = templates.get(i).getTemplate(); + if ("/" .equals(template)) continue; // skip allocation + if (builder != null) { + builder.append(template); + } else if (result != null) { + builder = new StringBuilder(result).append(template); + result = null; + } else { + result = template; + } + } + return result != null ? result : builder != null ? builder.toString() : ""; + } +} diff --git a/instrumentation/jersey-server-jakarta/src/main/java/brave/jakarta/jersey/server/TracingApplicationEventListener.java b/instrumentation/jersey-server-jakarta/src/main/java/brave/jakarta/jersey/server/TracingApplicationEventListener.java new file mode 100644 index 0000000000..646deb7502 --- /dev/null +++ b/instrumentation/jersey-server-jakarta/src/main/java/brave/jakarta/jersey/server/TracingApplicationEventListener.java @@ -0,0 +1,190 @@ +/* + * Copyright The OpenZipkin Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package brave.jakarta.jersey.server; + +import brave.Span; +import brave.http.HttpServerHandler; +import brave.http.HttpServerRequest; +import brave.http.HttpServerResponse; +import brave.http.HttpTracing; +import brave.internal.Nullable; +import brave.propagation.CurrentTraceContext; +import brave.propagation.CurrentTraceContext.Scope; +import jakarta.inject.Inject; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.container.Suspended; +import jakarta.ws.rs.ext.Provider; +import java.util.concurrent.atomic.AtomicReference; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.ManagedAsync; +import org.glassfish.jersey.server.internal.process.MappableException; +import org.glassfish.jersey.server.monitoring.ApplicationEvent; +import org.glassfish.jersey.server.monitoring.ApplicationEventListener; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEventListener; + +@Provider +public final class TracingApplicationEventListener implements ApplicationEventListener { + public static ApplicationEventListener create(HttpTracing httpTracing) { + return new TracingApplicationEventListener(httpTracing, new EventParser()); + } + + final CurrentTraceContext currentTraceContext; + final HttpServerHandler handler; + final EventParser parser; + + @Inject + TracingApplicationEventListener(HttpTracing httpTracing, EventParser parser) { + currentTraceContext = httpTracing.tracing().currentTraceContext(); + handler = HttpServerHandler.create(httpTracing); + this.parser = parser; + } + + @Override public void onEvent(ApplicationEvent event) { + // only onRequest is used + } + + @Override public RequestEventListener onRequest(RequestEvent event) { + if (event.getType() != RequestEvent.Type.START) return null; + Span span = handler.handleReceive(new ContainerRequestWrapper(event.getContainerRequest())); + return new TracingRequestEventListener(span, currentTraceContext.newScope(span.context())); + } + + // Scope reference invalidated when an asynchronous method is in use + class TracingRequestEventListener extends AtomicReference implements RequestEventListener { + final Span span; + volatile boolean async; + + TracingRequestEventListener(Span span, Scope scope) { + super(scope); + this.span = span; + } + + /** + * This keeps the span in scope as long as possible. In synchronous methods, the span remains in + * scope for the whole request/response lifecycle. {@linkplain ManagedAsync} and {@linkplain + * Suspended} requests are the worst case: the span is only visible until request filters + * complete. + */ + @Override + public void onEvent(RequestEvent event) { + Scope maybeScope; + switch (event.getType()) { + // Note: until REQUEST_MATCHED, we don't know metadata such as if the request is async or not + case REQUEST_MATCHED: + parser.requestMatched(event, span); + async = async(event); + break; + case REQUEST_FILTERED: + case RESOURCE_METHOD_FINISHED: + // If we scoped above, we have to close that to avoid leaks. + // Jersey-specific @ManagedAsync stays on the request thread until REQUEST_FILTERED + // Normal async methods sometimes stay on a thread until RESOURCE_METHOD_FINISHED, but + // this is not reliable. So, we eagerly close the scope from request filters, and re-apply + // it later when the resource method starts. + if (!async || (maybeScope = getAndSet(null)) == null) break; + maybeScope.close(); + break; + case RESOURCE_METHOD_START: + // If we are async, we have to re-scope the span as the resource method invocation is + // is likely on a different thread than the request filtering. + if (!async || get() != null) break; + set(currentTraceContext.newScope(span.context())); + break; + case FINISHED: + handler.handleSend(new RequestEventWrapper(event), span); + // In async FINISHED can happen before RESOURCE_METHOD_FINISHED, and on different threads! + // Don't close the scope unless it is a synchronous method. + if (!async && (maybeScope = getAndSet(null)) != null) { + maybeScope.close(); + } + break; + default: + } + } + } + + static boolean async(RequestEvent event) { + return event.getUriInfo().getMatchedResourceMethod().isManagedAsyncDeclared() + || event.getUriInfo().getMatchedResourceMethod().isSuspendDeclared(); + } + + static final class ContainerRequestWrapper extends HttpServerRequest { + final ContainerRequest delegate; + + ContainerRequestWrapper(ContainerRequest delegate) { + this.delegate = delegate; + } + + @Override public String route() { + return SpanCustomizingApplicationEventListener.route(delegate); + } + + @Override public Object unwrap() { + return delegate; + } + + @Override public String method() { + return delegate.getMethod(); + } + + @Override public String path() { + String result = delegate.getPath(false); + return result.indexOf('/') == 0 ? result : "/" + result; + } + + @Override public String url() { + return delegate.getUriInfo().getRequestUri().toString(); + } + + @Override public String header(String name) { + return delegate.getHeaderString(name); + } + + // NOTE: this currently lacks remote socket parsing even though some platforms might work. For + // example, org.glassfish.grizzly.http.server.Request.getRemoteAddr or + // HttpServletRequest.getRemoteAddr + } + + static final class RequestEventWrapper extends HttpServerResponse { + final RequestEvent event; + @Nullable final Throwable error; + ContainerRequestWrapper request; + + RequestEventWrapper(RequestEvent event) { + this.event = event; + this.error = SpanCustomizingApplicationEventListener.unwrapError(event); + } + + @Override public Object unwrap() { + return event; + } + + @Override public ContainerRequestWrapper request() { + if (request == null) request = new ContainerRequestWrapper(event.getContainerRequest()); + return request; + } + + @Override public Throwable error() { + return error; + } + + @Override public int statusCode() { + ContainerResponse response = event.getContainerResponse(); + if (response != null) return response.getStatus(); + + Throwable error = event.getException(); + // For example, if thrown in an async controller + if (error instanceof MappableException && error.getCause() != null) { + error = error.getCause(); + } + if (error instanceof WebApplicationException) { + return ((WebApplicationException) error).getResponse().getStatus(); + } + return 0; + } + } +} diff --git a/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/ContainerRequestWrapperTest.java b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/ContainerRequestWrapperTest.java new file mode 100644 index 0000000000..dd7949107c --- /dev/null +++ b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/ContainerRequestWrapperTest.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenZipkin Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package brave.jakarta.jersey.server; + +import brave.jakarta.jersey.server.TracingApplicationEventListener.ContainerRequestWrapper; +import java.net.URI; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ExtendedUriInfo; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ContainerRequestWrapperTest { + ContainerRequest request = mock(ContainerRequest.class); + + @Test void path_prefixesSlashWhenMissing() { + when(request.getPath(false)).thenReturn("bar"); + + assertThat(new ContainerRequestWrapper(request).path()) + .isEqualTo("/bar"); + } + + @Test void url_derivedFromExtendedUriInfo() { + ExtendedUriInfo uriInfo = mock(ExtendedUriInfo.class); + when(request.getUriInfo()).thenReturn(uriInfo); + when(uriInfo.getRequestUri()).thenReturn(URI.create("http://foo:8080/bar?hello=world")); + + assertThat(new ContainerRequestWrapper(request).url()) + .isEqualTo("http://foo:8080/bar?hello=world"); + } +} diff --git a/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/EventParserTest.java b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/EventParserTest.java new file mode 100644 index 0000000000..a758ce77d5 --- /dev/null +++ b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/EventParserTest.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenZipkin Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package brave.jakarta.jersey.server; + +import brave.SpanCustomizer; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ExtendedUriInfo; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class EventParserTest { + @Mock RequestEvent event; + @Mock ContainerRequest request; + @Mock ExtendedUriInfo uriInfo; + @Mock SpanCustomizer customizer; + + EventParser eventParser = new EventParser(); + + @Test void requestMatched_missingResourceMethodOk() { + when(event.getContainerRequest()).thenReturn(request); + when(request.getUriInfo()).thenReturn(uriInfo); + + eventParser.requestMatched(event, customizer); + + verifyNoMoreInteractions(customizer); + } +} diff --git a/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/ITSpanCustomizingApplicationEventListener.java b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/ITSpanCustomizingApplicationEventListener.java new file mode 100644 index 0000000000..ce4dc40ac2 --- /dev/null +++ b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/ITSpanCustomizingApplicationEventListener.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenZipkin Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package brave.jakarta.jersey.server; + +import brave.Span; +import brave.jakarta.servlet.TracingFilter; +import brave.test.http.ITServletContainer; +import brave.test.jakarta.http.Jetty11ServerController; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.FilterRegistration; +import java.util.EnumSet; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.util.log.Log; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.servlet.ServletContainer; +import org.junit.AssumptionViolatedException; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class ITSpanCustomizingApplicationEventListener extends ITServletContainer { + + public ITSpanCustomizingApplicationEventListener() { + super(new Jetty11ServerController(), Log.getLogger("org.eclipse.jetty.util.log")); + } + + @Override @Test public void reportsClientAddress() { + throw new AssumptionViolatedException("TODO!"); + } + + @Test void tagsResource() throws Exception { + get("/foo"); + + assertThat(testSpanHandler.takeRemoteSpan(Span.Kind.SERVER).tags()) + .containsEntry("jaxrs.resource.class", "TestResource") + .containsEntry("jaxrs.resource.method", "foo"); + } + + /** Tests that the span propagates between under asynchronous callbacks managed by jersey. */ + @Disabled("TODO: investigate race condition") + @Test void managedAsync() throws Exception { + get("/managedAsync"); + + testSpanHandler.takeRemoteSpan(Span.Kind.SERVER); + } + + @Override public void init(ServletContextHandler handler) { + ResourceConfig config = new ResourceConfig(); + config.register(new TestResource(httpTracing)); + config.register(SpanCustomizingApplicationEventListener.create()); + handler.addServlet(new ServletHolder(new ServletContainer(config)), "/*"); + + // add the underlying servlet tracing filter which the event listener decorates with more tags + FilterRegistration.Dynamic filterRegistration = + handler.getServletContext().addFilter("tracingFilter", TracingFilter.create(httpTracing)); + filterRegistration.setAsyncSupported(true); + // isMatchAfter=true is required for async tests to pass! + filterRegistration.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, "/*"); + } +} diff --git a/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/ITTracingApplicationEventListener.java b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/ITTracingApplicationEventListener.java new file mode 100644 index 0000000000..86b7011d2d --- /dev/null +++ b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/ITTracingApplicationEventListener.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenZipkin Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package brave.jakarta.jersey.server; + +import brave.Span; +import brave.test.http.ITServletContainer; +import brave.test.jakarta.http.Jetty11ServerController; +import okhttp3.Response; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.util.log.Log; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.servlet.ServletContainer; +import org.junit.AssumptionViolatedException; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class ITTracingApplicationEventListener extends ITServletContainer { + public ITTracingApplicationEventListener() { + super(new Jetty11ServerController(), Log.getLogger("org.eclipse.jetty.util.log")); + } + + @Override @Test public void reportsClientAddress() { + throw new AssumptionViolatedException("TODO!"); + } + + @Test void tagsResource() throws Exception { + get("/foo"); + + assertThat(testSpanHandler.takeRemoteSpan(Span.Kind.SERVER).tags()) + .containsEntry("jaxrs.resource.class", "TestResource") + .containsEntry("jaxrs.resource.method", "foo"); + } + + /** Tests that the span propagates between under asynchronous callbacks managed by jersey. */ + @Test void managedAsync() throws Exception { + Response response = get("/managedAsync"); + assertThat(response.isSuccessful()).withFailMessage("not successful: " + response).isTrue(); + + testSpanHandler.takeRemoteSpan(Span.Kind.SERVER); + } + + @Override public void init(ServletContextHandler handler) { + ResourceConfig config = new ResourceConfig(); + config.register(new TestResource(httpTracing)); + config.register(TracingApplicationEventListener.create(httpTracing)); + ServletHolder servlet = new ServletHolder(new ServletContainer(config)); + servlet.setAsyncSupported(true); + handler.addServlet(servlet, "/*"); + } +} diff --git a/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/InjectionTest.java b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/InjectionTest.java new file mode 100644 index 0000000000..91d42c2c5f --- /dev/null +++ b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/InjectionTest.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenZipkin Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package brave.jakarta.jersey.server; + +import brave.Tracing; +import brave.http.HttpTracing; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** This ensures all filters can be injected, supplied with only {@linkplain HttpTracing}. */ +public class InjectionTest { + Tracing tracing = Tracing.newBuilder().build(); + + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(HttpTracing.class).toInstance(HttpTracing.create(tracing)); + } + }); + + @AfterEach void close() { + tracing.close(); + } + + @Test void spanCustomizingApplicationEventListener() { + SpanCustomizingApplicationEventListener filter = + injector.getInstance(SpanCustomizingApplicationEventListener.class); + + assertThat(filter.parser.getClass()) + .isSameAs(EventParser.class); + } + + @Test void spanCustomizingApplicationEventListener_resource() { + SpanCustomizingApplicationEventListener filter = + injector.createChildInjector(new AbstractModule() { + @Override protected void configure() { + bind(EventParser.class).toInstance(EventParser.NOOP); + } + }).getInstance(SpanCustomizingApplicationEventListener.class); + + assertThat(filter.parser) + .isSameAs(EventParser.NOOP); + } + + @Test void tracingApplicationEventListener() { + TracingApplicationEventListener filter = + injector.getInstance(TracingApplicationEventListener.class); + + assertThat(filter.parser.getClass()) + .isSameAs(EventParser.class); + } + + @Test void tracingApplicationEventListener_resource() { + TracingApplicationEventListener filter = injector.createChildInjector(new AbstractModule() { + @Override protected void configure() { + bind(EventParser.class).toInstance(EventParser.NOOP); + } + }).getInstance(TracingApplicationEventListener.class); + + assertThat(filter.parser) + .isSameAs(EventParser.NOOP); + } +} diff --git a/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/RequestEventWrapperTest.java b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/RequestEventWrapperTest.java new file mode 100644 index 0000000000..bb824db89a --- /dev/null +++ b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/RequestEventWrapperTest.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenZipkin Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package brave.jakarta.jersey.server; + +import brave.jakarta.jersey.server.TracingApplicationEventListener.RequestEventWrapper; +import jakarta.ws.rs.ClientErrorException; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.internal.process.MappableException; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class RequestEventWrapperTest { + @Mock ContainerRequest request; + @Mock RequestEvent event; + @Mock ContainerResponse response; + + @Test void method() { + when(event.getContainerRequest()).thenReturn(request); + when(request.getMethod()).thenReturn("GET"); + + assertThat(new RequestEventWrapper(event).method()) + .isEqualTo("GET"); + } + + @Test void request() { + when(event.getContainerRequest()).thenReturn(request); + + assertThat(new RequestEventWrapper(event).request().unwrap()) + .isSameAs(request); + } + + @Test void statusCode() { + when(event.getContainerResponse()).thenReturn(response); + when(response.getStatus()).thenReturn(200); + + assertThat(new RequestEventWrapper(event).statusCode()).isEqualTo(200); + } + + @Test void statusCode_exception() { + when(event.getException()).thenReturn(new ClientErrorException(400)); + + assertThat(new RequestEventWrapper(event).statusCode()).isEqualTo(400); + } + + @Test void statusCode_mappableException() { + when(event.getException()).thenReturn(new MappableException(new ClientErrorException(400))); + + assertThat(new RequestEventWrapper(event).statusCode()).isEqualTo(400); + } + + @Test void statusCode_zeroNoResponse() { + assertThat(new RequestEventWrapper(event).statusCode()).isZero(); + } +} diff --git a/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/SpanCustomizingApplicationEventListenerTest.java b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/SpanCustomizingApplicationEventListenerTest.java new file mode 100644 index 0000000000..4854784c67 --- /dev/null +++ b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/SpanCustomizingApplicationEventListenerTest.java @@ -0,0 +1,211 @@ +/* + * Copyright The OpenZipkin Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package brave.jakarta.jersey.server; + +import brave.SpanCustomizer; +import java.net.URI; +import java.util.Arrays; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ExtendedUriInfo; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.uri.PathTemplate; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) // TODO: hunt down these +public class SpanCustomizingApplicationEventListenerTest { + @Mock + EventParser parser; + @Mock RequestEvent requestEvent; + @Mock ContainerRequest request; + @Mock ExtendedUriInfo uriInfo; + @Mock SpanCustomizer span; + SpanCustomizingApplicationEventListener listener; + + @BeforeEach void setup() { + listener = SpanCustomizingApplicationEventListener.create(parser); + when(requestEvent.getContainerRequest()).thenReturn(request); + when(request.getUriInfo()).thenReturn(uriInfo); + } + + @Test void onEvent_processesFINISHED() { + setEventType(RequestEvent.Type.FINISHED); + setBaseUri("/"); + + when(request.getProperty(SpanCustomizer.class.getName())).thenReturn(span); + + listener.onEvent(requestEvent); + + verify(parser).requestMatched(requestEvent, span); + } + + @Test void onEvent_setsErrorWhenNotAlreadySet() { + setEventType(RequestEvent.Type.FINISHED); + setBaseUri("/"); + + when(request.getProperty(SpanCustomizer.class.getName())).thenReturn(span); + + Exception error = new Exception(); + when(requestEvent.getException()).thenReturn(error); + when(request.getProperty("error")).thenReturn(null); + + listener.onEvent(requestEvent); + + verify(request).setProperty("error", error); + } + + /** Don't clobber user-defined properties! */ + @Test void onEvent_skipsErrorWhenSet() { + setEventType(RequestEvent.Type.FINISHED); + setBaseUri("/"); + + when(request.getProperty(SpanCustomizer.class.getName())).thenReturn(span); + + Exception error = new Exception(); + when(requestEvent.getException()).thenReturn(error); + when(request.getProperty("error")).thenReturn("madness"); + + listener.onEvent(requestEvent); + + verify(request).getProperty(SpanCustomizer.class.getName()); + verify(request).getProperty("error"); + verify(request).getUriInfo(); + verify(request).setProperty("http.route", ""); // empty means no route found + verifyNoMoreInteractions(request); // no setting of error + } + + @Test void onEvent_toleratesMissingCustomizer() { + setEventType(RequestEvent.Type.FINISHED); + setBaseUri("/"); + + listener.onEvent(requestEvent); + + verifyNoMoreInteractions(parser); + } + + @Test void onEvent_toleratesBadCustomizer() { + setEventType(RequestEvent.Type.FINISHED); + setBaseUri("/"); + + when(request.getProperty(SpanCustomizer.class.getName())).thenReturn("eyeballs"); + + listener.onEvent(requestEvent); + + verifyNoMoreInteractions(parser); + } + + @Test void onEvent_ignoresNotFinished() { + for (RequestEvent.Type type : RequestEvent.Type.values()) { + if (type == RequestEvent.Type.FINISHED) return; + + setEventType(type); + + listener.onEvent(requestEvent); + + verifyNoMoreInteractions(span); + } + } + + @Test void ignoresEventsExceptFinish() { + setBaseUri("/"); + when(uriInfo.getMatchedTemplates()).thenReturn(Arrays.asList( + new PathTemplate("/"), + new PathTemplate("/items/{itemId}") + )); + + assertThat(SpanCustomizingApplicationEventListener.route(request)) + .isEqualTo("/items/{itemId}"); + } + + @Test void route() { + setBaseUri("/"); + when(uriInfo.getMatchedTemplates()).thenReturn(Arrays.asList( + new PathTemplate("/"), + new PathTemplate("/items/{itemId}") + )); + + assertThat(SpanCustomizingApplicationEventListener.route(request)) + .isEqualTo("/items/{itemId}"); + } + + @Test void route_noPath() { + setBaseUri("/"); + when(uriInfo.getMatchedTemplates()).thenReturn(Arrays.asList( + new PathTemplate("/eggs") + )); + + assertThat(SpanCustomizingApplicationEventListener.route(request)) + .isEqualTo("/eggs"); + } + + /** not sure it is even possible for a template to match "/" "/".. */ + @Test void route_invalid() { + setBaseUri("/"); + when(uriInfo.getMatchedTemplates()).thenReturn(Arrays.asList( + new PathTemplate("/"), + new PathTemplate("/") + )); + + assertThat(SpanCustomizingApplicationEventListener.route(request)) + .isEmpty(); + } + + @Test void route_basePath() { + setBaseUri("/base"); + when(uriInfo.getMatchedTemplates()).thenReturn(Arrays.asList( + new PathTemplate("/"), + new PathTemplate("/items/{itemId}") + )); + + assertThat(SpanCustomizingApplicationEventListener.route(request)) + .isEqualTo("/base/items/{itemId}"); + } + + @Test void route_nested() { + setBaseUri("/"); + when(uriInfo.getMatchedTemplates()).thenReturn(Arrays.asList( + new PathTemplate("/"), + new PathTemplate("/items/{itemId}"), + new PathTemplate("/"), + new PathTemplate("/nested") + )); + + assertThat(SpanCustomizingApplicationEventListener.route(request)) + .isEqualTo("/nested/items/{itemId}"); + } + + /** when the path expression is on the type not on the method */ + @Test void route_nested_reverse() { + setBaseUri("/"); + when(uriInfo.getMatchedTemplates()).thenReturn(Arrays.asList( + new PathTemplate("/items/{itemId}"), + new PathTemplate("/"), + new PathTemplate("/nested"), + new PathTemplate("/") + )); + + assertThat(SpanCustomizingApplicationEventListener.route(request)) + .isEqualTo("/nested/items/{itemId}"); + } + + void setBaseUri(String path) { + when(uriInfo.getBaseUri()).thenReturn(URI.create(path)); + } + + void setEventType(RequestEvent.Type type) { + when(requestEvent.getType()).thenReturn(type); + } +} diff --git a/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/TestResource.java b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/TestResource.java new file mode 100644 index 0000000000..60031aa5aa --- /dev/null +++ b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/TestResource.java @@ -0,0 +1,133 @@ +/* + * Copyright The OpenZipkin Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package brave.jakarta.jersey.server; + +import brave.Tracer; +import brave.http.HttpTracing; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.OPTIONS; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.container.AsyncResponse; +import jakarta.ws.rs.container.Suspended; +import jakarta.ws.rs.core.Response; +import org.glassfish.jersey.server.ManagedAsync; + +import static brave.test.ITRemote.BAGGAGE_FIELD; +import static brave.test.http.ITHttpServer.NOT_READY_ISE; + +@Path("") +public class TestResource { + final Tracer tracer; + + TestResource(HttpTracing httpTracing) { + this.tracer = httpTracing.tracing().tracer(); + } + + @OPTIONS // intentionally leave out the @Path annotation + public Response root() { + return Response.ok().build(); + } + + @GET + @Path("foo") + public Response foo() { + return Response.ok().build(); + } + + @GET + @Path("extra") + public Response extra() { + return Response.ok(BAGGAGE_FIELD.getValue()).build(); + } + + @GET + @Path("badrequest") + public Response badrequest() { + return Response.status(400).build(); + } + + @GET + @Path("child") + public Response child() { + tracer.nextSpan().name("child").start().finish(); + return Response.status(200).build(); + } + + @GET + @Path("async") + public void async(@Suspended AsyncResponse response) { + if (tracer.currentSpan() == null) { + response.resume(new IllegalStateException("couldn't read current span!")); + return; + } + blockOnAsyncResult("foo", response); + } + + @GET + @Path("managedAsync") + @ManagedAsync + public void managedAsync(@Suspended AsyncResponse response) { + if (tracer.currentSpan() == null) { + response.resume(new IllegalStateException("couldn't read current span!")); + return; + } + response.resume("foo"); + } + + @GET + @Path("items/{itemId}") + public String item(@PathParam("itemId") String itemId) { + return itemId; + } + + @GET + @Path("async_items/{itemId}") + public void asyncItem(@PathParam("itemId") String itemId, @Suspended AsyncResponse response) { + blockOnAsyncResult(itemId, response); + } + + static void blockOnAsyncResult(String body, AsyncResponse response) { + Thread thread = new Thread(() -> response.resume(body)); + thread.start(); + try { + thread.join(); + } catch (InterruptedException e) { + } + } + + @GET + @Path("exception") + public Response notReady() { + throw new WebApplicationException(NOT_READY_ISE, 503); + } + + @GET + @Path("exceptionAsync") + public void notReadyAsync(@Suspended AsyncResponse response) { + Thread thread = new Thread(() -> response.resume( + new WebApplicationException(NOT_READY_ISE, 503) + )); + thread.start(); + try { + thread.join(); + } catch (InterruptedException e) { + } + } + + public static class NestedResource { + @GET + @Path("items/{itemId}") + public String item(@PathParam("itemId") String itemId) { + return itemId; + } + } + + @Path("nested") + public NestedResource nestedResource() { + return new NestedResource(); + } +} diff --git a/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/TracingApplicationEventListenerInjectionTest.java b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/TracingApplicationEventListenerInjectionTest.java new file mode 100644 index 0000000000..08e47c6ab9 --- /dev/null +++ b/instrumentation/jersey-server-jakarta/src/test/java/brave/jakarta/jersey/server/TracingApplicationEventListenerInjectionTest.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenZipkin Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package brave.jakarta.jersey.server; + +import brave.Tracing; +import brave.http.HttpTracing; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TracingApplicationEventListenerInjectionTest { + Tracing tracing = Tracing.newBuilder().build(); + + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + bind(HttpTracing.class).toInstance(HttpTracing.create(tracing)); + } + }); + + @AfterEach void close() { + tracing.close(); + } + + @Test void onlyRequiresHttpTracing() { + assertThat(injector.getInstance(TracingApplicationEventListener.class)) + .isNotNull(); + } +} diff --git a/instrumentation/jersey-server-jakarta/src/test/resources/log4j2.properties b/instrumentation/jersey-server-jakarta/src/test/resources/log4j2.properties new file mode 100755 index 0000000000..8162463e60 --- /dev/null +++ b/instrumentation/jersey-server-jakarta/src/test/resources/log4j2.properties @@ -0,0 +1,14 @@ +appenders=console +appender.console.type=Console +appender.console.name=STDOUT +appender.console.layout.type=PatternLayout +appender.console.layout.pattern=%d{ABSOLUTE} %-5p [%t] %C{2} (%F:%L) - %m%n +rootLogger.level=warn +rootLogger.appenderRefs=stdout +rootLogger.appenderRef.stdout.ref=STDOUT + +# mute logs that do not effect our tests +logger.wadl.name=org.glassfish.jersey.server.wadl.WadlFeature +logger.wadl.level=off +logger.model.name=org.glassfish.jersey.internal.inject.Providers +logger.model.level=off diff --git a/instrumentation/jersey-server/pom.xml b/instrumentation/jersey-server/pom.xml index c14740332a..5fcc1d9d45 100644 --- a/instrumentation/jersey-server/pom.xml +++ b/instrumentation/jersey-server/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/jms-jakarta/pom.xml b/instrumentation/jms-jakarta/pom.xml index 9f1a09bbf0..41e5f4d819 100644 --- a/instrumentation/jms-jakarta/pom.xml +++ b/instrumentation/jms-jakarta/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/jms/pom.xml b/instrumentation/jms/pom.xml index ad3bb46e8f..ff8dd6febd 100644 --- a/instrumentation/jms/pom.xml +++ b/instrumentation/jms/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/kafka-clients/pom.xml b/instrumentation/kafka-clients/pom.xml index d53fe340fa..a989a1c376 100644 --- a/instrumentation/kafka-clients/pom.xml +++ b/instrumentation/kafka-clients/pom.xml @@ -10,7 +10,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT brave-instrumentation-kafka-clients diff --git a/instrumentation/kafka-streams/pom.xml b/instrumentation/kafka-streams/pom.xml index 6c6359c9dd..162e202549 100644 --- a/instrumentation/kafka-streams/pom.xml +++ b/instrumentation/kafka-streams/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/messaging/pom.xml b/instrumentation/messaging/pom.xml index dfa6f09822..b582080e0f 100644 --- a/instrumentation/messaging/pom.xml +++ b/instrumentation/messaging/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/mongodb/pom.xml b/instrumentation/mongodb/pom.xml index 1dc048324d..557a0bb789 100644 --- a/instrumentation/mongodb/pom.xml +++ b/instrumentation/mongodb/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/mysql/pom.xml b/instrumentation/mysql/pom.xml index 319c1668a8..70bb0fd5eb 100644 --- a/instrumentation/mysql/pom.xml +++ b/instrumentation/mysql/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/mysql6/pom.xml b/instrumentation/mysql6/pom.xml index f72a45b817..69a5e4d182 100644 --- a/instrumentation/mysql6/pom.xml +++ b/instrumentation/mysql6/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/mysql8/pom.xml b/instrumentation/mysql8/pom.xml index 3a4644a58d..f602955f6c 100644 --- a/instrumentation/mysql8/pom.xml +++ b/instrumentation/mysql8/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/netty-codec-http/pom.xml b/instrumentation/netty-codec-http/pom.xml index 97d64aa588..c0ce1e9303 100644 --- a/instrumentation/netty-codec-http/pom.xml +++ b/instrumentation/netty-codec-http/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/okhttp3/pom.xml b/instrumentation/okhttp3/pom.xml index 64b972d2aa..4ac655cfd3 100644 --- a/instrumentation/okhttp3/pom.xml +++ b/instrumentation/okhttp3/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/pom.xml b/instrumentation/pom.xml index ae4a2bb41f..917fb32deb 100644 --- a/instrumentation/pom.xml +++ b/instrumentation/pom.xml @@ -11,7 +11,7 @@ io.zipkin.brave brave-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT brave-instrumentation-parent @@ -33,6 +33,7 @@ httpclient5 jaxrs2 jersey-server + jersey-server-jakarta jms jms-jakarta jakarta-jms diff --git a/instrumentation/rocketmq-client/pom.xml b/instrumentation/rocketmq-client/pom.xml index a19b3982a3..a6c9364aeb 100644 --- a/instrumentation/rocketmq-client/pom.xml +++ b/instrumentation/rocketmq-client/pom.xml @@ -10,7 +10,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT brave-instrumentation-rocketmq-client diff --git a/instrumentation/rpc/pom.xml b/instrumentation/rpc/pom.xml index a41cdfef81..562e6888e0 100644 --- a/instrumentation/rpc/pom.xml +++ b/instrumentation/rpc/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/servlet-jakarta/pom.xml b/instrumentation/servlet-jakarta/pom.xml index 468c9e114d..eae0a518fe 100644 --- a/instrumentation/servlet-jakarta/pom.xml +++ b/instrumentation/servlet-jakarta/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/servlet/pom.xml b/instrumentation/servlet/pom.xml index 6e00337f4d..0eae3dc758 100644 --- a/instrumentation/servlet/pom.xml +++ b/instrumentation/servlet/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/spring-rabbit/pom.xml b/instrumentation/spring-rabbit/pom.xml index b4a63e4eaf..364fbdf0f5 100644 --- a/instrumentation/spring-rabbit/pom.xml +++ b/instrumentation/spring-rabbit/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/spring-web/pom.xml b/instrumentation/spring-web/pom.xml index 4dab4cd955..01e8c1a38f 100644 --- a/instrumentation/spring-web/pom.xml +++ b/instrumentation/spring-web/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/spring-webmvc/pom.xml b/instrumentation/spring-webmvc/pom.xml index 4a0303cbe5..95a09443b5 100644 --- a/instrumentation/spring-webmvc/pom.xml +++ b/instrumentation/spring-webmvc/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/instrumentation/vertx-web/pom.xml b/instrumentation/vertx-web/pom.xml index 8e8bc2f9e1..6a5077fb68 100644 --- a/instrumentation/vertx-web/pom.xml +++ b/instrumentation/vertx-web/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-instrumentation-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0 diff --git a/pom.xml b/pom.xml index 62281dda69..481f9e9cda 100755 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ io.zipkin.brave brave-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT pom Brave (parent) @@ -93,6 +93,8 @@ 11.0.24 + + 3.0.17 5.0.0 3.9.0 diff --git a/spring-beans/pom.xml b/spring-beans/pom.xml index c866ac3287..0227ebd902 100644 --- a/spring-beans/pom.xml +++ b/spring-beans/pom.xml @@ -9,7 +9,7 @@ io.zipkin.brave brave-parent - 6.1.1-SNAPSHOT + 6.2.0-SNAPSHOT 4.0.0