Skip to content

Commit

Permalink
Capture http.route for pekko-http (#10799)
Browse files Browse the repository at this point in the history
Co-authored-by: Lauri Tulmin <tulmin@gmail.com>
  • Loading branch information
samwright and laurit committed Mar 12, 2024
1 parent 0437211 commit 1225eb8
Show file tree
Hide file tree
Showing 12 changed files with 486 additions and 44 deletions.
2 changes: 1 addition & 1 deletion docs/supported-libraries.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ These are the supported libraries and frameworks:
| [Apache Kafka Streams API](https://kafka.apache.org/documentation/streams/) | 0.11+ | N/A | [Messaging Spans] |
| [Apache MyFaces](https://myfaces.apache.org/) | 1.2+ (not including 3.x yet) | N/A | Provides `http.route` [2], Controller Spans [3] |
| [Apache Pekko Actors](https://pekko.apache.org/) | 1.0+ | N/A | Context propagation |
| [Apache Pekko HTTP](https://pekko.apache.org/) | 1.0+ | N/A | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics] |
| [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] |
| [Apache Pulsar](https://pulsar.apache.org/) | 2.8+ | N/A | [Messaging Spans] |
| [Apache RocketMQ gRPC/Protobuf-based Client](https://rocketmq.apache.org/) | 5.0+ | N/A | [Messaging Spans] |
| [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] |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import io.opentelemetry.context.Context;
import io.opentelemetry.javaagent.bootstrap.http.HttpServerResponseCustomizerHolder;
import io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.route.PekkoRouteHolder;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
Expand Down Expand Up @@ -117,6 +118,7 @@ public void onPush() {
if (PekkoHttpServerSingletons.instrumenter().shouldStart(parentContext, request)) {
Context context =
PekkoHttpServerSingletons.instrumenter().start(parentContext, request);
context = PekkoRouteHolder.init(context);
tracingRequest = new TracingRequest(context, request);
}
// event if span wasn't started we need to push TracingRequest to match response
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
return hasClassesNamed("org.apache.pekko.http.scaladsl.HttpExt");
}

@Override
public boolean isIndyModule() {
// PekkoHttpServerInstrumentationModule and PekkoHttpServerRouteInstrumentationModule share
// PekkoRouteHolder class
return false;
}

@Override
public List<TypeInstrumentation> typeInstrumentations() {
return asList(new HttpExtServerInstrumentation(), new GraphInterpreterInstrumentation());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.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<TypeDescription> typeMatcher() {
return namedOneOf(
"org.apache.pekko.http.scaladsl.server.PathMatcher$$anonfun$$tilde$1",
"org.apache.pekko.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/apache/incubator-pekko-http/blob/bea7d2b5c21e23d55556409226d136c282da27a3/http/src/main/scala/org/apache/pekko/http/scaladsl/server/PathMatcher.scala#L53
// https://github.com/apache/incubator-pekko-http/blob/bea7d2b5c21e23d55556409226d136c282da27a3/http/src/main/scala/org/apache/pekko/http/scaladsl/server/PathMatcher.scala#L57
// 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
PekkoRouteHolder.startSegment();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.route;

import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.returns;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;

import io.opentelemetry.instrumentation.api.util.VirtualField;
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;
import org.apache.pekko.http.scaladsl.model.Uri;
import org.apache.pekko.http.scaladsl.server.PathMatcher;

public class PathMatcherInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return named("org.apache.pekko.http.scaladsl.server.PathMatcher$");
}

@Override
public void transform(TypeTransformer transformer) {
transformer.applyAdviceToMethod(
named("apply")
.and(takesArgument(0, named("org.apache.pekko.http.scaladsl.model.Uri$Path")))
.and(returns(named("org.apache.pekko.http.scaladsl.server.PathMatcher"))),
this.getClass().getName() + "$ApplyAdvice");
}

@SuppressWarnings("unused")
public static class ApplyAdvice {

@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
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
VirtualField.find(PathMatcher.class, String.class).set(result, prefix.toString());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.route;

import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;

import io.opentelemetry.instrumentation.api.util.VirtualField;
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;
import org.apache.pekko.http.scaladsl.model.Uri;
import org.apache.pekko.http.scaladsl.server.PathMatcher;
import org.apache.pekko.http.scaladsl.server.PathMatchers;
import org.apache.pekko.http.scaladsl.server.PathMatchers$;

public class PathMatcherStaticInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return extendsClass(named("org.apache.pekko.http.scaladsl.server.PathMatcher"));
}

@Override
public void transform(TypeTransformer transformer) {
transformer.applyAdviceToMethod(
named("apply")
.and(takesArgument(0, named("org.apache.pekko.http.scaladsl.model.Uri$Path"))),
this.getClass().getName() + "$ApplyAdvice");
}

@SuppressWarnings("unused")
public static class ApplyAdvice {

@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void onExit(
@Advice.This PathMatcher<?> pathMatcher,
@Advice.Argument(0) Uri.Path path,
@Advice.Return PathMatcher.Matching<?> result) {
// result is either matched or unmatched, we only care about the matches
if (result.getClass() == PathMatcher.Matched.class) {
if (PathMatchers$.PathEnd$.class == pathMatcher.getClass()) {
PekkoRouteHolder.endMatched();
return;
}
// if present use the matched path that was remembered in PathMatcherInstrumentation,
// otherwise just use a *
String prefix = VirtualField.find(PathMatcher.class, String.class).get(pathMatcher);
if (prefix == null) {
if (PathMatchers.Slash$.class == pathMatcher.getClass()) {
prefix = "/";
} else {
prefix = "*";
}
}
if (prefix != null) {
PekkoRouteHolder.push(prefix);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.route;

import static java.util.Arrays.asList;

import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import java.util.List;

/**
* This instrumentation applies to classes in pekko-http.jar while
* PekkoHttpServerInstrumentationModule applies to classes in pekko-http-core.jar
*/
@AutoService(InstrumentationModule.class)
public class PekkoHttpServerRouteInstrumentationModule extends InstrumentationModule {
public PekkoHttpServerRouteInstrumentationModule() {
super("pekko-http", "pekko-http-1.0", "pekko-http-server", "pekko-http-server-route");
}

@Override
public boolean isIndyModule() {
// PekkoHttpServerInstrumentationModule and PekkoHttpServerRouteInstrumentationModule share
// PekkoRouteHolder class
return false;
}

@Override
public List<TypeInstrumentation> typeInstrumentations() {
return asList(
new PathMatcherInstrumentation(),
new PathMatcherStaticInstrumentation(),
new RouteConcatenationInstrumentation(),
new PathConcatenationInstrumentation());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.route;

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

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 PekkoRouteHolder implements ImplicitContextKeyed {
private static final ContextKey<PekkoRouteHolder> KEY = named("opentelemetry-pekko-route");

private String route = "";
private boolean newSegment;
private boolean endMatched;
private final Deque<String> stack = new ArrayDeque<>();

public static Context init(Context context) {
if (context.get(KEY) != null) {
return context;
}
return context.with(new PekkoRouteHolder());
}

public static void push(String path) {
PekkoRouteHolder holder = Context.current().get(KEY);
if (holder != null && holder.newSegment && !holder.endMatched) {
holder.route += path;
holder.newSegment = false;
}
}

public static void startSegment() {
PekkoRouteHolder holder = Context.current().get(KEY);
if (holder != null) {
holder.newSegment = true;
}
}

public static void endMatched() {
Context context = Context.current();
PekkoRouteHolder holder = context.get(KEY);
if (holder != null) {
holder.endMatched = true;
HttpServerRoute.update(context, HttpServerRouteSource.CONTROLLER, holder.route);
}
}

public static void save() {
PekkoRouteHolder holder = Context.current().get(KEY);
if (holder != null) {
holder.stack.push(holder.route);
holder.newSegment = true;
}
}

public static void restore() {
PekkoRouteHolder holder = Context.current().get(KEY);
if (holder != null) {
holder.route = holder.stack.pop();
holder.newSegment = true;
}
}

@Override
public Context storeInContext(Context context) {
return context.with(KEY, this);
}

private PekkoRouteHolder() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.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 RouteConcatenationInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return namedOneOf(
"org.apache.pekko.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation$$anonfun$$tilde$1",
"org.apache.pekko.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation");
}

@Override
public void transform(TypeTransformer transformer) {
transformer.applyAdviceToMethod(
namedOneOf("apply", "$anonfun$$tilde$1"), this.getClass().getName() + "$ApplyAdvice");
}

@SuppressWarnings("unused")
public static class ApplyAdvice {

@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
PekkoRouteHolder.save();
}

@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void onExit() {
PekkoRouteHolder.restore();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import io.opentelemetry.instrumentation.testing.junit.http.{
HttpServerTestOptions,
ServerEndpoint
}
import io.opentelemetry.semconv.SemanticAttributes

import java.util
import java.util.Collections
Expand All @@ -25,8 +26,13 @@ abstract class AbstractHttpServerInstrumentationTest
options.setTestCaptureHttpHeaders(false)
options.setHttpAttributes(
new Function[ServerEndpoint, util.Set[AttributeKey[_]]] {
override def apply(v1: ServerEndpoint): util.Set[AttributeKey[_]] =
Collections.emptySet()
override def apply(v1: ServerEndpoint): util.Set[AttributeKey[_]] = {
val set = new util.HashSet[AttributeKey[_]](
HttpServerTestOptions.DEFAULT_HTTP_ATTRIBUTES
)
set.remove(SemanticAttributes.HTTP_ROUTE)
set
}
}
)
options.setHasResponseCustomizer(
Expand Down
Loading

0 comments on commit 1225eb8

Please sign in to comment.