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")