Skip to content

Commit 1225eb8

Browse files
samwrightlaurit
andauthored
Capture http.route for pekko-http (#10799)
Co-authored-by: Lauri Tulmin <[email protected]>
1 parent 0437211 commit 1225eb8

12 files changed

+486
-44
lines changed

docs/supported-libraries.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ These are the supported libraries and frameworks:
3434
| [Apache Kafka Streams API](https://kafka.apache.org/documentation/streams/) | 0.11+ | N/A | [Messaging Spans] |
3535
| [Apache MyFaces](https://myfaces.apache.org/) | 1.2+ (not including 3.x yet) | N/A | Provides `http.route` [2], Controller Spans [3] |
3636
| [Apache Pekko Actors](https://pekko.apache.org/) | 1.0+ | N/A | Context propagation |
37-
| [Apache Pekko HTTP](https://pekko.apache.org/) | 1.0+ | N/A | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics] |
37+
| [Apache Pekko HTTP](https://pekko.apache.org/) | 1.0+ | N/A | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics], Provides `http.route` [2] |
3838
| [Apache Pulsar](https://pulsar.apache.org/) | 2.8+ | N/A | [Messaging Spans] |
3939
| [Apache RocketMQ gRPC/Protobuf-based Client](https://rocketmq.apache.org/) | 5.0+ | N/A | [Messaging Spans] |
4040
| [Apache RocketMQ Remoting-based Client](https://rocketmq.apache.org/) | 4.8+ | [opentelemetry-rocketmq-client-4.8](../instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library) | [Messaging Spans] |

instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoFlowWrapper.java

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import io.opentelemetry.context.Context;
1111
import io.opentelemetry.javaagent.bootstrap.http.HttpServerResponseCustomizerHolder;
12+
import io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.route.PekkoRouteHolder;
1213
import java.util.ArrayDeque;
1314
import java.util.Deque;
1415
import java.util.List;
@@ -117,6 +118,7 @@ public void onPush() {
117118
if (PekkoHttpServerSingletons.instrumenter().shouldStart(parentContext, request)) {
118119
Context context =
119120
PekkoHttpServerSingletons.instrumenter().start(parentContext, request);
121+
context = PekkoRouteHolder.init(context);
120122
tracingRequest = new TracingRequest(context, request);
121123
}
122124
// event if span wasn't started we need to push TracingRequest to match response

instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoHttpServerInstrumentationModule.java

+7
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
2727
return hasClassesNamed("org.apache.pekko.http.scaladsl.HttpExt");
2828
}
2929

30+
@Override
31+
public boolean isIndyModule() {
32+
// PekkoHttpServerInstrumentationModule and PekkoHttpServerRouteInstrumentationModule share
33+
// PekkoRouteHolder class
34+
return false;
35+
}
36+
3037
@Override
3138
public List<TypeInstrumentation> typeInstrumentations() {
3239
return asList(new HttpExtServerInstrumentation(), new GraphInterpreterInstrumentation());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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.route;
7+
8+
import static net.bytebuddy.matcher.ElementMatchers.namedOneOf;
9+
10+
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
11+
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
12+
import net.bytebuddy.asm.Advice;
13+
import net.bytebuddy.description.type.TypeDescription;
14+
import net.bytebuddy.matcher.ElementMatcher;
15+
16+
public class PathConcatenationInstrumentation implements TypeInstrumentation {
17+
@Override
18+
public ElementMatcher<TypeDescription> typeMatcher() {
19+
return namedOneOf(
20+
"org.apache.pekko.http.scaladsl.server.PathMatcher$$anonfun$$tilde$1",
21+
"org.apache.pekko.http.scaladsl.server.PathMatcher");
22+
}
23+
24+
@Override
25+
public void transform(TypeTransformer transformer) {
26+
transformer.applyAdviceToMethod(
27+
namedOneOf("apply", "$anonfun$append$1"), this.getClass().getName() + "$ApplyAdvice");
28+
}
29+
30+
@SuppressWarnings("unused")
31+
public static class ApplyAdvice {
32+
33+
@Advice.OnMethodEnter(suppress = Throwable.class)
34+
public static void onEnter() {
35+
// https://github.com/apache/incubator-pekko-http/blob/bea7d2b5c21e23d55556409226d136c282da27a3/http/src/main/scala/org/apache/pekko/http/scaladsl/server/PathMatcher.scala#L53
36+
// https://github.com/apache/incubator-pekko-http/blob/bea7d2b5c21e23d55556409226d136c282da27a3/http/src/main/scala/org/apache/pekko/http/scaladsl/server/PathMatcher.scala#L57
37+
// when routing dsl uses path("path1" / "path2") we are concatenating 3 segments "path1" and /
38+
// and "path2" we need to notify the matcher that a new segment has started, so it could be
39+
// captured in the route
40+
PekkoRouteHolder.startSegment();
41+
}
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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.route;
7+
8+
import static net.bytebuddy.matcher.ElementMatchers.named;
9+
import static net.bytebuddy.matcher.ElementMatchers.returns;
10+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
11+
12+
import io.opentelemetry.instrumentation.api.util.VirtualField;
13+
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
14+
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
15+
import net.bytebuddy.asm.Advice;
16+
import net.bytebuddy.description.type.TypeDescription;
17+
import net.bytebuddy.matcher.ElementMatcher;
18+
import org.apache.pekko.http.scaladsl.model.Uri;
19+
import org.apache.pekko.http.scaladsl.server.PathMatcher;
20+
21+
public class PathMatcherInstrumentation implements TypeInstrumentation {
22+
@Override
23+
public ElementMatcher<TypeDescription> typeMatcher() {
24+
return named("org.apache.pekko.http.scaladsl.server.PathMatcher$");
25+
}
26+
27+
@Override
28+
public void transform(TypeTransformer transformer) {
29+
transformer.applyAdviceToMethod(
30+
named("apply")
31+
.and(takesArgument(0, named("org.apache.pekko.http.scaladsl.model.Uri$Path")))
32+
.and(returns(named("org.apache.pekko.http.scaladsl.server.PathMatcher"))),
33+
this.getClass().getName() + "$ApplyAdvice");
34+
}
35+
36+
@SuppressWarnings("unused")
37+
public static class ApplyAdvice {
38+
39+
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
40+
public static void onEnter(
41+
@Advice.Argument(0) Uri.Path prefix, @Advice.Return PathMatcher<?> result) {
42+
// store the path being matched inside a VirtualField on the given matcher, so it can be used
43+
// for constructing the route
44+
VirtualField.find(PathMatcher.class, String.class).set(result, prefix.toString());
45+
}
46+
}
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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.route;
7+
8+
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass;
9+
import static net.bytebuddy.matcher.ElementMatchers.named;
10+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
11+
12+
import io.opentelemetry.instrumentation.api.util.VirtualField;
13+
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
14+
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
15+
import net.bytebuddy.asm.Advice;
16+
import net.bytebuddy.description.type.TypeDescription;
17+
import net.bytebuddy.matcher.ElementMatcher;
18+
import org.apache.pekko.http.scaladsl.model.Uri;
19+
import org.apache.pekko.http.scaladsl.server.PathMatcher;
20+
import org.apache.pekko.http.scaladsl.server.PathMatchers;
21+
import org.apache.pekko.http.scaladsl.server.PathMatchers$;
22+
23+
public class PathMatcherStaticInstrumentation implements TypeInstrumentation {
24+
@Override
25+
public ElementMatcher<TypeDescription> typeMatcher() {
26+
return extendsClass(named("org.apache.pekko.http.scaladsl.server.PathMatcher"));
27+
}
28+
29+
@Override
30+
public void transform(TypeTransformer transformer) {
31+
transformer.applyAdviceToMethod(
32+
named("apply")
33+
.and(takesArgument(0, named("org.apache.pekko.http.scaladsl.model.Uri$Path"))),
34+
this.getClass().getName() + "$ApplyAdvice");
35+
}
36+
37+
@SuppressWarnings("unused")
38+
public static class ApplyAdvice {
39+
40+
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
41+
public static void onExit(
42+
@Advice.This PathMatcher<?> pathMatcher,
43+
@Advice.Argument(0) Uri.Path path,
44+
@Advice.Return PathMatcher.Matching<?> result) {
45+
// result is either matched or unmatched, we only care about the matches
46+
if (result.getClass() == PathMatcher.Matched.class) {
47+
if (PathMatchers$.PathEnd$.class == pathMatcher.getClass()) {
48+
PekkoRouteHolder.endMatched();
49+
return;
50+
}
51+
// if present use the matched path that was remembered in PathMatcherInstrumentation,
52+
// otherwise just use a *
53+
String prefix = VirtualField.find(PathMatcher.class, String.class).get(pathMatcher);
54+
if (prefix == null) {
55+
if (PathMatchers.Slash$.class == pathMatcher.getClass()) {
56+
prefix = "/";
57+
} else {
58+
prefix = "*";
59+
}
60+
}
61+
if (prefix != null) {
62+
PekkoRouteHolder.push(prefix);
63+
}
64+
}
65+
}
66+
}
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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.route;
7+
8+
import static java.util.Arrays.asList;
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 java.util.List;
14+
15+
/**
16+
* This instrumentation applies to classes in pekko-http.jar while
17+
* PekkoHttpServerInstrumentationModule applies to classes in pekko-http-core.jar
18+
*/
19+
@AutoService(InstrumentationModule.class)
20+
public class PekkoHttpServerRouteInstrumentationModule extends InstrumentationModule {
21+
public PekkoHttpServerRouteInstrumentationModule() {
22+
super("pekko-http", "pekko-http-1.0", "pekko-http-server", "pekko-http-server-route");
23+
}
24+
25+
@Override
26+
public boolean isIndyModule() {
27+
// PekkoHttpServerInstrumentationModule and PekkoHttpServerRouteInstrumentationModule share
28+
// PekkoRouteHolder class
29+
return false;
30+
}
31+
32+
@Override
33+
public List<TypeInstrumentation> typeInstrumentations() {
34+
return asList(
35+
new PathMatcherInstrumentation(),
36+
new PathMatcherStaticInstrumentation(),
37+
new RouteConcatenationInstrumentation(),
38+
new PathConcatenationInstrumentation());
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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.route;
7+
8+
import static io.opentelemetry.context.ContextKey.named;
9+
10+
import io.opentelemetry.context.Context;
11+
import io.opentelemetry.context.ContextKey;
12+
import io.opentelemetry.context.ImplicitContextKeyed;
13+
import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute;
14+
import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource;
15+
import java.util.ArrayDeque;
16+
import java.util.Deque;
17+
18+
public class PekkoRouteHolder implements ImplicitContextKeyed {
19+
private static final ContextKey<PekkoRouteHolder> KEY = named("opentelemetry-pekko-route");
20+
21+
private String route = "";
22+
private boolean newSegment;
23+
private boolean endMatched;
24+
private final Deque<String> stack = new ArrayDeque<>();
25+
26+
public static Context init(Context context) {
27+
if (context.get(KEY) != null) {
28+
return context;
29+
}
30+
return context.with(new PekkoRouteHolder());
31+
}
32+
33+
public static void push(String path) {
34+
PekkoRouteHolder holder = Context.current().get(KEY);
35+
if (holder != null && holder.newSegment && !holder.endMatched) {
36+
holder.route += path;
37+
holder.newSegment = false;
38+
}
39+
}
40+
41+
public static void startSegment() {
42+
PekkoRouteHolder holder = Context.current().get(KEY);
43+
if (holder != null) {
44+
holder.newSegment = true;
45+
}
46+
}
47+
48+
public static void endMatched() {
49+
Context context = Context.current();
50+
PekkoRouteHolder holder = context.get(KEY);
51+
if (holder != null) {
52+
holder.endMatched = true;
53+
HttpServerRoute.update(context, HttpServerRouteSource.CONTROLLER, holder.route);
54+
}
55+
}
56+
57+
public static void save() {
58+
PekkoRouteHolder holder = Context.current().get(KEY);
59+
if (holder != null) {
60+
holder.stack.push(holder.route);
61+
holder.newSegment = true;
62+
}
63+
}
64+
65+
public static void restore() {
66+
PekkoRouteHolder holder = Context.current().get(KEY);
67+
if (holder != null) {
68+
holder.route = holder.stack.pop();
69+
holder.newSegment = true;
70+
}
71+
}
72+
73+
@Override
74+
public Context storeInContext(Context context) {
75+
return context.with(KEY, this);
76+
}
77+
78+
private PekkoRouteHolder() {}
79+
}
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.pekkohttp.v1_0.server.route;
7+
8+
import static net.bytebuddy.matcher.ElementMatchers.namedOneOf;
9+
10+
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
11+
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
12+
import net.bytebuddy.asm.Advice;
13+
import net.bytebuddy.description.type.TypeDescription;
14+
import net.bytebuddy.matcher.ElementMatcher;
15+
16+
public class RouteConcatenationInstrumentation implements TypeInstrumentation {
17+
@Override
18+
public ElementMatcher<TypeDescription> typeMatcher() {
19+
return namedOneOf(
20+
"org.apache.pekko.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation$$anonfun$$tilde$1",
21+
"org.apache.pekko.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation");
22+
}
23+
24+
@Override
25+
public void transform(TypeTransformer transformer) {
26+
transformer.applyAdviceToMethod(
27+
namedOneOf("apply", "$anonfun$$tilde$1"), this.getClass().getName() + "$ApplyAdvice");
28+
}
29+
30+
@SuppressWarnings("unused")
31+
public static class ApplyAdvice {
32+
33+
@Advice.OnMethodEnter(suppress = Throwable.class)
34+
public static void onEnter() {
35+
// when routing dsl uses concat(path(...) {...}, path(...) {...}) we'll restore the currently
36+
// matched route after each matcher so that match attempts that failed wouldn't get recorded
37+
// in the route
38+
PekkoRouteHolder.save();
39+
}
40+
41+
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
42+
public static void onExit() {
43+
PekkoRouteHolder.restore();
44+
}
45+
}
46+
}

instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/AbstractHttpServerInstrumentationTest.scala

+8-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import io.opentelemetry.instrumentation.testing.junit.http.{
1111
HttpServerTestOptions,
1212
ServerEndpoint
1313
}
14+
import io.opentelemetry.semconv.SemanticAttributes
1415

1516
import java.util
1617
import java.util.Collections
@@ -25,8 +26,13 @@ abstract class AbstractHttpServerInstrumentationTest
2526
options.setTestCaptureHttpHeaders(false)
2627
options.setHttpAttributes(
2728
new Function[ServerEndpoint, util.Set[AttributeKey[_]]] {
28-
override def apply(v1: ServerEndpoint): util.Set[AttributeKey[_]] =
29-
Collections.emptySet()
29+
override def apply(v1: ServerEndpoint): util.Set[AttributeKey[_]] = {
30+
val set = new util.HashSet[AttributeKey[_]](
31+
HttpServerTestOptions.DEFAULT_HTTP_ATTRIBUTES
32+
)
33+
set.remove(SemanticAttributes.HTTP_ROUTE)
34+
set
35+
}
3036
}
3137
)
3238
options.setHasResponseCustomizer(

0 commit comments

Comments
 (0)