From 1a3d8203b1353a466f6a84482de6a211aab2e91d Mon Sep 17 00:00:00 2001 From: Abhishek Rao Date: Wed, 26 Nov 2025 10:50:33 -0500 Subject: [PATCH 1/3] fix(akka): improve handling of http.route --- .../akkahttp/server/AkkaFlowWrapper.java | 11 +++ ...aHttpServerRouteInstrumentationModule.java | 3 +- .../server/route/AkkaRouteHolder.java | 88 ++++++++++--------- .../route/AkkaRouteHolderSingletons.java | 17 ++++ .../server/route/AkkaRouteWrapper.java | 46 ++++++++++ .../PathConcatenationInstrumentation.java | 43 --------- .../route/PathMatcherInstrumentation.java | 3 +- .../PathMatcherStaticInstrumentation.java | 21 +++-- .../RouteConcatenationInstrumentation.java | 52 ++++------- .../akkahttp/AkkaHttpServerRouteTest.scala | 50 ++++++++++- 10 files changed, 202 insertions(+), 132 deletions(-) create mode 100644 instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaRouteHolderSingletons.java create mode 100644 instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaRouteWrapper.java delete mode 100644 instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathConcatenationInstrumentation.java diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaFlowWrapper.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaFlowWrapper.java index d39e3df81534..4500aea45201 100644 --- a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaFlowWrapper.java +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaFlowWrapper.java @@ -23,6 +23,8 @@ import akka.stream.stage.GraphStageLogic; import akka.stream.stage.OutHandler; import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; import io.opentelemetry.javaagent.bootstrap.http.HttpServerResponseCustomizerHolder; import io.opentelemetry.javaagent.instrumentation.akkahttp.server.route.AkkaRouteHolder; import java.util.ArrayDeque; @@ -160,6 +162,15 @@ public void onPush() { response = (HttpResponse) response.addHeaders(headers); } + AkkaRouteHolder routeHolder = AkkaRouteHolder.get(tracingRequest.context); + if (routeHolder != null) { + routeHolder.pushIfNotCompletelyMatched("*"); + HttpServerRoute.update( + tracingRequest.context, + HttpServerRouteSource.CONTROLLER, + routeHolder.route()); + } + instrumenter().end(tracingRequest.context, tracingRequest.request, response, null); } push(responseOut, response); diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaHttpServerRouteInstrumentationModule.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaHttpServerRouteInstrumentationModule.java index 2dc87b99ba69..c85299f320b9 100644 --- a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaHttpServerRouteInstrumentationModule.java +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaHttpServerRouteInstrumentationModule.java @@ -39,7 +39,6 @@ public List typeInstrumentations() { return asList( new PathMatcherInstrumentation(), new PathMatcherStaticInstrumentation(), - new RouteConcatenationInstrumentation(), - new PathConcatenationInstrumentation()); + new RouteConcatenationInstrumentation()); } } 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 3f192e5c7d2c..f322e065b596 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 @@ -7,21 +7,20 @@ import static io.opentelemetry.context.ContextKey.named; +import akka.http.scaladsl.model.Uri; import io.opentelemetry.context.Context; import io.opentelemetry.context.ContextKey; import io.opentelemetry.context.ImplicitContextKeyed; -import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; -import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; import java.util.ArrayDeque; import java.util.Deque; public class AkkaRouteHolder implements ImplicitContextKeyed { private static final ContextKey KEY = named("opentelemetry-akka-route"); - private String route = ""; - private boolean newSegment = true; - private boolean endMatched; - private final Deque stack = new ArrayDeque<>(); + private StringBuilder route = new StringBuilder(); + private Uri.Path lastUnmatchedPath = null; + private boolean lastWasMatched = false; + private final Deque savedStates = new ArrayDeque<>(); public static Context init(Context context) { if (context.get(KEY) != null) { @@ -30,52 +29,51 @@ public static Context init(Context context) { return context.with(new AkkaRouteHolder()); } - public static void push(String path) { - AkkaRouteHolder holder = Context.current().get(KEY); - if (holder != null && holder.newSegment && !holder.endMatched) { - holder.route += path; - holder.newSegment = false; - } + public static AkkaRouteHolder get(Context context) { + return context.get(KEY); } - public static void startSegment() { - AkkaRouteHolder holder = Context.current().get(KEY); - if (holder != null) { - holder.newSegment = true; + public void push(Uri.Path beforeMatch, Uri.Path afterMatch, String pathToPush) { + // Only accept the suggested 'pathToPush' if: + // - either this is the first match, or + // - the unmatched part of the path from the previous match is what the current match + // acted upon. This avoids pushes from PathMatchers that compose other PathMatchers, + // instead only accepting pushes from leaf-nodes in the PathMatcher hierarchy that actually + // act on the path. + // AND: + // - some part of the path has now been matched by this matcher + if ((lastUnmatchedPath == null || lastUnmatchedPath.equals(beforeMatch)) + && !afterMatch.equals(beforeMatch)) { + route.append(pathToPush); + lastUnmatchedPath = afterMatch; } + lastWasMatched = true; } - public static void endMatched() { - Context context = Context.current(); - AkkaRouteHolder holder = context.get(KEY); - if (holder != null) { - holder.endMatched = true; - HttpServerRoute.update(context, HttpServerRouteSource.CONTROLLER, holder.route); - } + public void didNotMatch() { + lastWasMatched = false; } - public static void save() { - AkkaRouteHolder holder = Context.current().get(KEY); - if (holder != null) { - holder.stack.push(holder.route); - holder.newSegment = true; + public void pushIfNotCompletelyMatched(String pathToPush) { + if (lastUnmatchedPath != null && !lastUnmatchedPath.isEmpty()) { + route.append(pathToPush); } } - public static void restore() { - AkkaRouteHolder holder = Context.current().get(KEY); - if (holder != null) { - holder.route = holder.stack.pop(); - holder.newSegment = true; - } + public String route() { + return lastWasMatched ? route.toString() : null; + } + + public void save() { + savedStates.add(new State(lastUnmatchedPath, route)); + route = new StringBuilder(route); } - // 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; + public void restore() { + State popped = savedStates.pollLast(); + if (popped != null) { + lastUnmatchedPath = popped.lastUnmatchedPath; + route = popped.route; } } @@ -85,4 +83,14 @@ public Context storeInContext(Context context) { } private AkkaRouteHolder() {} + + private static class State { + private final Uri.Path lastUnmatchedPath; + private final StringBuilder route; + + private State(Uri.Path lastUnmatchedPath, StringBuilder route) { + this.lastUnmatchedPath = lastUnmatchedPath; + this.route = route; + } + } } diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaRouteHolderSingletons.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaRouteHolderSingletons.java new file mode 100644 index 000000000000..a8ad79df4233 --- /dev/null +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaRouteHolderSingletons.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkahttp.server.route; + +import akka.http.scaladsl.server.PathMatcher; +import io.opentelemetry.instrumentation.api.util.VirtualField; + +public final class AkkaRouteHolderSingletons { + + public static final VirtualField, String> PREFIX = + VirtualField.find(PathMatcher.class, String.class); + + private AkkaRouteHolderSingletons() {} +} diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaRouteWrapper.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaRouteWrapper.java new file mode 100644 index 000000000000..dfb475a377f5 --- /dev/null +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaRouteWrapper.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkahttp.server.route; + +import akka.http.scaladsl.server.RequestContext; +import akka.http.scaladsl.server.RouteResult; +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; +import scala.Function1; +import scala.concurrent.Future; +import scala.runtime.AbstractFunction1; + +public class AkkaRouteWrapper extends AbstractFunction1> { + private final Function1> route; + + public AkkaRouteWrapper(Function1> route) { + this.route = route; + } + + @Override + public Future apply(RequestContext ctx) { + Context context = Java8BytecodeBridge.currentContext(); + AkkaRouteHolder routeHolder = AkkaRouteHolder.get(context); + if (routeHolder == null) { + return route.apply(ctx); + } else { + routeHolder.save(); + return route + .apply(ctx) + .map( + new AbstractFunction1() { + @Override + public RouteResult apply(RouteResult result) { + if (result.getClass() == RouteResult.Rejected.class) { + routeHolder.restore(); + } + return result; + } + }, + ctx.executionContext()); + } + } +} diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathConcatenationInstrumentation.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathConcatenationInstrumentation.java deleted file mode 100644 index 8897a9ebd3bf..000000000000 --- a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathConcatenationInstrumentation.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.akkahttp.server.route; - -import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; - -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 PathConcatenationInstrumentation implements TypeInstrumentation { - @Override - public ElementMatcher typeMatcher() { - return namedOneOf( - "akka.http.scaladsl.server.PathMatcher$$anonfun$$tilde$1", - "akka.http.scaladsl.server.PathMatcher"); - } - - @Override - public void transform(TypeTransformer transformer) { - transformer.applyAdviceToMethod( - namedOneOf("apply", "$anonfun$append$1"), this.getClass().getName() + "$ApplyAdvice"); - } - - @SuppressWarnings("unused") - public static class ApplyAdvice { - - @Advice.OnMethodEnter(suppress = Throwable.class) - public static void onEnter() { - // https://github.com/akka/akka-http/blob/0fedb87671ecc450e7378713105ea1dc1d9d0c7d/akka-http/src/main/scala/akka/http/scaladsl/server/PathMatcher.scala#L43 - // https://github.com/akka/akka-http/blob/0fedb87671ecc450e7378713105ea1dc1d9d0c7d/akka-http/src/main/scala/akka/http/scaladsl/server/PathMatcher.scala#L47 - // when routing dsl uses path("path1" / "path2") we are concatenating 3 segments "path1" and / - // and "path2" we need to notify the matcher that a new segment has started, so it could be - // captured in the route - AkkaRouteHolder.startSegment(); - } - } -} diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathMatcherInstrumentation.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathMatcherInstrumentation.java index f8dc1172c74b..6ca3c70d3a08 100644 --- a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathMatcherInstrumentation.java +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathMatcherInstrumentation.java @@ -5,6 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.akkahttp.server.route; +import static io.opentelemetry.javaagent.instrumentation.akkahttp.server.route.AkkaRouteHolderSingletons.PREFIX; import static net.bytebuddy.matcher.ElementMatchers.named; import static net.bytebuddy.matcher.ElementMatchers.returns; import static net.bytebuddy.matcher.ElementMatchers.takesArgument; @@ -40,7 +41,7 @@ public static void onEnter( @Advice.Argument(0) Uri.Path prefix, @Advice.Return PathMatcher result) { // store the path being matched inside a VirtualField on the given matcher, so it can be used // for constructing the route - PathMatcherUtil.setMatched(result, prefix.toString()); + PREFIX.set(result, prefix.toString()); } } } diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathMatcherStaticInstrumentation.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathMatcherStaticInstrumentation.java index e65640b2ad09..c46e67cdc585 100644 --- a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathMatcherStaticInstrumentation.java +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathMatcherStaticInstrumentation.java @@ -6,12 +6,15 @@ package io.opentelemetry.javaagent.instrumentation.akkahttp.server.route; import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static io.opentelemetry.javaagent.instrumentation.akkahttp.server.route.AkkaRouteHolderSingletons.PREFIX; import static net.bytebuddy.matcher.ElementMatchers.named; import static net.bytebuddy.matcher.ElementMatchers.takesArgument; import akka.http.scaladsl.model.Uri; import akka.http.scaladsl.server.PathMatcher; import akka.http.scaladsl.server.PathMatchers; +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import net.bytebuddy.asm.Advice; @@ -40,14 +43,16 @@ public static void onExit( @Advice.Argument(0) Uri.Path path, @Advice.Return PathMatcher.Matching result) { // result is either matched or unmatched, we only care about the matches + Context context = Java8BytecodeBridge.currentContext(); + AkkaRouteHolder routeHolder = AkkaRouteHolder.get(context); + if (routeHolder == null) { + return; + } if (result.getClass() == PathMatcher.Matched.class) { - if (PathMatchers.PathEnd$.class == pathMatcher.getClass()) { - AkkaRouteHolder.endMatched(); - return; - } + PathMatcher.Matched match = (PathMatcher.Matched) result; // if present use the matched path that was remembered in PathMatcherInstrumentation, // otherwise just use a * - String prefix = PathMatcherUtil.getMatched(pathMatcher); + String prefix = PREFIX.get(pathMatcher); if (prefix == null) { if (PathMatchers.Slash$.class == pathMatcher.getClass()) { prefix = "/"; @@ -55,9 +60,9 @@ public static void onExit( prefix = "*"; } } - if (prefix != null) { - AkkaRouteHolder.push(prefix); - } + routeHolder.push(path, match.pathRest(), prefix); + } else { + routeHolder.didNotMatch(); } } } 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 eb13f402bc1e..661d9e5793b0 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 @@ -5,63 +5,41 @@ package io.opentelemetry.javaagent.instrumentation.akkahttp.server.route; -import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; +import static net.bytebuddy.matcher.ElementMatchers.named; +import akka.http.scaladsl.server.RequestContext; +import akka.http.scaladsl.server.RouteResult; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import net.bytebuddy.asm.Advice; +import net.bytebuddy.asm.Advice.AssignReturned; +import net.bytebuddy.asm.Advice.AssignReturned.ToArguments.ToArgument; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; +import scala.Function1; +import scala.concurrent.Future; 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"); + return named("akka.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation"); } @Override public void transform(TypeTransformer transformer) { - transformer.applyAdviceToMethod( - 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"); + transformer.applyAdviceToMethod(isConstructor(), this.getClass().getName() + "$ApplyAdvice"); + transformer.applyAdviceToMethod(named("$tilde"), this.getClass().getName() + "$ApplyAdvice"); } @SuppressWarnings("unused") public static class ApplyAdvice { + @AssignReturned.ToArguments(@ToArgument(0)) @Advice.OnMethodEnter(suppress = Throwable.class) - public static void onEnter() { - // when routing dsl uses concat(path(...) {...}, path(...) {...}) we'll restore the currently - // matched route after each matcher so that match attempts that failed wouldn't get recorded - // in the route - AkkaRouteHolder.save(); - } - - @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) - public static void onExit() { - AkkaRouteHolder.restore(); - } - } - - @SuppressWarnings("unused") - public static class Apply2Advice { - - @Advice.OnMethodEnter(suppress = Throwable.class) - public static void onEnter() { - AkkaRouteHolder.reset(); + public static Object onEnter( + @Advice.Argument(0) Function1> route) { + return new AkkaRouteWrapper(route); } } } 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 index 3920c6aeb15e..732018a4a722 100644 --- 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 @@ -7,6 +7,7 @@ package io.opentelemetry.javaagent.instrumentation.akkahttp import akka.actor.ActorSystem import akka.http.scaladsl.Http +import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Directives.{ IntNumber, complete, @@ -14,7 +15,9 @@ import akka.http.scaladsl.server.Directives.{ path, pathEndOrSingleSlash, pathPrefix, - pathSingleSlash + pathSingleSlash, + JavaUUID, + Segment } import akka.http.scaladsl.server.Route import akka.stream.ActorMaterializer @@ -79,6 +82,51 @@ class AkkaHttpServerRouteTest { test(route, "/test/1", "GET /test/*") } + @Test def testRouteWithUUID(): Unit = { + val route = + pathPrefix("foo") { + pathPrefix("api") { + pathPrefix("v2") { + pathPrefix("bar") { + path(JavaUUID) { _ => + complete("ok") + } + } + } + } + } + + test( + route, + "/foo/api/v2/bar/5bb7c7d8-0128-4349-86af-fe718f4f8059", + "GET /foo/api/v2/bar/*" + ) + } + + @Test def testRouteWithSegment(): Unit = { + val route = + pathPrefix("api") { + pathPrefix("v2") { + pathPrefix("orders") { + path(Segment) { _ => + complete("ok") + } + } + } + } + + test(route, "/api/v2/orders/order123", "GET /api/v2/orders/*") + } + + @Test def testRouteWithSubSegment(): Unit = { + val route = + pathPrefix("api" / "v2" / "orders" / Segment / "status") { _ => + complete("ok") + } + + test(route, "/api/v2/orders/order123/status", "GET /api/v2/orders/*/status") + } + def test(route: Route, path: String, spanName: String): Unit = { val port = PortUtils.findOpenPort val address: URI = buildAddress(port) From e7cbd06e93c27f6bbf7c207ab8015398866bf181 Mon Sep 17 00:00:00 2001 From: Lauri Tulmin Date: Fri, 5 Dec 2025 10:18:17 +0200 Subject: [PATCH 2/3] review --- ...lderSingletons.java => AkkaRouteUtil.java} | 4 +- .../server/route/AkkaRouteWrapper.java | 3 +- .../route/PathMatcherInstrumentation.java | 2 +- .../PathMatcherStaticInstrumentation.java | 2 +- .../server/route/PathMatcherUtil.java | 25 --- .../akkahttp/AkkaHttpServerRouteTest.scala | 173 ++++++++++++++++-- 6 files changed, 160 insertions(+), 49 deletions(-) rename instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/{AkkaRouteHolderSingletons.java => AkkaRouteUtil.java} (82%) delete mode 100644 instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathMatcherUtil.java diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaRouteHolderSingletons.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaRouteUtil.java similarity index 82% rename from instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaRouteHolderSingletons.java rename to instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaRouteUtil.java index a8ad79df4233..041e76253bf1 100644 --- a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaRouteHolderSingletons.java +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaRouteUtil.java @@ -8,10 +8,10 @@ import akka.http.scaladsl.server.PathMatcher; import io.opentelemetry.instrumentation.api.util.VirtualField; -public final class AkkaRouteHolderSingletons { +public final class AkkaRouteUtil { public static final VirtualField, String> PREFIX = VirtualField.find(PathMatcher.class, String.class); - private AkkaRouteHolderSingletons() {} + private AkkaRouteUtil() {} } diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaRouteWrapper.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaRouteWrapper.java index dfb475a377f5..3004daf2d19e 100644 --- a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaRouteWrapper.java +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaRouteWrapper.java @@ -8,7 +8,6 @@ import akka.http.scaladsl.server.RequestContext; import akka.http.scaladsl.server.RouteResult; import io.opentelemetry.context.Context; -import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; import scala.Function1; import scala.concurrent.Future; import scala.runtime.AbstractFunction1; @@ -22,7 +21,7 @@ public AkkaRouteWrapper(Function1> route) { @Override public Future apply(RequestContext ctx) { - Context context = Java8BytecodeBridge.currentContext(); + Context context = Context.current(); AkkaRouteHolder routeHolder = AkkaRouteHolder.get(context); if (routeHolder == null) { return route.apply(ctx); diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathMatcherInstrumentation.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathMatcherInstrumentation.java index 6ca3c70d3a08..4caf2b27c62c 100644 --- a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathMatcherInstrumentation.java +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathMatcherInstrumentation.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.akkahttp.server.route; -import static io.opentelemetry.javaagent.instrumentation.akkahttp.server.route.AkkaRouteHolderSingletons.PREFIX; +import static io.opentelemetry.javaagent.instrumentation.akkahttp.server.route.AkkaRouteUtil.PREFIX; import static net.bytebuddy.matcher.ElementMatchers.named; import static net.bytebuddy.matcher.ElementMatchers.returns; import static net.bytebuddy.matcher.ElementMatchers.takesArgument; diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathMatcherStaticInstrumentation.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathMatcherStaticInstrumentation.java index c46e67cdc585..872ddcc4d349 100644 --- a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathMatcherStaticInstrumentation.java +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathMatcherStaticInstrumentation.java @@ -6,7 +6,7 @@ package io.opentelemetry.javaagent.instrumentation.akkahttp.server.route; import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; -import static io.opentelemetry.javaagent.instrumentation.akkahttp.server.route.AkkaRouteHolderSingletons.PREFIX; +import static io.opentelemetry.javaagent.instrumentation.akkahttp.server.route.AkkaRouteUtil.PREFIX; import static net.bytebuddy.matcher.ElementMatchers.named; import static net.bytebuddy.matcher.ElementMatchers.takesArgument; diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathMatcherUtil.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathMatcherUtil.java deleted file mode 100644 index 2f320ff499f8..000000000000 --- a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathMatcherUtil.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.akkahttp.server.route; - -import akka.http.scaladsl.server.PathMatcher; -import io.opentelemetry.instrumentation.api.util.VirtualField; - -public class PathMatcherUtil { - - private static final VirtualField, String> PATH_MATCHER_ROUTE = - VirtualField.find(PathMatcher.class, String.class); - - public static void setMatched(PathMatcher matcher, String route) { - PATH_MATCHER_ROUTE.set(matcher, route); - } - - public static String getMatched(PathMatcher matcher) { - return PATH_MATCHER_ROUTE.get(matcher); - } - - private PathMatcherUtil() {} -} 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 index 732018a4a722..75eb6f137166 100644 --- 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 @@ -8,17 +8,6 @@ package io.opentelemetry.javaagent.instrumentation.akkahttp import akka.actor.ActorSystem import akka.http.scaladsl.Http import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server.Directives.{ - IntNumber, - complete, - concat, - path, - pathEndOrSingleSlash, - pathPrefix, - pathSingleSlash, - JavaUUID, - Segment -} import akka.http.scaladsl.server.Route import akka.stream.ActorMaterializer import io.opentelemetry.instrumentation.test.utils.PortUtils @@ -32,6 +21,8 @@ import io.opentelemetry.testing.internal.armeria.common.{ import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.{AfterAll, Test, TestInstance} import org.junit.jupiter.api.extension.RegisterExtension +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource import java.net.{URI, URISyntaxException} import java.util.function.Consumer @@ -62,24 +53,164 @@ class AkkaHttpServerRouteTest { test(route, "/test", "GET /test") } - @Test def testRoute(): Unit = { + @Test def testPathPrefix(): Unit = { + val route = + pathPrefix("a") { + pathPrefix("b") { + path("c") { + complete("ok") + } + } + } + + test(route, "/a/b/c", "GET /a/b/c") + } + + @Test def testTrailingSlash(): Unit = { + val route = path("foo"./) { complete("ok") } + test(route, "/foo/", "GET /foo/") + } + + @Test def testSlash(): Unit = { + val route = path("foo" / "bar") { complete("ok") } + test(route, "/foo/bar", "GET /foo/bar") + } + + @Test def testEncodedSlash(): Unit = { + val route = path("foo/bar") { complete("ok") } + test(route, "/foo%2Fbar", "GET /foo%2Fbar") + } + + @Test def testSeparateOnSlashes(): Unit = { + val route = path(separateOnSlashes("foo/bar")) { complete("ok") } + test(route, "/foo/bar", "GET /foo/bar") + } + + @Test def testMatchRegex(): Unit = { + val route = path("foo" / """number-\d+""".r) { _ => complete("ok") } + test(route, "/foo/number-123", "GET /foo/*") + } + + @ParameterizedTest + @CsvSource( + Array( + "/i42, GET /i*", + "/hCAFE, GET /h*" + ) + ) + def testPipe(requestPath: String, expectedName: String): Unit = { + val route = path("i" ~ IntNumber | "h" ~ HexIntNumber) { _ => + complete("ok") + } + test(route, requestPath, expectedName) + } + + @ParameterizedTest + @CsvSource( + Array( + "/colours/red, GET /colours/red", + "/colours/green, GET /colours/green" + ) + ) + def testMapExtractor(requestPath: String, expectedName: String): Unit = { + val route = path("colours" / Map("red" -> 1, "green" -> 2, "blue" -> 3)) { + _ => complete("ok") + } + test(route, requestPath, expectedName) + } + + @ParameterizedTest + @CsvSource( + Array( + "/fooish, GET /foo*", + "/fooish/123, GET /foo*" + ) + ) + def testNotMatch(requestPath: String, expectedName: String): Unit = { + val route = pathPrefix("foo" ~ !"bar") { complete("ok") } + test(route, requestPath, expectedName) + } + + @Test def testProvide(): Unit = { + val route = pathPrefix("foo") { + provide("hi") { _ => + path("bar") { + complete("ok") + } + } + } + test(route, "/foo/bar", "GET /foo/bar") + } + + @ParameterizedTest + @CsvSource( + Array( + "/foo/bar/X42/edit, GET /foo/bar/X*/edit", + "/foo/bar/X/edit, GET /foo/bar/X/edit" + ) + ) + def testOptional(requestPath: String, expectedName: String): Unit = { + val route = path("foo" / "bar" / "X" ~ IntNumber.? / ("edit" | "create")) { + _ => complete("ok") + } + test(route, requestPath, expectedName) + } + + @Test def testNoMatches(): Unit = { + val route = path("foo" / "bar") { complete("ok") } + test( + route, + "/foo/wrong", + "GET", + 404, + "The requested resource could not be found." + ) + } + + @Test def testError(): Unit = { + val route = path("foo" / IntNumber) { _ => + failWith(new RuntimeException("oops")) + } + test( + route, + "/foo/123", + "GET /foo/*", + 500, + "There was an internal server error." + ) + } + + @Test def testConcat(): Unit = { val route = concat( pathEndOrSingleSlash { complete("root") }, + path(".+".r / "wrong1") { _ => + complete("wrong1") + }, pathPrefix("test") { concat( pathSingleSlash { complete("test") }, - path(IntNumber) { _ => - complete("ok") + pathPrefix("foo") { + concat( + path(IntNumber) { _ => + complete("ok") + } + ) + }, + path("something-else") { + complete("test") } ) + }, + path("test" / "wrong2") { + complete("wrong2") } ) - test(route, "/test/1", "GET /test/*") + test(route, "/test/foo/1", "GET /test/foo/*") } @Test def testRouteWithUUID(): Unit = { @@ -127,7 +258,13 @@ class AkkaHttpServerRouteTest { test(route, "/api/v2/orders/order123/status", "GET /api/v2/orders/*/status") } - def test(route: Route, path: String, spanName: String): Unit = { + def test( + route: Route, + path: String, + spanName: String, + expectedStatus: Int = 200, + expectedMsg: String = "ok" + ): Unit = { val port = PortUtils.findOpenPort val address: URI = buildAddress(port) val binding = @@ -138,8 +275,8 @@ class AkkaHttpServerRouteTest { address.resolve(path).toString ) val response = client.execute(request).aggregate.join - assertThat(response.status.code).isEqualTo(200) - assertThat(response.contentUtf8).isEqualTo("ok") + assertThat(response.status.code).isEqualTo(expectedStatus) + assertThat(response.contentUtf8).isEqualTo(expectedMsg) testing.waitAndAssertTraces(new Consumer[TraceAssert] { override def accept(trace: TraceAssert): Unit = From f3457bb0d7d62d30922390e2444d2cd6c2613eb4 Mon Sep 17 00:00:00 2001 From: Lauri Tulmin Date: Fri, 5 Dec 2025 10:26:53 +0200 Subject: [PATCH 3/3] review --- .../akkahttp/client/HttpExtClientInstrumentation.java | 5 ++--- .../instrumentation/akkahttp/server/AkkaFlowWrapper.java | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/HttpExtClientInstrumentation.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/HttpExtClientInstrumentation.java index dde25f95739b..9bc6aec0a0b2 100644 --- a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/HttpExtClientInstrumentation.java +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/HttpExtClientInstrumentation.java @@ -5,7 +5,6 @@ package io.opentelemetry.javaagent.instrumentation.akkahttp.client; -import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; import static io.opentelemetry.javaagent.instrumentation.akkahttp.client.AkkaHttpClientSingletons.instrumenter; import static io.opentelemetry.javaagent.instrumentation.akkahttp.client.AkkaHttpClientSingletons.setter; import static net.bytebuddy.matcher.ElementMatchers.named; @@ -56,7 +55,7 @@ private AdviceScope(Context context, Scope scope) { } public static AdviceScope start(HttpRequest request) { - Context parentContext = currentContext(); + Context parentContext = Context.current(); if (!instrumenter().shouldStart(parentContext, request)) { return null; } @@ -83,7 +82,7 @@ public Future end( if (throwable == null) { responseFuture.onComplete( new OnCompleteHandler(context, request), actorSystem.dispatcher()); - return FutureWrapper.wrap(responseFuture, actorSystem.dispatcher(), currentContext()); + return FutureWrapper.wrap(responseFuture, actorSystem.dispatcher(), Context.current()); } else { instrumenter().end(context, request, null, throwable); } diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaFlowWrapper.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaFlowWrapper.java index 4500aea45201..3259209f014f 100644 --- a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaFlowWrapper.java +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaFlowWrapper.java @@ -5,7 +5,6 @@ package io.opentelemetry.javaagent.instrumentation.akkahttp.server; -import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; import static io.opentelemetry.javaagent.instrumentation.akkahttp.server.AkkaHttpServerSingletons.errorResponse; import static io.opentelemetry.javaagent.instrumentation.akkahttp.server.AkkaHttpServerSingletons.instrumenter; @@ -118,7 +117,7 @@ public void onPush() { HttpRequest request = grab(requestIn); TracingRequest tracingRequest = TracingRequest.EMPTY; - Context parentContext = currentContext(); + Context parentContext = Context.current(); if (instrumenter().shouldStart(parentContext, request)) { Context context = instrumenter().start(parentContext, request); context = AkkaRouteHolder.init(context);