diff --git a/.fossa.yml b/.fossa.yml
index fb2c373fe00e..ac92e1573593 100644
--- a/.fossa.yml
+++ b/.fossa.yml
@@ -76,6 +76,9 @@ targets:
- type: gradle
path: ./
target: ':instrumentation:apache-shenyu-2.4:javaagent'
+ - type: gradle
+ path: ./
+ target: ':instrumentation:avaje-jex-3.0:javaagent'
- type: gradle
path: ./
target: ':instrumentation:c3p0-0.9:javaagent'
diff --git a/docs/supported-libraries.md b/docs/supported-libraries.md
index c09994f27d9e..76c034d4ea65 100644
--- a/docs/supported-libraries.md
+++ b/docs/supported-libraries.md
@@ -46,6 +46,7 @@ These are the supported libraries and frameworks:
| [Armeria](https://armeria.dev) | 1.3+ | [opentelemetry-armeria-1.3](../instrumentation/armeria/armeria-1.3/library) | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics] |
| [Armeria gRPC](https://armeria.dev) | 1.14+ | | [RPC Client Spans], [RPC Client Metrics], [RPC Server Spans], [RPC Server Metrics] |
| [AsyncHttpClient](https://github.com/AsyncHttpClient/async-http-client) | 1.9+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] |
+| [Avaje Jex](https://avaje.io/jex/) | 3.0+ | N/A | Provides `http.route` [2] |
| [AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/java-handler.html) | 1.0+ | [opentelemetry-aws-lambda-core-1.0](../instrumentation/aws-lambda/aws-lambda-core-1.0/library),
[opentelemetry-aws-lambda-events-2.2](../instrumentation/aws-lambda/aws-lambda-events-2.2/library) | [FaaS Server Spans] |
| [AWS SDK](https://aws.amazon.com/sdk-for-java/) | 1.11 - 1.12.583,
2.2+ | [opentelemetry-aws-sdk-1.11](../instrumentation/aws-sdk/aws-sdk-1.11/library),
[opentelemetry-aws-sdk-1.11-autoconfigure](../instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure),
[opentelemetry-aws-sdk-2.2](../instrumentation/aws-sdk/aws-sdk-2.2/library),
[opentelemetry-aws-sdk-2.2-autoconfigure](../instrumentation/aws-sdk/aws-sdk-2.2/library-autoconfigure) | [Messaging Spans], [Database Client Spans], [Database Client Metrics] [6], [HTTP Client Spans] |
| [Azure Core](https://docs.microsoft.com/en-us/java/api/overview/azure/core-readme) | 1.14+ | N/A | Context propagation |
diff --git a/instrumentation/avaje-jex-3.0/javaagent/build.gradle.kts b/instrumentation/avaje-jex-3.0/javaagent/build.gradle.kts
new file mode 100644
index 000000000000..eba6e4ff0171
--- /dev/null
+++ b/instrumentation/avaje-jex-3.0/javaagent/build.gradle.kts
@@ -0,0 +1,22 @@
+plugins {
+ id("otel.javaagent-instrumentation")
+}
+
+muzzle {
+ pass {
+ group.set("io.avaje")
+ module.set("avaje-jex")
+ versions.set("[3.0,)")
+ assertInverse.set(true)
+ }
+}
+
+otelJava {
+ minJavaVersionSupported.set(JavaVersion.VERSION_21)
+}
+
+dependencies {
+ library("io.avaje:avaje-jex:3.0")
+ testLibrary("org.eclipse.jetty:jetty-http-spi:12.0.19")
+ testInstrumentation(project(":instrumentation:jetty:jetty-12.0:javaagent"))
+}
diff --git a/instrumentation/avaje-jex-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/avaje/jex/v3_0/JexInstrumentation.java b/instrumentation/avaje-jex-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/avaje/jex/v3_0/JexInstrumentation.java
new file mode 100644
index 000000000000..0aaadb2f135e
--- /dev/null
+++ b/instrumentation/avaje-jex-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/avaje/jex/v3_0/JexInstrumentation.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.avaje.jex.v3_0;
+
+import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
+import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasSuperType;
+import static net.bytebuddy.matcher.ElementMatchers.isInterface;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.not;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+
+import io.avaje.jex.http.Context;
+import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute;
+import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+
+public class JexInstrumentation implements TypeInstrumentation {
+
+ @Override
+ public ElementMatcher classLoaderOptimization() {
+ return hasClassesNamed("io.avaje.jex.http.ExchangeHandler");
+ }
+
+ @Override
+ public ElementMatcher typeMatcher() {
+ return hasSuperType(named("io.avaje.jex.http.ExchangeHandler")).and(not(isInterface()));
+ }
+
+ @Override
+ public void transform(TypeTransformer transformer) {
+ transformer.applyAdviceToMethod(
+ named("handle").and(takesArgument(0, named("io.avaje.jex.http.Context"))),
+ this.getClass().getName() + "$HandlerAdapterAdvice");
+ }
+
+ @SuppressWarnings("unused")
+ public static class HandlerAdapterAdvice {
+
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static void onMethodExecute(@Advice.Argument(0) Context ctx) {
+ HttpServerRoute.update(
+ io.opentelemetry.context.Context.current(),
+ HttpServerRouteSource.CONTROLLER,
+ ctx.matchedPath());
+ }
+ }
+}
diff --git a/instrumentation/avaje-jex-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/avaje/jex/v3_0/JexInstrumentationModule.java b/instrumentation/avaje-jex-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/avaje/jex/v3_0/JexInstrumentationModule.java
new file mode 100644
index 000000000000..037f5d0b3caa
--- /dev/null
+++ b/instrumentation/avaje-jex-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/avaje/jex/v3_0/JexInstrumentationModule.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.avaje.jex.v3_0;
+
+import static java.util.Collections.singletonList;
+
+import com.google.auto.service.AutoService;
+import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import java.util.List;
+
+@SuppressWarnings("unused")
+@AutoService(InstrumentationModule.class)
+public class JexInstrumentationModule extends InstrumentationModule {
+
+ public JexInstrumentationModule() {
+ super("avaje-jex", "avaje-jex-3.0");
+ }
+
+ @Override
+ public List typeInstrumentations() {
+ return singletonList(new JexInstrumentation());
+ }
+}
diff --git a/instrumentation/avaje-jex-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/avaje/jex/v3_0/JexTest.java b/instrumentation/avaje-jex-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/avaje/jex/v3_0/JexTest.java
new file mode 100644
index 000000000000..e22a10b1e850
--- /dev/null
+++ b/instrumentation/avaje-jex-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/avaje/jex/v3_0/JexTest.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.avaje.jex.v3_0;
+
+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies;
+import static io.opentelemetry.semconv.ClientAttributes.CLIENT_ADDRESS;
+import static io.opentelemetry.semconv.ErrorAttributes.ERROR_TYPE;
+import static io.opentelemetry.semconv.HttpAttributes.HTTP_REQUEST_METHOD;
+import static io.opentelemetry.semconv.HttpAttributes.HTTP_RESPONSE_STATUS_CODE;
+import static io.opentelemetry.semconv.HttpAttributes.HTTP_ROUTE;
+import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PEER_ADDRESS;
+import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PEER_PORT;
+import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PROTOCOL_VERSION;
+import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS;
+import static io.opentelemetry.semconv.ServerAttributes.SERVER_PORT;
+import static io.opentelemetry.semconv.UrlAttributes.URL_PATH;
+import static io.opentelemetry.semconv.UrlAttributes.URL_SCHEME;
+import static io.opentelemetry.semconv.UserAgentAttributes.USER_AGENT_ORIGINAL;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.avaje.jex.Jex.Server;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
+import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
+import io.opentelemetry.testing.internal.armeria.client.WebClient;
+import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+class JexTest {
+
+ @RegisterExtension
+ private static final InstrumentationExtension testing = AgentInstrumentationExtension.create();
+
+ private static Server app;
+ private static int port;
+ private static WebClient client;
+
+ @BeforeAll
+ static void setup() {
+ app = TestJexJavaApplication.initJex();
+ port = app.port();
+ client = WebClient.of("http://localhost:" + port);
+ }
+
+ @AfterAll
+ static void cleanup() {
+ app.shutdown();
+ }
+
+ @Test
+ void testSpanNameAndHttpRouteSpanWithPathParamResponseSuccessful() {
+ String id = "123";
+ AggregatedHttpResponse response = client.get("/test/param/" + id).aggregate().join();
+ String content = response.contentUtf8();
+
+ assertThat(content).isEqualTo(id);
+ assertThat(response.status().code()).isEqualTo(200);
+ testing.waitAndAssertTraces(
+ trace ->
+ trace.hasSpansSatisfyingExactly(
+ span ->
+ span.hasName("GET /test/param/{id}")
+ .hasKind(SpanKind.SERVER)
+ .hasNoParent()
+ .hasAttributesSatisfyingExactly(
+ equalTo(URL_SCHEME, "http"),
+ equalTo(URL_PATH, "/test/param/" + id),
+ equalTo(HTTP_REQUEST_METHOD, "GET"),
+ equalTo(HTTP_RESPONSE_STATUS_CODE, 200),
+ satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)),
+ equalTo(HTTP_ROUTE, "/test/param/{id}"),
+ equalTo(NETWORK_PROTOCOL_VERSION, "1.1"),
+ equalTo(SERVER_ADDRESS, "localhost"),
+ equalTo(SERVER_PORT, port),
+ equalTo(CLIENT_ADDRESS, "127.0.0.1"),
+ equalTo(NETWORK_PEER_ADDRESS, "127.0.0.1"),
+ satisfies(NETWORK_PEER_PORT, val -> val.isInstanceOf(Long.class)))));
+ }
+
+ @Test
+ void testSpanNameAndHttpRouteSpanResponseError() {
+ client.get("/test/error").aggregate().join();
+
+ testing.waitAndAssertTraces(
+ trace ->
+ trace.hasSpansSatisfyingExactly(
+ span ->
+ span.hasName("GET /test/error")
+ .hasKind(SpanKind.SERVER)
+ .hasNoParent()
+ .hasAttributesSatisfyingExactly(
+ equalTo(URL_SCHEME, "http"),
+ equalTo(URL_PATH, "/test/error"),
+ equalTo(HTTP_REQUEST_METHOD, "GET"),
+ equalTo(HTTP_RESPONSE_STATUS_CODE, 500),
+ satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)),
+ equalTo(HTTP_ROUTE, "/test/error"),
+ equalTo(NETWORK_PROTOCOL_VERSION, "1.1"),
+ equalTo(SERVER_ADDRESS, "localhost"),
+ equalTo(SERVER_PORT, port),
+ equalTo(ERROR_TYPE, "500"),
+ equalTo(CLIENT_ADDRESS, "127.0.0.1"),
+ equalTo(NETWORK_PEER_ADDRESS, "127.0.0.1"),
+ satisfies(NETWORK_PEER_PORT, val -> val.isInstanceOf(Long.class)))));
+ }
+
+ @Test
+ void testHttpRouteMetricWithPathParamResponseSuccessful() {
+ String id = "123";
+ AggregatedHttpResponse response = client.get("/test/param/" + id).aggregate().join();
+ String content = response.contentUtf8();
+ String instrumentation = "io.opentelemetry.jetty-12.0";
+
+ assertThat(content).isEqualTo(id);
+ assertThat(response.status().code()).isEqualTo(200);
+ testing.waitAndAssertMetrics(
+ instrumentation,
+ "http.server.request.duration",
+ metrics ->
+ metrics.anySatisfy(
+ metric ->
+ assertThat(metric)
+ .hasHistogramSatisfying(
+ histogram ->
+ histogram.hasPointsSatisfying(
+ point -> point.hasAttribute(HTTP_ROUTE, "/test/param/{id}")))));
+ }
+}
diff --git a/instrumentation/avaje-jex-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/avaje/jex/v3_0/TestJexJavaApplication.java b/instrumentation/avaje-jex-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/avaje/jex/v3_0/TestJexJavaApplication.java
new file mode 100644
index 000000000000..0340af2bc609
--- /dev/null
+++ b/instrumentation/avaje-jex-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/avaje/jex/v3_0/TestJexJavaApplication.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.avaje.jex.v3_0;
+
+import io.avaje.jex.Jex;
+import io.avaje.jex.Jex.Server;
+
+public class TestJexJavaApplication {
+
+ private TestJexJavaApplication() {}
+
+ public static Server initJex() {
+ Jex app = Jex.create().contextPath("/test");
+ app.get(
+ "/param/{id}",
+ ctx -> {
+ String paramId = ctx.pathParam("id");
+ ctx.write(paramId);
+ });
+ app.get(
+ "/error",
+ ctx -> {
+ throw new RuntimeException("boom");
+ });
+ return app.port(0).start();
+ }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 37b2859bcc6a..14229a67488a 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -156,6 +156,7 @@ include(":instrumentation:armeria:armeria-1.3:testing")
include(":instrumentation:armeria:armeria-grpc-1.14:javaagent")
include(":instrumentation:async-http-client:async-http-client-1.9:javaagent")
include(":instrumentation:async-http-client:async-http-client-2.0:javaagent")
+include(":instrumentation:avaje-jex-3.0:javaagent")
include(":instrumentation:aws-lambda:aws-lambda-core-1.0:javaagent")
include(":instrumentation:aws-lambda:aws-lambda-core-1.0:library")
include(":instrumentation:aws-lambda:aws-lambda-core-1.0:testing")