From 42d71772224ddc38fb515a90d26dfaae10acbbfa Mon Sep 17 00:00:00 2001 From: Lauri Tulmin Date: Fri, 9 Aug 2024 09:46:17 +0300 Subject: [PATCH] Improve akka route handling with java dsl (#11926) --- .../akka-http-10.0/javaagent/build.gradle.kts | 20 ++++ .../akkahttp/AkkaHttpServerJavaRouteTest.java | 64 ++++++++++ .../server/route/AkkaRouteHolder.java | 11 +- .../RouteConcatenationInstrumentation.java | 23 +++- .../akkahttp/AkkaHttpServerRouteTest.scala | 113 ++++++++++++++++++ .../PathConcatenationInstrumentation.java | 8 +- .../v1_0/server/route/PekkoRouteHolder.java | 10 +- .../RouteConcatenationInstrumentation.java | 19 ++- .../v1_0/PekkoHttpServerJavaRouteTest.java | 64 ++++++++++ .../v1_0/PekkoHttpServerRouteTest.scala | 113 ++++++++++++++++++ 10 files changed, 432 insertions(+), 13 deletions(-) create mode 100644 instrumentation/akka/akka-http-10.0/javaagent/src/javaRouteTest/java/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpServerJavaRouteTest.java create mode 100644 instrumentation/akka/akka-http-10.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpServerRouteTest.scala create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerJavaRouteTest.java create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerRouteTest.scala diff --git a/instrumentation/akka/akka-http-10.0/javaagent/build.gradle.kts b/instrumentation/akka/akka-http-10.0/javaagent/build.gradle.kts index 0c4f96c18022..ff1e6c9cccfc 100644 --- a/instrumentation/akka/akka-http-10.0/javaagent/build.gradle.kts +++ b/instrumentation/akka/akka-http-10.0/javaagent/build.gradle.kts @@ -42,6 +42,22 @@ dependencies { latestDepTestLibrary("com.typesafe.akka:akka-stream_2.13:+") } +testing { + suites { + val javaRouteTest by registering(JvmTestSuite::class) { + dependencies { + if (findProperty("testLatestDeps") as Boolean) { + implementation("com.typesafe.akka:akka-http_2.13:+") + implementation("com.typesafe.akka:akka-stream_2.13:+") + } else { + implementation("com.typesafe.akka:akka-http_2.12:10.2.0") + implementation("com.typesafe.akka:akka-stream_2.12:2.6.21") + } + } + } + } +} + tasks { withType().configureEach { // required on jdk17 @@ -52,6 +68,10 @@ tasks { systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) } + + check { + dependsOn(testing.suites) + } } if (findProperty("testLatestDeps") as Boolean) { diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/javaRouteTest/java/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpServerJavaRouteTest.java b/instrumentation/akka/akka-http-10.0/javaagent/src/javaRouteTest/java/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpServerJavaRouteTest.java new file mode 100644 index 000000000000..be1357c8d9bf --- /dev/null +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/javaRouteTest/java/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpServerJavaRouteTest.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkahttp; + +import static akka.http.javadsl.server.PathMatchers.integerSegment; +import static org.assertj.core.api.Assertions.assertThat; + +import akka.actor.ActorSystem; +import akka.http.javadsl.Http; +import akka.http.javadsl.ServerBinding; +import akka.http.javadsl.server.AllDirectives; +import akka.http.javadsl.server.Route; +import io.opentelemetry.instrumentation.test.utils.PortUtils; +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.AggregatedHttpRequest; +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse; +import io.opentelemetry.testing.internal.armeria.common.HttpMethod; +import java.util.concurrent.CompletionStage; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class AkkaHttpServerJavaRouteTest extends AllDirectives { + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private final WebClient client = WebClient.of(); + + @Test + void testRoute() { + ActorSystem system = ActorSystem.create("my-system"); + int port = PortUtils.findOpenPort(); + Http http = Http.get(system); + + Route route = + concat( + pathEndOrSingleSlash(() -> complete("root")), + pathPrefix( + "test", + () -> + concat( + pathSingleSlash(() -> complete("test")), + path(integerSegment(), (i) -> complete("ok"))))); + + CompletionStage binding = http.newServerAt("localhost", port).bind(route); + try { + AggregatedHttpRequest request = + AggregatedHttpRequest.of(HttpMethod.GET, "h1c://localhost:" + port + "/test/1"); + AggregatedHttpResponse response = client.execute(request).aggregate().join(); + + assertThat(response.status().code()).isEqualTo(200); + assertThat(response.contentUtf8()).isEqualTo("ok"); + + testing.waitAndAssertTraces( + trace -> trace.hasSpansSatisfyingExactly(span -> span.hasName("GET /test/*"))); + } finally { + binding.thenCompose(ServerBinding::unbind).thenAccept(unbound -> system.terminate()); + } + } +} diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaRouteHolder.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaRouteHolder.java index 9fa98d8c2add..3f192e5c7d2c 100644 --- a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaRouteHolder.java +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaRouteHolder.java @@ -19,7 +19,7 @@ public class AkkaRouteHolder implements ImplicitContextKeyed { private static final ContextKey KEY = named("opentelemetry-akka-route"); private String route = ""; - private boolean newSegment; + private boolean newSegment = true; private boolean endMatched; private final Deque stack = new ArrayDeque<>(); @@ -70,6 +70,15 @@ public static void restore() { } } + // reset the state to when save was called + public static void reset() { + AkkaRouteHolder holder = Context.current().get(KEY); + if (holder != null) { + holder.route = holder.stack.peek(); + holder.newSegment = true; + } + } + @Override public Context storeInContext(Context context) { return context.with(KEY, this); diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/RouteConcatenationInstrumentation.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/RouteConcatenationInstrumentation.java index de13aba9fb30..eb13f402bc1e 100644 --- a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/RouteConcatenationInstrumentation.java +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/RouteConcatenationInstrumentation.java @@ -17,14 +17,26 @@ public class RouteConcatenationInstrumentation implements TypeInstrumentation { @Override public ElementMatcher typeMatcher() { return namedOneOf( + // scala 2.11 "akka.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation$$anonfun$$tilde$1", + // scala 2.12 and later "akka.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation"); } @Override public void transform(TypeTransformer transformer) { transformer.applyAdviceToMethod( - namedOneOf("apply", "$anonfun$$tilde$1"), this.getClass().getName() + "$ApplyAdvice"); + namedOneOf( + // scala 2.11 + "apply", + // scala 2.12 and later + "$anonfun$$tilde$1"), + this.getClass().getName() + "$ApplyAdvice"); + + // This advice seems to be only needed when defining routes with java dsl. Since java dsl tests + // use scala 2.12 we are going to skip instrumenting this for scala 2.11. + transformer.applyAdviceToMethod( + namedOneOf("$anonfun$$tilde$2"), this.getClass().getName() + "$Apply2Advice"); } @SuppressWarnings("unused") @@ -43,4 +55,13 @@ public static void onExit() { AkkaRouteHolder.restore(); } } + + @SuppressWarnings("unused") + public static class Apply2Advice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter() { + AkkaRouteHolder.reset(); + } + } } diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpServerRouteTest.scala b/instrumentation/akka/akka-http-10.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpServerRouteTest.scala new file mode 100644 index 000000000000..3920c6aeb15e --- /dev/null +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpServerRouteTest.scala @@ -0,0 +1,113 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkahttp + +import akka.actor.ActorSystem +import akka.http.scaladsl.Http +import akka.http.scaladsl.server.Directives.{ + IntNumber, + complete, + concat, + path, + pathEndOrSingleSlash, + pathPrefix, + pathSingleSlash +} +import akka.http.scaladsl.server.Route +import akka.stream.ActorMaterializer +import io.opentelemetry.instrumentation.test.utils.PortUtils +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension +import io.opentelemetry.sdk.testing.assertj.{SpanDataAssert, TraceAssert} +import io.opentelemetry.testing.internal.armeria.client.WebClient +import io.opentelemetry.testing.internal.armeria.common.{ + AggregatedHttpRequest, + HttpMethod +} +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.{AfterAll, Test, TestInstance} +import org.junit.jupiter.api.extension.RegisterExtension + +import java.net.{URI, URISyntaxException} +import java.util.function.Consumer +import scala.concurrent.duration.DurationInt +import scala.concurrent.Await + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class AkkaHttpServerRouteTest { + @RegisterExtension private val testing: AgentInstrumentationExtension = + AgentInstrumentationExtension.create + private val client: WebClient = WebClient.of() + + implicit val system: ActorSystem = ActorSystem("my-system") + implicit val materializer: ActorMaterializer = ActorMaterializer() + + private def buildAddress(port: Int): URI = try + new URI("http://localhost:" + port + "/") + catch { + case exception: URISyntaxException => + throw new IllegalStateException(exception) + } + + @Test def testSimple(): Unit = { + val route = path("test") { + complete("ok") + } + + test(route, "/test", "GET /test") + } + + @Test def testRoute(): Unit = { + val route = concat( + pathEndOrSingleSlash { + complete("root") + }, + pathPrefix("test") { + concat( + pathSingleSlash { + complete("test") + }, + path(IntNumber) { _ => + complete("ok") + } + ) + } + ) + + test(route, "/test/1", "GET /test/*") + } + + def test(route: Route, path: String, spanName: String): Unit = { + val port = PortUtils.findOpenPort + val address: URI = buildAddress(port) + val binding = + Await.result(Http().bindAndHandle(route, "localhost", port), 10.seconds) + try { + val request = AggregatedHttpRequest.of( + HttpMethod.GET, + address.resolve(path).toString + ) + val response = client.execute(request).aggregate.join + assertThat(response.status.code).isEqualTo(200) + assertThat(response.contentUtf8).isEqualTo("ok") + + testing.waitAndAssertTraces(new Consumer[TraceAssert] { + override def accept(trace: TraceAssert): Unit = + trace.hasSpansSatisfyingExactly(new Consumer[SpanDataAssert] { + override def accept(span: SpanDataAssert): Unit = { + span.hasName(spanName) + } + }) + }) + } finally { + binding.unbind() + } + } + + @AfterAll + def cleanUp(): Unit = { + system.terminate() + } +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/PathConcatenationInstrumentation.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/PathConcatenationInstrumentation.java index 2f98af499ef4..d11310208bb6 100644 --- a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/PathConcatenationInstrumentation.java +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/PathConcatenationInstrumentation.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.route; -import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.named; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; @@ -16,15 +16,13 @@ public class PathConcatenationInstrumentation implements TypeInstrumentation { @Override public ElementMatcher typeMatcher() { - return namedOneOf( - "org.apache.pekko.http.scaladsl.server.PathMatcher$$anonfun$$tilde$1", - "org.apache.pekko.http.scaladsl.server.PathMatcher"); + return named("org.apache.pekko.http.scaladsl.server.PathMatcher"); } @Override public void transform(TypeTransformer transformer) { transformer.applyAdviceToMethod( - namedOneOf("apply", "$anonfun$append$1"), this.getClass().getName() + "$ApplyAdvice"); + named("$anonfun$append$1"), this.getClass().getName() + "$ApplyAdvice"); } @SuppressWarnings("unused") diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/PekkoRouteHolder.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/PekkoRouteHolder.java index 1d8a5e56054d..91ece866fdbd 100644 --- a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/PekkoRouteHolder.java +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/PekkoRouteHolder.java @@ -19,7 +19,7 @@ public class PekkoRouteHolder implements ImplicitContextKeyed { private static final ContextKey KEY = named("opentelemetry-pekko-route"); private String route = ""; - private boolean newSegment; + private boolean newSegment = true; private boolean endMatched; private final Deque stack = new ArrayDeque<>(); @@ -62,6 +62,14 @@ public static void save() { } } + public static void reset() { + PekkoRouteHolder holder = Context.current().get(KEY); + if (holder != null) { + holder.route = holder.stack.peek(); + holder.newSegment = true; + } + } + public static void restore() { PekkoRouteHolder holder = Context.current().get(KEY); if (holder != null) { diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/RouteConcatenationInstrumentation.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/RouteConcatenationInstrumentation.java index e5bf3b0d005a..9a00975e6b8f 100644 --- a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/RouteConcatenationInstrumentation.java +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/RouteConcatenationInstrumentation.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.route; -import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.named; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; @@ -16,15 +16,15 @@ public class RouteConcatenationInstrumentation implements TypeInstrumentation { @Override public ElementMatcher typeMatcher() { - return namedOneOf( - "org.apache.pekko.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation$$anonfun$$tilde$1", - "org.apache.pekko.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation"); + return named("org.apache.pekko.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation"); } @Override public void transform(TypeTransformer transformer) { transformer.applyAdviceToMethod( - namedOneOf("apply", "$anonfun$$tilde$1"), this.getClass().getName() + "$ApplyAdvice"); + named("$anonfun$$tilde$1"), this.getClass().getName() + "$ApplyAdvice"); + transformer.applyAdviceToMethod( + named("$anonfun$$tilde$2"), this.getClass().getName() + "$Apply2Advice"); } @SuppressWarnings("unused") @@ -43,4 +43,13 @@ public static void onExit() { PekkoRouteHolder.restore(); } } + + @SuppressWarnings("unused") + public static class Apply2Advice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter() { + PekkoRouteHolder.reset(); + } + } } diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerJavaRouteTest.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerJavaRouteTest.java new file mode 100644 index 000000000000..bb239e4058c3 --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerJavaRouteTest.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0; + +import static org.apache.pekko.http.javadsl.server.PathMatchers.integerSegment; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.instrumentation.test.utils.PortUtils; +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.AggregatedHttpRequest; +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse; +import io.opentelemetry.testing.internal.armeria.common.HttpMethod; +import java.util.concurrent.CompletionStage; +import org.apache.pekko.actor.ActorSystem; +import org.apache.pekko.http.javadsl.Http; +import org.apache.pekko.http.javadsl.ServerBinding; +import org.apache.pekko.http.javadsl.server.AllDirectives; +import org.apache.pekko.http.javadsl.server.Route; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class PekkoHttpServerJavaRouteTest extends AllDirectives { + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private final WebClient client = WebClient.of(); + + @Test + void testRoute() { + ActorSystem system = ActorSystem.create("my-system"); + int port = PortUtils.findOpenPort(); + Http http = Http.get(system); + + Route route = + concat( + pathEndOrSingleSlash(() -> complete("root")), + pathPrefix( + "test", + () -> + concat( + pathSingleSlash(() -> complete("test")), + path(integerSegment(), (i) -> complete("ok"))))); + + CompletionStage binding = http.newServerAt("localhost", port).bind(route); + try { + AggregatedHttpRequest request = + AggregatedHttpRequest.of(HttpMethod.GET, "h1c://localhost:" + port + "/test/1"); + AggregatedHttpResponse response = client.execute(request).aggregate().join(); + + assertThat(response.status().code()).isEqualTo(200); + assertThat(response.contentUtf8()).isEqualTo("ok"); + + testing.waitAndAssertTraces( + trace -> trace.hasSpansSatisfyingExactly(span -> span.hasName("GET /test/*"))); + } finally { + binding.thenCompose(ServerBinding::unbind).thenAccept(unbound -> system.terminate()); + } + } +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerRouteTest.scala b/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerRouteTest.scala new file mode 100644 index 000000000000..e01f6eee0460 --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerRouteTest.scala @@ -0,0 +1,113 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0 + +import io.opentelemetry.instrumentation.test.utils.PortUtils +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension +import io.opentelemetry.sdk.testing.assertj.{SpanDataAssert, TraceAssert} +import io.opentelemetry.testing.internal.armeria.client.WebClient +import io.opentelemetry.testing.internal.armeria.common.{ + AggregatedHttpRequest, + HttpMethod +} +import org.apache.pekko.actor.ActorSystem +import org.apache.pekko.http.scaladsl.Http +import org.apache.pekko.http.scaladsl.server.Route +import org.apache.pekko.http.scaladsl.server.Directives.{ + IntNumber, + complete, + concat, + path, + pathEndOrSingleSlash, + pathPrefix, + pathSingleSlash +} +import org.apache.pekko.stream.ActorMaterializer +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.extension.RegisterExtension +import org.junit.jupiter.api.{AfterAll, Test, TestInstance} + +import java.net.{URI, URISyntaxException} +import java.util.function.Consumer +import scala.concurrent.Await +import scala.concurrent.duration.DurationInt + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class PekkoHttpServerRouteTest { + @RegisterExtension private val testing: AgentInstrumentationExtension = + AgentInstrumentationExtension.create + private val client: WebClient = WebClient.of() + + implicit val system: ActorSystem = ActorSystem("my-system") + implicit val materializer: ActorMaterializer = ActorMaterializer() + + private def buildAddress(port: Int): URI = try + new URI("http://localhost:" + port + "/") + catch { + case exception: URISyntaxException => + throw new IllegalStateException(exception) + } + + @Test def testSimple(): Unit = { + val route = path("test") { + complete("ok") + } + + test(route, "/test", "GET /test") + } + + @Test def testRoute(): Unit = { + val route = concat( + pathEndOrSingleSlash { + complete("root") + }, + pathPrefix("test") { + concat( + pathSingleSlash { + complete("test") + }, + path(IntNumber) { _ => + complete("ok") + } + ) + } + ) + + test(route, "/test/1", "GET /test/*") + } + + def test(route: Route, path: String, spanName: String): Unit = { + val port = PortUtils.findOpenPort + val address: URI = buildAddress(port) + val binding = + Await.result(Http().bindAndHandle(route, "localhost", port), 10.seconds) + try { + val request = AggregatedHttpRequest.of( + HttpMethod.GET, + address.resolve(path).toString + ) + val response = client.execute(request).aggregate.join + assertThat(response.status.code).isEqualTo(200) + assertThat(response.contentUtf8).isEqualTo("ok") + + testing.waitAndAssertTraces(new Consumer[TraceAssert] { + override def accept(trace: TraceAssert): Unit = + trace.hasSpansSatisfyingExactly(new Consumer[SpanDataAssert] { + override def accept(span: SpanDataAssert): Unit = { + span.hasName(spanName) + } + }) + }) + } finally { + binding.unbind() + } + } + + @AfterAll + def cleanUp(): Unit = { + system.terminate() + } +}