Skip to content

Commit e900aee

Browse files
droidnxsobs-gh-abhishekraolaurit
authored
fix(akka): improve handling of http.route (#15504)
Co-authored-by: Abhishek Rao <[email protected]> Co-authored-by: Lauri Tulmin <[email protected]>
1 parent 8069f2e commit e900aee

File tree

12 files changed

+356
-177
lines changed

12 files changed

+356
-177
lines changed

instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/HttpExtClientInstrumentation.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
package io.opentelemetry.javaagent.instrumentation.akkahttp.client;
77

8-
import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext;
98
import static io.opentelemetry.javaagent.instrumentation.akkahttp.client.AkkaHttpClientSingletons.instrumenter;
109
import static io.opentelemetry.javaagent.instrumentation.akkahttp.client.AkkaHttpClientSingletons.setter;
1110
import static net.bytebuddy.matcher.ElementMatchers.named;
@@ -56,7 +55,7 @@ private AdviceScope(Context context, Scope scope) {
5655
}
5756

5857
public static AdviceScope start(HttpRequest request) {
59-
Context parentContext = currentContext();
58+
Context parentContext = Context.current();
6059
if (!instrumenter().shouldStart(parentContext, request)) {
6160
return null;
6261
}
@@ -83,7 +82,7 @@ public Future<HttpResponse> end(
8382
if (throwable == null) {
8483
responseFuture.onComplete(
8584
new OnCompleteHandler(context, request), actorSystem.dispatcher());
86-
return FutureWrapper.wrap(responseFuture, actorSystem.dispatcher(), currentContext());
85+
return FutureWrapper.wrap(responseFuture, actorSystem.dispatcher(), Context.current());
8786
} else {
8887
instrumenter().end(context, request, null, throwable);
8988
}

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
package io.opentelemetry.javaagent.instrumentation.akkahttp.server;
77

8-
import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext;
98
import static io.opentelemetry.javaagent.instrumentation.akkahttp.server.AkkaHttpServerSingletons.errorResponse;
109
import static io.opentelemetry.javaagent.instrumentation.akkahttp.server.AkkaHttpServerSingletons.instrumenter;
1110

@@ -23,6 +22,8 @@
2322
import akka.stream.stage.GraphStageLogic;
2423
import akka.stream.stage.OutHandler;
2524
import io.opentelemetry.context.Context;
25+
import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute;
26+
import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource;
2627
import io.opentelemetry.javaagent.bootstrap.http.HttpServerResponseCustomizerHolder;
2728
import io.opentelemetry.javaagent.instrumentation.akkahttp.server.route.AkkaRouteHolder;
2829
import java.util.ArrayDeque;
@@ -116,7 +117,7 @@ public void onPush() {
116117
HttpRequest request = grab(requestIn);
117118

118119
TracingRequest tracingRequest = TracingRequest.EMPTY;
119-
Context parentContext = currentContext();
120+
Context parentContext = Context.current();
120121
if (instrumenter().shouldStart(parentContext, request)) {
121122
Context context = instrumenter().start(parentContext, request);
122123
context = AkkaRouteHolder.init(context);
@@ -160,6 +161,15 @@ public void onPush() {
160161
response = (HttpResponse) response.addHeaders(headers);
161162
}
162163

164+
AkkaRouteHolder routeHolder = AkkaRouteHolder.get(tracingRequest.context);
165+
if (routeHolder != null) {
166+
routeHolder.pushIfNotCompletelyMatched("*");
167+
HttpServerRoute.update(
168+
tracingRequest.context,
169+
HttpServerRouteSource.CONTROLLER,
170+
routeHolder.route());
171+
}
172+
163173
instrumenter().end(tracingRequest.context, tracingRequest.request, response, null);
164174
}
165175
push(responseOut, response);

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ public List<TypeInstrumentation> typeInstrumentations() {
3939
return asList(
4040
new PathMatcherInstrumentation(),
4141
new PathMatcherStaticInstrumentation(),
42-
new RouteConcatenationInstrumentation(),
43-
new PathConcatenationInstrumentation());
42+
new RouteConcatenationInstrumentation());
4443
}
4544
}

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

Lines changed: 48 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,20 @@
77

88
import static io.opentelemetry.context.ContextKey.named;
99

10+
import akka.http.scaladsl.model.Uri;
1011
import io.opentelemetry.context.Context;
1112
import io.opentelemetry.context.ContextKey;
1213
import io.opentelemetry.context.ImplicitContextKeyed;
13-
import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute;
14-
import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource;
1514
import java.util.ArrayDeque;
1615
import java.util.Deque;
1716

1817
public class AkkaRouteHolder implements ImplicitContextKeyed {
1918
private static final ContextKey<AkkaRouteHolder> KEY = named("opentelemetry-akka-route");
2019

21-
private String route = "";
22-
private boolean newSegment = true;
23-
private boolean endMatched;
24-
private final Deque<String> stack = new ArrayDeque<>();
20+
private StringBuilder route = new StringBuilder();
21+
private Uri.Path lastUnmatchedPath = null;
22+
private boolean lastWasMatched = false;
23+
private final Deque<State> savedStates = new ArrayDeque<>();
2524

2625
public static Context init(Context context) {
2726
if (context.get(KEY) != null) {
@@ -30,52 +29,51 @@ public static Context init(Context context) {
3029
return context.with(new AkkaRouteHolder());
3130
}
3231

33-
public static void push(String path) {
34-
AkkaRouteHolder holder = Context.current().get(KEY);
35-
if (holder != null && holder.newSegment && !holder.endMatched) {
36-
holder.route += path;
37-
holder.newSegment = false;
38-
}
32+
public static AkkaRouteHolder get(Context context) {
33+
return context.get(KEY);
3934
}
4035

41-
public static void startSegment() {
42-
AkkaRouteHolder holder = Context.current().get(KEY);
43-
if (holder != null) {
44-
holder.newSegment = true;
36+
public void push(Uri.Path beforeMatch, Uri.Path afterMatch, String pathToPush) {
37+
// Only accept the suggested 'pathToPush' if:
38+
// - either this is the first match, or
39+
// - the unmatched part of the path from the previous match is what the current match
40+
// acted upon. This avoids pushes from PathMatchers that compose other PathMatchers,
41+
// instead only accepting pushes from leaf-nodes in the PathMatcher hierarchy that actually
42+
// act on the path.
43+
// AND:
44+
// - some part of the path has now been matched by this matcher
45+
if ((lastUnmatchedPath == null || lastUnmatchedPath.equals(beforeMatch))
46+
&& !afterMatch.equals(beforeMatch)) {
47+
route.append(pathToPush);
48+
lastUnmatchedPath = afterMatch;
4549
}
50+
lastWasMatched = true;
4651
}
4752

48-
public static void endMatched() {
49-
Context context = Context.current();
50-
AkkaRouteHolder holder = context.get(KEY);
51-
if (holder != null) {
52-
holder.endMatched = true;
53-
HttpServerRoute.update(context, HttpServerRouteSource.CONTROLLER, holder.route);
54-
}
53+
public void didNotMatch() {
54+
lastWasMatched = false;
5555
}
5656

57-
public static void save() {
58-
AkkaRouteHolder holder = Context.current().get(KEY);
59-
if (holder != null) {
60-
holder.stack.push(holder.route);
61-
holder.newSegment = true;
57+
public void pushIfNotCompletelyMatched(String pathToPush) {
58+
if (lastUnmatchedPath != null && !lastUnmatchedPath.isEmpty()) {
59+
route.append(pathToPush);
6260
}
6361
}
6462

65-
public static void restore() {
66-
AkkaRouteHolder holder = Context.current().get(KEY);
67-
if (holder != null) {
68-
holder.route = holder.stack.pop();
69-
holder.newSegment = true;
70-
}
63+
public String route() {
64+
return lastWasMatched ? route.toString() : null;
65+
}
66+
67+
public void save() {
68+
savedStates.add(new State(lastUnmatchedPath, route));
69+
route = new StringBuilder(route);
7170
}
7271

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;
72+
public void restore() {
73+
State popped = savedStates.pollLast();
74+
if (popped != null) {
75+
lastUnmatchedPath = popped.lastUnmatchedPath;
76+
route = popped.route;
7977
}
8078
}
8179

@@ -85,4 +83,14 @@ public Context storeInContext(Context context) {
8583
}
8684

8785
private AkkaRouteHolder() {}
86+
87+
private static class State {
88+
private final Uri.Path lastUnmatchedPath;
89+
private final StringBuilder route;
90+
91+
private State(Uri.Path lastUnmatchedPath, StringBuilder route) {
92+
this.lastUnmatchedPath = lastUnmatchedPath;
93+
this.route = route;
94+
}
95+
}
8896
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.akkahttp.server.route;
7+
8+
import akka.http.scaladsl.server.PathMatcher;
9+
import io.opentelemetry.instrumentation.api.util.VirtualField;
10+
11+
public final class AkkaRouteUtil {
12+
13+
public static final VirtualField<PathMatcher<?>, String> PREFIX =
14+
VirtualField.find(PathMatcher.class, String.class);
15+
16+
private AkkaRouteUtil() {}
17+
}
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.akkahttp.server.route;
7+
8+
import akka.http.scaladsl.server.RequestContext;
9+
import akka.http.scaladsl.server.RouteResult;
10+
import io.opentelemetry.context.Context;
11+
import scala.Function1;
12+
import scala.concurrent.Future;
13+
import scala.runtime.AbstractFunction1;
14+
15+
public class AkkaRouteWrapper extends AbstractFunction1<RequestContext, Future<RouteResult>> {
16+
private final Function1<RequestContext, Future<RouteResult>> route;
17+
18+
public AkkaRouteWrapper(Function1<RequestContext, Future<RouteResult>> route) {
19+
this.route = route;
20+
}
21+
22+
@Override
23+
public Future<RouteResult> apply(RequestContext ctx) {
24+
Context context = Context.current();
25+
AkkaRouteHolder routeHolder = AkkaRouteHolder.get(context);
26+
if (routeHolder == null) {
27+
return route.apply(ctx);
28+
} else {
29+
routeHolder.save();
30+
return route
31+
.apply(ctx)
32+
.map(
33+
new AbstractFunction1<RouteResult, RouteResult>() {
34+
@Override
35+
public RouteResult apply(RouteResult result) {
36+
if (result.getClass() == RouteResult.Rejected.class) {
37+
routeHolder.restore();
38+
}
39+
return result;
40+
}
41+
},
42+
ctx.executionContext());
43+
}
44+
}
45+
}

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

Lines changed: 0 additions & 43 deletions
This file was deleted.

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package io.opentelemetry.javaagent.instrumentation.akkahttp.server.route;
77

8+
import static io.opentelemetry.javaagent.instrumentation.akkahttp.server.route.AkkaRouteUtil.PREFIX;
89
import static net.bytebuddy.matcher.ElementMatchers.named;
910
import static net.bytebuddy.matcher.ElementMatchers.returns;
1011
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
@@ -40,7 +41,7 @@ public static void onEnter(
4041
@Advice.Argument(0) Uri.Path prefix, @Advice.Return PathMatcher<?> result) {
4142
// store the path being matched inside a VirtualField on the given matcher, so it can be used
4243
// for constructing the route
43-
PathMatcherUtil.setMatched(result, prefix.toString());
44+
PREFIX.set(result, prefix.toString());
4445
}
4546
}
4647
}

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

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@
66
package io.opentelemetry.javaagent.instrumentation.akkahttp.server.route;
77

88
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass;
9+
import static io.opentelemetry.javaagent.instrumentation.akkahttp.server.route.AkkaRouteUtil.PREFIX;
910
import static net.bytebuddy.matcher.ElementMatchers.named;
1011
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
1112

1213
import akka.http.scaladsl.model.Uri;
1314
import akka.http.scaladsl.server.PathMatcher;
1415
import akka.http.scaladsl.server.PathMatchers;
16+
import io.opentelemetry.context.Context;
17+
import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge;
1518
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
1619
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
1720
import net.bytebuddy.asm.Advice;
@@ -40,24 +43,26 @@ public static void onExit(
4043
@Advice.Argument(0) Uri.Path path,
4144
@Advice.Return PathMatcher.Matching<?> result) {
4245
// result is either matched or unmatched, we only care about the matches
46+
Context context = Java8BytecodeBridge.currentContext();
47+
AkkaRouteHolder routeHolder = AkkaRouteHolder.get(context);
48+
if (routeHolder == null) {
49+
return;
50+
}
4351
if (result.getClass() == PathMatcher.Matched.class) {
44-
if (PathMatchers.PathEnd$.class == pathMatcher.getClass()) {
45-
AkkaRouteHolder.endMatched();
46-
return;
47-
}
52+
PathMatcher.Matched<?> match = (PathMatcher.Matched<?>) result;
4853
// if present use the matched path that was remembered in PathMatcherInstrumentation,
4954
// otherwise just use a *
50-
String prefix = PathMatcherUtil.getMatched(pathMatcher);
55+
String prefix = PREFIX.get(pathMatcher);
5156
if (prefix == null) {
5257
if (PathMatchers.Slash$.class == pathMatcher.getClass()) {
5358
prefix = "/";
5459
} else {
5560
prefix = "*";
5661
}
5762
}
58-
if (prefix != null) {
59-
AkkaRouteHolder.push(prefix);
60-
}
63+
routeHolder.push(path, match.pathRest(), prefix);
64+
} else {
65+
routeHolder.didNotMatch();
6166
}
6267
}
6368
}

0 commit comments

Comments
 (0)