Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Capture http.route for akka-http #10039

Merged
merged 4 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import akka.stream.stage.OutHandler;
import io.opentelemetry.context.Context;
import io.opentelemetry.javaagent.bootstrap.http.HttpServerResponseCustomizerHolder;
import io.opentelemetry.javaagent.instrumentation.akkahttp.server.route.AkkaRouteHolder;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
Expand Down Expand Up @@ -118,6 +119,7 @@ public void onPush() {
Context parentContext = currentContext();
if (instrumenter().shouldStart(parentContext, request)) {
Context context = instrumenter().start(parentContext, request);
context = AkkaRouteHolder.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("akka.http.scaladsl.HttpExt");
}

@Override
public boolean isIndyModule() {
// AkkaHttpServerInstrumentationModule and AkkaHttpServerRouteInstrumentationModule share
// AkkaRouteHolder 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,40 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.akkahttp.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 akka-http.jar while
* AkkaHttpServerInstrumentationModule applies to classes in akka-http-core.jar
*/
@AutoService(InstrumentationModule.class)
public class AkkaHttpServerRouteInstrumentationModule extends InstrumentationModule {
public AkkaHttpServerRouteInstrumentationModule() {
super("akka-http", "akka-http-10.0", "akka-http-server", "akka-http-server-route");
}

@Override
public boolean isIndyModule() {
// AkkaHttpServerInstrumentationModule and AkkaHttpServerRouteInstrumentationModule share
// AkkaRouteHolder 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.akkahttp.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 AkkaRouteHolder implements ImplicitContextKeyed {
private static final ContextKey<AkkaRouteHolder> KEY = named("opentelemetry-akka-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 AkkaRouteHolder());
}

public static void push(String path) {
AkkaRouteHolder holder = Context.current().get(KEY);
if (holder != null && holder.newSegment && !holder.endMatched) {
holder.route += path;
breedx-splk marked this conversation as resolved.
Show resolved Hide resolved
holder.newSegment = false;
}
}

public static void startSegment() {
AkkaRouteHolder holder = Context.current().get(KEY);
if (holder != null) {
holder.newSegment = 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 static void save() {
AkkaRouteHolder holder = Context.current().get(KEY);
if (holder != null) {
holder.stack.push(holder.route);
holder.newSegment = true;
}
}

public static void restore() {
AkkaRouteHolder 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 AkkaRouteHolder() {}
}
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.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<TypeDescription> typeMatcher() {
return namedOneOf(
"akka.http.scaladsl.server.PathMatcher$$anonfun$$tilde$1",
breedx-splk marked this conversation as resolved.
Show resolved Hide resolved
"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();
breedx-splk marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
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.akkahttp.server.route;

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

import akka.http.scaladsl.model.Uri;
import akka.http.scaladsl.server.PathMatcher;
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;

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

@Override
public void transform(TypeTransformer transformer) {
transformer.applyAdviceToMethod(
named("apply")
.and(takesArgument(0, named("akka.http.scaladsl.model.Uri$Path")))
.and(returns(named("akka.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) {
// remember the path for given matcher, so it could be used for constructing the route
laurit marked this conversation as resolved.
Show resolved Hide resolved
VirtualField.find(PathMatcher.class, String.class).set(result, prefix.toString());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.akkahttp.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 akka.http.scaladsl.model.Uri;
import akka.http.scaladsl.server.PathMatcher;
import akka.http.scaladsl.server.PathMatchers;
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;

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

@Override
public void transform(TypeTransformer transformer) {
transformer.applyAdviceToMethod(
named("apply").and(takesArgument(0, named("akka.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()) {
AkkaRouteHolder.endMatched();
return;
}
// for remember the matched path in PathMatcherInstrumentation, otherwise we 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) {
AkkaRouteHolder.push(prefix);
}
}
}
}
}
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.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 RouteConcatenationInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return namedOneOf(
"akka.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation$$anonfun$$tilde$1",
"akka.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
AkkaRouteHolder.save();
}

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

import java.util
import java.util.Collections
import java.util.function.{Function, Predicate}

abstract class AbstractHttpServerInstrumentationTest
Expand All @@ -25,8 +25,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)
breedx-splk marked this conversation as resolved.
Show resolved Hide resolved
set
}
}
)
options.setHasResponseCustomizer(
Expand Down
Loading
Loading