Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4da0675
Apply Play routes to root HTTP spans in OpenTelemetry mode
zacharycmontoya Nov 2, 2026
8b2fdb8
Capture Akka HTTP route templates in OpenTelemetry mode
zacharycmontoya Nov 2, 2026
a8673bd
Support nested Netty HTTP span suppression in OpenTelemetry mode
zacharycmontoya Nov 2, 2026
6b33693
Emit one Spring WebClient span in OpenTelemetry mode
zacharycmontoya Nov 2, 2026
7e940a7
Fix WebFlux span expectations in OpenTelemetry mode
mcculls Nov 3, 2026
e318582
Avoid unused Akka HTTP route matcher lists
mcculls Nov 3, 2026
98227c1
Reuse cached OpenTelemetry semantics flag in Play
mcculls Nov 3, 2026
67188e6
Share Netty HTTP client parent assignment
mcculls Nov 3, 2026
4a1cbc3
Consolidate WebFlux client test completion and assertions
mcculls Nov 3, 2026
8aad14d
Close Netty client parent scope in one place
mcculls Nov 3, 2026
69379cc
Defer Akka HTTP path rendering until a matcher succeeds
mcculls Nov 3, 2026
486b604
Return early from Netty client callbacks for suppressed spans
mcculls Nov 3, 2026
30b1b42
Capture Pekko HTTP route templates in OpenTelemetry mode
mcculls Nov 3, 2026
7f38168
Apply Play 2.3 and 2.4 routes to root HTTP spans in OpenTelemetry mode
mcculls Nov 3, 2026
46862bb
Fix HTTP route templates after rejected Akka and Pekko alternatives
mcculls Nov 3, 2026
6f52ba3
Move Akka and Pekko HTTP route state into propagated context
mcculls Nov 3, 2026
5a4acd7
Drop Akka and Pekko HTTP routes from rejected requests
mcculls Nov 3, 2026
9a9fc10
Preserve transport SSRF checks for suppressed Netty client spans
zacharycmontoya Nov 3, 2026
c724123
Match encoded Akka and Pekko route paths consistently
zacharycmontoya Nov 3, 2026
5732116
Restore upstream context after suppressed HTTP response headers
zacharycmontoya Nov 3, 2026
8c5dec8
Discard Akka and Pekko routes rejected by later directives
zacharycmontoya Nov 3, 2026
f78e11c
Limit WebClient span suppression to its transport exchange
zacharycmontoya Nov 3, 2026
628617d
fixup: Preserve transport SSRF checks
zacharycmontoya Nov 4, 2026
89f5818
Revert changes to WebFlux => netty span suppression since it's not st…
zacharycmontoya Nov 4, 2026
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 @@ -162,7 +162,10 @@
0 akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
0 akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport$class
0 akka.http.scaladsl.unmarshalling.*
0 akka.http.scaladsl.server.PathMatcher$Matched
# Path matchers supply route templates when OTel HTTP semantics are enabled
0 akka.http.scaladsl.server.PathMatcher*
0 akka.http.scaladsl.server.ImplicitPathMatcherConstruction*
0 akka.http.scaladsl.server.RouteResult$Rejected
0 akka.http.scaladsl.server.directives.ParameterDirectives$class
0 akka.http.scaladsl.server.directives.ParameterDirectives
0 akka.http.scaladsl.server.directives.BasicDirectives$class
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import datadog.trace.agent.test.base.HttpServer
import datadog.trace.agent.test.base.HttpServerTest
import datadog.trace.agent.test.base.WithHttpServer
import datadog.trace.agent.test.naming.TestingGenericHttpNamingConventions
import datadog.trace.agent.test.naming.TestingOtelHttpSemantics
import datadog.trace.api.config.TraceInstrumentationConfig
Expand Down Expand Up @@ -279,6 +280,136 @@ class AkkaHttpServerInstrumentationSyncV1ForkedTest extends AkkaHttpServerInstru
class AkkaHttpServerInstrumentationSyncOtelSemanticsForkedTest extends AkkaHttpServerInstrumentationSyncV0ForkedTest implements TestingOtelHttpSemantics {
}

abstract class AkkaHttpRouteOtelSemanticsTest extends WithHttpServer<AkkaHttpTestWebServer> implements TestingGenericHttpNamingConventions.ServerV0, TestingOtelHttpSemantics {
@Override
void configurePreAgent() {
super.configurePreAgent()
injectSysConfig(TraceInstrumentationConfig.LEGACY_CONTEXT_MANAGER_ENABLED, "true")
}

@Override
HttpServer server() {
new AkkaHttpTestWebServer(AkkaHttpTestWebServer.BindAndHandle())
}

def "matched paths produce stable server routes"() {
when:
def response = client.newCall(new Request.Builder().url(address.resolve(path).toString()).build()).execute()
assert response.code() == 200
response.close()
TEST_WRITER.waitForTraces(1)

then:
def serverSpan = TEST_WRITER.flatten().find { it.getTag('component')?.toString() == 'akka-http-server' }
serverSpan != null
serverSpan.getTag('http.route') == route
serverSpan.resourceName.toString() == "GET $route"

where:
path | route
'/success' | '/success'
'/directive-rejection/value?required=yes' | '/directive-rejection/{param}'
'/items/a%20b' | '/items/{param}'
'/items/a%2Fb' | '/items/{param}'
'/items/a%25b' | '/items/{param}'
'/query?some=value' | '/query'
'/injected-id/ping/1' | '/injected-id/ping/{param}'
'/injected-id/ping/2' | '/injected-id/ping/{param}'
'/injected-id/fing/3' | '/injected-id/fing/{param}'
'/route-async/4/detail/5' | '/route-async/{param}/detail/{param}'
'/route-alternative/6/accepted' | '/route-alternative/{param}/accepted'
}

def "rejected paths do not produce server routes"() {
when:
def unmatched = client.newCall(new Request.Builder().url(address.resolve('/no-such-route').toString()).build()).execute()
assert unmatched.code() == 404
unmatched.close()
TEST_WRITER.waitForTraces(1)
def unmatchedSpan = TEST_WRITER.flatten().find { it.getTag('component')?.toString() == 'akka-http-server' }
TEST_WRITER.clear()
def response = client.newCall(new Request.Builder().url(address.resolve(path).toString()).build()).execute()
assert response.code() == 404
response.close()
TEST_WRITER.waitForTraces(1)

then:
def serverSpan = TEST_WRITER.flatten().find { it.getTag('component')?.toString() == 'akka-http-server' }
serverSpan != null
serverSpan.getTag('http.route') == null
serverSpan.resourceName.toString() == unmatchedSpan.resourceName.toString()

where:
path << ['/route-alternative/6/other', '/route-async/4/other']
}

def "a fallback does not retain a route rejected by a later directive"() {
when:
def response = client.newCall(new Request.Builder().url(address.resolve(path).toString()).build()).execute()
assert response.code() == 200
assert response.body().string() == 'fallback'
response.close()
TEST_WRITER.waitForTraces(1)

then:
def serverSpan = TEST_WRITER.flatten().find { it.getTag('component')?.toString() == 'akka-http-server' }
serverSpan.getTag('http.route') == null
serverSpan.resourceName.toString() == 'GET'

where:
path << ['/directive-rejection/value', '/header-rejection/value']
}

def "concurrent requests retain their own routes across asynchronous matching and rejection"() {
given:
def counter = new AtomicInteger()
int requests = 40

when:
ThreadUtils.runConcurrently(8, requests, {
int id = counter.incrementAndGet()
boolean async = id % 2 == 0
String path = async ? "/route-async/$id/detail/${id + 1}" : "/route-alternative/$id/accepted"
def response = client.newCall(new Request.Builder().url(address.resolve(path).toString()).build()).execute()
try {
assert response.code() == 200
assert response.body().string() == (async ? "$id:${id + 1}".toString() : id.toString())
} finally {
response.close()
}
})
TEST_WRITER.waitForTraces(requests)

then:
def serverSpans = TEST_WRITER.flatten().findAll { it.getTag('component')?.toString() == 'akka-http-server' }
serverSpans.size() == requests
serverSpans.every { span ->
String route = span.getTag('url.path').toString().startsWith('/route-async/') ?
'/route-async/{param}/detail/{param}' : '/route-alternative/{param}/accepted'
span.getTag('http.route') == route && span.resourceName.toString() == "GET $route"
}
}
}

class AkkaHttpRouteOtelSemanticsForkedTest extends AkkaHttpRouteOtelSemanticsTest {
}

class AkkaHttpRouteOtelSemanticsRawResourceForkedTest extends AkkaHttpRouteOtelSemanticsTest {
@Override
void configurePreAgent() {
super.configurePreAgent()
injectSysConfig(TraceInstrumentationConfig.HTTP_SERVER_RAW_RESOURCE, "true")
}
}

class AkkaHttpRouteOtelSemanticsContextSwapForkedTest extends AkkaHttpRouteOtelSemanticsTest {
@Override
void configurePreAgent() {
super.configurePreAgent()
injectSysConfig(TraceInstrumentationConfig.LEGACY_CONTEXT_MANAGER_ENABLED, "false")
}
}

class AkkaHttpServerInstrumentationAsyncTest extends AkkaHttpServerInstrumentationTest {
@Override
HttpServer server() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,27 @@ object AkkaHttpTestWebServer {
complete(HttpResponse(status = ERROR.getStatus, entity = ERROR.getBody))
} ~ path(EXCEPTION.relativePath) {
throw new Exception(EXCEPTION.getBody)
} ~ pathPrefix("directive-rejection") {
path(Segment) { _ =>
onSuccess(Future { 1 }) { _ =>
parameter("required") { _ => complete("accepted") }
}
} ~ complete("fallback")
} ~ pathPrefix("header-rejection") {
path(Segment) { _ =>
headerValueByName("authorization") { _ => complete("accepted") }
} ~ complete("fallback")
} ~ path("items" / Segment) { _ =>
complete("item")
} ~ pathPrefix("route-async" / IntNumber) { id =>
onSuccess(Future { id }) { _ =>
path("detail" / IntNumber) { detail =>
complete(s"$id:$detail")
}
}
} ~ pathPrefix("route-alternative") {
pathPrefix(IntNumber) { _ => reject } ~
path(IntNumber / "accepted") { id => complete(s"$id") }
} ~ pathPrefix("injected-id") {
path("ping" / IntNumber) { id =>
val traceId = AgentTracer.activeSpan().getTraceId
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package datadog.trace.instrumentation.akkahttp;

import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.extendsClass;
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.isBridge;
import static net.bytebuddy.matcher.ElementMatchers.isConstructor;
import static net.bytebuddy.matcher.ElementMatchers.isDeclaredBy;
import static net.bytebuddy.matcher.ElementMatchers.not;
import static net.bytebuddy.matcher.ElementMatchers.returns;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;

import akka.http.scaladsl.model.Uri;
import akka.http.scaladsl.server.PathMatcher;
import com.google.auto.service.AutoService;
import datadog.trace.agent.tooling.Instrumenter;
import datadog.trace.agent.tooling.InstrumenterModule;
import datadog.trace.api.Config;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;

/** Tracks matched Akka HTTP route templates and directive rejection results. */
@AutoService(InstrumenterModule.class)
public final class AkkaHttpPathMatcherInstrumentation extends InstrumenterModule.Tracing
implements Instrumenter.ForTypeHierarchy, Instrumenter.HasMethodAdvice {
public AkkaHttpPathMatcherInstrumentation() {
super("akka-http", "akka-http-server");
}

@Override
public boolean isEnabled() {
return super.isEnabled() && Config.get().isTraceOtelSemanticsEnabled();
}

@Override
public String hierarchyMarkerType() {
return "akka.http.scaladsl.server.PathMatcher";
}

@Override
public ElementMatcher<TypeDescription> hierarchyMatcher() {
return extendsClass(named(hierarchyMarkerType()))
.or(named("akka.http.scaladsl.server.RouteResult$Rejected"));
}

@Override
public void methodAdvice(MethodTransformer transformer) {
transformer.applyAdvice(
isConstructor().and(isDeclaredBy(named("akka.http.scaladsl.server.RouteResult$Rejected"))),
getClass().getName() + "$RejectedAdvice");
transformer.applyAdvice(
named("apply")
.and(takesArguments(1))
.and(takesArgument(0, named("akka.http.scaladsl.model.Uri$Path")))
.and(returns(named("akka.http.scaladsl.server.PathMatcher$Matching")))
.and(not(isBridge())),
getClass().getName() + "$MatchAdvice");
}

public static final class RejectedAdvice {
@Advice.OnMethodExit(suppress = Throwable.class)
public static void exit() {
AkkaHttpRouteState.onRejected();
}
}

public static final class MatchAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static AkkaHttpRouteHelper.Frame enter(@Advice.Argument(0) Uri.Path path) {
return AkkaHttpRouteHelper.enter(path);
}

@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void exit(
@Advice.Enter AkkaHttpRouteHelper.Frame frame,
@Advice.Return PathMatcher.Matching<?> matching,
@Advice.Thrown Throwable throwable) {
AkkaHttpRouteHelper.exit(frame, matching, throwable);
}
}
}
Loading
Loading