Added jersey-micrometer module (#5391)

Signed-off-by: Marcin Grzejszczak <marcin@grzejszczak.pl>
Signed-off-by: Maxim Nesen <maxim.nesen@oracle.com>
Co-authored-by: Maxim Nesen <maxim.nesen@oracle.com>
diff --git a/bom/pom.xml b/bom/pom.xml
index 2525415..7bc29fa 100644
--- a/bom/pom.xml
+++ b/bom/pom.xml
@@ -155,6 +155,11 @@
             </dependency>
             <dependency>
                 <groupId>org.glassfish.jersey.ext</groupId>
+                <artifactId>jersey-micrometer</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.glassfish.jersey.ext</groupId>
                 <artifactId>jersey-metainf-services</artifactId>
                 <version>${project.version}</version>
             </dependency>
diff --git a/examples/micrometer/README.MD b/examples/micrometer/README.MD
new file mode 100644
index 0000000..609a6ea
--- /dev/null
+++ b/examples/micrometer/README.MD
@@ -0,0 +1,57 @@
+[//]: # " Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. "
+[//]: # "  "
+[//]: # " This program and the accompanying materials are made available under the "
+[//]: # " terms of the Eclipse Public License v. 2.0, which is available at "
+[//]: # " http://www.eclipse.org/legal/epl-2.0. "
+[//]: # "  "
+[//]: # " This Source Code may also be made available under the following Secondary "
+[//]: # " Licenses when the conditions for such availability set forth in the "
+[//]: # " Eclipse Public License v. 2.0 are satisfied: GNU General Public License, "
+[//]: # " version 2 with the GNU Classpath Exception, which is available at "
+[//]: # " https://www.gnu.org/software/classpath/license.html. "
+[//]: # "  "
+[//]: # " SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 "
+
+jersey-micrometer-webapp
+==========================================================
+
+This example demonstrates basics of Micrometer Jersey integration
+
+Contents
+--------
+
+The mapping of the URI path space is presented in the following table:
+
+URI path                                   | Resource class            | HTTP methods
+------------------------------------------ | ------------------------- | --------------
+**_/micro/meter_**                            | JerseyResource            | GET
+**_/micro/metrics_**                            | JerseyResource            | GET
+**_/micro/metrics/metrics_**                            | JerseyResource            | GET
+
+Sample Response
+---------------
+
+```javascript
+--- (micro/meter)
+Hello World!
+---- (micro/metrics)   
+Listing available meters: http.shared.metrics;
+---- (micro/metric/metrics)
+Overall requests counts: 9, total time (millis): 35.799483
+```
+
+
+Running the Example
+-------------------
+
+Run the example using [Grizzly](https://javaee.github.io/grizzly/) container as follows:
+
+>     mvn clean compile exec:java
+
+- <http://localhost:8080/micro/meter>
+- after few request to the main page go to the url
+- - <http://localhost:8080/micro/metrics>
+- and see the list of available meters
+- then go to the 
+- - <http://localhost:8080/micro/metrics/metrics>
+- and see statistics for the micro/meter page
\ No newline at end of file
diff --git a/examples/micrometer/pom.xml b/examples/micrometer/pom.xml
new file mode 100644
index 0000000..df423e1
--- /dev/null
+++ b/examples/micrometer/pom.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+
+    This program and the accompanying materials are made available under the
+    terms of the Eclipse Public License v. 2.0, which is available at
+    http://www.eclipse.org/legal/epl-2.0.
+
+    This Source Code may also be made available under the following Secondary
+    Licenses when the conditions for such availability set forth in the
+    Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+    version 2 with the GNU Classpath Exception, which is available at
+    https://www.gnu.org/software/classpath/license.html.
+
+    SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.glassfish.jersey.examples</groupId>
+        <artifactId>project</artifactId>
+        <version>2.41-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>jersey-micrometer-webapp</artifactId>
+    <packaging>jar</packaging>
+    <name>jersey-micrometer-example-webapp</name>
+
+    <description>Micrometer/Jersey metrics basic example</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.glassfish.jersey.containers</groupId>
+            <artifactId>jersey-container-grizzly2-http</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.containers</groupId>
+            <artifactId>jersey-container-servlet</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.ext</groupId>
+            <artifactId>jersey-micrometer</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.inject</groupId>
+            <artifactId>jersey-hk2</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.test-framework</groupId>
+            <artifactId>jersey-test-framework-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.test-framework.providers</groupId>
+            <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>exec-maven-plugin</artifactId>
+                <configuration>
+                    <mainClass>org.glassfish.jersey.examples.micrometer.App</mainClass>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+    <profiles>
+        <profile>
+            <id>pre-release</id>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.codehaus.mojo</groupId>
+                        <artifactId>xml-maven-plugin</artifactId>
+                    </plugin>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-assembly-plugin</artifactId>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+
+</project>
diff --git a/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/App.java b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/App.java
new file mode 100644
index 0000000..92dcf22
--- /dev/null
+++ b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/App.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.examples.micrometer;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
+import org.glassfish.jersey.micrometer.server.DefaultJerseyTagsProvider;
+import org.glassfish.jersey.micrometer.server.MetricsApplicationEventListener;
+import org.glassfish.jersey.server.ResourceConfig;
+
+import org.glassfish.grizzly.http.server.HttpServer;
+
+public class App {
+
+    private static final URI BASE_URI = URI.create("http://localhost:8080/micro/");
+    public static final String ROOT_PATH = "meter";
+
+    public static void main(String[] args) {
+        try {
+            System.out.println("Micrometer/ Jersey Basic Example App");
+
+            final MeterRegistry registry = new SimpleMeterRegistry();
+
+            final ResourceConfig resourceConfig = new ResourceConfig(MicrometerResource.class)
+                    .register(new MetricsApplicationEventListener(registry, new DefaultJerseyTagsProvider(),
+                    "http.shared.metrics", true))
+                    .register(new MetricsResource(registry));
+            final HttpServer server = GrizzlyHttpServerFactory.createHttpServer(BASE_URI, resourceConfig, false);
+            Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
+                @Override
+                public void run() {
+                    server.shutdownNow();
+                }
+            }));
+            server.start();
+
+            System.out.println(String.format("Application started.\nTry out                        %s%s\n"
+                            + "After several requests go to   %s%s\nAnd after that go to the       %s%s\n"
+                            + "Stop the application using CTRL+C",
+                    BASE_URI, ROOT_PATH, BASE_URI, "metrics", BASE_URI, "metrics/metrics"));
+            Thread.currentThread().join();
+        } catch (IOException | InterruptedException ex) {
+            Logger.getLogger(App.class.getName()).log(Level.SEVERE, null, ex);
+        }
+
+    }
+}
diff --git a/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/MetricsResource.java b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/MetricsResource.java
new file mode 100644
index 0000000..60e9194
--- /dev/null
+++ b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/MetricsResource.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.examples.micrometer;
+
+import io.micrometer.core.instrument.Meter;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Timer;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import java.util.concurrent.TimeUnit;
+
+@Path("metrics")
+public class MetricsResource {
+
+    private final MeterRegistry registry;
+
+    public MetricsResource(MeterRegistry registry) {
+        this.registry = registry;
+    }
+
+    @GET
+    @Produces("text/plain")
+    public String getMeters() {
+        final StringBuffer result = new StringBuffer();
+        try {
+            result.append("Listing available meters: ");
+            for (final Meter meter : registry.getMeters()) {
+                result.append(meter.getId().getName());
+                result.append("; ");
+            }
+        } catch (Exception ex) {
+            System.out.println(ex);
+            result.append("Exception occured, see log for details...");
+            result.append(ex.toString());
+        }
+        return result.toString();
+    }
+    @GET
+    @Path("metrics")
+    @Produces("text/plain")
+    public String getMetrics() {
+        final StringBuffer result = new StringBuffer();
+        try {
+            final Timer timer = registry.get("http.shared.metrics")
+                    .tags("method", "GET", "uri", "/micro/meter", "status", "200", "exception", "None", "outcome", "SUCCESS")
+                    .timer();
+            result.append(String.format("Overall requests counts: %d, total time (millis): %f",
+                    timer.count(), timer.totalTime(TimeUnit.MILLISECONDS)));
+        } catch (Exception ex) {
+            System.out.println(ex);
+            result.append("Exception occured, see log for details...");
+            result.append(ex.toString());
+        }
+        return result.toString();
+    }
+}
diff --git a/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/MicrometerResource.java b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/MicrometerResource.java
new file mode 100644
index 0000000..7ff1083
--- /dev/null
+++ b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/MicrometerResource.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.examples.micrometer;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+
+@Path("meter")
+public class MicrometerResource {
+    public static final String CLICHED_MESSAGE = "Hello World!";
+
+    @GET
+    @Produces("text/plain")
+    public String getHello() {
+        return CLICHED_MESSAGE;
+    }
+
+}
diff --git a/examples/micrometer/src/test/java/org/glassfish/jersey/examples/micrometer/MicrometerTest.java b/examples/micrometer/src/test/java/org/glassfish/jersey/examples/micrometer/MicrometerTest.java
new file mode 100644
index 0000000..4a5f4ee
--- /dev/null
+++ b/examples/micrometer/src/test/java/org/glassfish/jersey/examples/micrometer/MicrometerTest.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.examples.micrometer;
+
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Timer;
+import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+import org.glassfish.jersey.micrometer.server.DefaultJerseyTagsProvider;
+import org.glassfish.jersey.micrometer.server.MetricsApplicationEventListener;
+import org.glassfish.jersey.test.JerseyTest;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.junit.jupiter.api.Test;
+
+import javax.ws.rs.core.Application;
+
+import java.util.concurrent.TimeUnit;
+
+import static org.glassfish.jersey.examples.micrometer.MicrometerResource.CLICHED_MESSAGE;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+public class MicrometerTest extends JerseyTest {
+
+    static final String TIMER_METRIC_NAME = "http.server.requests";
+
+    MeterRegistry registry;
+
+    @Override
+    protected Application configure() {
+        registry = new SimpleMeterRegistry();
+        MetricsApplicationEventListener metricsListener = new MetricsApplicationEventListener(registry,
+                new DefaultJerseyTagsProvider(), TIMER_METRIC_NAME, true);
+        return new ResourceConfig(MicrometerResource.class)
+                .register(metricsListener)
+                .register(new MetricsResource(registry));
+    }
+
+    @Test
+    void meterResourceTest() throws InterruptedException {
+        String response = target("/meter").request().get(String.class);
+        assertEquals(response, CLICHED_MESSAGE);
+        // Jersey metrics are recorded asynchronously to the request completing
+        Thread.sleep(10);
+        Timer timer = registry.get(TIMER_METRIC_NAME)
+                .tags("method", "GET", "uri", "/meter", "status", "200", "exception", "None", "outcome", "SUCCESS")
+                .timer();
+        assertEquals(timer.count(), 1);
+        assertNotNull(timer.totalTime(TimeUnit.NANOSECONDS));
+    }
+
+}
\ No newline at end of file
diff --git a/examples/pom.xml b/examples/pom.xml
index ba5480b..3cf0079 100644
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -102,6 +102,7 @@
         <module>managed-client-simple-webapp</module>
         <!--<module>monitoring-webapp</module>-->
         <module>multipart-webapp</module>
+        <module>micrometer</module>
         <module>open-tracing</module>
         <module>osgi-helloworld-webapp</module>
         <module>osgi-http-service</module>
diff --git a/ext/micrometer/pom.xml b/ext/micrometer/pom.xml
new file mode 100644
index 0000000..800083e
--- /dev/null
+++ b/ext/micrometer/pom.xml
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+
+    This program and the accompanying materials are made available under the
+    terms of the Eclipse Public License v. 2.0, which is available at
+    http://www.eclipse.org/legal/epl-2.0.
+
+    This Source Code may also be made available under the following Secondary
+    Licenses when the conditions for such availability set forth in the
+    Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+    version 2 with the GNU Classpath Exception, which is available at
+    https://www.gnu.org/software/classpath/license.html.
+
+    SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>project</artifactId>
+        <groupId>org.glassfish.jersey.ext</groupId>
+        <version>2.41-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>jersey-micrometer</artifactId>
+
+    <dependencies>
+
+        <dependency>
+            <groupId>io.micrometer</groupId>
+            <artifactId>micrometer-core</artifactId>
+            <version>${micrometer.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.glassfish.jersey.core</groupId>
+            <artifactId>jersey-common</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.glassfish.jersey.core</groupId>
+            <artifactId>jersey-server</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.glassfish.jersey.test-framework.providers</groupId>
+            <artifactId>jersey-test-framework-provider-inmemory</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.glassfish.jersey.test-framework</groupId>
+            <artifactId>jersey-test-framework-core</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.aspectj</groupId>
+            <artifactId>aspectjweaver</artifactId>
+            <version>${aspectj.weaver.version}</version>
+            <scope>test</scope>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>io.micrometer</groupId>
+            <artifactId>micrometer-tracing-integration-test</artifactId>
+            <version>${micrometer-tracing.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+    </dependencies>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <inherited>true</inherited>
+                <extensions>true</extensions>
+                <configuration>
+                    <instructions>
+                        <Export-Package>
+                            org.glassfish.jersey.micrometer.server.*;version=${project.version}
+                        </Export-Package>
+                        <Import-Package>
+                            org.eclipse.microprofile.micrometer.server.*;version="!",
+                            *
+                        </Import-Package>
+                    </instructions>
+                    <unpackBundle>true</unpackBundle>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
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 0000000..82b8c54
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/AnnotationFinder.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package org.glassfish.jersey.micrometer.server;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.AnnotatedElement;
+
+public interface AnnotationFinder {
+
+    AnnotationFinder DEFAULT = new AnnotationFinder() {
+    };
+
+    /**
+     * The default implementation performs a simple search for a declared annotation
+     * matching the search type. Spring provides a more sophisticated annotation search
+     * utility that matches on meta-annotations as well.
+     * @param annotatedElement The element to search.
+     * @param annotationType The annotation type class.
+     * @param <A> Annotation type to search for.
+     * @return A matching annotation.
+     */
+    @SuppressWarnings("unchecked")
+    default <A extends Annotation> A findAnnotation(AnnotatedElement annotatedElement, Class<A> 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 0000000..172465b
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/DefaultJerseyObservationConvention.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package org.glassfish.jersey.micrometer.server;
+
+import io.micrometer.common.KeyValues;
+import io.micrometer.common.lang.Nullable;
+import org.glassfish.jersey.server.ContainerRequest;
+import org.glassfish.jersey.server.ContainerResponse;
+import org.glassfish.jersey.server.monitoring.RequestEvent;
+
+/**
+ * Default implementation for {@link JerseyObservationConvention}.
+ *
+ * @author Marcin Grzejszczak
+ * @since 2.41
+ */
+public class DefaultJerseyObservationConvention implements JerseyObservationConvention {
+
+    private final String metricsName;
+
+    public DefaultJerseyObservationConvention(String metricsName) {
+        this.metricsName = metricsName;
+    }
+
+    @Override
+    public KeyValues getLowCardinalityKeyValues(JerseyContext context) {
+        RequestEvent event = context.getRequestEvent();
+        ContainerRequest request = context.getCarrier();
+        ContainerResponse response = context.getResponse();
+        return KeyValues.of(JerseyKeyValues.method(request), JerseyKeyValues.uri(event),
+                JerseyKeyValues.exception(event), JerseyKeyValues.status(response), JerseyKeyValues.outcome(response));
+    }
+
+    @Override
+    public String getName() {
+        return this.metricsName;
+    }
+
+    @Nullable
+    @Override
+    public String getContextualName(JerseyContext context) {
+        if (context.getCarrier() == null) {
+            return null;
+        }
+        return "HTTP " + context.getCarrier().getMethod();
+    }
+
+}
diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/DefaultJerseyTagsProvider.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/DefaultJerseyTagsProvider.java
new file mode 100644
index 0000000..6c080a4
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/DefaultJerseyTagsProvider.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package org.glassfish.jersey.micrometer.server;
+
+import io.micrometer.core.instrument.Tag;
+import io.micrometer.core.instrument.Tags;
+import org.glassfish.jersey.server.ContainerResponse;
+import org.glassfish.jersey.server.monitoring.RequestEvent;
+
+/**
+ * Default implementation for {@link JerseyTagsProvider}.
+ *
+ * @author Michael Weirauch
+ * @author Johnny Lim
+ * @since 2.41
+ */
+public final class DefaultJerseyTagsProvider implements JerseyTagsProvider {
+
+    @Override
+    public Iterable<Tag> 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<Tag> 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 0000000..97ff36a
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyContext.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package org.glassfish.jersey.micrometer.server;
+
+import java.util.List;
+
+import io.micrometer.observation.transport.ReceiverContext;
+import io.micrometer.observation.transport.RequestReplyReceiverContext;
+import org.glassfish.jersey.server.ContainerRequest;
+import org.glassfish.jersey.server.ContainerResponse;
+import org.glassfish.jersey.server.monitoring.RequestEvent;
+
+/**
+ * A {@link ReceiverContext} for Jersey.
+ *
+ * @author Marcin Grzejszczak
+ * @since 2.41
+ */
+public class JerseyContext extends RequestReplyReceiverContext<ContainerRequest, ContainerResponse> {
+
+    private RequestEvent requestEvent;
+
+    public JerseyContext(RequestEvent requestEvent) {
+        super((carrier, key) -> {
+            List<String> 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 0000000..66cdaf7
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyKeyValues.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package org.glassfish.jersey.micrometer.server;
+
+import io.micrometer.common.KeyValue;
+import io.micrometer.common.util.StringUtils;
+import io.micrometer.core.instrument.binder.http.Outcome;
+import org.glassfish.jersey.server.ContainerRequest;
+import org.glassfish.jersey.server.ContainerResponse;
+import org.glassfish.jersey.server.ExtendedUriInfo;
+import org.glassfish.jersey.server.monitoring.RequestEvent;
+
+/**
+ * Factory methods for {@link KeyValue KeyValues} associated with a request-response
+ * exchange that is handled by Jersey server.
+ */
+class JerseyKeyValues {
+
+    private static final KeyValue URI_NOT_FOUND = JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.URI
+        .withValue("NOT_FOUND");
+
+    private static final KeyValue URI_REDIRECTION = JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.URI
+        .withValue("REDIRECTION");
+
+    private static final KeyValue URI_ROOT = JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.URI
+        .withValue("root");
+
+    private static final KeyValue EXCEPTION_NONE = JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.EXCEPTION
+        .withValue("None");
+
+    private static final KeyValue STATUS_SERVER_ERROR = JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.STATUS
+        .withValue("500");
+
+    private static final KeyValue METHOD_UNKNOWN = JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.METHOD
+        .withValue("UNKNOWN");
+
+    private JerseyKeyValues() {
+    }
+
+    /**
+     * Creates a {@code method} KeyValue based on the {@link ContainerRequest#getMethod()
+     * method} of the given {@code request}.
+     * @param request the container request
+     * @return the method KeyValue whose value is a capitalized method (e.g. GET).
+     */
+    static KeyValue method(ContainerRequest request) {
+        return (request != null)
+                ? JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.METHOD.withValue(request.getMethod())
+                : METHOD_UNKNOWN;
+    }
+
+    /**
+     * Creates a {@code status} KeyValue based on the status of the given
+     * {@code response}.
+     * @param response the container response
+     * @return the status KeyValue derived from the status of the response
+     */
+    static KeyValue status(ContainerResponse response) {
+        /* In case there is no response we are dealing with an unmapped exception. */
+        return (response != null) ? JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.STATUS
+            .withValue(Integer.toString(response.getStatus())) : STATUS_SERVER_ERROR;
+    }
+
+    /**
+     * Creates a {@code uri} KeyValue based on the URI of the given {@code event}. Uses
+     * the {@link ExtendedUriInfo#getMatchedTemplates()} if available. {@code REDIRECTION}
+     * for 3xx responses, {@code NOT_FOUND} for 404 responses.
+     * @param event the request event
+     * @return the uri KeyValue derived from the request event
+     */
+    static KeyValue uri(RequestEvent event) {
+        ContainerResponse response = event.getContainerResponse();
+        if (response != null) {
+            int status = response.getStatus();
+            if (JerseyTags.isRedirection(status) && event.getUriInfo().getMatchedResourceMethod() == null) {
+                return URI_REDIRECTION;
+            }
+            if (status == 404 && event.getUriInfo().getMatchedResourceMethod() == null) {
+                return URI_NOT_FOUND;
+            }
+        }
+        String matchingPattern = JerseyTags.getMatchingPattern(event);
+        if (matchingPattern.equals("/")) {
+            return URI_ROOT;
+        }
+        return JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.URI.withValue(matchingPattern);
+    }
+
+    /**
+     * Creates an {@code exception} KeyValue based on the {@link Class#getSimpleName()
+     * simple name} of the class of the given {@code exception}.
+     * @param event the request event
+     * @return the exception KeyValue derived from the exception
+     */
+    static KeyValue exception(RequestEvent event) {
+        Throwable exception = event.getException();
+        if (exception == null) {
+            return EXCEPTION_NONE;
+        }
+        ContainerResponse response = event.getContainerResponse();
+        if (response != null) {
+            int status = response.getStatus();
+            if (status == 404 || JerseyTags.isRedirection(status)) {
+                return EXCEPTION_NONE;
+            }
+        }
+        if (exception.getCause() != null) {
+            exception = exception.getCause();
+        }
+        String simpleName = exception.getClass().getSimpleName();
+        return JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.EXCEPTION
+            .withValue(StringUtils.isNotEmpty(simpleName) ? simpleName : exception.getClass().getName());
+    }
+
+    /**
+     * Creates an {@code outcome} KeyValue based on the status of the given
+     * {@code response}.
+     * @param response the container response
+     * @return the outcome KeyValue derived from the status of the response
+     */
+    static KeyValue outcome(ContainerResponse response) {
+        if (response != null) {
+            Outcome outcome = Outcome.forStatus(response.getStatus());
+            return JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.OUTCOME.withValue(outcome.name());
+        }
+        /* In case there is no response we are dealing with an unmapped exception. */
+        return JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.OUTCOME
+            .withValue(Outcome.SERVER_ERROR.name());
+    }
+
+}
diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyObservationConvention.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyObservationConvention.java
new file mode 100644
index 0000000..24bb475
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyObservationConvention.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package org.glassfish.jersey.micrometer.server;
+
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationConvention;
+
+/**
+ * Provides names and {@link io.micrometer.common.KeyValues} for Jersey request
+ * observations.
+ *
+ * @author Marcin Grzejszczak
+ * @since 2.41
+ */
+public interface JerseyObservationConvention extends ObservationConvention<JerseyContext> {
+
+    @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 0000000..bebbde9
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyObservationDocumentation.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package org.glassfish.jersey.micrometer.server;
+
+import io.micrometer.common.docs.KeyName;
+import io.micrometer.common.lang.NonNullApi;
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationConvention;
+import io.micrometer.observation.docs.ObservationDocumentation;
+
+/**
+ * An {@link ObservationDocumentation} for Jersey.
+ *
+ * @author Marcin Grzejszczak
+ * @since 2.41
+ */
+@NonNullApi
+public enum JerseyObservationDocumentation implements ObservationDocumentation {
+
+    /**
+     * Default observation for Jersey.
+     */
+    DEFAULT {
+        @Override
+        public Class<? 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 0000000..d723c7c
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyTags.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package org.glassfish.jersey.micrometer.server;
+
+import java.util.List;
+import java.util.regex.Pattern;
+
+import io.micrometer.common.util.StringUtils;
+import io.micrometer.core.instrument.Tag;
+import io.micrometer.core.instrument.binder.http.Outcome;
+import org.glassfish.jersey.server.ContainerRequest;
+import org.glassfish.jersey.server.ContainerResponse;
+import org.glassfish.jersey.server.ExtendedUriInfo;
+import org.glassfish.jersey.server.monitoring.RequestEvent;
+import org.glassfish.jersey.uri.UriTemplate;
+
+/**
+ * Factory methods for {@link Tag Tags} associated with a request-response exchange that
+ * is handled by Jersey server.
+ *
+ * @author Michael Weirauch
+ * @author Johnny Lim
+ * @since 2.41
+ */
+public final class JerseyTags {
+
+    private static final Tag URI_NOT_FOUND = Tag.of("uri", "NOT_FOUND");
+
+    private static final Tag URI_REDIRECTION = Tag.of("uri", "REDIRECTION");
+
+    private static final Tag URI_ROOT = Tag.of("uri", "root");
+
+    private static final Tag EXCEPTION_NONE = Tag.of("exception", "None");
+
+    private static final Tag STATUS_SERVER_ERROR = Tag.of("status", "500");
+
+    private static final Tag METHOD_UNKNOWN = Tag.of("method", "UNKNOWN");
+
+    static final Pattern TRAILING_SLASH_PATTERN = Pattern.compile("/$");
+
+    static final Pattern MULTIPLE_SLASH_PATTERN = Pattern.compile("//+");
+
+    private JerseyTags() {
+    }
+
+    /**
+     * Creates a {@code method} tag based on the {@link ContainerRequest#getMethod()
+     * method} of the given {@code request}.
+     * @param request the container request
+     * @return the method tag whose value is a capitalized method (e.g. GET).
+     */
+    public static Tag method(ContainerRequest request) {
+        return (request != null) ? Tag.of("method", request.getMethod()) : METHOD_UNKNOWN;
+    }
+
+    /**
+     * Creates a {@code status} tag based on the status of the given {@code response}.
+     * @param response the container response
+     * @return the status tag derived from the status of the response
+     */
+    public static Tag status(ContainerResponse response) {
+        /* In case there is no response we are dealing with an unmapped exception. */
+        return (response != null) ? Tag.of("status", Integer.toString(response.getStatus())) : STATUS_SERVER_ERROR;
+    }
+
+    /**
+     * Creates a {@code uri} tag based on the URI of the given {@code event}. Uses the
+     * {@link ExtendedUriInfo#getMatchedTemplates()} if available. {@code REDIRECTION} for
+     * 3xx responses, {@code NOT_FOUND} for 404 responses.
+     * @param event the request event
+     * @return the uri tag derived from the request event
+     */
+    public static Tag uri(RequestEvent event) {
+        ContainerResponse response = event.getContainerResponse();
+        if (response != null) {
+            int status = response.getStatus();
+            if (isRedirection(status) && event.getUriInfo().getMatchedResourceMethod() == null) {
+                return URI_REDIRECTION;
+            }
+            if (status == 404 && event.getUriInfo().getMatchedResourceMethod() == null) {
+                return URI_NOT_FOUND;
+            }
+        }
+        String matchingPattern = getMatchingPattern(event);
+        if (matchingPattern.equals("/")) {
+            return URI_ROOT;
+        }
+        return Tag.of("uri", matchingPattern);
+    }
+
+    static boolean isRedirection(int status) {
+        return 300 <= status && status < 400;
+    }
+
+    static String getMatchingPattern(RequestEvent event) {
+        ExtendedUriInfo uriInfo = event.getUriInfo();
+        List<UriTemplate> 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 0000000..c1d2da0
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyTagsProvider.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package org.glassfish.jersey.micrometer.server;
+
+import io.micrometer.core.instrument.Tag;
+import org.glassfish.jersey.server.monitoring.RequestEvent;
+
+/**
+ * Provides {@link Tag Tags} for Jersey request metrics.
+ *
+ * @author Michael Weirauch
+ * @since 2.41
+ */
+public interface JerseyTagsProvider {
+
+    /**
+     * Provides tags to be associated with metrics for the given {@code event}.
+     * @param event the request event
+     * @return tags to associate with metrics recorded for the request
+     */
+    Iterable<Tag> 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<Tag> 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 0000000..30ccc36
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/MetricsApplicationEventListener.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package org.glassfish.jersey.micrometer.server;
+
+import io.micrometer.core.instrument.MeterRegistry;
+import org.glassfish.jersey.server.monitoring.ApplicationEvent;
+import org.glassfish.jersey.server.monitoring.ApplicationEventListener;
+import org.glassfish.jersey.server.monitoring.RequestEvent;
+import org.glassfish.jersey.server.monitoring.RequestEventListener;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * The Micrometer {@link ApplicationEventListener} which registers
+ * {@link RequestEventListener} for instrumenting Jersey server requests.
+ *
+ * @author Michael Weirauch
+ * @since 2.41
+ */
+public class MetricsApplicationEventListener implements ApplicationEventListener {
+
+    private final MeterRegistry meterRegistry;
+
+    private final JerseyTagsProvider tagsProvider;
+
+    private final String metricName;
+
+    private final AnnotationFinder annotationFinder;
+
+    private final boolean autoTimeRequests;
+
+    public MetricsApplicationEventListener(MeterRegistry registry, JerseyTagsProvider tagsProvider, String metricName,
+            boolean autoTimeRequests) {
+        this(registry, tagsProvider, metricName, autoTimeRequests, AnnotationFinder.DEFAULT);
+    }
+
+    public MetricsApplicationEventListener(MeterRegistry registry, JerseyTagsProvider tagsProvider, String metricName,
+            boolean autoTimeRequests, AnnotationFinder annotationFinder) {
+        this.meterRegistry = requireNonNull(registry);
+        this.tagsProvider = requireNonNull(tagsProvider);
+        this.metricName = requireNonNull(metricName);
+        this.annotationFinder = requireNonNull(annotationFinder);
+        this.autoTimeRequests = autoTimeRequests;
+    }
+
+    @Override
+    public void onEvent(ApplicationEvent event) {
+    }
+
+    @Override
+    public RequestEventListener onRequest(RequestEvent requestEvent) {
+        return new MetricsRequestEventListener(meterRegistry, tagsProvider, metricName, autoTimeRequests,
+                annotationFinder);
+    }
+
+}
diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListener.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListener.java
new file mode 100644
index 0000000..9036637
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListener.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package org.glassfish.jersey.micrometer.server;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import io.micrometer.core.annotation.Timed;
+import io.micrometer.core.instrument.LongTaskTimer;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Timer;
+import org.glassfish.jersey.server.ContainerRequest;
+import org.glassfish.jersey.server.model.ResourceMethod;
+import org.glassfish.jersey.server.monitoring.RequestEvent;
+import org.glassfish.jersey.server.monitoring.RequestEventListener;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * {@link RequestEventListener} recording timings for Jersey server requests.
+ *
+ * @author Michael Weirauch
+ * @author Jon Schneider
+ * @since 2.41
+ */
+public class MetricsRequestEventListener implements RequestEventListener {
+
+    private final Map<ContainerRequest, Timer.Sample> shortTaskSample = Collections
+        .synchronizedMap(new IdentityHashMap<>());
+
+    private final Map<ContainerRequest, Collection<LongTaskTimer.Sample>> longTaskSamples = Collections
+        .synchronizedMap(new IdentityHashMap<>());
+
+    private final Map<ContainerRequest, Set<Timed>> 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<Timed> 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<LongTaskTimer.Sample> longSamples = this.longTaskSamples.remove(containerRequest);
+                if (longSamples != null) {
+                    for (LongTaskTimer.Sample longSample : longSamples) {
+                        longSample.stop();
+                    }
+                }
+                break;
+        }
+    }
+
+    private void time(RequestEvent event, ContainerRequest containerRequest) {
+        Set<Timed> timedAnnotations;
+        timedAnnotations = annotations(event);
+
+        timedAnnotationsOnRequest.put(containerRequest, timedAnnotations);
+        shortTaskSample.put(containerRequest, Timer.start(registry));
+
+        List<LongTaskTimer.Sample> 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<Timer> shortTimers(Set<Timed> 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<LongTaskTimer> longTaskTimers(Set<Timed> 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<Timed> annotations(RequestEvent event) {
+        final Set<Timed> 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 0000000..6f519f0
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationApplicationEventListener.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package org.glassfish.jersey.micrometer.server;
+
+import io.micrometer.observation.ObservationRegistry;
+import org.glassfish.jersey.server.monitoring.ApplicationEvent;
+import org.glassfish.jersey.server.monitoring.ApplicationEventListener;
+import org.glassfish.jersey.server.monitoring.RequestEvent;
+import org.glassfish.jersey.server.monitoring.RequestEventListener;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * The Micrometer {@link ApplicationEventListener} which registers
+ * {@link RequestEventListener} for instrumenting Jersey server requests with
+ * observations.
+ *
+ * @author Marcin Grzejszczak
+ * @since 2.41
+ */
+public class ObservationApplicationEventListener implements ApplicationEventListener {
+
+    private final ObservationRegistry observationRegistry;
+
+    private final String metricName;
+
+    private final JerseyObservationConvention jerseyObservationConvention;
+
+    public ObservationApplicationEventListener(ObservationRegistry observationRegistry, String metricName) {
+        this(observationRegistry, metricName, null);
+    }
+
+    public ObservationApplicationEventListener(ObservationRegistry observationRegistry, String metricName,
+            JerseyObservationConvention jerseyObservationConvention) {
+        this.observationRegistry = requireNonNull(observationRegistry);
+        this.metricName = requireNonNull(metricName);
+        this.jerseyObservationConvention = jerseyObservationConvention;
+    }
+
+    @Override
+    public void onEvent(ApplicationEvent event) {
+    }
+
+    @Override
+    public RequestEventListener onRequest(RequestEvent requestEvent) {
+        return new ObservationRequestEventListener(observationRegistry, metricName, jerseyObservationConvention);
+    }
+
+}
diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationRequestEventListener.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationRequestEventListener.java
new file mode 100644
index 0000000..96c463c
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationRequestEventListener.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package org.glassfish.jersey.micrometer.server;
+
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Objects;
+
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationRegistry;
+import org.glassfish.jersey.server.ContainerRequest;
+import org.glassfish.jersey.server.monitoring.RequestEvent;
+import org.glassfish.jersey.server.monitoring.RequestEventListener;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * {@link RequestEventListener} recording observations for Jersey server requests.
+ *
+ * @author Marcin Grzejszczak
+ * @since 2.41
+ */
+public class ObservationRequestEventListener implements RequestEventListener {
+
+    private final Map<ContainerRequest, ObservationScopeAndContext> 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 0000000..42d4745
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/TimedFinder.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package org.glassfish.jersey.micrometer.server;
+
+import java.lang.reflect.AnnotatedElement;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import io.micrometer.core.annotation.Timed;
+import io.micrometer.core.annotation.TimedSet;
+
+class TimedFinder {
+
+    private final AnnotationFinder annotationFinder;
+
+    TimedFinder(AnnotationFinder annotationFinder) {
+        this.annotationFinder = annotationFinder;
+    }
+
+    Set<Timed> 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 0000000..0d9e95e
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+/**
+ * Binders for Jersey. Code ported from <a href="https://github.com/micrometer-metrics/micrometer/tree/v1.10.9/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server">Micrometer repository</a>.
+ */
+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 0000000..4bf1759
--- /dev/null
+++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/DefaultJerseyTagsProviderTest.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package org.glassfish.jersey.micrometer.server;
+
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.ws.rs.NotAcceptableException;
+
+import io.micrometer.core.instrument.Tag;
+import io.micrometer.core.instrument.Tags;
+import org.glassfish.jersey.server.ContainerRequest;
+import org.glassfish.jersey.server.ContainerResponse;
+import org.glassfish.jersey.server.ExtendedUriInfo;
+import org.glassfish.jersey.server.internal.monitoring.RequestEventImpl.Builder;
+import org.glassfish.jersey.server.monitoring.RequestEvent;
+import org.glassfish.jersey.server.monitoring.RequestEvent.Type;
+import org.glassfish.jersey.uri.UriTemplate;
+import org.junit.jupiter.api.Test;
+
+import static java.util.stream.StreamSupport.stream;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for {@link DefaultJerseyTagsProvider}.
+ *
+ * @author Michael Weirauch
+ * @author Johnny Lim
+ */
+class DefaultJerseyTagsProviderTest {
+
+    private final DefaultJerseyTagsProvider tagsProvider = new DefaultJerseyTagsProvider();
+
+    @Test
+    void testRootPath() {
+        assertThat(tagsProvider.httpRequestTags(event(200, null, "/", (String[]) null)))
+            .containsExactlyInAnyOrder(tagsFrom("root", 200, null, "SUCCESS"));
+    }
+
+    @Test
+    void templatedPathsAreReturned() {
+        assertThat(tagsProvider.httpRequestTags(event(200, null, "/", "/", "/hello/{name}")))
+            .containsExactlyInAnyOrder(tagsFrom("/hello/{name}", 200, null, "SUCCESS"));
+    }
+
+    @Test
+    void applicationPathIsPresent() {
+        assertThat(tagsProvider.httpRequestTags(event(200, null, "/app", "/", "/hello")))
+            .containsExactlyInAnyOrder(tagsFrom("/app/hello", 200, null, "SUCCESS"));
+    }
+
+    @Test
+    void notFoundsAreShunted() {
+        assertThat(tagsProvider.httpRequestTags(event(404, null, "/app", "/", "/not-found")))
+            .containsExactlyInAnyOrder(tagsFrom("NOT_FOUND", 404, null, "CLIENT_ERROR"));
+    }
+
+    @Test
+    void redirectsAreShunted() {
+        assertThat(tagsProvider.httpRequestTags(event(301, null, "/app", "/", "/redirect301")))
+            .containsExactlyInAnyOrder(tagsFrom("REDIRECTION", 301, null, "REDIRECTION"));
+        assertThat(tagsProvider.httpRequestTags(event(302, null, "/app", "/", "/redirect302")))
+            .containsExactlyInAnyOrder(tagsFrom("REDIRECTION", 302, null, "REDIRECTION"));
+        assertThat(tagsProvider.httpRequestTags(event(399, null, "/app", "/", "/redirect399")))
+            .containsExactlyInAnyOrder(tagsFrom("REDIRECTION", 399, null, "REDIRECTION"));
+    }
+
+    @Test
+    @SuppressWarnings("serial")
+    void exceptionsAreMappedCorrectly() {
+        assertThat(tagsProvider.httpRequestTags(event(500, new IllegalArgumentException(), "/app", (String[]) null)))
+            .containsExactlyInAnyOrder(tagsFrom("/app", 500, "IllegalArgumentException", "SERVER_ERROR"));
+        assertThat(tagsProvider.httpRequestTags(
+                event(500, new IllegalArgumentException(new NullPointerException()), "/app", (String[]) null)))
+            .containsExactlyInAnyOrder(tagsFrom("/app", 500, "NullPointerException", "SERVER_ERROR"));
+        assertThat(tagsProvider.httpRequestTags(event(406, new NotAcceptableException(), "/app", (String[]) null)))
+            .containsExactlyInAnyOrder(tagsFrom("/app", 406, "NotAcceptableException", "CLIENT_ERROR"));
+        assertThat(tagsProvider.httpRequestTags(event(500, new Exception("anonymous") {
+        }, "/app", (String[]) null))).containsExactlyInAnyOrder(tagsFrom("/app", 500,
+                "org.glassfish.jersey.micrometer.server.DefaultJerseyTagsProviderTest$1", "SERVER_ERROR"));
+    }
+
+    @Test
+    void longRequestTags() {
+        assertThat(tagsProvider.httpLongRequestTags(event(0, null, "/app", (String[]) null)))
+            .containsExactlyInAnyOrder(Tag.of("method", "GET"), Tag.of("uri", "/app"));
+    }
+
+    private static RequestEvent event(Integer status, Exception exception, String baseUri,
+            String... uriTemplateStrings) {
+        Builder builder = new Builder();
+
+        ContainerRequest containerRequest = mock(ContainerRequest.class);
+        when(containerRequest.getMethod()).thenReturn("GET");
+        builder.setContainerRequest(containerRequest);
+
+        ContainerResponse containerResponse = mock(ContainerResponse.class);
+        when(containerResponse.getStatus()).thenReturn(status);
+        builder.setContainerResponse(containerResponse);
+
+        builder.setException(exception, null);
+
+        ExtendedUriInfo extendedUriInfo = mock(ExtendedUriInfo.class);
+        when(extendedUriInfo.getBaseUri())
+            .thenReturn(URI.create("http://localhost:8080" + (baseUri == null ? "/" : baseUri)));
+        List<UriTemplate> 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<Tag> 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 0000000..b2edeea
--- /dev/null
+++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListenerTest.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package org.glassfish.jersey.micrometer.server;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.core.Application;
+
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Tag;
+import io.micrometer.core.instrument.Tags;
+import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+import org.glassfish.jersey.micrometer.server.mapper.ResourceGoneExceptionMapper;
+import org.glassfish.jersey.micrometer.server.resources.TestResource;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link MetricsApplicationEventListener}.
+ *
+ * @author Michael Weirauch
+ * @author Johnny Lim
+ */
+class MetricsRequestEventListenerTest extends JerseyTest {
+
+    static {
+        Logger.getLogger("org.glassfish.jersey").setLevel(Level.OFF);
+    }
+
+    private static final String METRIC_NAME = "http.server.requests";
+
+    private MeterRegistry registry;
+
+    @Override
+    protected Application configure() {
+        registry = new SimpleMeterRegistry();
+
+        final MetricsApplicationEventListener listener = new MetricsApplicationEventListener(registry,
+                new DefaultJerseyTagsProvider(), METRIC_NAME, true);
+
+        final ResourceConfig config = new ResourceConfig();
+        config.register(listener);
+        config.register(TestResource.class);
+        config.register(ResourceGoneExceptionMapper.class);
+
+        return config;
+    }
+
+    @Test
+    void resourcesAreTimed() {
+        target("/").request().get();
+        target("hello").request().get();
+        target("hello/").request().get();
+        target("hello/peter").request().get();
+        target("sub-resource/sub-hello/peter").request().get();
+
+        assertThat(registry.get(METRIC_NAME).tags(tagsFrom("root", "200", "SUCCESS", null)).timer().count())
+            .isEqualTo(1);
+
+        assertThat(registry.get(METRIC_NAME).tags(tagsFrom("/hello", "200", "SUCCESS", null)).timer().count())
+            .isEqualTo(2);
+
+        assertThat(registry.get(METRIC_NAME).tags(tagsFrom("/hello/{name}", "200", "SUCCESS", null)).timer().count())
+            .isEqualTo(1);
+
+        assertThat(registry.get(METRIC_NAME)
+            .tags(tagsFrom("/sub-resource/sub-hello/{name}", "200", "SUCCESS", null))
+            .timer()
+            .count()).isEqualTo(1);
+
+        // assert we are not auto-timing long task @Timed
+        assertThat(registry.getMeters()).hasSize(4);
+    }
+
+    @Test
+    void notFoundIsAccumulatedUnderSameUri() {
+        try {
+            target("not-found").request().get();
+        }
+        catch (NotFoundException ignored) {
+        }
+
+        assertThat(registry.get(METRIC_NAME).tags(tagsFrom("NOT_FOUND", "404", "CLIENT_ERROR", null)).timer().count())
+            .isEqualTo(1);
+    }
+
+    @Test
+    void notFoundIsReportedWithUriOfMatchedResource() {
+        try {
+            target("throws-not-found-exception").request().get();
+        }
+        catch (NotFoundException ignored) {
+        }
+
+        assertThat(registry.get(METRIC_NAME)
+            .tags(tagsFrom("/throws-not-found-exception", "404", "CLIENT_ERROR", null))
+            .timer()
+            .count()).isEqualTo(1);
+    }
+
+    @Test
+    void redirectsAreReportedWithUriOfMatchedResource() {
+        target("redirect/302").request().get();
+        target("redirect/307").request().get();
+
+        assertThat(registry.get(METRIC_NAME)
+            .tags(tagsFrom("/redirect/{status}", "302", "REDIRECTION", null))
+            .timer()
+            .count()).isEqualTo(1);
+
+        assertThat(registry.get(METRIC_NAME)
+            .tags(tagsFrom("/redirect/{status}", "307", "REDIRECTION", null))
+            .timer()
+            .count()).isEqualTo(1);
+    }
+
+    @Test
+    void exceptionsAreMappedCorrectly() {
+        try {
+            target("throws-exception").request().get();
+        }
+        catch (Exception ignored) {
+        }
+        try {
+            target("throws-webapplication-exception").request().get();
+        }
+        catch (Exception ignored) {
+        }
+        try {
+            target("throws-mappable-exception").request().get();
+        }
+        catch (Exception ignored) {
+        }
+
+        assertThat(registry.get(METRIC_NAME)
+            .tags(tagsFrom("/throws-exception", "500", "SERVER_ERROR", "IllegalArgumentException"))
+            .timer()
+            .count()).isEqualTo(1);
+
+        assertThat(registry.get(METRIC_NAME)
+            .tags(tagsFrom("/throws-webapplication-exception", "401", "CLIENT_ERROR", "NotAuthorizedException"))
+            .timer()
+            .count()).isEqualTo(1);
+
+        assertThat(registry.get(METRIC_NAME)
+            .tags(tagsFrom("/throws-mappable-exception", "410", "CLIENT_ERROR", "ResourceGoneException"))
+            .timer()
+            .count()).isEqualTo(1);
+    }
+
+    private static Iterable<Tag> 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 0000000..992864a
--- /dev/null
+++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListenerTimedTest.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package org.glassfish.jersey.micrometer.server;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.ws.rs.ProcessingException;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.Response;
+
+import io.micrometer.core.Issue;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Tag;
+import io.micrometer.core.instrument.Tags;
+import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+import org.glassfish.jersey.micrometer.server.resources.TimedOnClassResource;
+import org.glassfish.jersey.micrometer.server.resources.TimedResource;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+
+/**
+ * @author Michael Weirauch
+ */
+class MetricsRequestEventListenerTimedTest extends JerseyTest {
+
+    static {
+        Logger.getLogger("org.glassfish.jersey").setLevel(Level.OFF);
+    }
+
+    private static final String METRIC_NAME = "http.server.requests";
+
+    private MeterRegistry registry;
+
+    private CountDownLatch longTaskRequestStartedLatch;
+
+    private CountDownLatch longTaskRequestReleaseLatch;
+
+    @Override
+    protected Application configure() {
+        registry = new SimpleMeterRegistry();
+        longTaskRequestStartedLatch = new CountDownLatch(1);
+        longTaskRequestReleaseLatch = new CountDownLatch(1);
+
+        final MetricsApplicationEventListener listener = new MetricsApplicationEventListener(registry,
+                new DefaultJerseyTagsProvider(), METRIC_NAME, false);
+
+        final ResourceConfig config = new ResourceConfig();
+        config.register(listener);
+        config.register(new TimedResource(longTaskRequestStartedLatch, longTaskRequestReleaseLatch));
+        config.register(TimedOnClassResource.class);
+
+        return config;
+    }
+
+    @Test
+    void resourcesAndNotFoundsAreNotAutoTimed() {
+        target("not-timed").request().get();
+        target("not-found").request().get();
+
+        assertThat(registry.find(METRIC_NAME).tags(tagsFrom("/not-timed", 200)).timer()).isNull();
+
+        assertThat(registry.find(METRIC_NAME).tags(tagsFrom("NOT_FOUND", 404)).timer()).isNull();
+    }
+
+    @Test
+    void resourcesWithAnnotationAreTimed() {
+        target("timed").request().get();
+        target("multi-timed").request().get();
+
+        assertThat(registry.get(METRIC_NAME).tags(tagsFrom("/timed", 200)).timer().count()).isEqualTo(1);
+
+        assertThat(registry.get("multi1").tags(tagsFrom("/multi-timed", 200)).timer().count()).isEqualTo(1);
+
+        assertThat(registry.get("multi2").tags(tagsFrom("/multi-timed", 200)).timer().count()).isEqualTo(1);
+    }
+
+    @Test
+    void longTaskTimerSupported() throws InterruptedException, ExecutionException, TimeoutException {
+        final Future<Response> 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<Response> 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<Tag> 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 0000000..99f654d
--- /dev/null
+++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/exception/ResourceGoneException.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package org.glassfish.jersey.micrometer.server.exception;
+
+public class ResourceGoneException extends RuntimeException {
+
+    public ResourceGoneException() {
+        super();
+    }
+
+    public ResourceGoneException(String message) {
+        super(message);
+    }
+
+    public ResourceGoneException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+}
diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/mapper/ResourceGoneExceptionMapper.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/mapper/ResourceGoneExceptionMapper.java
new file mode 100644
index 0000000..745ae7c
--- /dev/null
+++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/mapper/ResourceGoneExceptionMapper.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package org.glassfish.jersey.micrometer.server.mapper;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.ext.ExceptionMapper;
+
+import org.glassfish.jersey.micrometer.server.exception.ResourceGoneException;
+
+public class ResourceGoneExceptionMapper implements ExceptionMapper<ResourceGoneException> {
+
+    @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 0000000..597e6a6
--- /dev/null
+++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/observation/AbstractObservationRequestEventListenerTest.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package org.glassfish.jersey.micrometer.server.observation;
+
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.ws.rs.core.Application;
+
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Tag;
+import io.micrometer.core.instrument.Tags;
+import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler;
+import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+import io.micrometer.observation.ObservationHandler.FirstMatchingCompositeObservationHandler;
+import io.micrometer.observation.ObservationRegistry;
+import io.micrometer.tracing.Tracer;
+import io.micrometer.tracing.exporter.FinishedSpan;
+import io.micrometer.tracing.handler.DefaultTracingObservationHandler;
+import io.micrometer.tracing.handler.PropagatingReceiverTracingObservationHandler;
+import io.micrometer.tracing.handler.PropagatingSenderTracingObservationHandler;
+import io.micrometer.tracing.propagation.Propagator;
+import io.micrometer.tracing.test.simple.SpanAssert;
+import io.micrometer.tracing.test.simple.SpansAssert;
+import org.glassfish.jersey.micrometer.server.ObservationApplicationEventListener;
+import org.glassfish.jersey.micrometer.server.ObservationRequestEventListener;
+import org.glassfish.jersey.micrometer.server.resources.TestResource;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+import zipkin2.CheckResult;
+import zipkin2.reporter.Sender;
+import zipkin2.reporter.urlconnection.URLConnectionSender;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link ObservationRequestEventListener}.
+ *
+ * @author Marcin Grzejsczak
+ */
+abstract class AbstractObservationRequestEventListenerTest extends JerseyTest {
+
+    static {
+        Logger.getLogger("org.glassfish.jersey").setLevel(Level.OFF);
+    }
+
+    private static final String METRIC_NAME = "http.server.requests";
+
+    ObservationRegistry observationRegistry;
+
+    MeterRegistry registry;
+
+    Boolean zipkinAvailable;
+
+    Sender sender;
+
+    @Override
+    protected Application configure() {
+        observationRegistry = ObservationRegistry.create();
+        registry = new SimpleMeterRegistry();
+        sender = URLConnectionSender.create("http://localhost:9411/api/v2/spans");
+
+        observationRegistry.observationConfig().observationHandler(new DefaultMeterObservationHandler(registry));
+
+        configureRegistry(observationRegistry);
+
+        final ObservationApplicationEventListener listener =
+                new ObservationApplicationEventListener(observationRegistry, METRIC_NAME);
+
+        final ResourceConfig config = new ResourceConfig();
+        config.register(listener);
+        config.register(TestResource.class);
+
+        return config;
+    }
+
+    abstract void configureRegistry(ObservationRegistry registry);
+
+    abstract List<FinishedSpan> 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<FinishedSpan> 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<Tag> 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 0000000..0490129
--- /dev/null
+++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/observation/ObservationApplicationEventListenerTest.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package org.glassfish.jersey.micrometer.server.observation;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import brave.Tracing;
+import brave.Tracing.Builder;
+import brave.context.slf4j.MDCScopeDecorator;
+import brave.handler.SpanHandler;
+import brave.propagation.B3Propagation;
+import brave.propagation.B3Propagation.Format;
+import brave.propagation.ThreadLocalCurrentTraceContext;
+import brave.sampler.Sampler;
+import brave.test.TestSpanHandler;
+import io.micrometer.observation.ObservationRegistry;
+import io.micrometer.tracing.CurrentTraceContext;
+import io.micrometer.tracing.Tracer;
+import io.micrometer.tracing.brave.bridge.BraveBaggageManager;
+import io.micrometer.tracing.brave.bridge.BraveCurrentTraceContext;
+import io.micrometer.tracing.brave.bridge.BraveFinishedSpan;
+import io.micrometer.tracing.brave.bridge.BravePropagator;
+import io.micrometer.tracing.brave.bridge.BraveTracer;
+import io.micrometer.tracing.exporter.FinishedSpan;
+import io.micrometer.tracing.otel.bridge.ArrayListSpanProcessor;
+import io.micrometer.tracing.otel.bridge.OtelBaggageManager;
+import io.micrometer.tracing.otel.bridge.OtelCurrentTraceContext;
+import io.micrometer.tracing.otel.bridge.OtelFinishedSpan;
+import io.micrometer.tracing.otel.bridge.OtelPropagator;
+import io.micrometer.tracing.otel.bridge.OtelTracer;
+import io.micrometer.tracing.otel.bridge.Slf4JBaggageEventListener;
+import io.micrometer.tracing.otel.bridge.Slf4JEventListener;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.context.propagation.ContextPropagators;
+import io.opentelemetry.exporter.zipkin.ZipkinSpanExporterBuilder;
+import io.opentelemetry.extension.trace.propagation.B3Propagator;
+import io.opentelemetry.sdk.OpenTelemetrySdk;
+import io.opentelemetry.sdk.resources.Resource;
+import io.opentelemetry.sdk.trace.SdkTracerProvider;
+import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder;
+import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
+import io.opentelemetry.sdk.trace.export.SpanExporter;
+import io.opentelemetry.semconv.resource.attributes.ResourceAttributes;
+import org.glassfish.jersey.micrometer.server.ObservationApplicationEventListener;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Nested;
+import zipkin2.Span;
+import zipkin2.reporter.AsyncReporter;
+import zipkin2.reporter.brave.ZipkinSpanHandler;
+
+import static io.opentelemetry.sdk.trace.samplers.Sampler.alwaysOn;
+
+/**
+ * Tests for {@link ObservationApplicationEventListener}.
+ *
+ * @author Marcin Grzejsczak
+ */
+class ObservationApplicationEventListenerTest {
+
+    @Nested
+    class BraveObservationRequestEventListenerTest extends AbstractObservationRequestEventListenerTest {
+
+        Tracing tracing;
+
+        TestSpanHandler testSpanHandler;
+
+        AsyncReporter<Span> 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<FinishedSpan> 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<FinishedSpan> 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 0000000..661e1fa
--- /dev/null
+++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TestResource.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package org.glassfish.jersey.micrometer.server.resources;
+
+import java.net.URI;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.NotAuthorizedException;
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.RedirectionException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
+import org.glassfish.jersey.micrometer.server.exception.ResourceGoneException;
+
+/**
+ * @author Michael Weirauch
+ */
+@Path("/")
+@Produces(MediaType.TEXT_PLAIN)
+public class TestResource {
+
+    @Produces(MediaType.TEXT_PLAIN)
+    public static class SubResource {
+
+        @GET
+        @Path("sub-hello/{name}")
+        public String hello(@PathParam("name") String name) {
+            return "hello " + name;
+        }
+
+    }
+
+    @GET
+    public String index() {
+        return "index";
+    }
+
+    @GET
+    @Path("hello")
+    public String hello() {
+        return "hello";
+    }
+
+    @GET
+    @Path("hello/{name}")
+    public String hello(@PathParam("name") String name) {
+        return "hello " + name;
+    }
+
+    @GET
+    @Path("throws-not-found-exception")
+    public String throwsNotFoundException() {
+        throw new NotFoundException();
+    }
+
+    @GET
+    @Path("throws-exception")
+    public String throwsException() {
+        throw new IllegalArgumentException();
+    }
+
+    @GET
+    @Path("throws-webapplication-exception")
+    public String throwsWebApplicationException() {
+        throw new NotAuthorizedException("notauth", Response.status(Status.UNAUTHORIZED).build());
+    }
+
+    @GET
+    @Path("throws-mappable-exception")
+    public String throwsMappableException() {
+        throw new ResourceGoneException("Resource has been permanently removed.");
+    }
+
+    @GET
+    @Path("redirect/{status}")
+    public Response redirect(@PathParam("status") int status) {
+        if (status == 307) {
+            throw new RedirectionException(status, URI.create("hello"));
+        }
+        return Response.status(status).header("Location", "/hello").build();
+    }
+
+    @Path("/sub-resource")
+    public SubResource subResource() {
+        return new SubResource();
+    }
+
+}
diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedOnClassResource.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedOnClassResource.java
new file mode 100644
index 0000000..89d6d37
--- /dev/null
+++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedOnClassResource.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package org.glassfish.jersey.micrometer.server.resources;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+import io.micrometer.core.annotation.Timed;
+
+/**
+ * @author Michael Weirauch
+ */
+@Path("/class")
+@Produces(MediaType.TEXT_PLAIN)
+@Timed(extraTags = { "on", "class" })
+public class TimedOnClassResource {
+
+    @GET
+    @Path("inherited")
+    public String inherited() {
+        return "inherited";
+    }
+
+    @GET
+    @Path("on-method")
+    @Timed(extraTags = { "on", "method" })
+    public String onMethod() {
+        return "on-method";
+    }
+
+}
diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedResource.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedResource.java
new file mode 100644
index 0000000..723d3a0
--- /dev/null
+++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedResource.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package org.glassfish.jersey.micrometer.server.resources;
+
+import java.util.concurrent.CountDownLatch;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+import io.micrometer.core.annotation.Timed;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * @author Michael Weirauch
+ */
+@Path("/")
+@Produces(MediaType.TEXT_PLAIN)
+public class TimedResource {
+
+    private final CountDownLatch longTaskRequestStartedLatch;
+
+    private final CountDownLatch longTaskRequestReleaseLatch;
+
+    public TimedResource(CountDownLatch longTaskRequestStartedLatch, CountDownLatch longTaskRequestReleaseLatch) {
+        this.longTaskRequestStartedLatch = requireNonNull(longTaskRequestStartedLatch);
+        this.longTaskRequestReleaseLatch = requireNonNull(longTaskRequestReleaseLatch);
+    }
+
+    @GET
+    @Path("not-timed")
+    public String notTimed() {
+        return "not-timed";
+    }
+
+    @GET
+    @Path("timed")
+    @Timed
+    public String timed() {
+        return "timed";
+    }
+
+    @GET
+    @Path("multi-timed")
+    @Timed("multi1")
+    @Timed("multi2")
+    public String multiTimed() {
+        return "multi-timed";
+    }
+
+    /*
+     * Async server side processing (AsyncResponse) is not supported in the in-memory test
+     * container.
+     */
+    @GET
+    @Path("long-timed")
+    @Timed
+    @Timed(value = "long.task.in.request", longTask = true)
+    public String longTimed() {
+        longTaskRequestStartedLatch.countDown();
+        try {
+            longTaskRequestReleaseLatch.await();
+        }
+        catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+        return "long-timed";
+    }
+
+    @GET
+    @Path("just-long-timed")
+    @Timed(value = "long.task.in.request", longTask = true)
+    public String justLongTimed() {
+        longTaskRequestStartedLatch.countDown();
+        try {
+            longTaskRequestReleaseLatch.await();
+        }
+        catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+        return "long-timed";
+    }
+
+    @GET
+    @Path("long-timed-unnamed")
+    @Timed
+    @Timed(longTask = true)
+    public String longTimedUnnamed() {
+        return "long-timed-unnamed";
+    }
+
+}
diff --git a/ext/pom.xml b/ext/pom.xml
index 01ebf57..9ed36a1 100644
--- a/ext/pom.xml
+++ b/ext/pom.xml
@@ -44,6 +44,7 @@
         <module>cdi</module>
         <module>entity-filtering</module>
         <module>metainf-services</module>
+        <module>micrometer</module>
         <module>mvc</module>
         <module>mvc-bean-validation</module>
         <module>mvc-freemarker</module>
diff --git a/ext/spring4/pom.xml b/ext/spring4/pom.xml
index fdec30f..98a6f4a 100644
--- a/ext/spring4/pom.xml
+++ b/ext/spring4/pom.xml
@@ -156,7 +156,7 @@
         <dependency>
             <groupId>org.aspectj</groupId>
             <artifactId>aspectjweaver</artifactId>
-            <version>1.6.11</version>
+            <version>${aspectj.weaver.version}</version>
             <scope>test</scope>
         </dependency>
 
diff --git a/ext/spring5/pom.xml b/ext/spring5/pom.xml
index efad20a..70dc290 100644
--- a/ext/spring5/pom.xml
+++ b/ext/spring5/pom.xml
@@ -160,7 +160,7 @@
         <dependency>
             <groupId>org.aspectj</groupId>
             <artifactId>aspectjweaver</artifactId>
-            <version>1.6.11</version>
+            <version>${aspectj.weaver.version}</version>
             <scope>test</scope>
         </dependency>
 
diff --git a/pom.xml b/pom.xml
index ff90123..37579bb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2222,6 +2222,8 @@
         <!-- asm is now source integrated - keeping this property to see the version -->
         <!-- see core-server/src/main/java/jersey/repackaged/asm/.. -->
         <asm.version>9.5</asm.version>
+        <!--required for spring (ext) modules integration -->
+        <aspectj.weaver.version>1.6.11</aspectj.weaver.version>
 <!--        <bnd.plugin.version>2.3.6</bnd.plugin.version>-->
         <commons.io.version>2.13.0</commons.io.version>
 <!--        <commons-lang3.version>3.3.2</commons-lang3.version>-->
@@ -2316,6 +2318,8 @@
         <jsp.version>2.3.6</jsp.version>
         <jstl.version>1.2.7</jstl.version>
         <jta.api.version>1.3.3</jta.api.version>
+        <micrometer.version>1.10.10</micrometer.version>
+        <micrometer-tracing.version>1.0.9</micrometer-tracing.version>
         <microprofile.config.version>2.0.1</microprofile.config.version>
         <microprofile.rest.client.version>2.0</microprofile.rest.client.version>
         <mimepull.version>1.9.15</mimepull.version>