Skip to content

Commit 9c00e71

Browse files
SentryManlaurit
andauthored
Add Avaje Jex Instrumentation (#13733)
Co-authored-by: Lauri Tulmin <[email protected]>
1 parent e79ec1c commit 9c00e71

File tree

8 files changed

+274
-0
lines changed

8 files changed

+274
-0
lines changed

.fossa.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ targets:
7676
- type: gradle
7777
path: ./
7878
target: ':instrumentation:apache-shenyu-2.4:javaagent'
79+
- type: gradle
80+
path: ./
81+
target: ':instrumentation:avaje-jex-3.0:javaagent'
7982
- type: gradle
8083
path: ./
8184
target: ':instrumentation:c3p0-0.9:javaagent'

docs/supported-libraries.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ These are the supported libraries and frameworks:
4646
| [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] |
4747
| [Armeria gRPC](https://armeria.dev) | 1.14+ | | [RPC Client Spans], [RPC Client Metrics], [RPC Server Spans], [RPC Server Metrics] |
4848
| [AsyncHttpClient](https://github.com/AsyncHttpClient/async-http-client) | 1.9+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] |
49+
| [Avaje Jex](https://avaje.io/jex/) | 3.0+ | N/A | Provides `http.route` [2] |
4950
| [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),<br>[opentelemetry-aws-lambda-events-2.2](../instrumentation/aws-lambda/aws-lambda-events-2.2/library) | [FaaS Server Spans] |
5051
| [AWS SDK](https://aws.amazon.com/sdk-for-java/) | 1.11 - 1.12.583,<br>2.2+ | [opentelemetry-aws-sdk-1.11](../instrumentation/aws-sdk/aws-sdk-1.11/library),<br>[opentelemetry-aws-sdk-1.11-autoconfigure](../instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure),<br>[opentelemetry-aws-sdk-2.2](../instrumentation/aws-sdk/aws-sdk-2.2/library),<br>[opentelemetry-aws-sdk-2.2-autoconfigure](../instrumentation/aws-sdk/aws-sdk-2.2/library-autoconfigure) | [Messaging Spans], [Database Client Spans], [Database Client Metrics]&nbsp;[6], [HTTP Client Spans] |
5152
| [Azure Core](https://docs.microsoft.com/en-us/java/api/overview/azure/core-readme) | 1.14+ | N/A | Context propagation |
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
plugins {
2+
id("otel.javaagent-instrumentation")
3+
}
4+
5+
muzzle {
6+
pass {
7+
group.set("io.avaje")
8+
module.set("avaje-jex")
9+
versions.set("[3.0,)")
10+
assertInverse.set(true)
11+
}
12+
}
13+
14+
otelJava {
15+
minJavaVersionSupported.set(JavaVersion.VERSION_21)
16+
}
17+
18+
dependencies {
19+
library("io.avaje:avaje-jex:3.0")
20+
testLibrary("org.eclipse.jetty:jetty-http-spi:12.0.19")
21+
testInstrumentation(project(":instrumentation:jetty:jetty-12.0:javaagent"))
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.avaje.jex.v3_0;
7+
8+
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
9+
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasSuperType;
10+
import static net.bytebuddy.matcher.ElementMatchers.isInterface;
11+
import static net.bytebuddy.matcher.ElementMatchers.named;
12+
import static net.bytebuddy.matcher.ElementMatchers.not;
13+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
14+
15+
import io.avaje.jex.http.Context;
16+
import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute;
17+
import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource;
18+
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
19+
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
20+
import net.bytebuddy.asm.Advice;
21+
import net.bytebuddy.description.type.TypeDescription;
22+
import net.bytebuddy.matcher.ElementMatcher;
23+
24+
public class JexInstrumentation implements TypeInstrumentation {
25+
26+
@Override
27+
public ElementMatcher<ClassLoader> classLoaderOptimization() {
28+
return hasClassesNamed("io.avaje.jex.http.ExchangeHandler");
29+
}
30+
31+
@Override
32+
public ElementMatcher<TypeDescription> typeMatcher() {
33+
return hasSuperType(named("io.avaje.jex.http.ExchangeHandler")).and(not(isInterface()));
34+
}
35+
36+
@Override
37+
public void transform(TypeTransformer transformer) {
38+
transformer.applyAdviceToMethod(
39+
named("handle").and(takesArgument(0, named("io.avaje.jex.http.Context"))),
40+
this.getClass().getName() + "$HandlerAdapterAdvice");
41+
}
42+
43+
@SuppressWarnings("unused")
44+
public static class HandlerAdapterAdvice {
45+
46+
@Advice.OnMethodEnter(suppress = Throwable.class)
47+
public static void onMethodExecute(@Advice.Argument(0) Context ctx) {
48+
HttpServerRoute.update(
49+
io.opentelemetry.context.Context.current(),
50+
HttpServerRouteSource.CONTROLLER,
51+
ctx.matchedPath());
52+
}
53+
}
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.avaje.jex.v3_0;
7+
8+
import static java.util.Collections.singletonList;
9+
10+
import com.google.auto.service.AutoService;
11+
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
12+
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
13+
import java.util.List;
14+
15+
@SuppressWarnings("unused")
16+
@AutoService(InstrumentationModule.class)
17+
public class JexInstrumentationModule extends InstrumentationModule {
18+
19+
public JexInstrumentationModule() {
20+
super("avaje-jex", "avaje-jex-3.0");
21+
}
22+
23+
@Override
24+
public List<TypeInstrumentation> typeInstrumentations() {
25+
return singletonList(new JexInstrumentation());
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.avaje.jex.v3_0;
7+
8+
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
9+
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
10+
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies;
11+
import static io.opentelemetry.semconv.ClientAttributes.CLIENT_ADDRESS;
12+
import static io.opentelemetry.semconv.ErrorAttributes.ERROR_TYPE;
13+
import static io.opentelemetry.semconv.HttpAttributes.HTTP_REQUEST_METHOD;
14+
import static io.opentelemetry.semconv.HttpAttributes.HTTP_RESPONSE_STATUS_CODE;
15+
import static io.opentelemetry.semconv.HttpAttributes.HTTP_ROUTE;
16+
import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PEER_ADDRESS;
17+
import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PEER_PORT;
18+
import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PROTOCOL_VERSION;
19+
import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS;
20+
import static io.opentelemetry.semconv.ServerAttributes.SERVER_PORT;
21+
import static io.opentelemetry.semconv.UrlAttributes.URL_PATH;
22+
import static io.opentelemetry.semconv.UrlAttributes.URL_SCHEME;
23+
import static io.opentelemetry.semconv.UserAgentAttributes.USER_AGENT_ORIGINAL;
24+
import static org.assertj.core.api.Assertions.assertThat;
25+
26+
import io.avaje.jex.Jex.Server;
27+
import io.opentelemetry.api.trace.SpanKind;
28+
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
29+
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
30+
import io.opentelemetry.testing.internal.armeria.client.WebClient;
31+
import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse;
32+
import org.junit.jupiter.api.AfterAll;
33+
import org.junit.jupiter.api.BeforeAll;
34+
import org.junit.jupiter.api.Test;
35+
import org.junit.jupiter.api.extension.RegisterExtension;
36+
37+
class JexTest {
38+
39+
@RegisterExtension
40+
private static final InstrumentationExtension testing = AgentInstrumentationExtension.create();
41+
42+
private static Server app;
43+
private static int port;
44+
private static WebClient client;
45+
46+
@BeforeAll
47+
static void setup() {
48+
app = TestJexJavaApplication.initJex();
49+
port = app.port();
50+
client = WebClient.of("http://localhost:" + port);
51+
}
52+
53+
@AfterAll
54+
static void cleanup() {
55+
app.shutdown();
56+
}
57+
58+
@Test
59+
void testSpanNameAndHttpRouteSpanWithPathParamResponseSuccessful() {
60+
String id = "123";
61+
AggregatedHttpResponse response = client.get("/test/param/" + id).aggregate().join();
62+
String content = response.contentUtf8();
63+
64+
assertThat(content).isEqualTo(id);
65+
assertThat(response.status().code()).isEqualTo(200);
66+
testing.waitAndAssertTraces(
67+
trace ->
68+
trace.hasSpansSatisfyingExactly(
69+
span ->
70+
span.hasName("GET /test/param/{id}")
71+
.hasKind(SpanKind.SERVER)
72+
.hasNoParent()
73+
.hasAttributesSatisfyingExactly(
74+
equalTo(URL_SCHEME, "http"),
75+
equalTo(URL_PATH, "/test/param/" + id),
76+
equalTo(HTTP_REQUEST_METHOD, "GET"),
77+
equalTo(HTTP_RESPONSE_STATUS_CODE, 200),
78+
satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)),
79+
equalTo(HTTP_ROUTE, "/test/param/{id}"),
80+
equalTo(NETWORK_PROTOCOL_VERSION, "1.1"),
81+
equalTo(SERVER_ADDRESS, "localhost"),
82+
equalTo(SERVER_PORT, port),
83+
equalTo(CLIENT_ADDRESS, "127.0.0.1"),
84+
equalTo(NETWORK_PEER_ADDRESS, "127.0.0.1"),
85+
satisfies(NETWORK_PEER_PORT, val -> val.isInstanceOf(Long.class)))));
86+
}
87+
88+
@Test
89+
void testSpanNameAndHttpRouteSpanResponseError() {
90+
client.get("/test/error").aggregate().join();
91+
92+
testing.waitAndAssertTraces(
93+
trace ->
94+
trace.hasSpansSatisfyingExactly(
95+
span ->
96+
span.hasName("GET /test/error")
97+
.hasKind(SpanKind.SERVER)
98+
.hasNoParent()
99+
.hasAttributesSatisfyingExactly(
100+
equalTo(URL_SCHEME, "http"),
101+
equalTo(URL_PATH, "/test/error"),
102+
equalTo(HTTP_REQUEST_METHOD, "GET"),
103+
equalTo(HTTP_RESPONSE_STATUS_CODE, 500),
104+
satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)),
105+
equalTo(HTTP_ROUTE, "/test/error"),
106+
equalTo(NETWORK_PROTOCOL_VERSION, "1.1"),
107+
equalTo(SERVER_ADDRESS, "localhost"),
108+
equalTo(SERVER_PORT, port),
109+
equalTo(ERROR_TYPE, "500"),
110+
equalTo(CLIENT_ADDRESS, "127.0.0.1"),
111+
equalTo(NETWORK_PEER_ADDRESS, "127.0.0.1"),
112+
satisfies(NETWORK_PEER_PORT, val -> val.isInstanceOf(Long.class)))));
113+
}
114+
115+
@Test
116+
void testHttpRouteMetricWithPathParamResponseSuccessful() {
117+
String id = "123";
118+
AggregatedHttpResponse response = client.get("/test/param/" + id).aggregate().join();
119+
String content = response.contentUtf8();
120+
String instrumentation = "io.opentelemetry.jetty-12.0";
121+
122+
assertThat(content).isEqualTo(id);
123+
assertThat(response.status().code()).isEqualTo(200);
124+
testing.waitAndAssertMetrics(
125+
instrumentation,
126+
"http.server.request.duration",
127+
metrics ->
128+
metrics.anySatisfy(
129+
metric ->
130+
assertThat(metric)
131+
.hasHistogramSatisfying(
132+
histogram ->
133+
histogram.hasPointsSatisfying(
134+
point -> point.hasAttribute(HTTP_ROUTE, "/test/param/{id}")))));
135+
}
136+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.avaje.jex.v3_0;
7+
8+
import io.avaje.jex.Jex;
9+
import io.avaje.jex.Jex.Server;
10+
11+
public class TestJexJavaApplication {
12+
13+
private TestJexJavaApplication() {}
14+
15+
public static Server initJex() {
16+
Jex app = Jex.create().contextPath("/test");
17+
app.get(
18+
"/param/{id}",
19+
ctx -> {
20+
String paramId = ctx.pathParam("id");
21+
ctx.write(paramId);
22+
});
23+
app.get(
24+
"/error",
25+
ctx -> {
26+
throw new RuntimeException("boom");
27+
});
28+
return app.port(0).start();
29+
}
30+
}

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ include(":instrumentation:armeria:armeria-1.3:testing")
156156
include(":instrumentation:armeria:armeria-grpc-1.14:javaagent")
157157
include(":instrumentation:async-http-client:async-http-client-1.9:javaagent")
158158
include(":instrumentation:async-http-client:async-http-client-2.0:javaagent")
159+
include(":instrumentation:avaje-jex-3.0:javaagent")
159160
include(":instrumentation:aws-lambda:aws-lambda-core-1.0:javaagent")
160161
include(":instrumentation:aws-lambda:aws-lambda-core-1.0:library")
161162
include(":instrumentation:aws-lambda:aws-lambda-core-1.0:testing")

0 commit comments

Comments
 (0)