Skip to content

Commit 8537d21

Browse files
authored
add javalin instrumentation (#11587)
1 parent 51cb6f0 commit 8537d21

File tree

7 files changed

+306
-0
lines changed

7 files changed

+306
-0
lines changed

docs/supported-libraries.md

+1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ These are the supported libraries and frameworks:
7979
| [Java Executors](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executor.html) | Java 8+ | N/A | Context propagation |
8080
| [Java Http Client](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/package-summary.html) | Java 11+ | [opentelemetry-java-http-client](../instrumentation/java-http-client/library) | [HTTP Client Spans], [HTTP Client Metrics] |
8181
| [java.util.logging](https://docs.oracle.com/javase/8/docs/api/java/util/logging/package-summary.html) | Java 8+ | N/A | none |
82+
| [Javalin](https://javalin.io/) | 5.0.0+ | N/A | Provides `http.route` [2] |
8283
| [Java Platform](https://docs.oracle.com/javase/8/docs/api/java/lang/management/ManagementFactory.html) | Java 8+ | [opentelemetry-runtime-telemetry-java8](../instrumentation/runtime-telemetry/runtime-telemetry-java8/library),<br>[opentelemetry-runtime-telemetry-java17](../instrumentation/runtime-telemetry/runtime-telemetry-java17/library),<br>[opentelemetry-resources](../instrumentation/resources/library) | [JVM Runtime Metrics] |
8384
| [JAX-RS](https://javaee.github.io/javaee-spec/javadocs/javax/ws/rs/package-summary.html) | 0.5+ | N/A | Provides `http.route` [2], Controller Spans [3] |
8485
| [JAX-RS Client](https://javaee.github.io/javaee-spec/javadocs/javax/ws/rs/client/package-summary.html) | 1.1+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] |
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.javalin")
8+
module.set("javalin")
9+
versions.set("[5.0.0,)")
10+
assertInverse.set(true)
11+
}
12+
}
13+
14+
otelJava {
15+
minJavaVersionSupported.set(JavaVersion.VERSION_11)
16+
}
17+
18+
dependencies {
19+
library("io.javalin:javalin:5.0.0")
20+
21+
testInstrumentation(project(":instrumentation:jetty:jetty-11.0:javaagent"))
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.javalin.v5_0;
7+
8+
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasSuperType;
9+
import static net.bytebuddy.matcher.ElementMatchers.isInterface;
10+
import static net.bytebuddy.matcher.ElementMatchers.named;
11+
import static net.bytebuddy.matcher.ElementMatchers.not;
12+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
13+
14+
import io.javalin.http.Context;
15+
import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute;
16+
import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource;
17+
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
18+
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
19+
import net.bytebuddy.asm.Advice;
20+
import net.bytebuddy.description.type.TypeDescription;
21+
import net.bytebuddy.matcher.ElementMatcher;
22+
23+
public class JavalinInstrumentation implements TypeInstrumentation {
24+
25+
@Override
26+
public ElementMatcher<TypeDescription> typeMatcher() {
27+
return hasSuperType(named("io.javalin.http.Handler")).and(not(isInterface()));
28+
}
29+
30+
@Override
31+
public void transform(TypeTransformer transformer) {
32+
transformer.applyAdviceToMethod(
33+
named("handle").and(takesArgument(0, named("io.javalin.http.Context"))),
34+
this.getClass().getName() + "$HandlerAdapterAdvice");
35+
}
36+
37+
@SuppressWarnings("unused")
38+
public static class HandlerAdapterAdvice {
39+
40+
@Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
41+
public static void onAfterExecute(@Advice.Argument(0) Context ctx, @Advice.Thrown Throwable t) {
42+
HttpServerRoute.update(
43+
io.opentelemetry.context.Context.current(),
44+
HttpServerRouteSource.CONTROLLER,
45+
ctx.endpointHandlerPath());
46+
}
47+
}
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.javalin.v5_0;
7+
8+
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
9+
import static java.util.Collections.singletonList;
10+
11+
import com.google.auto.service.AutoService;
12+
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
13+
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
14+
import java.util.List;
15+
import net.bytebuddy.matcher.ElementMatcher;
16+
17+
@SuppressWarnings("unused")
18+
@AutoService(InstrumentationModule.class)
19+
public class JavalinInstrumentationModule extends InstrumentationModule {
20+
21+
public JavalinInstrumentationModule() {
22+
super("javalin", "javalin-5");
23+
}
24+
25+
@Override
26+
public List<TypeInstrumentation> typeInstrumentations() {
27+
return singletonList(new JavalinInstrumentation());
28+
}
29+
30+
@Override
31+
public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
32+
return hasClassesNamed("io.javalin.http.Handler");
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.javalin.v5_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.HttpAttributes.HTTP_ROUTE;
12+
13+
import io.javalin.Javalin;
14+
import io.opentelemetry.api.trace.SpanKind;
15+
import io.opentelemetry.instrumentation.test.utils.PortUtils;
16+
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
17+
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
18+
import io.opentelemetry.semconv.ClientAttributes;
19+
import io.opentelemetry.semconv.ErrorAttributes;
20+
import io.opentelemetry.semconv.HttpAttributes;
21+
import io.opentelemetry.semconv.NetworkAttributes;
22+
import io.opentelemetry.semconv.ServerAttributes;
23+
import io.opentelemetry.semconv.UrlAttributes;
24+
import io.opentelemetry.semconv.UserAgentAttributes;
25+
import io.opentelemetry.testing.internal.armeria.client.WebClient;
26+
import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse;
27+
import org.junit.jupiter.api.AfterAll;
28+
import org.junit.jupiter.api.BeforeAll;
29+
import org.junit.jupiter.api.Test;
30+
import org.junit.jupiter.api.extension.RegisterExtension;
31+
32+
class JavalinTest {
33+
34+
@RegisterExtension
35+
private static final InstrumentationExtension testing = AgentInstrumentationExtension.create();
36+
37+
private static Javalin app;
38+
private static int port;
39+
private static WebClient client;
40+
41+
@BeforeAll
42+
static void setup() {
43+
port = PortUtils.findOpenPort();
44+
app = TestJavalinJavaApplication.initJavalin(port);
45+
client = WebClient.of("http://localhost:" + port);
46+
}
47+
48+
@AfterAll
49+
static void cleanup() {
50+
app.stop();
51+
}
52+
53+
@Test
54+
void testSpanNameAndHttpRouteSpanWithPathParamResponseSuccessful() {
55+
String id = "123";
56+
AggregatedHttpResponse response = client.get("/param/" + id).aggregate().join();
57+
String content = response.contentUtf8();
58+
59+
assertThat(content).isEqualTo(id);
60+
assertThat(response.status().code()).isEqualTo(200);
61+
testing.waitAndAssertTraces(
62+
trace ->
63+
trace.hasSpansSatisfyingExactly(
64+
span ->
65+
span.hasName("GET /param/{id}")
66+
.hasKind(SpanKind.SERVER)
67+
.hasNoParent()
68+
.hasAttributesSatisfyingExactly(
69+
equalTo(UrlAttributes.URL_SCHEME, "http"),
70+
equalTo(UrlAttributes.URL_PATH, "/param/" + id),
71+
equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"),
72+
equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200),
73+
satisfies(
74+
UserAgentAttributes.USER_AGENT_ORIGINAL,
75+
val -> val.isInstanceOf(String.class)),
76+
equalTo(HTTP_ROUTE, "/param/{id}"),
77+
equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"),
78+
equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"),
79+
equalTo(ServerAttributes.SERVER_PORT, port),
80+
equalTo(ClientAttributes.CLIENT_ADDRESS, "127.0.0.1"),
81+
equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"),
82+
satisfies(
83+
NetworkAttributes.NETWORK_PEER_PORT,
84+
val -> val.isInstanceOf(Long.class)))));
85+
}
86+
87+
@Test
88+
void testSpanNameAndHttpRouteSpanResponseError() {
89+
client.get("/error").aggregate().join();
90+
91+
testing.waitAndAssertTraces(
92+
trace ->
93+
trace.hasSpansSatisfyingExactly(
94+
span ->
95+
span.hasName("GET /error")
96+
.hasKind(SpanKind.SERVER)
97+
.hasNoParent()
98+
.hasAttributesSatisfyingExactly(
99+
equalTo(UrlAttributes.URL_SCHEME, "http"),
100+
equalTo(UrlAttributes.URL_PATH, "/error"),
101+
equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"),
102+
equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 500),
103+
satisfies(
104+
UserAgentAttributes.USER_AGENT_ORIGINAL,
105+
val -> val.isInstanceOf(String.class)),
106+
equalTo(HTTP_ROUTE, "/error"),
107+
equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"),
108+
equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"),
109+
equalTo(ServerAttributes.SERVER_PORT, port),
110+
equalTo(ErrorAttributes.ERROR_TYPE, "500"),
111+
equalTo(ClientAttributes.CLIENT_ADDRESS, "127.0.0.1"),
112+
equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"),
113+
satisfies(
114+
NetworkAttributes.NETWORK_PEER_PORT,
115+
val -> val.isInstanceOf(Long.class)))));
116+
}
117+
118+
@Test
119+
public void testSpanNameAndHttpRouteSpanAsyncRouteResponseSuccessful() {
120+
AggregatedHttpResponse response = client.get("/async").aggregate().join();
121+
122+
assertThat(response.status().code()).isEqualTo(200);
123+
testing.waitAndAssertTraces(
124+
trace ->
125+
trace.hasSpansSatisfyingExactly(
126+
span ->
127+
span.hasName("GET /async")
128+
.hasKind(SpanKind.SERVER)
129+
.hasNoParent()
130+
.hasAttributesSatisfyingExactly(
131+
equalTo(UrlAttributes.URL_SCHEME, "http"),
132+
equalTo(UrlAttributes.URL_PATH, "/async"),
133+
equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"),
134+
equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200),
135+
satisfies(
136+
UserAgentAttributes.USER_AGENT_ORIGINAL,
137+
val -> val.isInstanceOf(String.class)),
138+
equalTo(HTTP_ROUTE, "/async"),
139+
equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"),
140+
equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"),
141+
equalTo(ServerAttributes.SERVER_PORT, port),
142+
equalTo(ClientAttributes.CLIENT_ADDRESS, "127.0.0.1"),
143+
equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"),
144+
satisfies(
145+
NetworkAttributes.NETWORK_PEER_PORT,
146+
val -> val.isInstanceOf(Long.class)))));
147+
}
148+
149+
@Test
150+
void testHttpRouteMetricWithPathParamResponseSuccessful() {
151+
String id = "123";
152+
AggregatedHttpResponse response = client.get("/param/" + id).aggregate().join();
153+
String content = response.contentUtf8();
154+
String instrumentation = "io.opentelemetry.jetty-11.0";
155+
156+
assertThat(content).isEqualTo(id);
157+
assertThat(response.status().code()).isEqualTo(200);
158+
testing.waitAndAssertMetrics(
159+
instrumentation,
160+
"http.server.request.duration",
161+
metrics ->
162+
metrics.anySatisfy(
163+
metric ->
164+
assertThat(metric)
165+
.hasHistogramSatisfying(
166+
histogram ->
167+
histogram.hasPointsSatisfying(
168+
point -> point.hasAttribute(HTTP_ROUTE, "/param/{id}")))));
169+
}
170+
}
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.javalin.v5_0;
7+
8+
import io.javalin.Javalin;
9+
10+
public class TestJavalinJavaApplication {
11+
12+
private TestJavalinJavaApplication() {}
13+
14+
public static Javalin initJavalin(int port) {
15+
Javalin app = Javalin.create().start(port);
16+
app.get(
17+
"/param/{id}",
18+
ctx -> {
19+
String paramId = ctx.pathParam("id");
20+
ctx.result(paramId);
21+
});
22+
app.get(
23+
"/error",
24+
ctx -> {
25+
throw new RuntimeException("boom");
26+
});
27+
app.get("/async", ctx -> ctx.async(() -> ctx.result("ok")));
28+
return app;
29+
}
30+
}

settings.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,7 @@ include(":instrumentation:java-http-client:library")
302302
include(":instrumentation:java-http-client:testing")
303303
include(":instrumentation:java-util-logging:javaagent")
304304
include(":instrumentation:java-util-logging:shaded-stub-for-instrumenting")
305+
include(":instrumentation:javalin-5.0:javaagent")
305306
include(":instrumentation:jaxrs:jaxrs-1.0:javaagent")
306307
include(":instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-annotations:javaagent")
307308
include(":instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-arquillian-testing")

0 commit comments

Comments
 (0)