Skip to content

Commit 42d7177

Browse files
authored
Improve akka route handling with java dsl (#11926)
1 parent d15927a commit 42d7177

File tree

10 files changed

+432
-13
lines changed

10 files changed

+432
-13
lines changed

instrumentation/akka/akka-http-10.0/javaagent/build.gradle.kts

+20
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,22 @@ dependencies {
4242
latestDepTestLibrary("com.typesafe.akka:akka-stream_2.13:+")
4343
}
4444

45+
testing {
46+
suites {
47+
val javaRouteTest by registering(JvmTestSuite::class) {
48+
dependencies {
49+
if (findProperty("testLatestDeps") as Boolean) {
50+
implementation("com.typesafe.akka:akka-http_2.13:+")
51+
implementation("com.typesafe.akka:akka-stream_2.13:+")
52+
} else {
53+
implementation("com.typesafe.akka:akka-http_2.12:10.2.0")
54+
implementation("com.typesafe.akka:akka-stream_2.12:2.6.21")
55+
}
56+
}
57+
}
58+
}
59+
}
60+
4561
tasks {
4662
withType<Test>().configureEach {
4763
// required on jdk17
@@ -52,6 +68,10 @@ tasks {
5268

5369
systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean)
5470
}
71+
72+
check {
73+
dependsOn(testing.suites)
74+
}
5575
}
5676

5777
if (findProperty("testLatestDeps") as Boolean) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.akkahttp;
7+
8+
import static akka.http.javadsl.server.PathMatchers.integerSegment;
9+
import static org.assertj.core.api.Assertions.assertThat;
10+
11+
import akka.actor.ActorSystem;
12+
import akka.http.javadsl.Http;
13+
import akka.http.javadsl.ServerBinding;
14+
import akka.http.javadsl.server.AllDirectives;
15+
import akka.http.javadsl.server.Route;
16+
import io.opentelemetry.instrumentation.test.utils.PortUtils;
17+
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
18+
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
19+
import io.opentelemetry.testing.internal.armeria.client.WebClient;
20+
import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpRequest;
21+
import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse;
22+
import io.opentelemetry.testing.internal.armeria.common.HttpMethod;
23+
import java.util.concurrent.CompletionStage;
24+
import org.junit.jupiter.api.Test;
25+
import org.junit.jupiter.api.extension.RegisterExtension;
26+
27+
class AkkaHttpServerJavaRouteTest extends AllDirectives {
28+
@RegisterExtension
29+
private static final InstrumentationExtension testing = AgentInstrumentationExtension.create();
30+
31+
private final WebClient client = WebClient.of();
32+
33+
@Test
34+
void testRoute() {
35+
ActorSystem system = ActorSystem.create("my-system");
36+
int port = PortUtils.findOpenPort();
37+
Http http = Http.get(system);
38+
39+
Route route =
40+
concat(
41+
pathEndOrSingleSlash(() -> complete("root")),
42+
pathPrefix(
43+
"test",
44+
() ->
45+
concat(
46+
pathSingleSlash(() -> complete("test")),
47+
path(integerSegment(), (i) -> complete("ok")))));
48+
49+
CompletionStage<ServerBinding> binding = http.newServerAt("localhost", port).bind(route);
50+
try {
51+
AggregatedHttpRequest request =
52+
AggregatedHttpRequest.of(HttpMethod.GET, "h1c://localhost:" + port + "/test/1");
53+
AggregatedHttpResponse response = client.execute(request).aggregate().join();
54+
55+
assertThat(response.status().code()).isEqualTo(200);
56+
assertThat(response.contentUtf8()).isEqualTo("ok");
57+
58+
testing.waitAndAssertTraces(
59+
trace -> trace.hasSpansSatisfyingExactly(span -> span.hasName("GET /test/*")));
60+
} finally {
61+
binding.thenCompose(ServerBinding::unbind).thenAccept(unbound -> system.terminate());
62+
}
63+
}
64+
}

instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaRouteHolder.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public class AkkaRouteHolder implements ImplicitContextKeyed {
1919
private static final ContextKey<AkkaRouteHolder> KEY = named("opentelemetry-akka-route");
2020

2121
private String route = "";
22-
private boolean newSegment;
22+
private boolean newSegment = true;
2323
private boolean endMatched;
2424
private final Deque<String> stack = new ArrayDeque<>();
2525

@@ -70,6 +70,15 @@ public static void restore() {
7070
}
7171
}
7272

73+
// reset the state to when save was called
74+
public static void reset() {
75+
AkkaRouteHolder holder = Context.current().get(KEY);
76+
if (holder != null) {
77+
holder.route = holder.stack.peek();
78+
holder.newSegment = true;
79+
}
80+
}
81+
7382
@Override
7483
public Context storeInContext(Context context) {
7584
return context.with(KEY, this);

instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/RouteConcatenationInstrumentation.java

+22-1
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,26 @@ public class RouteConcatenationInstrumentation implements TypeInstrumentation {
1717
@Override
1818
public ElementMatcher<TypeDescription> typeMatcher() {
1919
return namedOneOf(
20+
// scala 2.11
2021
"akka.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation$$anonfun$$tilde$1",
22+
// scala 2.12 and later
2123
"akka.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation");
2224
}
2325

2426
@Override
2527
public void transform(TypeTransformer transformer) {
2628
transformer.applyAdviceToMethod(
27-
namedOneOf("apply", "$anonfun$$tilde$1"), this.getClass().getName() + "$ApplyAdvice");
29+
namedOneOf(
30+
// scala 2.11
31+
"apply",
32+
// scala 2.12 and later
33+
"$anonfun$$tilde$1"),
34+
this.getClass().getName() + "$ApplyAdvice");
35+
36+
// This advice seems to be only needed when defining routes with java dsl. Since java dsl tests
37+
// use scala 2.12 we are going to skip instrumenting this for scala 2.11.
38+
transformer.applyAdviceToMethod(
39+
namedOneOf("$anonfun$$tilde$2"), this.getClass().getName() + "$Apply2Advice");
2840
}
2941

3042
@SuppressWarnings("unused")
@@ -43,4 +55,13 @@ public static void onExit() {
4355
AkkaRouteHolder.restore();
4456
}
4557
}
58+
59+
@SuppressWarnings("unused")
60+
public static class Apply2Advice {
61+
62+
@Advice.OnMethodEnter(suppress = Throwable.class)
63+
public static void onEnter() {
64+
AkkaRouteHolder.reset();
65+
}
66+
}
4667
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.akkahttp
7+
8+
import akka.actor.ActorSystem
9+
import akka.http.scaladsl.Http
10+
import akka.http.scaladsl.server.Directives.{
11+
IntNumber,
12+
complete,
13+
concat,
14+
path,
15+
pathEndOrSingleSlash,
16+
pathPrefix,
17+
pathSingleSlash
18+
}
19+
import akka.http.scaladsl.server.Route
20+
import akka.stream.ActorMaterializer
21+
import io.opentelemetry.instrumentation.test.utils.PortUtils
22+
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension
23+
import io.opentelemetry.sdk.testing.assertj.{SpanDataAssert, TraceAssert}
24+
import io.opentelemetry.testing.internal.armeria.client.WebClient
25+
import io.opentelemetry.testing.internal.armeria.common.{
26+
AggregatedHttpRequest,
27+
HttpMethod
28+
}
29+
import org.assertj.core.api.Assertions.assertThat
30+
import org.junit.jupiter.api.{AfterAll, Test, TestInstance}
31+
import org.junit.jupiter.api.extension.RegisterExtension
32+
33+
import java.net.{URI, URISyntaxException}
34+
import java.util.function.Consumer
35+
import scala.concurrent.duration.DurationInt
36+
import scala.concurrent.Await
37+
38+
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
39+
class AkkaHttpServerRouteTest {
40+
@RegisterExtension private val testing: AgentInstrumentationExtension =
41+
AgentInstrumentationExtension.create
42+
private val client: WebClient = WebClient.of()
43+
44+
implicit val system: ActorSystem = ActorSystem("my-system")
45+
implicit val materializer: ActorMaterializer = ActorMaterializer()
46+
47+
private def buildAddress(port: Int): URI = try
48+
new URI("http://localhost:" + port + "/")
49+
catch {
50+
case exception: URISyntaxException =>
51+
throw new IllegalStateException(exception)
52+
}
53+
54+
@Test def testSimple(): Unit = {
55+
val route = path("test") {
56+
complete("ok")
57+
}
58+
59+
test(route, "/test", "GET /test")
60+
}
61+
62+
@Test def testRoute(): Unit = {
63+
val route = concat(
64+
pathEndOrSingleSlash {
65+
complete("root")
66+
},
67+
pathPrefix("test") {
68+
concat(
69+
pathSingleSlash {
70+
complete("test")
71+
},
72+
path(IntNumber) { _ =>
73+
complete("ok")
74+
}
75+
)
76+
}
77+
)
78+
79+
test(route, "/test/1", "GET /test/*")
80+
}
81+
82+
def test(route: Route, path: String, spanName: String): Unit = {
83+
val port = PortUtils.findOpenPort
84+
val address: URI = buildAddress(port)
85+
val binding =
86+
Await.result(Http().bindAndHandle(route, "localhost", port), 10.seconds)
87+
try {
88+
val request = AggregatedHttpRequest.of(
89+
HttpMethod.GET,
90+
address.resolve(path).toString
91+
)
92+
val response = client.execute(request).aggregate.join
93+
assertThat(response.status.code).isEqualTo(200)
94+
assertThat(response.contentUtf8).isEqualTo("ok")
95+
96+
testing.waitAndAssertTraces(new Consumer[TraceAssert] {
97+
override def accept(trace: TraceAssert): Unit =
98+
trace.hasSpansSatisfyingExactly(new Consumer[SpanDataAssert] {
99+
override def accept(span: SpanDataAssert): Unit = {
100+
span.hasName(spanName)
101+
}
102+
})
103+
})
104+
} finally {
105+
binding.unbind()
106+
}
107+
}
108+
109+
@AfterAll
110+
def cleanUp(): Unit = {
111+
system.terminate()
112+
}
113+
}

instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/PathConcatenationInstrumentation.java

+3-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.route;
77

8-
import static net.bytebuddy.matcher.ElementMatchers.namedOneOf;
8+
import static net.bytebuddy.matcher.ElementMatchers.named;
99

1010
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
1111
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
@@ -16,15 +16,13 @@
1616
public class PathConcatenationInstrumentation implements TypeInstrumentation {
1717
@Override
1818
public ElementMatcher<TypeDescription> typeMatcher() {
19-
return namedOneOf(
20-
"org.apache.pekko.http.scaladsl.server.PathMatcher$$anonfun$$tilde$1",
21-
"org.apache.pekko.http.scaladsl.server.PathMatcher");
19+
return named("org.apache.pekko.http.scaladsl.server.PathMatcher");
2220
}
2321

2422
@Override
2523
public void transform(TypeTransformer transformer) {
2624
transformer.applyAdviceToMethod(
27-
namedOneOf("apply", "$anonfun$append$1"), this.getClass().getName() + "$ApplyAdvice");
25+
named("$anonfun$append$1"), this.getClass().getName() + "$ApplyAdvice");
2826
}
2927

3028
@SuppressWarnings("unused")

instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/PekkoRouteHolder.java

+9-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public class PekkoRouteHolder implements ImplicitContextKeyed {
1919
private static final ContextKey<PekkoRouteHolder> KEY = named("opentelemetry-pekko-route");
2020

2121
private String route = "";
22-
private boolean newSegment;
22+
private boolean newSegment = true;
2323
private boolean endMatched;
2424
private final Deque<String> stack = new ArrayDeque<>();
2525

@@ -62,6 +62,14 @@ public static void save() {
6262
}
6363
}
6464

65+
public static void reset() {
66+
PekkoRouteHolder holder = Context.current().get(KEY);
67+
if (holder != null) {
68+
holder.route = holder.stack.peek();
69+
holder.newSegment = true;
70+
}
71+
}
72+
6573
public static void restore() {
6674
PekkoRouteHolder holder = Context.current().get(KEY);
6775
if (holder != null) {

instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/RouteConcatenationInstrumentation.java

+14-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.route;
77

8-
import static net.bytebuddy.matcher.ElementMatchers.namedOneOf;
8+
import static net.bytebuddy.matcher.ElementMatchers.named;
99

1010
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
1111
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
@@ -16,15 +16,15 @@
1616
public class RouteConcatenationInstrumentation implements TypeInstrumentation {
1717
@Override
1818
public ElementMatcher<TypeDescription> typeMatcher() {
19-
return namedOneOf(
20-
"org.apache.pekko.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation$$anonfun$$tilde$1",
21-
"org.apache.pekko.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation");
19+
return named("org.apache.pekko.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation");
2220
}
2321

2422
@Override
2523
public void transform(TypeTransformer transformer) {
2624
transformer.applyAdviceToMethod(
27-
namedOneOf("apply", "$anonfun$$tilde$1"), this.getClass().getName() + "$ApplyAdvice");
25+
named("$anonfun$$tilde$1"), this.getClass().getName() + "$ApplyAdvice");
26+
transformer.applyAdviceToMethod(
27+
named("$anonfun$$tilde$2"), this.getClass().getName() + "$Apply2Advice");
2828
}
2929

3030
@SuppressWarnings("unused")
@@ -43,4 +43,13 @@ public static void onExit() {
4343
PekkoRouteHolder.restore();
4444
}
4545
}
46+
47+
@SuppressWarnings("unused")
48+
public static class Apply2Advice {
49+
50+
@Advice.OnMethodEnter(suppress = Throwable.class)
51+
public static void onEnter() {
52+
PekkoRouteHolder.reset();
53+
}
54+
}
4655
}

0 commit comments

Comments
 (0)