diff --git a/ext/micrometer/pom.xml b/ext/micrometer/pom.xml
new file mode 100644
index 00000000000..5e19969a978
--- /dev/null
+++ b/ext/micrometer/pom.xml
@@ -0,0 +1,104 @@
+
+
+
+
+
+ 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
+ 1.6.11
+ test
+
+
+
+ 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 00000000000..bdfa49c123a
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/AnnotationFinder.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2019, 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 00000000000..b1eb4f9b901
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/DefaultJerseyObservationConvention.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2019, 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 00000000000..3ce14198090
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/DefaultJerseyTagsProvider.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2019, 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 00000000000..14354eb21e9
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyContext.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2019, 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 00000000000..e035f78c522
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyKeyValues.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (c) 2019, 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 00000000000..20b4da200cf
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyObservationConvention.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2019, 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 00000000000..cf049397668
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyObservationDocumentation.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2019, 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 extends ObservationConvention extends Observation.Context>> 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 00000000000..b415189e0d1
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyTags.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (c) 2019, 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 00000000000..77fba18be0b
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyTagsProvider.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2019, 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 00000000000..e1df4519045
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/MetricsApplicationEventListener.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2019, 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 00000000000..0b2418b7257
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListener.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (c) 2019, 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 00000000000..419828f3855
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationApplicationEventListener.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2019, 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 00000000000..37517e4b61f
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationRequestEventListener.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (c) 2019, 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 00000000000..82b93038d52
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/TimedFinder.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2019, 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 00000000000..d09ece7c892
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2019, 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 00000000000..3ede36b443e
--- /dev/null
+++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/DefaultJerseyTagsProviderTest.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (c) 2019, 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 00000000000..eb96e5dc3b6
--- /dev/null
+++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListenerTest.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (c) 2019, 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 00000000000..a95e831846b
--- /dev/null
+++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListenerTimedTest.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (c) 2019, 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 00000000000..9a2bd2544b4
--- /dev/null
+++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/exception/ResourceGoneException.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2019, 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 00000000000..b532c0364a1
--- /dev/null
+++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/mapper/ResourceGoneExceptionMapper.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2019, 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 00000000000..d018bc9d69c
--- /dev/null
+++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/observation/AbstractObservationRequestEventListenerTest.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (c) 2019, 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 00000000000..896e49b93f3
--- /dev/null
+++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/observation/ObservationApplicationEventListenerTest.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (c) 2019, 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 00000000000..19bc45a445d
--- /dev/null
+++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TestResource.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2019, 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 00000000000..ff040af3262
--- /dev/null
+++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedOnClassResource.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2019, 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 00000000000..4515b415e8a
--- /dev/null
+++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedResource.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2019, 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 01ebf574f8d..9ed36a142ae 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/pom.xml b/pom.xml
index 60b47161d1a..f656fb1b5c0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2299,6 +2299,8 @@
2.3.6
1.2.7
1.3.3
+ 1.10.10
+ 1.0.9
2.0.1
2.0
1.9.15