Skip to content

Commit ffeb80e

Browse files
masonedmisonlaurit
andauthored
Add tapir path matching within pekko instrumentation (#13386)
Co-authored-by: Lauri Tulmin <[email protected]>
1 parent 0970da7 commit ffeb80e

File tree

6 files changed

+334
-27
lines changed

6 files changed

+334
-27
lines changed

instrumentation/pekko/pekko-http-1.0/javaagent/build.gradle.kts

+51-4
Original file line numberDiff line numberDiff line change
@@ -10,35 +10,79 @@ muzzle {
1010
versions.set("[1.0,)")
1111
assertInverse.set(true)
1212
extraDependency("org.apache.pekko:pekko-stream_2.12:1.0.1")
13+
excludeInstrumentationName("tapir-pekko-http-server")
1314
}
1415
pass {
1516
group.set("org.apache.pekko")
1617
module.set("pekko-http_2.13")
1718
versions.set("[1.0,)")
1819
assertInverse.set(true)
1920
extraDependency("org.apache.pekko:pekko-stream_2.13:1.0.1")
21+
excludeInstrumentationName("tapir-pekko-http-server")
2022
}
2123
pass {
2224
group.set("org.apache.pekko")
2325
module.set("pekko-http_3")
2426
versions.set("[1.0,)")
2527
assertInverse.set(true)
2628
extraDependency("org.apache.pekko:pekko-stream_3:1.0.1")
29+
excludeInstrumentationName("tapir-pekko-http-server")
30+
}
31+
pass {
32+
group.set("com.softwaremill.sttp.tapir")
33+
module.set("tapir-pekko-http-server_2.12")
34+
versions.set("[1.7,)")
35+
assertInverse.set(true)
36+
excludeInstrumentationName("pekko-http-server")
37+
}
38+
pass {
39+
group.set("com.softwaremill.sttp.tapir")
40+
module.set("tapir-pekko-http-server_2.13")
41+
versions.set("[1.7,)")
42+
assertInverse.set(true)
43+
excludeInstrumentationName("pekko-http-server")
44+
}
45+
pass {
46+
group.set("com.softwaremill.sttp.tapir")
47+
module.set("tapir-pekko-http-server_3")
48+
versions.set("[1.7,)")
49+
assertInverse.set(true)
50+
excludeInstrumentationName("pekko-http-server")
2751
}
2852
}
2953

3054
dependencies {
3155
library("org.apache.pekko:pekko-http_2.12:1.0.0")
3256
library("org.apache.pekko:pekko-stream_2.12:1.0.1")
33-
34-
testImplementation("com.softwaremill.sttp.tapir:tapir-pekko-http-server_2.12:1.7.0")
57+
compileOnly("com.softwaremill.sttp.tapir:tapir-pekko-http-server_2.12:1.7.0")
3558

3659
testInstrumentation(project(":instrumentation:pekko:pekko-actor-1.0:javaagent"))
3760
testInstrumentation(project(":instrumentation:executors:javaagent"))
3861

3962
latestDepTestLibrary("org.apache.pekko:pekko-http_2.13:latest.release")
4063
latestDepTestLibrary("org.apache.pekko:pekko-stream_2.13:latest.release")
41-
latestDepTestLibrary("com.softwaremill.sttp.tapir:tapir-pekko-http-server_2.13:latest.release")
64+
}
65+
66+
testing {
67+
suites {
68+
val tapirTest by registering(JvmTestSuite::class) {
69+
dependencies {
70+
// this only exists to make Intellij happy since it doesn't (currently at least) understand our
71+
// inclusion of this artifact inside :testing-common
72+
compileOnly(project.dependencies.project(":testing:armeria-shaded-for-testing", configuration = "shadow"))
73+
74+
if (findProperty("testLatestDeps") as Boolean) {
75+
implementation("com.typesafe.akka:akka-http_2.13:latest.release")
76+
implementation("com.typesafe.akka:akka-stream_2.13:latest.release")
77+
implementation("com.softwaremill.sttp.tapir:tapir-pekko-http-server_2.13:latest.release")
78+
} else {
79+
implementation("org.apache.pekko:pekko-http_2.12:1.0.0")
80+
implementation("org.apache.pekko:pekko-stream_2.12:1.0.1")
81+
implementation("com.softwaremill.sttp.tapir:tapir-pekko-http-server_2.12:1.7.0")
82+
}
83+
}
84+
}
85+
}
4286
}
4387

4488
tasks {
@@ -51,6 +95,10 @@ tasks {
5195

5296
systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean)
5397
}
98+
99+
check {
100+
dependsOn(testing.suites)
101+
}
54102
}
55103

56104
if (findProperty("testLatestDeps") as Boolean) {
@@ -59,7 +107,6 @@ if (findProperty("testLatestDeps") as Boolean) {
59107
testImplementation {
60108
exclude("org.apache.pekko", "pekko-http_2.12")
61109
exclude("org.apache.pekko", "pekko-stream_2.12")
62-
exclude("com.softwaremill.sttp.tapir", "tapir-pekko-http-server_2.12")
63110
}
64111
}
65112
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.tapir;
7+
8+
import io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.route.PekkoRouteHolder;
9+
import org.apache.pekko.http.scaladsl.server.RequestContext;
10+
import org.apache.pekko.http.scaladsl.server.RouteResult;
11+
import scala.Function1;
12+
import scala.Function2;
13+
import scala.Option;
14+
import scala.PartialFunction;
15+
import scala.Unit;
16+
import scala.concurrent.Future;
17+
import scala.util.Try;
18+
import sttp.tapir.EndpointInput;
19+
import sttp.tapir.server.ServerEndpoint;
20+
21+
public class RouteWrapper implements Function1<RequestContext, Future<RouteResult>> {
22+
private final Function1<RequestContext, Future<RouteResult>> route;
23+
private final ServerEndpoint<?, ?> serverEndpoint;
24+
25+
public RouteWrapper(
26+
ServerEndpoint<?, ?> serverEndpoint, Function1<RequestContext, Future<RouteResult>> route) {
27+
this.route = route;
28+
this.serverEndpoint = serverEndpoint;
29+
}
30+
31+
public class Finalizer implements PartialFunction<Try<RouteResult>, Unit> {
32+
@Override
33+
public boolean isDefinedAt(Try<RouteResult> tryResult) {
34+
return tryResult.isSuccess();
35+
}
36+
37+
@Override
38+
public Unit apply(Try<RouteResult> tryResult) {
39+
if (tryResult.isSuccess()) {
40+
RouteResult result = tryResult.get();
41+
if (result.getClass() == RouteResult.Complete.class) {
42+
String path =
43+
serverEndpoint.showPathTemplate(
44+
(index, pc) ->
45+
pc.name().isDefined() ? "{" + pc.name().get() + "}" : "{param" + index + "}",
46+
Option.apply(
47+
(Function2<Object, EndpointInput.Query<?>, String>)
48+
(index, q) -> q.name() + "={" + q.name() + "}"),
49+
false,
50+
"*",
51+
Option.apply("*"),
52+
Option.apply("*"));
53+
54+
PekkoRouteHolder.push(path);
55+
PekkoRouteHolder.endMatched();
56+
}
57+
}
58+
return null;
59+
}
60+
}
61+
62+
@Override
63+
public Future<RouteResult> apply(RequestContext ctx) {
64+
return route.apply(ctx).andThen(new Finalizer(), ctx.executionContext());
65+
}
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.tapir;
7+
8+
import static net.bytebuddy.matcher.ElementMatchers.named;
9+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
10+
11+
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
12+
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
13+
import net.bytebuddy.asm.Advice;
14+
import net.bytebuddy.description.type.TypeDescription;
15+
import net.bytebuddy.matcher.ElementMatcher;
16+
import org.apache.pekko.http.scaladsl.server.RequestContext;
17+
import org.apache.pekko.http.scaladsl.server.RouteResult;
18+
import scala.Function1;
19+
import scala.concurrent.Future;
20+
import sttp.tapir.server.ServerEndpoint;
21+
22+
public class TapirPathInstrumentation implements TypeInstrumentation {
23+
24+
@Override
25+
public ElementMatcher<TypeDescription> typeMatcher() {
26+
return named("sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter");
27+
}
28+
29+
@Override
30+
public void transform(TypeTransformer transformer) {
31+
transformer.applyAdviceToMethod(
32+
named("toRoute").and(takesArgument(0, named("sttp.tapir.server.ServerEndpoint"))),
33+
this.getClass().getName() + "$ApplyAdvice");
34+
}
35+
36+
@SuppressWarnings("unused")
37+
public static class ApplyAdvice {
38+
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
39+
public static void onExit(
40+
@Advice.Argument(0) ServerEndpoint<?, ?> endpoint,
41+
@Advice.Return(readOnly = false) Function1<RequestContext, Future<RouteResult>> route) {
42+
route = new RouteWrapper(endpoint, route);
43+
}
44+
}
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.tapir;
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 io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule;
14+
import java.util.List;
15+
16+
@AutoService(InstrumentationModule.class)
17+
public class TapirPekkoHttpServerRouteInstrumentationModule extends InstrumentationModule
18+
implements ExperimentalInstrumentationModule {
19+
public TapirPekkoHttpServerRouteInstrumentationModule() {
20+
super(
21+
"pekko-http",
22+
"pekko-http-1.0",
23+
"pekko-http-server",
24+
"pekko-http-server-route",
25+
"tapir-pekko-http-server",
26+
"tapir-pekko-http-server-route");
27+
}
28+
29+
@Override
30+
public String getModuleGroup() {
31+
return "pekko-server";
32+
}
33+
34+
@Override
35+
public List<TypeInstrumentation> typeInstrumentations() {
36+
return singletonList(new TapirPathInstrumentation());
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0
7+
8+
import io.opentelemetry.instrumentation.test.utils.PortUtils
9+
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension
10+
import io.opentelemetry.sdk.testing.assertj.{SpanDataAssert, TraceAssert}
11+
import io.opentelemetry.testing.internal.armeria.client.WebClient
12+
import io.opentelemetry.testing.internal.armeria.common.{
13+
AggregatedHttpRequest,
14+
HttpMethod
15+
}
16+
import org.apache.pekko.actor.ActorSystem
17+
import org.apache.pekko.http.scaladsl.Http
18+
import org.apache.pekko.http.scaladsl.server.Directives.{
19+
IntNumber,
20+
complete,
21+
concat,
22+
path,
23+
pathEndOrSingleSlash,
24+
pathPrefix,
25+
pathSingleSlash
26+
}
27+
import org.apache.pekko.http.scaladsl.server.Route
28+
import org.assertj.core.api.Assertions.assertThat
29+
import org.junit.jupiter.api.extension.RegisterExtension
30+
import org.junit.jupiter.api.{AfterAll, Test, TestInstance}
31+
import sttp.tapir._
32+
import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter
33+
34+
import java.net.{URI, URISyntaxException}
35+
import java.util.function.Consumer
36+
import scala.concurrent.duration.DurationInt
37+
import scala.concurrent.{Await, Future}
38+
39+
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
40+
class TapirHttpServerRouteTest {
41+
@RegisterExtension private val testing: AgentInstrumentationExtension =
42+
AgentInstrumentationExtension.create
43+
private val client: WebClient = WebClient.of()
44+
45+
implicit val system: ActorSystem = ActorSystem("my-system")
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+
@Test def testTapirRoutes(): Unit = {
83+
val interpreter = PekkoHttpServerInterpreter()(system.dispatcher)
84+
def makeRoute(input: EndpointInput[Unit]) = {
85+
interpreter.toRoute(
86+
endpoint.get
87+
.in(input)
88+
.errorOut(stringBody)
89+
.out(stringBody)
90+
.serverLogicPure[Future](_ => Right("ok"))
91+
)
92+
}
93+
94+
val routes = concat(
95+
concat(makeRoute("test" / "1"), makeRoute("test" / "2")),
96+
concat(makeRoute("test" / "3"), makeRoute("test" / "4"))
97+
)
98+
99+
test(routes, "/test/4", "GET /test/4")
100+
}
101+
102+
def test(route: Route, path: String, spanName: String): Unit = {
103+
val port = PortUtils.findOpenPort
104+
val address: URI = buildAddress(port)
105+
val binding =
106+
Await.result(Http().bindAndHandle(route, "localhost", port), 10.seconds)
107+
try {
108+
val request = AggregatedHttpRequest.of(
109+
HttpMethod.GET,
110+
address.resolve(path).toString
111+
)
112+
val response = client.execute(request).aggregate.join
113+
assertThat(response.status.code).isEqualTo(200)
114+
assertThat(response.contentUtf8).isEqualTo("ok")
115+
116+
testing.waitAndAssertTraces(new Consumer[TraceAssert] {
117+
override def accept(trace: TraceAssert): Unit =
118+
trace.hasSpansSatisfyingExactly(new Consumer[SpanDataAssert] {
119+
override def accept(span: SpanDataAssert): Unit = {
120+
span.hasName(spanName)
121+
}
122+
})
123+
})
124+
} finally {
125+
binding.unbind()
126+
}
127+
}
128+
129+
@AfterAll
130+
def cleanUp(): Unit = {
131+
system.terminate()
132+
}
133+
}

0 commit comments

Comments
 (0)