Skip to content

Commit 26be8a0

Browse files
fix(akka): improve handling of http.route
1 parent f693cc4 commit 26be8a0

File tree

10 files changed

+202
-132
lines changed

10 files changed

+202
-132
lines changed

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import akka.stream.stage.GraphStageLogic;
2424
import akka.stream.stage.OutHandler;
2525
import io.opentelemetry.context.Context;
26+
import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute;
27+
import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource;
2628
import io.opentelemetry.javaagent.bootstrap.http.HttpServerResponseCustomizerHolder;
2729
import io.opentelemetry.javaagent.instrumentation.akkahttp.server.route.AkkaRouteHolder;
2830
import java.util.ArrayDeque;
@@ -160,6 +162,15 @@ public void onPush() {
160162
response = (HttpResponse) response.addHeaders(headers);
161163
}
162164

165+
AkkaRouteHolder routeHolder = AkkaRouteHolder.get(tracingRequest.context);
166+
if (routeHolder != null) {
167+
routeHolder.pushIfNotCompletelyMatched("*");
168+
HttpServerRoute.update(
169+
tracingRequest.context,
170+
HttpServerRouteSource.CONTROLLER,
171+
routeHolder.route());
172+
}
173+
163174
instrumenter().end(tracingRequest.context, tracingRequest.request, response, null);
164175
}
165176
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 AkkaRouteHolderSingletons {
12+
13+
public static final VirtualField<PathMatcher<?>, String> PREFIX =
14+
VirtualField.find(PathMatcher.class, String.class);
15+
16+
private AkkaRouteHolderSingletons() {}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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 io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge;
12+
import scala.Function1;
13+
import scala.concurrent.Future;
14+
import scala.runtime.AbstractFunction1;
15+
16+
public class AkkaRouteWrapper extends AbstractFunction1<RequestContext, Future<RouteResult>> {
17+
private final Function1<RequestContext, Future<RouteResult>> route;
18+
19+
public AkkaRouteWrapper(Function1<RequestContext, Future<RouteResult>> route) {
20+
this.route = route;
21+
}
22+
23+
@Override
24+
public Future<RouteResult> apply(RequestContext ctx) {
25+
Context context = Java8BytecodeBridge.currentContext();
26+
AkkaRouteHolder routeHolder = AkkaRouteHolder.get(context);
27+
if (routeHolder == null) {
28+
return route.apply(ctx);
29+
} else {
30+
routeHolder.save();
31+
return route
32+
.apply(ctx)
33+
.map(
34+
new AbstractFunction1<RouteResult, RouteResult>() {
35+
@Override
36+
public RouteResult apply(RouteResult result) {
37+
if (result.getClass() == RouteResult.Rejected.class) {
38+
routeHolder.restore();
39+
}
40+
return result;
41+
}
42+
},
43+
ctx.executionContext());
44+
}
45+
}
46+
}

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.AkkaRouteHolderSingletons.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.AkkaRouteHolderSingletons.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)