diff --git a/bom/pom.xml b/bom/pom.xml index 2525415c36..7bc29fa2e8 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -153,6 +153,11 @@ jersey-entity-filtering ${project.version} + + org.glassfish.jersey.ext + jersey-micrometer + ${project.version} + org.glassfish.jersey.ext jersey-metainf-services diff --git a/examples/micrometer/README.MD b/examples/micrometer/README.MD new file mode 100644 index 0000000000..609a6ea151 --- /dev/null +++ b/examples/micrometer/README.MD @@ -0,0 +1,57 @@ +[//]: # " Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. " +[//]: # " " +[//]: # " This program and the accompanying materials are made available under the " +[//]: # " terms of the Eclipse Public License v. 2.0, which is available at " +[//]: # " http://www.eclipse.org/legal/epl-2.0. " +[//]: # " " +[//]: # " This Source Code may also be made available under the following Secondary " +[//]: # " Licenses when the conditions for such availability set forth in the " +[//]: # " Eclipse Public License v. 2.0 are satisfied: GNU General Public License, " +[//]: # " version 2 with the GNU Classpath Exception, which is available at " +[//]: # " https://www.gnu.org/software/classpath/license.html. " +[//]: # " " +[//]: # " SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 " + +jersey-micrometer-webapp +========================================================== + +This example demonstrates basics of Micrometer Jersey integration + +Contents +-------- + +The mapping of the URI path space is presented in the following table: + +URI path | Resource class | HTTP methods +------------------------------------------ | ------------------------- | -------------- +**_/micro/meter_** | JerseyResource | GET +**_/micro/metrics_** | JerseyResource | GET +**_/micro/metrics/metrics_** | JerseyResource | GET + +Sample Response +--------------- + +```javascript +--- (micro/meter) +Hello World! +---- (micro/metrics) +Listing available meters: http.shared.metrics; +---- (micro/metric/metrics) +Overall requests counts: 9, total time (millis): 35.799483 +``` + + +Running the Example +------------------- + +Run the example using [Grizzly](https://javaee.github.io/grizzly/) container as follows: + +> mvn clean compile exec:java + +- +- after few request to the main page go to the url +- - +- and see the list of available meters +- then go to the +- - +- and see statistics for the micro/meter page \ No newline at end of file diff --git a/examples/micrometer/pom.xml b/examples/micrometer/pom.xml new file mode 100644 index 0000000000..df423e1631 --- /dev/null +++ b/examples/micrometer/pom.xml @@ -0,0 +1,98 @@ + + + + 4.0.0 + + + org.glassfish.jersey.examples + project + 2.41-SNAPSHOT + + + jersey-micrometer-webapp + jar + jersey-micrometer-example-webapp + + Micrometer/Jersey metrics basic example + + + + org.glassfish.jersey.containers + jersey-container-grizzly2-http + + + org.glassfish.jersey.containers + jersey-container-servlet + + + org.glassfish.jersey.ext + jersey-micrometer + + + org.glassfish.jersey.inject + jersey-hk2 + + + org.glassfish.jersey.test-framework + jersey-test-framework-core + test + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-grizzly2 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + + + + + org.codehaus.mojo + exec-maven-plugin + + org.glassfish.jersey.examples.micrometer.App + + + + + + + + pre-release + + + + org.codehaus.mojo + xml-maven-plugin + + + org.apache.maven.plugins + maven-assembly-plugin + + + + + + + diff --git a/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/App.java b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/App.java new file mode 100644 index 0000000000..92dcf22e85 --- /dev/null +++ b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/App.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.examples.micrometer; + +import java.io.IOException; +import java.net.URI; +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory; +import org.glassfish.jersey.micrometer.server.DefaultJerseyTagsProvider; +import org.glassfish.jersey.micrometer.server.MetricsApplicationEventListener; +import org.glassfish.jersey.server.ResourceConfig; + +import org.glassfish.grizzly.http.server.HttpServer; + +public class App { + + private static final URI BASE_URI = URI.create("http://localhost:8080/micro/"); + public static final String ROOT_PATH = "meter"; + + public static void main(String[] args) { + try { + System.out.println("Micrometer/ Jersey Basic Example App"); + + final MeterRegistry registry = new SimpleMeterRegistry(); + + final ResourceConfig resourceConfig = new ResourceConfig(MicrometerResource.class) + .register(new MetricsApplicationEventListener(registry, new DefaultJerseyTagsProvider(), + "http.shared.metrics", true)) + .register(new MetricsResource(registry)); + final HttpServer server = GrizzlyHttpServerFactory.createHttpServer(BASE_URI, resourceConfig, false); + Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { + @Override + public void run() { + server.shutdownNow(); + } + })); + server.start(); + + System.out.println(String.format("Application started.\nTry out %s%s\n" + + "After several requests go to %s%s\nAnd after that go to the %s%s\n" + + "Stop the application using CTRL+C", + BASE_URI, ROOT_PATH, BASE_URI, "metrics", BASE_URI, "metrics/metrics")); + Thread.currentThread().join(); + } catch (IOException | InterruptedException ex) { + Logger.getLogger(App.class.getName()).log(Level.SEVERE, null, ex); + } + + } +} diff --git a/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/MetricsResource.java b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/MetricsResource.java new file mode 100644 index 0000000000..60e9194d8e --- /dev/null +++ b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/MetricsResource.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.examples.micrometer; + +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import java.util.concurrent.TimeUnit; + +@Path("metrics") +public class MetricsResource { + + private final MeterRegistry registry; + + public MetricsResource(MeterRegistry registry) { + this.registry = registry; + } + + @GET + @Produces("text/plain") + public String getMeters() { + final StringBuffer result = new StringBuffer(); + try { + result.append("Listing available meters: "); + for (final Meter meter : registry.getMeters()) { + result.append(meter.getId().getName()); + result.append("; "); + } + } catch (Exception ex) { + System.out.println(ex); + result.append("Exception occured, see log for details..."); + result.append(ex.toString()); + } + return result.toString(); + } + @GET + @Path("metrics") + @Produces("text/plain") + public String getMetrics() { + final StringBuffer result = new StringBuffer(); + try { + final Timer timer = registry.get("http.shared.metrics") + .tags("method", "GET", "uri", "/micro/meter", "status", "200", "exception", "None", "outcome", "SUCCESS") + .timer(); + result.append(String.format("Overall requests counts: %d, total time (millis): %f", + timer.count(), timer.totalTime(TimeUnit.MILLISECONDS))); + } catch (Exception ex) { + System.out.println(ex); + result.append("Exception occured, see log for details..."); + result.append(ex.toString()); + } + return result.toString(); + } +} diff --git a/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/MicrometerResource.java b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/MicrometerResource.java new file mode 100644 index 0000000000..7ff108337a --- /dev/null +++ b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/MicrometerResource.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.examples.micrometer; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; + +@Path("meter") +public class MicrometerResource { + public static final String CLICHED_MESSAGE = "Hello World!"; + + @GET + @Produces("text/plain") + public String getHello() { + return CLICHED_MESSAGE; + } + +} diff --git a/examples/micrometer/src/test/java/org/glassfish/jersey/examples/micrometer/MicrometerTest.java b/examples/micrometer/src/test/java/org/glassfish/jersey/examples/micrometer/MicrometerTest.java new file mode 100644 index 0000000000..4a5f4eea6e --- /dev/null +++ b/examples/micrometer/src/test/java/org/glassfish/jersey/examples/micrometer/MicrometerTest.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.examples.micrometer; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.glassfish.jersey.micrometer.server.DefaultJerseyTagsProvider; +import org.glassfish.jersey.micrometer.server.MetricsApplicationEventListener; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.server.ResourceConfig; +import org.junit.jupiter.api.Test; + +import javax.ws.rs.core.Application; + +import java.util.concurrent.TimeUnit; + +import static org.glassfish.jersey.examples.micrometer.MicrometerResource.CLICHED_MESSAGE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class MicrometerTest extends JerseyTest { + + static final String TIMER_METRIC_NAME = "http.server.requests"; + + MeterRegistry registry; + + @Override + protected Application configure() { + registry = new SimpleMeterRegistry(); + MetricsApplicationEventListener metricsListener = new MetricsApplicationEventListener(registry, + new DefaultJerseyTagsProvider(), TIMER_METRIC_NAME, true); + return new ResourceConfig(MicrometerResource.class) + .register(metricsListener) + .register(new MetricsResource(registry)); + } + + @Test + void meterResourceTest() throws InterruptedException { + String response = target("/meter").request().get(String.class); + assertEquals(response, CLICHED_MESSAGE); + // Jersey metrics are recorded asynchronously to the request completing + Thread.sleep(10); + Timer timer = registry.get(TIMER_METRIC_NAME) + .tags("method", "GET", "uri", "/meter", "status", "200", "exception", "None", "outcome", "SUCCESS") + .timer(); + assertEquals(timer.count(), 1); + assertNotNull(timer.totalTime(TimeUnit.NANOSECONDS)); + } + +} \ No newline at end of file diff --git a/examples/pom.xml b/examples/pom.xml index ba5480b91d..3cf0079764 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -102,6 +102,7 @@ managed-client-simple-webapp multipart-webapp + micrometer open-tracing osgi-helloworld-webapp osgi-http-service diff --git a/ext/micrometer/pom.xml b/ext/micrometer/pom.xml new file mode 100644 index 0000000000..800083ec1d --- /dev/null +++ b/ext/micrometer/pom.xml @@ -0,0 +1,105 @@ + + + + + + project + org.glassfish.jersey.ext + 2.41-SNAPSHOT + + 4.0.0 + + jersey-micrometer + + + + + io.micrometer + micrometer-core + ${micrometer.version} + + + + org.glassfish.jersey.core + jersey-common + ${project.version} + + + + org.glassfish.jersey.core + jersey-server + ${project.version} + + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-inmemory + ${project.version} + test + + + + org.glassfish.jersey.test-framework + jersey-test-framework-core + ${project.version} + test + + + + org.aspectj + aspectjweaver + ${aspectj.weaver.version} + test + true + + + + io.micrometer + micrometer-tracing-integration-test + ${micrometer-tracing.version} + test + + + + + + + org.apache.felix + maven-bundle-plugin + true + true + + + + org.glassfish.jersey.micrometer.server.*;version=${project.version} + + + org.eclipse.microprofile.micrometer.server.*;version="!", + * + + + true + + + + + + diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/AnnotationFinder.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/AnnotationFinder.java new file mode 100644 index 0000000000..82b8c5439f --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/AnnotationFinder.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; + +public interface AnnotationFinder { + + AnnotationFinder DEFAULT = new AnnotationFinder() { + }; + + /** + * The default implementation performs a simple search for a declared annotation + * matching the search type. Spring provides a more sophisticated annotation search + * utility that matches on meta-annotations as well. + * @param annotatedElement The element to search. + * @param annotationType The annotation type class. + * @param Annotation type to search for. + * @return A matching annotation. + */ + @SuppressWarnings("unchecked") + default A findAnnotation(AnnotatedElement annotatedElement, Class annotationType) { + Annotation[] anns = annotatedElement.getDeclaredAnnotations(); + for (Annotation ann : anns) { + if (ann.annotationType() == annotationType) { + return (A) ann; + } + } + return null; + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/DefaultJerseyObservationConvention.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/DefaultJerseyObservationConvention.java new file mode 100644 index 0000000000..172465b15c --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/DefaultJerseyObservationConvention.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import io.micrometer.common.KeyValues; +import io.micrometer.common.lang.Nullable; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.monitoring.RequestEvent; + +/** + * Default implementation for {@link JerseyObservationConvention}. + * + * @author Marcin Grzejszczak + * @since 2.41 + */ +public class DefaultJerseyObservationConvention implements JerseyObservationConvention { + + private final String metricsName; + + public DefaultJerseyObservationConvention(String metricsName) { + this.metricsName = metricsName; + } + + @Override + public KeyValues getLowCardinalityKeyValues(JerseyContext context) { + RequestEvent event = context.getRequestEvent(); + ContainerRequest request = context.getCarrier(); + ContainerResponse response = context.getResponse(); + return KeyValues.of(JerseyKeyValues.method(request), JerseyKeyValues.uri(event), + JerseyKeyValues.exception(event), JerseyKeyValues.status(response), JerseyKeyValues.outcome(response)); + } + + @Override + public String getName() { + return this.metricsName; + } + + @Nullable + @Override + public String getContextualName(JerseyContext context) { + if (context.getCarrier() == null) { + return null; + } + return "HTTP " + context.getCarrier().getMethod(); + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/DefaultJerseyTagsProvider.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/DefaultJerseyTagsProvider.java new file mode 100644 index 0000000000..6c080a440a --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/DefaultJerseyTagsProvider.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.monitoring.RequestEvent; + +/** + * Default implementation for {@link JerseyTagsProvider}. + * + * @author Michael Weirauch + * @author Johnny Lim + * @since 2.41 + */ +public final class DefaultJerseyTagsProvider implements JerseyTagsProvider { + + @Override + public Iterable httpRequestTags(RequestEvent event) { + ContainerResponse response = event.getContainerResponse(); + return Tags.of(JerseyTags.method(event.getContainerRequest()), JerseyTags.uri(event), + JerseyTags.exception(event), JerseyTags.status(response), JerseyTags.outcome(response)); + } + + @Override + public Iterable httpLongRequestTags(RequestEvent event) { + return Tags.of(JerseyTags.method(event.getContainerRequest()), JerseyTags.uri(event)); + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyContext.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyContext.java new file mode 100644 index 0000000000..97ff36a610 --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyContext.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import java.util.List; + +import io.micrometer.observation.transport.ReceiverContext; +import io.micrometer.observation.transport.RequestReplyReceiverContext; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.monitoring.RequestEvent; + +/** + * A {@link ReceiverContext} for Jersey. + * + * @author Marcin Grzejszczak + * @since 2.41 + */ +public class JerseyContext extends RequestReplyReceiverContext { + + private RequestEvent requestEvent; + + public JerseyContext(RequestEvent requestEvent) { + super((carrier, key) -> { + List requestHeader = carrier.getRequestHeader(key); + if (requestHeader == null || requestHeader.isEmpty()) { + return null; + } + return requestHeader.get(0); + }); + this.requestEvent = requestEvent; + setCarrier(requestEvent.getContainerRequest()); + } + + public void setRequestEvent(RequestEvent requestEvent) { + this.requestEvent = requestEvent; + } + + public RequestEvent getRequestEvent() { + return requestEvent; + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyKeyValues.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyKeyValues.java new file mode 100644 index 0000000000..66cdaf7b1e --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyKeyValues.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.util.StringUtils; +import io.micrometer.core.instrument.binder.http.Outcome; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.ExtendedUriInfo; +import org.glassfish.jersey.server.monitoring.RequestEvent; + +/** + * Factory methods for {@link KeyValue KeyValues} associated with a request-response + * exchange that is handled by Jersey server. + */ +class JerseyKeyValues { + + private static final KeyValue URI_NOT_FOUND = JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.URI + .withValue("NOT_FOUND"); + + private static final KeyValue URI_REDIRECTION = JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.URI + .withValue("REDIRECTION"); + + private static final KeyValue URI_ROOT = JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.URI + .withValue("root"); + + private static final KeyValue EXCEPTION_NONE = JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.EXCEPTION + .withValue("None"); + + private static final KeyValue STATUS_SERVER_ERROR = JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.STATUS + .withValue("500"); + + private static final KeyValue METHOD_UNKNOWN = JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.METHOD + .withValue("UNKNOWN"); + + private JerseyKeyValues() { + } + + /** + * Creates a {@code method} KeyValue based on the {@link ContainerRequest#getMethod() + * method} of the given {@code request}. + * @param request the container request + * @return the method KeyValue whose value is a capitalized method (e.g. GET). + */ + static KeyValue method(ContainerRequest request) { + return (request != null) + ? JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.METHOD.withValue(request.getMethod()) + : METHOD_UNKNOWN; + } + + /** + * Creates a {@code status} KeyValue based on the status of the given + * {@code response}. + * @param response the container response + * @return the status KeyValue derived from the status of the response + */ + static KeyValue status(ContainerResponse response) { + /* In case there is no response we are dealing with an unmapped exception. */ + return (response != null) ? JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.STATUS + .withValue(Integer.toString(response.getStatus())) : STATUS_SERVER_ERROR; + } + + /** + * Creates a {@code uri} KeyValue based on the URI of the given {@code event}. Uses + * the {@link ExtendedUriInfo#getMatchedTemplates()} if available. {@code REDIRECTION} + * for 3xx responses, {@code NOT_FOUND} for 404 responses. + * @param event the request event + * @return the uri KeyValue derived from the request event + */ + static KeyValue uri(RequestEvent event) { + ContainerResponse response = event.getContainerResponse(); + if (response != null) { + int status = response.getStatus(); + if (JerseyTags.isRedirection(status) && event.getUriInfo().getMatchedResourceMethod() == null) { + return URI_REDIRECTION; + } + if (status == 404 && event.getUriInfo().getMatchedResourceMethod() == null) { + return URI_NOT_FOUND; + } + } + String matchingPattern = JerseyTags.getMatchingPattern(event); + if (matchingPattern.equals("/")) { + return URI_ROOT; + } + return JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.URI.withValue(matchingPattern); + } + + /** + * Creates an {@code exception} KeyValue based on the {@link Class#getSimpleName() + * simple name} of the class of the given {@code exception}. + * @param event the request event + * @return the exception KeyValue derived from the exception + */ + static KeyValue exception(RequestEvent event) { + Throwable exception = event.getException(); + if (exception == null) { + return EXCEPTION_NONE; + } + ContainerResponse response = event.getContainerResponse(); + if (response != null) { + int status = response.getStatus(); + if (status == 404 || JerseyTags.isRedirection(status)) { + return EXCEPTION_NONE; + } + } + if (exception.getCause() != null) { + exception = exception.getCause(); + } + String simpleName = exception.getClass().getSimpleName(); + return JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.EXCEPTION + .withValue(StringUtils.isNotEmpty(simpleName) ? simpleName : exception.getClass().getName()); + } + + /** + * Creates an {@code outcome} KeyValue based on the status of the given + * {@code response}. + * @param response the container response + * @return the outcome KeyValue derived from the status of the response + */ + static KeyValue outcome(ContainerResponse response) { + if (response != null) { + Outcome outcome = Outcome.forStatus(response.getStatus()); + return JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.OUTCOME.withValue(outcome.name()); + } + /* In case there is no response we are dealing with an unmapped exception. */ + return JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.OUTCOME + .withValue(Outcome.SERVER_ERROR.name()); + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyObservationConvention.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyObservationConvention.java new file mode 100644 index 0000000000..24bb4754bf --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyObservationConvention.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; + +/** + * Provides names and {@link io.micrometer.common.KeyValues} for Jersey request + * observations. + * + * @author Marcin Grzejszczak + * @since 2.41 + */ +public interface JerseyObservationConvention extends ObservationConvention { + + @Override + default boolean supportsContext(Observation.Context context) { + return context instanceof JerseyContext; + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyObservationDocumentation.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyObservationDocumentation.java new file mode 100644 index 0000000000..bebbde9716 --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyObservationDocumentation.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import io.micrometer.common.docs.KeyName; +import io.micrometer.common.lang.NonNullApi; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.docs.ObservationDocumentation; + +/** + * An {@link ObservationDocumentation} for Jersey. + * + * @author Marcin Grzejszczak + * @since 2.41 + */ +@NonNullApi +public enum JerseyObservationDocumentation implements ObservationDocumentation { + + /** + * Default observation for Jersey. + */ + DEFAULT { + @Override + public Class> getDefaultConvention() { + return DefaultJerseyObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return JerseyLegacyLowCardinalityTags.values(); + } + }; + + @NonNullApi + enum JerseyLegacyLowCardinalityTags implements KeyName { + + OUTCOME { + @Override + public String asString() { + return "outcome"; + } + }, + + METHOD { + @Override + public String asString() { + return "method"; + } + }, + + URI { + @Override + public String asString() { + return "uri"; + } + }, + + EXCEPTION { + @Override + public String asString() { + return "exception"; + } + }, + + STATUS { + @Override + public String asString() { + return "status"; + } + } + + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyTags.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyTags.java new file mode 100644 index 0000000000..d723c7c1b9 --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyTags.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import java.util.List; +import java.util.regex.Pattern; + +import io.micrometer.common.util.StringUtils; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.binder.http.Outcome; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.ExtendedUriInfo; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.uri.UriTemplate; + +/** + * Factory methods for {@link Tag Tags} associated with a request-response exchange that + * is handled by Jersey server. + * + * @author Michael Weirauch + * @author Johnny Lim + * @since 2.41 + */ +public final class JerseyTags { + + private static final Tag URI_NOT_FOUND = Tag.of("uri", "NOT_FOUND"); + + private static final Tag URI_REDIRECTION = Tag.of("uri", "REDIRECTION"); + + private static final Tag URI_ROOT = Tag.of("uri", "root"); + + private static final Tag EXCEPTION_NONE = Tag.of("exception", "None"); + + private static final Tag STATUS_SERVER_ERROR = Tag.of("status", "500"); + + private static final Tag METHOD_UNKNOWN = Tag.of("method", "UNKNOWN"); + + static final Pattern TRAILING_SLASH_PATTERN = Pattern.compile("/$"); + + static final Pattern MULTIPLE_SLASH_PATTERN = Pattern.compile("//+"); + + private JerseyTags() { + } + + /** + * Creates a {@code method} tag based on the {@link ContainerRequest#getMethod() + * method} of the given {@code request}. + * @param request the container request + * @return the method tag whose value is a capitalized method (e.g. GET). + */ + public static Tag method(ContainerRequest request) { + return (request != null) ? Tag.of("method", request.getMethod()) : METHOD_UNKNOWN; + } + + /** + * Creates a {@code status} tag based on the status of the given {@code response}. + * @param response the container response + * @return the status tag derived from the status of the response + */ + public static Tag status(ContainerResponse response) { + /* In case there is no response we are dealing with an unmapped exception. */ + return (response != null) ? Tag.of("status", Integer.toString(response.getStatus())) : STATUS_SERVER_ERROR; + } + + /** + * Creates a {@code uri} tag based on the URI of the given {@code event}. Uses the + * {@link ExtendedUriInfo#getMatchedTemplates()} if available. {@code REDIRECTION} for + * 3xx responses, {@code NOT_FOUND} for 404 responses. + * @param event the request event + * @return the uri tag derived from the request event + */ + public static Tag uri(RequestEvent event) { + ContainerResponse response = event.getContainerResponse(); + if (response != null) { + int status = response.getStatus(); + if (isRedirection(status) && event.getUriInfo().getMatchedResourceMethod() == null) { + return URI_REDIRECTION; + } + if (status == 404 && event.getUriInfo().getMatchedResourceMethod() == null) { + return URI_NOT_FOUND; + } + } + String matchingPattern = getMatchingPattern(event); + if (matchingPattern.equals("/")) { + return URI_ROOT; + } + return Tag.of("uri", matchingPattern); + } + + static boolean isRedirection(int status) { + return 300 <= status && status < 400; + } + + static String getMatchingPattern(RequestEvent event) { + ExtendedUriInfo uriInfo = event.getUriInfo(); + List templates = uriInfo.getMatchedTemplates(); + + StringBuilder sb = new StringBuilder(); + sb.append(uriInfo.getBaseUri().getPath()); + for (int i = templates.size() - 1; i >= 0; i--) { + sb.append(templates.get(i).getTemplate()); + } + String multipleSlashCleaned = MULTIPLE_SLASH_PATTERN.matcher(sb.toString()).replaceAll("/"); + if (multipleSlashCleaned.equals("/")) { + return multipleSlashCleaned; + } + return TRAILING_SLASH_PATTERN.matcher(multipleSlashCleaned).replaceAll(""); + } + + /** + * Creates an {@code exception} tag based on the {@link Class#getSimpleName() simple + * name} of the class of the given {@code exception}. + * @param event the request event + * @return the exception tag derived from the exception + */ + public static Tag exception(RequestEvent event) { + Throwable exception = event.getException(); + if (exception == null) { + return EXCEPTION_NONE; + } + ContainerResponse response = event.getContainerResponse(); + if (response != null) { + int status = response.getStatus(); + if (status == 404 || isRedirection(status)) { + return EXCEPTION_NONE; + } + } + if (exception.getCause() != null) { + exception = exception.getCause(); + } + String simpleName = exception.getClass().getSimpleName(); + return Tag.of("exception", StringUtils.isNotEmpty(simpleName) ? simpleName : exception.getClass().getName()); + } + + /** + * Creates an {@code outcome} tag based on the status of the given {@code response}. + * @param response the container response + * @return the outcome tag derived from the status of the response + */ + public static Tag outcome(ContainerResponse response) { + if (response != null) { + return Outcome.forStatus(response.getStatus()).asTag(); + } + /* In case there is no response we are dealing with an unmapped exception. */ + return Outcome.SERVER_ERROR.asTag(); + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyTagsProvider.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyTagsProvider.java new file mode 100644 index 0000000000..c1d2da017a --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyTagsProvider.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import io.micrometer.core.instrument.Tag; +import org.glassfish.jersey.server.monitoring.RequestEvent; + +/** + * Provides {@link Tag Tags} for Jersey request metrics. + * + * @author Michael Weirauch + * @since 2.41 + */ +public interface JerseyTagsProvider { + + /** + * Provides tags to be associated with metrics for the given {@code event}. + * @param event the request event + * @return tags to associate with metrics recorded for the request + */ + Iterable httpRequestTags(RequestEvent event); + + /** + * Provides tags to be associated with the + * {@link io.micrometer.core.instrument.LongTaskTimer} which instruments the given + * long-running {@code event}. + * @param event the request event + * @return tags to associate with metrics recorded for the request + */ + Iterable httpLongRequestTags(RequestEvent event); + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/MetricsApplicationEventListener.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/MetricsApplicationEventListener.java new file mode 100644 index 0000000000..30ccc362d6 --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/MetricsApplicationEventListener.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import io.micrometer.core.instrument.MeterRegistry; +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 static java.util.Objects.requireNonNull; + +/** + * The Micrometer {@link ApplicationEventListener} which registers + * {@link RequestEventListener} for instrumenting Jersey server requests. + * + * @author Michael Weirauch + * @since 2.41 + */ +public class MetricsApplicationEventListener implements ApplicationEventListener { + + private final MeterRegistry meterRegistry; + + private final JerseyTagsProvider tagsProvider; + + private final String metricName; + + private final AnnotationFinder annotationFinder; + + private final boolean autoTimeRequests; + + public MetricsApplicationEventListener(MeterRegistry registry, JerseyTagsProvider tagsProvider, String metricName, + boolean autoTimeRequests) { + this(registry, tagsProvider, metricName, autoTimeRequests, AnnotationFinder.DEFAULT); + } + + public MetricsApplicationEventListener(MeterRegistry registry, JerseyTagsProvider tagsProvider, String metricName, + boolean autoTimeRequests, AnnotationFinder annotationFinder) { + this.meterRegistry = requireNonNull(registry); + this.tagsProvider = requireNonNull(tagsProvider); + this.metricName = requireNonNull(metricName); + this.annotationFinder = requireNonNull(annotationFinder); + this.autoTimeRequests = autoTimeRequests; + } + + @Override + public void onEvent(ApplicationEvent event) { + } + + @Override + public RequestEventListener onRequest(RequestEvent requestEvent) { + return new MetricsRequestEventListener(meterRegistry, tagsProvider, metricName, autoTimeRequests, + annotationFinder); + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListener.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListener.java new file mode 100644 index 0000000000..9036637bee --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListener.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import io.micrometer.core.annotation.Timed; +import io.micrometer.core.instrument.LongTaskTimer; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.model.ResourceMethod; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEventListener; + +import static java.util.Objects.requireNonNull; + +/** + * {@link RequestEventListener} recording timings for Jersey server requests. + * + * @author Michael Weirauch + * @author Jon Schneider + * @since 2.41 + */ +public class MetricsRequestEventListener implements RequestEventListener { + + private final Map shortTaskSample = Collections + .synchronizedMap(new IdentityHashMap<>()); + + private final Map> longTaskSamples = Collections + .synchronizedMap(new IdentityHashMap<>()); + + private final Map> timedAnnotationsOnRequest = Collections + .synchronizedMap(new IdentityHashMap<>()); + + private final MeterRegistry registry; + + private final JerseyTagsProvider tagsProvider; + + private boolean autoTimeRequests; + + private final TimedFinder timedFinder; + + private final String metricName; + + public MetricsRequestEventListener(MeterRegistry registry, JerseyTagsProvider tagsProvider, String metricName, + boolean autoTimeRequests, AnnotationFinder annotationFinder) { + this.registry = requireNonNull(registry); + this.tagsProvider = requireNonNull(tagsProvider); + this.metricName = requireNonNull(metricName); + this.autoTimeRequests = autoTimeRequests; + this.timedFinder = new TimedFinder(annotationFinder); + } + + @Override + public void onEvent(RequestEvent event) { + ContainerRequest containerRequest = event.getContainerRequest(); + Set timedAnnotations; + + switch (event.getType()) { + case ON_EXCEPTION: + if (!isNotFoundException(event)) { + break; + } + time(event, containerRequest); + break; + case REQUEST_MATCHED: + time(event, containerRequest); + break; + case FINISHED: + timedAnnotations = timedAnnotationsOnRequest.remove(containerRequest); + Timer.Sample shortSample = shortTaskSample.remove(containerRequest); + + if (shortSample != null) { + for (Timer timer : shortTimers(timedAnnotations, event)) { + shortSample.stop(timer); + } + } + + Collection longSamples = this.longTaskSamples.remove(containerRequest); + if (longSamples != null) { + for (LongTaskTimer.Sample longSample : longSamples) { + longSample.stop(); + } + } + break; + } + } + + private void time(RequestEvent event, ContainerRequest containerRequest) { + Set timedAnnotations; + timedAnnotations = annotations(event); + + timedAnnotationsOnRequest.put(containerRequest, timedAnnotations); + shortTaskSample.put(containerRequest, Timer.start(registry)); + + List longTaskSamples = longTaskTimers(timedAnnotations, event).stream() + .map(LongTaskTimer::start) + .collect(Collectors.toList()); + if (!longTaskSamples.isEmpty()) { + this.longTaskSamples.put(containerRequest, longTaskSamples); + } + } + + private boolean isNotFoundException(RequestEvent event) { + Throwable t = event.getException(); + if (t == null) { + return false; + } + String className = t.getClass().getCanonicalName(); + return className.equals("jakarta.ws.rs.NotFoundException") || className.equals("javax.ws.rs.NotFoundException"); + } + + private Set shortTimers(Set timed, RequestEvent event) { + /* + * Given we didn't find any matching resource method, 404s will be only recorded + * when auto-time-requests is enabled. On par with WebMVC instrumentation. + */ + if ((timed == null || timed.isEmpty()) && autoTimeRequests) { + return Collections.singleton(registry.timer(metricName, tagsProvider.httpRequestTags(event))); + } + + if (timed == null) { + return Collections.emptySet(); + } + + return timed.stream() + .filter(annotation -> !annotation.longTask()) + .map(t -> Timer.builder(t, metricName).tags(tagsProvider.httpRequestTags(event)).register(registry)) + .collect(Collectors.toSet()); + } + + private Set longTaskTimers(Set timed, RequestEvent event) { + return timed.stream() + .filter(Timed::longTask) + .map(LongTaskTimer::builder) + .map(b -> b.tags(tagsProvider.httpLongRequestTags(event)).register(registry)) + .collect(Collectors.toSet()); + } + + private Set annotations(RequestEvent event) { + final Set timed = new HashSet<>(); + + final ResourceMethod matchingResourceMethod = event.getUriInfo().getMatchedResourceMethod(); + if (matchingResourceMethod != null) { + // collect on method level + timed.addAll(timedFinder.findTimedAnnotations(matchingResourceMethod.getInvocable().getHandlingMethod())); + + // fallback on class level + if (timed.isEmpty()) { + timed.addAll(timedFinder.findTimedAnnotations( + matchingResourceMethod.getInvocable().getHandlingMethod().getDeclaringClass())); + } + } + return timed; + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationApplicationEventListener.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationApplicationEventListener.java new file mode 100644 index 0000000000..6f519f0c22 --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationApplicationEventListener.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import io.micrometer.observation.ObservationRegistry; +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 static java.util.Objects.requireNonNull; + +/** + * The Micrometer {@link ApplicationEventListener} which registers + * {@link RequestEventListener} for instrumenting Jersey server requests with + * observations. + * + * @author Marcin Grzejszczak + * @since 2.41 + */ +public class ObservationApplicationEventListener implements ApplicationEventListener { + + private final ObservationRegistry observationRegistry; + + private final String metricName; + + private final JerseyObservationConvention jerseyObservationConvention; + + public ObservationApplicationEventListener(ObservationRegistry observationRegistry, String metricName) { + this(observationRegistry, metricName, null); + } + + public ObservationApplicationEventListener(ObservationRegistry observationRegistry, String metricName, + JerseyObservationConvention jerseyObservationConvention) { + this.observationRegistry = requireNonNull(observationRegistry); + this.metricName = requireNonNull(metricName); + this.jerseyObservationConvention = jerseyObservationConvention; + } + + @Override + public void onEvent(ApplicationEvent event) { + } + + @Override + public RequestEventListener onRequest(RequestEvent requestEvent) { + return new ObservationRequestEventListener(observationRegistry, metricName, jerseyObservationConvention); + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationRequestEventListener.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationRequestEventListener.java new file mode 100644 index 0000000000..96c463cddc --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationRequestEventListener.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Objects; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEventListener; + +import static java.util.Objects.requireNonNull; + +/** + * {@link RequestEventListener} recording observations for Jersey server requests. + * + * @author Marcin Grzejszczak + * @since 2.41 + */ +public class ObservationRequestEventListener implements RequestEventListener { + + private final Map observations = Collections + .synchronizedMap(new IdentityHashMap<>()); + + private final ObservationRegistry registry; + + private final JerseyObservationConvention customConvention; + + private final String metricName; + + private final JerseyObservationConvention defaultConvention; + + public ObservationRequestEventListener(ObservationRegistry registry, String metricName) { + this(registry, metricName, null); + } + + public ObservationRequestEventListener(ObservationRegistry registry, String metricName, + JerseyObservationConvention customConvention) { + this.registry = requireNonNull(registry); + this.metricName = requireNonNull(metricName); + this.customConvention = customConvention; + this.defaultConvention = new DefaultJerseyObservationConvention(this.metricName); + } + + @Override + public void onEvent(RequestEvent event) { + ContainerRequest containerRequest = event.getContainerRequest(); + + switch (event.getType()) { + case ON_EXCEPTION: + if (!isNotFoundException(event)) { + break; + } + startObservation(event); + break; + case REQUEST_MATCHED: + startObservation(event); + break; + case RESP_FILTERS_START: + ObservationScopeAndContext observationScopeAndContext = observations.get(containerRequest); + if (observationScopeAndContext != null) { + observationScopeAndContext.jerseyContext.setResponse(event.getContainerResponse()); + observationScopeAndContext.jerseyContext.setRequestEvent(event); + } + break; + case FINISHED: + ObservationScopeAndContext finishedObservation = observations.remove(containerRequest); + if (finishedObservation != null) { + finishedObservation.jerseyContext.setRequestEvent(event); + Observation.Scope observationScope = finishedObservation.observationScope; + observationScope.close(); + observationScope.getCurrentObservation().stop(); + } + break; + default: + break; + } + } + + private void startObservation(RequestEvent event) { + JerseyContext jerseyContext = new JerseyContext(event); + Observation observation = JerseyObservationDocumentation.DEFAULT.start(this.customConvention, + this.defaultConvention, () -> jerseyContext, this.registry); + Observation.Scope scope = observation.openScope(); + observations.put(event.getContainerRequest(), new ObservationScopeAndContext(scope, jerseyContext)); + } + + private boolean isNotFoundException(RequestEvent event) { + Throwable t = event.getException(); + if (t == null) { + return false; + } + String className = t.getClass().getCanonicalName(); + return className.equals("jakarta.ws.rs.NotFoundException") || className.equals("javax.ws.rs.NotFoundException"); + } + + private static class ObservationScopeAndContext { + + final Observation.Scope observationScope; + + final JerseyContext jerseyContext; + + ObservationScopeAndContext(Observation.Scope observationScope, JerseyContext jerseyContext) { + this.observationScope = observationScope; + this.jerseyContext = jerseyContext; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ObservationScopeAndContext that = (ObservationScopeAndContext) o; + return Objects.equals(observationScope, that.observationScope) + && Objects.equals(jerseyContext, that.jerseyContext); + } + + @Override + public int hashCode() { + return Objects.hash(observationScope, jerseyContext); + } + + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/TimedFinder.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/TimedFinder.java new file mode 100644 index 0000000000..42d47451bc --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/TimedFinder.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import java.lang.reflect.AnnotatedElement; +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; + +import io.micrometer.core.annotation.Timed; +import io.micrometer.core.annotation.TimedSet; + +class TimedFinder { + + private final AnnotationFinder annotationFinder; + + TimedFinder(AnnotationFinder annotationFinder) { + this.annotationFinder = annotationFinder; + } + + Set findTimedAnnotations(AnnotatedElement element) { + Timed t = annotationFinder.findAnnotation(element, Timed.class); + if (t != null) { + return Collections.singleton(t); + } + + TimedSet ts = annotationFinder.findAnnotation(element, TimedSet.class); + if (ts != null) { + return Arrays.stream(ts.value()).collect(Collectors.toSet()); + } + + return Collections.emptySet(); + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/package-info.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/package-info.java new file mode 100644 index 0000000000..0d9e95e804 --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/** + * Binders for Jersey. Code ported from Micrometer repository. + */ +package org.glassfish.jersey.micrometer.server; diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/DefaultJerseyTagsProviderTest.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/DefaultJerseyTagsProviderTest.java new file mode 100644 index 0000000000..4bf17590e6 --- /dev/null +++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/DefaultJerseyTagsProviderTest.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import javax.ws.rs.NotAcceptableException; + +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.ExtendedUriInfo; +import org.glassfish.jersey.server.internal.monitoring.RequestEventImpl.Builder; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEvent.Type; +import org.glassfish.jersey.uri.UriTemplate; +import org.junit.jupiter.api.Test; + +import static java.util.stream.StreamSupport.stream; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link DefaultJerseyTagsProvider}. + * + * @author Michael Weirauch + * @author Johnny Lim + */ +class DefaultJerseyTagsProviderTest { + + private final DefaultJerseyTagsProvider tagsProvider = new DefaultJerseyTagsProvider(); + + @Test + void testRootPath() { + assertThat(tagsProvider.httpRequestTags(event(200, null, "/", (String[]) null))) + .containsExactlyInAnyOrder(tagsFrom("root", 200, null, "SUCCESS")); + } + + @Test + void templatedPathsAreReturned() { + assertThat(tagsProvider.httpRequestTags(event(200, null, "/", "/", "/hello/{name}"))) + .containsExactlyInAnyOrder(tagsFrom("/hello/{name}", 200, null, "SUCCESS")); + } + + @Test + void applicationPathIsPresent() { + assertThat(tagsProvider.httpRequestTags(event(200, null, "/app", "/", "/hello"))) + .containsExactlyInAnyOrder(tagsFrom("/app/hello", 200, null, "SUCCESS")); + } + + @Test + void notFoundsAreShunted() { + assertThat(tagsProvider.httpRequestTags(event(404, null, "/app", "/", "/not-found"))) + .containsExactlyInAnyOrder(tagsFrom("NOT_FOUND", 404, null, "CLIENT_ERROR")); + } + + @Test + void redirectsAreShunted() { + assertThat(tagsProvider.httpRequestTags(event(301, null, "/app", "/", "/redirect301"))) + .containsExactlyInAnyOrder(tagsFrom("REDIRECTION", 301, null, "REDIRECTION")); + assertThat(tagsProvider.httpRequestTags(event(302, null, "/app", "/", "/redirect302"))) + .containsExactlyInAnyOrder(tagsFrom("REDIRECTION", 302, null, "REDIRECTION")); + assertThat(tagsProvider.httpRequestTags(event(399, null, "/app", "/", "/redirect399"))) + .containsExactlyInAnyOrder(tagsFrom("REDIRECTION", 399, null, "REDIRECTION")); + } + + @Test + @SuppressWarnings("serial") + void exceptionsAreMappedCorrectly() { + assertThat(tagsProvider.httpRequestTags(event(500, new IllegalArgumentException(), "/app", (String[]) null))) + .containsExactlyInAnyOrder(tagsFrom("/app", 500, "IllegalArgumentException", "SERVER_ERROR")); + assertThat(tagsProvider.httpRequestTags( + event(500, new IllegalArgumentException(new NullPointerException()), "/app", (String[]) null))) + .containsExactlyInAnyOrder(tagsFrom("/app", 500, "NullPointerException", "SERVER_ERROR")); + assertThat(tagsProvider.httpRequestTags(event(406, new NotAcceptableException(), "/app", (String[]) null))) + .containsExactlyInAnyOrder(tagsFrom("/app", 406, "NotAcceptableException", "CLIENT_ERROR")); + assertThat(tagsProvider.httpRequestTags(event(500, new Exception("anonymous") { + }, "/app", (String[]) null))).containsExactlyInAnyOrder(tagsFrom("/app", 500, + "org.glassfish.jersey.micrometer.server.DefaultJerseyTagsProviderTest$1", "SERVER_ERROR")); + } + + @Test + void longRequestTags() { + assertThat(tagsProvider.httpLongRequestTags(event(0, null, "/app", (String[]) null))) + .containsExactlyInAnyOrder(Tag.of("method", "GET"), Tag.of("uri", "/app")); + } + + private static RequestEvent event(Integer status, Exception exception, String baseUri, + String... uriTemplateStrings) { + Builder builder = new Builder(); + + ContainerRequest containerRequest = mock(ContainerRequest.class); + when(containerRequest.getMethod()).thenReturn("GET"); + builder.setContainerRequest(containerRequest); + + ContainerResponse containerResponse = mock(ContainerResponse.class); + when(containerResponse.getStatus()).thenReturn(status); + builder.setContainerResponse(containerResponse); + + builder.setException(exception, null); + + ExtendedUriInfo extendedUriInfo = mock(ExtendedUriInfo.class); + when(extendedUriInfo.getBaseUri()) + .thenReturn(URI.create("http://localhost:8080" + (baseUri == null ? "/" : baseUri))); + List uriTemplates = uriTemplateStrings == null ? Collections.emptyList() + : Arrays.stream(uriTemplateStrings).map(uri -> new UriTemplate(uri)).collect(Collectors.toList()); + // UriTemplate are returned in reverse order + Collections.reverse(uriTemplates); + when(extendedUriInfo.getMatchedTemplates()).thenReturn(uriTemplates); + builder.setExtendedUriInfo(extendedUriInfo); + + return builder.build(Type.FINISHED); + } + + private static Tag[] tagsFrom(String uri, int status, String exception, String outcome) { + Iterable expectedTags = Tags.of("method", "GET", "uri", uri, "status", String.valueOf(status), "exception", + exception == null ? "None" : exception, "outcome", outcome); + + return stream(expectedTags.spliterator(), false).toArray(Tag[]::new); + } + +} diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListenerTest.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListenerTest.java new file mode 100644 index 0000000000..b2edeeab26 --- /dev/null +++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListenerTest.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.NotFoundException; +import javax.ws.rs.core.Application; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.glassfish.jersey.micrometer.server.mapper.ResourceGoneExceptionMapper; +import org.glassfish.jersey.micrometer.server.resources.TestResource; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MetricsApplicationEventListener}. + * + * @author Michael Weirauch + * @author Johnny Lim + */ +class MetricsRequestEventListenerTest extends JerseyTest { + + static { + Logger.getLogger("org.glassfish.jersey").setLevel(Level.OFF); + } + + private static final String METRIC_NAME = "http.server.requests"; + + private MeterRegistry registry; + + @Override + protected Application configure() { + registry = new SimpleMeterRegistry(); + + final MetricsApplicationEventListener listener = new MetricsApplicationEventListener(registry, + new DefaultJerseyTagsProvider(), METRIC_NAME, true); + + final ResourceConfig config = new ResourceConfig(); + config.register(listener); + config.register(TestResource.class); + config.register(ResourceGoneExceptionMapper.class); + + return config; + } + + @Test + void resourcesAreTimed() { + target("/").request().get(); + target("hello").request().get(); + target("hello/").request().get(); + target("hello/peter").request().get(); + target("sub-resource/sub-hello/peter").request().get(); + + assertThat(registry.get(METRIC_NAME).tags(tagsFrom("root", "200", "SUCCESS", null)).timer().count()) + .isEqualTo(1); + + assertThat(registry.get(METRIC_NAME).tags(tagsFrom("/hello", "200", "SUCCESS", null)).timer().count()) + .isEqualTo(2); + + assertThat(registry.get(METRIC_NAME).tags(tagsFrom("/hello/{name}", "200", "SUCCESS", null)).timer().count()) + .isEqualTo(1); + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("/sub-resource/sub-hello/{name}", "200", "SUCCESS", null)) + .timer() + .count()).isEqualTo(1); + + // assert we are not auto-timing long task @Timed + assertThat(registry.getMeters()).hasSize(4); + } + + @Test + void notFoundIsAccumulatedUnderSameUri() { + try { + target("not-found").request().get(); + } + catch (NotFoundException ignored) { + } + + assertThat(registry.get(METRIC_NAME).tags(tagsFrom("NOT_FOUND", "404", "CLIENT_ERROR", null)).timer().count()) + .isEqualTo(1); + } + + @Test + void notFoundIsReportedWithUriOfMatchedResource() { + try { + target("throws-not-found-exception").request().get(); + } + catch (NotFoundException ignored) { + } + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("/throws-not-found-exception", "404", "CLIENT_ERROR", null)) + .timer() + .count()).isEqualTo(1); + } + + @Test + void redirectsAreReportedWithUriOfMatchedResource() { + target("redirect/302").request().get(); + target("redirect/307").request().get(); + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("/redirect/{status}", "302", "REDIRECTION", null)) + .timer() + .count()).isEqualTo(1); + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("/redirect/{status}", "307", "REDIRECTION", null)) + .timer() + .count()).isEqualTo(1); + } + + @Test + void exceptionsAreMappedCorrectly() { + try { + target("throws-exception").request().get(); + } + catch (Exception ignored) { + } + try { + target("throws-webapplication-exception").request().get(); + } + catch (Exception ignored) { + } + try { + target("throws-mappable-exception").request().get(); + } + catch (Exception ignored) { + } + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("/throws-exception", "500", "SERVER_ERROR", "IllegalArgumentException")) + .timer() + .count()).isEqualTo(1); + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("/throws-webapplication-exception", "401", "CLIENT_ERROR", "NotAuthorizedException")) + .timer() + .count()).isEqualTo(1); + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("/throws-mappable-exception", "410", "CLIENT_ERROR", "ResourceGoneException")) + .timer() + .count()).isEqualTo(1); + } + + private static Iterable tagsFrom(String uri, String status, String outcome, String exception) { + return Tags.of("method", "GET", "uri", uri, "status", status, "outcome", outcome, "exception", + exception == null ? "None" : exception); + } + +} diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListenerTimedTest.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListenerTimedTest.java new file mode 100644 index 0000000000..992864aacb --- /dev/null +++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListenerTimedTest.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; + +import io.micrometer.core.Issue; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.glassfish.jersey.micrometer.server.resources.TimedOnClassResource; +import org.glassfish.jersey.micrometer.server.resources.TimedResource; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Michael Weirauch + */ +class MetricsRequestEventListenerTimedTest extends JerseyTest { + + static { + Logger.getLogger("org.glassfish.jersey").setLevel(Level.OFF); + } + + private static final String METRIC_NAME = "http.server.requests"; + + private MeterRegistry registry; + + private CountDownLatch longTaskRequestStartedLatch; + + private CountDownLatch longTaskRequestReleaseLatch; + + @Override + protected Application configure() { + registry = new SimpleMeterRegistry(); + longTaskRequestStartedLatch = new CountDownLatch(1); + longTaskRequestReleaseLatch = new CountDownLatch(1); + + final MetricsApplicationEventListener listener = new MetricsApplicationEventListener(registry, + new DefaultJerseyTagsProvider(), METRIC_NAME, false); + + final ResourceConfig config = new ResourceConfig(); + config.register(listener); + config.register(new TimedResource(longTaskRequestStartedLatch, longTaskRequestReleaseLatch)); + config.register(TimedOnClassResource.class); + + return config; + } + + @Test + void resourcesAndNotFoundsAreNotAutoTimed() { + target("not-timed").request().get(); + target("not-found").request().get(); + + assertThat(registry.find(METRIC_NAME).tags(tagsFrom("/not-timed", 200)).timer()).isNull(); + + assertThat(registry.find(METRIC_NAME).tags(tagsFrom("NOT_FOUND", 404)).timer()).isNull(); + } + + @Test + void resourcesWithAnnotationAreTimed() { + target("timed").request().get(); + target("multi-timed").request().get(); + + assertThat(registry.get(METRIC_NAME).tags(tagsFrom("/timed", 200)).timer().count()).isEqualTo(1); + + assertThat(registry.get("multi1").tags(tagsFrom("/multi-timed", 200)).timer().count()).isEqualTo(1); + + assertThat(registry.get("multi2").tags(tagsFrom("/multi-timed", 200)).timer().count()).isEqualTo(1); + } + + @Test + void longTaskTimerSupported() throws InterruptedException, ExecutionException, TimeoutException { + final Future future = target("long-timed").request().async().get(); + + /* + * Wait until the request has arrived at the server side. (Async client processing + * might be slower in triggering the request resulting in the assertions below to + * fail. Thread.sleep() is not an option, so resort to CountDownLatch.) + */ + longTaskRequestStartedLatch.await(5, TimeUnit.SECONDS); + + // the request is not timed, yet + assertThat(registry.find(METRIC_NAME).tags(tagsFrom("/timed", 200)).timer()).isNull(); + + // the long running task is timed + assertThat(registry.get("long.task.in.request") + .tags(Tags.of("method", "GET", "uri", "/long-timed")) + .longTaskTimer() + .activeTasks()).isEqualTo(1); + + // finish the long running request + longTaskRequestReleaseLatch.countDown(); + future.get(5, TimeUnit.SECONDS); + + // the request is timed after the long running request completed + assertThat(registry.get(METRIC_NAME).tags(tagsFrom("/long-timed", 200)).timer().count()).isEqualTo(1); + } + + @Test + @Issue("gh-2861") + void longTaskTimerOnlyOneMeter() throws InterruptedException, ExecutionException, TimeoutException { + final Future future = target("just-long-timed").request().async().get(); + + /* + * Wait until the request has arrived at the server side. (Async client processing + * might be slower in triggering the request resulting in the assertions below to + * fail. Thread.sleep() is not an option, so resort to CountDownLatch.) + */ + longTaskRequestStartedLatch.await(5, TimeUnit.SECONDS); + + // the long running task is timed + assertThat(registry.get("long.task.in.request") + .tags(Tags.of("method", "GET", "uri", "/just-long-timed")) + .longTaskTimer() + .activeTasks()).isEqualTo(1); + + // finish the long running request + longTaskRequestReleaseLatch.countDown(); + future.get(5, TimeUnit.SECONDS); + + // no meters registered except the one checked above + assertThat(registry.getMeters().size()).isOne(); + } + + @Test + void unnamedLongTaskTimerIsNotSupported() { + assertThatExceptionOfType(ProcessingException.class) + .isThrownBy(() -> target("long-timed-unnamed").request().get()) + .withCauseInstanceOf(IllegalArgumentException.class); + } + + @Test + void classLevelAnnotationIsInherited() { + target("/class/inherited").request().get(); + + assertThat(registry.get(METRIC_NAME) + .tags(Tags.concat(tagsFrom("/class/inherited", 200), Tags.of("on", "class"))) + .timer() + .count()).isEqualTo(1); + } + + @Test + void methodLevelAnnotationOverridesClassLevel() { + target("/class/on-method").request().get(); + + assertThat(registry.get(METRIC_NAME) + .tags(Tags.concat(tagsFrom("/class/on-method", 200), Tags.of("on", "method"))) + .timer() + .count()).isEqualTo(1); + + // class level annotation is not picked up + assertThat(registry.getMeters()).hasSize(1); + } + + private static Iterable tagsFrom(String uri, int status) { + return Tags.of("method", "GET", "uri", uri, "status", String.valueOf(status), "exception", "None"); + } + +} diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/exception/ResourceGoneException.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/exception/ResourceGoneException.java new file mode 100644 index 0000000000..99f654dc99 --- /dev/null +++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/exception/ResourceGoneException.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server.exception; + +public class ResourceGoneException extends RuntimeException { + + public ResourceGoneException() { + super(); + } + + public ResourceGoneException(String message) { + super(message); + } + + public ResourceGoneException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/mapper/ResourceGoneExceptionMapper.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/mapper/ResourceGoneExceptionMapper.java new file mode 100644 index 0000000000..745ae7cc78 --- /dev/null +++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/mapper/ResourceGoneExceptionMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server.mapper; + +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.ext.ExceptionMapper; + +import org.glassfish.jersey.micrometer.server.exception.ResourceGoneException; + +public class ResourceGoneExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(ResourceGoneException exception) { + return Response.status(Status.GONE).build(); + } + +} diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/observation/AbstractObservationRequestEventListenerTest.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/observation/AbstractObservationRequestEventListenerTest.java new file mode 100644 index 0000000000..597e6a61a7 --- /dev/null +++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/observation/AbstractObservationRequestEventListenerTest.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server.observation; + +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.core.Application; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.observation.ObservationHandler.FirstMatchingCompositeObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.exporter.FinishedSpan; +import io.micrometer.tracing.handler.DefaultTracingObservationHandler; +import io.micrometer.tracing.handler.PropagatingReceiverTracingObservationHandler; +import io.micrometer.tracing.handler.PropagatingSenderTracingObservationHandler; +import io.micrometer.tracing.propagation.Propagator; +import io.micrometer.tracing.test.simple.SpanAssert; +import io.micrometer.tracing.test.simple.SpansAssert; +import org.glassfish.jersey.micrometer.server.ObservationApplicationEventListener; +import org.glassfish.jersey.micrometer.server.ObservationRequestEventListener; +import org.glassfish.jersey.micrometer.server.resources.TestResource; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; +import zipkin2.CheckResult; +import zipkin2.reporter.Sender; +import zipkin2.reporter.urlconnection.URLConnectionSender; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ObservationRequestEventListener}. + * + * @author Marcin Grzejsczak + */ +abstract class AbstractObservationRequestEventListenerTest extends JerseyTest { + + static { + Logger.getLogger("org.glassfish.jersey").setLevel(Level.OFF); + } + + private static final String METRIC_NAME = "http.server.requests"; + + ObservationRegistry observationRegistry; + + MeterRegistry registry; + + Boolean zipkinAvailable; + + Sender sender; + + @Override + protected Application configure() { + observationRegistry = ObservationRegistry.create(); + registry = new SimpleMeterRegistry(); + sender = URLConnectionSender.create("http://localhost:9411/api/v2/spans"); + + observationRegistry.observationConfig().observationHandler(new DefaultMeterObservationHandler(registry)); + + configureRegistry(observationRegistry); + + final ObservationApplicationEventListener listener = + new ObservationApplicationEventListener(observationRegistry, METRIC_NAME); + + final ResourceConfig config = new ResourceConfig(); + config.register(listener); + config.register(TestResource.class); + + return config; + } + + abstract void configureRegistry(ObservationRegistry registry); + + abstract List getFinishedSpans(); + + boolean isZipkinAvailable() { + if (zipkinAvailable == null) { + CheckResult checkResult = sender.check(); + zipkinAvailable = checkResult.ok(); + } + return zipkinAvailable; + } + + void setupTracing(Tracer tracer, Propagator propagator) { + observationRegistry.observationConfig() + .observationHandler(new FirstMatchingCompositeObservationHandler( + new PropagatingSenderTracingObservationHandler<>(tracer, propagator), + new PropagatingReceiverTracingObservationHandler<>(tracer, propagator), + new DefaultTracingObservationHandler(tracer))); + } + + @Test + void resourcesAreTimed() { + target("sub-resource/sub-hello/peter").request().get(); + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("/sub-resource/sub-hello/{name}", "200", "SUCCESS", null)) + .timer() + .count()).isEqualTo(1); + // Timer and Long Task Timer + assertThat(registry.getMeters()).hasSize(2); + + List finishedSpans = getFinishedSpans(); + SpansAssert.assertThat(finishedSpans).hasSize(1); + FinishedSpan finishedSpan = finishedSpans.get(0); + System.out.println("Trace Id [" + finishedSpan.getTraceId() + "]"); + SpanAssert.assertThat(finishedSpan) + .hasNameEqualTo("HTTP GET") + .hasTag("exception", "None") + .hasTag("method", "GET") + .hasTag("outcome", "SUCCESS") + .hasTag("status", "200") + .hasTag("uri", "/sub-resource/sub-hello/{name}"); + } + + private static Iterable tagsFrom(String uri, String status, String outcome, String exception) { + return Tags.of("method", "GET", "uri", uri, "status", status, "outcome", outcome, "exception", + exception == null ? "None" : exception); + } +} diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/observation/ObservationApplicationEventListenerTest.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/observation/ObservationApplicationEventListenerTest.java new file mode 100644 index 0000000000..0490129633 --- /dev/null +++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/observation/ObservationApplicationEventListenerTest.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server.observation; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import brave.Tracing; +import brave.Tracing.Builder; +import brave.context.slf4j.MDCScopeDecorator; +import brave.handler.SpanHandler; +import brave.propagation.B3Propagation; +import brave.propagation.B3Propagation.Format; +import brave.propagation.ThreadLocalCurrentTraceContext; +import brave.sampler.Sampler; +import brave.test.TestSpanHandler; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.CurrentTraceContext; +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.brave.bridge.BraveBaggageManager; +import io.micrometer.tracing.brave.bridge.BraveCurrentTraceContext; +import io.micrometer.tracing.brave.bridge.BraveFinishedSpan; +import io.micrometer.tracing.brave.bridge.BravePropagator; +import io.micrometer.tracing.brave.bridge.BraveTracer; +import io.micrometer.tracing.exporter.FinishedSpan; +import io.micrometer.tracing.otel.bridge.ArrayListSpanProcessor; +import io.micrometer.tracing.otel.bridge.OtelBaggageManager; +import io.micrometer.tracing.otel.bridge.OtelCurrentTraceContext; +import io.micrometer.tracing.otel.bridge.OtelFinishedSpan; +import io.micrometer.tracing.otel.bridge.OtelPropagator; +import io.micrometer.tracing.otel.bridge.OtelTracer; +import io.micrometer.tracing.otel.bridge.Slf4JBaggageEventListener; +import io.micrometer.tracing.otel.bridge.Slf4JEventListener; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.exporter.zipkin.ZipkinSpanExporterBuilder; +import io.opentelemetry.extension.trace.propagation.B3Propagator; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import org.glassfish.jersey.micrometer.server.ObservationApplicationEventListener; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Nested; +import zipkin2.Span; +import zipkin2.reporter.AsyncReporter; +import zipkin2.reporter.brave.ZipkinSpanHandler; + +import static io.opentelemetry.sdk.trace.samplers.Sampler.alwaysOn; + +/** + * Tests for {@link ObservationApplicationEventListener}. + * + * @author Marcin Grzejsczak + */ +class ObservationApplicationEventListenerTest { + + @Nested + class BraveObservationRequestEventListenerTest extends AbstractObservationRequestEventListenerTest { + + Tracing tracing; + + TestSpanHandler testSpanHandler; + + AsyncReporter reporter; + + @Override + void configureRegistry(ObservationRegistry registry) { + testSpanHandler = new TestSpanHandler(); + + reporter = AsyncReporter.create(sender); + + SpanHandler spanHandler = ZipkinSpanHandler + .create(reporter); + + ThreadLocalCurrentTraceContext braveCurrentTraceContext = ThreadLocalCurrentTraceContext.newBuilder() + .addScopeDecorator(MDCScopeDecorator.get()) // Example of Brave's + // automatic MDC setup + .build(); + + CurrentTraceContext bridgeContext = new BraveCurrentTraceContext(braveCurrentTraceContext); + + Builder builder = Tracing.newBuilder() + .currentTraceContext(braveCurrentTraceContext) + .supportsJoin(false) + .traceId128Bit(true) + .propagationFactory(B3Propagation.newFactoryBuilder().injectFormat(Format.SINGLE).build()) + .sampler(Sampler.ALWAYS_SAMPLE) + .addSpanHandler(testSpanHandler) + .localServiceName("brave-test"); + + if (isZipkinAvailable()) { + builder.addSpanHandler(spanHandler); + } + + tracing = builder + .build(); + brave.Tracer braveTracer = tracing.tracer(); + Tracer tracer = new BraveTracer(braveTracer, bridgeContext, new BraveBaggageManager()); + BravePropagator bravePropagator = new BravePropagator(tracing); + setupTracing(tracer, bravePropagator); + } + + @Override + List getFinishedSpans() { + return testSpanHandler.spans().stream().map(BraveFinishedSpan::new).collect(Collectors.toList()); + } + + @AfterEach + void cleanup() { + if (isZipkinAvailable()) { + reporter.flush(); + reporter.close(); + } + tracing.close(); + } + } + + @Nested + class OtelObservationRequestEventListenerTest extends AbstractObservationRequestEventListenerTest { + + SdkTracerProvider sdkTracerProvider; + + ArrayListSpanProcessor processor; + + @Override + void configureRegistry(ObservationRegistry registry) { + processor = new ArrayListSpanProcessor(); + + SpanExporter spanExporter = new ZipkinSpanExporterBuilder() + .setSender(sender) + .build(); + + SdkTracerProviderBuilder builder = SdkTracerProvider.builder() + .setSampler(alwaysOn()) + .addSpanProcessor(processor) + .setResource(Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, "otel-test"))); + + if (isZipkinAvailable()) { + builder.addSpanProcessor(SimpleSpanProcessor.create(spanExporter)); + } + + sdkTracerProvider = builder + .build(); + + ContextPropagators contextPropagators = ContextPropagators.create(B3Propagator.injectingSingleHeader()); + + OpenTelemetrySdk openTelemetrySdk = OpenTelemetrySdk.builder() + .setTracerProvider(sdkTracerProvider) + .setPropagators(contextPropagators) + .build(); + + io.opentelemetry.api.trace.Tracer otelTracer = openTelemetrySdk.getTracerProvider() + .get("io.micrometer.micrometer-tracing"); + + OtelCurrentTraceContext otelCurrentTraceContext = new OtelCurrentTraceContext(); + + Slf4JEventListener slf4JEventListener = new Slf4JEventListener(); + + Slf4JBaggageEventListener slf4JBaggageEventListener = new Slf4JBaggageEventListener(Collections.emptyList()); + + OtelTracer tracer = new OtelTracer(otelTracer, otelCurrentTraceContext, event -> { + slf4JEventListener.onEvent(event); + slf4JBaggageEventListener.onEvent(event); + }, new OtelBaggageManager(otelCurrentTraceContext, Collections.emptyList(), Collections.emptyList())); + OtelPropagator otelPropagator = new OtelPropagator(contextPropagators, otelTracer); + setupTracing(tracer, otelPropagator); + } + + @Override + List getFinishedSpans() { + return processor.spans().stream().map(OtelFinishedSpan::fromOtel).collect(Collectors.toList()); + } + + @AfterEach + void cleanup() { + sdkTracerProvider.close(); + } + } +} diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TestResource.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TestResource.java new file mode 100644 index 0000000000..661e1fa4e4 --- /dev/null +++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TestResource.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server.resources; + +import java.net.URI; + +import javax.ws.rs.GET; +import javax.ws.rs.NotAuthorizedException; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.RedirectionException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import org.glassfish.jersey.micrometer.server.exception.ResourceGoneException; + +/** + * @author Michael Weirauch + */ +@Path("/") +@Produces(MediaType.TEXT_PLAIN) +public class TestResource { + + @Produces(MediaType.TEXT_PLAIN) + public static class SubResource { + + @GET + @Path("sub-hello/{name}") + public String hello(@PathParam("name") String name) { + return "hello " + name; + } + + } + + @GET + public String index() { + return "index"; + } + + @GET + @Path("hello") + public String hello() { + return "hello"; + } + + @GET + @Path("hello/{name}") + public String hello(@PathParam("name") String name) { + return "hello " + name; + } + + @GET + @Path("throws-not-found-exception") + public String throwsNotFoundException() { + throw new NotFoundException(); + } + + @GET + @Path("throws-exception") + public String throwsException() { + throw new IllegalArgumentException(); + } + + @GET + @Path("throws-webapplication-exception") + public String throwsWebApplicationException() { + throw new NotAuthorizedException("notauth", Response.status(Status.UNAUTHORIZED).build()); + } + + @GET + @Path("throws-mappable-exception") + public String throwsMappableException() { + throw new ResourceGoneException("Resource has been permanently removed."); + } + + @GET + @Path("redirect/{status}") + public Response redirect(@PathParam("status") int status) { + if (status == 307) { + throw new RedirectionException(status, URI.create("hello")); + } + return Response.status(status).header("Location", "/hello").build(); + } + + @Path("/sub-resource") + public SubResource subResource() { + return new SubResource(); + } + +} diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedOnClassResource.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedOnClassResource.java new file mode 100644 index 0000000000..89d6d37d58 --- /dev/null +++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedOnClassResource.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server.resources; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import io.micrometer.core.annotation.Timed; + +/** + * @author Michael Weirauch + */ +@Path("/class") +@Produces(MediaType.TEXT_PLAIN) +@Timed(extraTags = { "on", "class" }) +public class TimedOnClassResource { + + @GET + @Path("inherited") + public String inherited() { + return "inherited"; + } + + @GET + @Path("on-method") + @Timed(extraTags = { "on", "method" }) + public String onMethod() { + return "on-method"; + } + +} diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedResource.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedResource.java new file mode 100644 index 0000000000..723d3a0a8a --- /dev/null +++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedResource.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server.resources; + +import java.util.concurrent.CountDownLatch; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import io.micrometer.core.annotation.Timed; + +import static java.util.Objects.requireNonNull; + +/** + * @author Michael Weirauch + */ +@Path("/") +@Produces(MediaType.TEXT_PLAIN) +public class TimedResource { + + private final CountDownLatch longTaskRequestStartedLatch; + + private final CountDownLatch longTaskRequestReleaseLatch; + + public TimedResource(CountDownLatch longTaskRequestStartedLatch, CountDownLatch longTaskRequestReleaseLatch) { + this.longTaskRequestStartedLatch = requireNonNull(longTaskRequestStartedLatch); + this.longTaskRequestReleaseLatch = requireNonNull(longTaskRequestReleaseLatch); + } + + @GET + @Path("not-timed") + public String notTimed() { + return "not-timed"; + } + + @GET + @Path("timed") + @Timed + public String timed() { + return "timed"; + } + + @GET + @Path("multi-timed") + @Timed("multi1") + @Timed("multi2") + public String multiTimed() { + return "multi-timed"; + } + + /* + * Async server side processing (AsyncResponse) is not supported in the in-memory test + * container. + */ + @GET + @Path("long-timed") + @Timed + @Timed(value = "long.task.in.request", longTask = true) + public String longTimed() { + longTaskRequestStartedLatch.countDown(); + try { + longTaskRequestReleaseLatch.await(); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + return "long-timed"; + } + + @GET + @Path("just-long-timed") + @Timed(value = "long.task.in.request", longTask = true) + public String justLongTimed() { + longTaskRequestStartedLatch.countDown(); + try { + longTaskRequestReleaseLatch.await(); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + return "long-timed"; + } + + @GET + @Path("long-timed-unnamed") + @Timed + @Timed(longTask = true) + public String longTimedUnnamed() { + return "long-timed-unnamed"; + } + +} diff --git a/ext/pom.xml b/ext/pom.xml index 01ebf574f8..9ed36a142a 100644 --- a/ext/pom.xml +++ b/ext/pom.xml @@ -44,6 +44,7 @@ cdi entity-filtering metainf-services + micrometer mvc mvc-bean-validation mvc-freemarker diff --git a/ext/spring4/pom.xml b/ext/spring4/pom.xml index fdec30f06d..98a6f4ad80 100644 --- a/ext/spring4/pom.xml +++ b/ext/spring4/pom.xml @@ -156,7 +156,7 @@ org.aspectj aspectjweaver - 1.6.11 + ${aspectj.weaver.version} test diff --git a/ext/spring5/pom.xml b/ext/spring5/pom.xml index efad20a9e9..70dc2902bc 100644 --- a/ext/spring5/pom.xml +++ b/ext/spring5/pom.xml @@ -160,7 +160,7 @@ org.aspectj aspectjweaver - 1.6.11 + ${aspectj.weaver.version} test diff --git a/pom.xml b/pom.xml index 60b47161d1..38698c1301 100644 --- a/pom.xml +++ b/pom.xml @@ -2205,6 +2205,8 @@ 9.5 + + 1.6.11 2.13.0 @@ -2299,6 +2301,8 @@ 2.3.6 1.2.7 1.3.3 + 1.10.10 + 1.0.9 2.0.1 2.0 1.9.15