Skip to content

Commit

Permalink
add javalin instrumentation (#11587)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasamoroso committed Jul 1, 2024
1 parent 51cb6f0 commit 8537d21
Show file tree
Hide file tree
Showing 7 changed files with 306 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/supported-libraries.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ These are the supported libraries and frameworks:
| [Java Executors](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executor.html) | Java 8+ | N/A | Context propagation |
| [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] |
| [java.util.logging](https://docs.oracle.com/javase/8/docs/api/java/util/logging/package-summary.html) | Java 8+ | N/A | none |
| [Javalin](https://javalin.io/) | 5.0.0+ | N/A | Provides `http.route` [2] |
| [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] |
| [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] |
| [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] |
Expand Down
22 changes: 22 additions & 0 deletions instrumentation/javalin-5.0/javaagent/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
plugins {
id("otel.javaagent-instrumentation")
}

muzzle {
pass {
group.set("io.javalin")
module.set("javalin")
versions.set("[5.0.0,)")
assertInverse.set(true)
}
}

otelJava {
minJavaVersionSupported.set(JavaVersion.VERSION_11)
}

dependencies {
library("io.javalin:javalin:5.0.0")

testInstrumentation(project(":instrumentation:jetty:jetty-11.0:javaagent"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.javalin.v5_0;

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.javalin.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 JavalinInstrumentation implements TypeInstrumentation {

@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return hasSuperType(named("io.javalin.http.Handler")).and(not(isInterface()));
}

@Override
public void transform(TypeTransformer transformer) {
transformer.applyAdviceToMethod(
named("handle").and(takesArgument(0, named("io.javalin.http.Context"))),
this.getClass().getName() + "$HandlerAdapterAdvice");
}

@SuppressWarnings("unused")
public static class HandlerAdapterAdvice {

@Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
public static void onAfterExecute(@Advice.Argument(0) Context ctx, @Advice.Thrown Throwable t) {
HttpServerRoute.update(
io.opentelemetry.context.Context.current(),
HttpServerRouteSource.CONTROLLER,
ctx.endpointHandlerPath());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.javalin.v5_0;

import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
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;
import net.bytebuddy.matcher.ElementMatcher;

@SuppressWarnings("unused")
@AutoService(InstrumentationModule.class)
public class JavalinInstrumentationModule extends InstrumentationModule {

public JavalinInstrumentationModule() {
super("javalin", "javalin-5");
}

@Override
public List<TypeInstrumentation> typeInstrumentations() {
return singletonList(new JavalinInstrumentation());
}

@Override
public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
return hasClassesNamed("io.javalin.http.Handler");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.javalin.v5_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.HttpAttributes.HTTP_ROUTE;

import io.javalin.Javalin;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.instrumentation.test.utils.PortUtils;
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
import io.opentelemetry.semconv.ClientAttributes;
import io.opentelemetry.semconv.ErrorAttributes;
import io.opentelemetry.semconv.HttpAttributes;
import io.opentelemetry.semconv.NetworkAttributes;
import io.opentelemetry.semconv.ServerAttributes;
import io.opentelemetry.semconv.UrlAttributes;
import io.opentelemetry.semconv.UserAgentAttributes;
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 JavalinTest {

@RegisterExtension
private static final InstrumentationExtension testing = AgentInstrumentationExtension.create();

private static Javalin app;
private static int port;
private static WebClient client;

@BeforeAll
static void setup() {
port = PortUtils.findOpenPort();
app = TestJavalinJavaApplication.initJavalin(port);
client = WebClient.of("http://localhost:" + port);
}

@AfterAll
static void cleanup() {
app.stop();
}

@Test
void testSpanNameAndHttpRouteSpanWithPathParamResponseSuccessful() {
String id = "123";
AggregatedHttpResponse response = client.get("/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 /param/{id}")
.hasKind(SpanKind.SERVER)
.hasNoParent()
.hasAttributesSatisfyingExactly(
equalTo(UrlAttributes.URL_SCHEME, "http"),
equalTo(UrlAttributes.URL_PATH, "/param/" + id),
equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"),
equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200),
satisfies(
UserAgentAttributes.USER_AGENT_ORIGINAL,
val -> val.isInstanceOf(String.class)),
equalTo(HTTP_ROUTE, "/param/{id}"),
equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"),
equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"),
equalTo(ServerAttributes.SERVER_PORT, port),
equalTo(ClientAttributes.CLIENT_ADDRESS, "127.0.0.1"),
equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"),
satisfies(
NetworkAttributes.NETWORK_PEER_PORT,
val -> val.isInstanceOf(Long.class)))));
}

@Test
void testSpanNameAndHttpRouteSpanResponseError() {
client.get("/error").aggregate().join();

testing.waitAndAssertTraces(
trace ->
trace.hasSpansSatisfyingExactly(
span ->
span.hasName("GET /error")
.hasKind(SpanKind.SERVER)
.hasNoParent()
.hasAttributesSatisfyingExactly(
equalTo(UrlAttributes.URL_SCHEME, "http"),
equalTo(UrlAttributes.URL_PATH, "/error"),
equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"),
equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 500),
satisfies(
UserAgentAttributes.USER_AGENT_ORIGINAL,
val -> val.isInstanceOf(String.class)),
equalTo(HTTP_ROUTE, "/error"),
equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"),
equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"),
equalTo(ServerAttributes.SERVER_PORT, port),
equalTo(ErrorAttributes.ERROR_TYPE, "500"),
equalTo(ClientAttributes.CLIENT_ADDRESS, "127.0.0.1"),
equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"),
satisfies(
NetworkAttributes.NETWORK_PEER_PORT,
val -> val.isInstanceOf(Long.class)))));
}

@Test
public void testSpanNameAndHttpRouteSpanAsyncRouteResponseSuccessful() {
AggregatedHttpResponse response = client.get("/async").aggregate().join();

assertThat(response.status().code()).isEqualTo(200);
testing.waitAndAssertTraces(
trace ->
trace.hasSpansSatisfyingExactly(
span ->
span.hasName("GET /async")
.hasKind(SpanKind.SERVER)
.hasNoParent()
.hasAttributesSatisfyingExactly(
equalTo(UrlAttributes.URL_SCHEME, "http"),
equalTo(UrlAttributes.URL_PATH, "/async"),
equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"),
equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200),
satisfies(
UserAgentAttributes.USER_AGENT_ORIGINAL,
val -> val.isInstanceOf(String.class)),
equalTo(HTTP_ROUTE, "/async"),
equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"),
equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"),
equalTo(ServerAttributes.SERVER_PORT, port),
equalTo(ClientAttributes.CLIENT_ADDRESS, "127.0.0.1"),
equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"),
satisfies(
NetworkAttributes.NETWORK_PEER_PORT,
val -> val.isInstanceOf(Long.class)))));
}

@Test
void testHttpRouteMetricWithPathParamResponseSuccessful() {
String id = "123";
AggregatedHttpResponse response = client.get("/param/" + id).aggregate().join();
String content = response.contentUtf8();
String instrumentation = "io.opentelemetry.jetty-11.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, "/param/{id}")))));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.javalin.v5_0;

import io.javalin.Javalin;

public class TestJavalinJavaApplication {

private TestJavalinJavaApplication() {}

public static Javalin initJavalin(int port) {
Javalin app = Javalin.create().start(port);
app.get(
"/param/{id}",
ctx -> {
String paramId = ctx.pathParam("id");
ctx.result(paramId);
});
app.get(
"/error",
ctx -> {
throw new RuntimeException("boom");
});
app.get("/async", ctx -> ctx.async(() -> ctx.result("ok")));
return app;
}
}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ include(":instrumentation:java-http-client:library")
include(":instrumentation:java-http-client:testing")
include(":instrumentation:java-util-logging:javaagent")
include(":instrumentation:java-util-logging:shaded-stub-for-instrumenting")
include(":instrumentation:javalin-5.0:javaagent")
include(":instrumentation:jaxrs:jaxrs-1.0:javaagent")
include(":instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-annotations:javaagent")
include(":instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-arquillian-testing")
Expand Down

0 comments on commit 8537d21

Please sign in to comment.