Skip to content

[codex] Fix java.net.http async trace propagation#1214

Merged
FrankChen021 merged 5 commits into
masterfrom
codex/fix-java-net-http-sendasync-propagation
Jul 2, 2026
Merged

[codex] Fix java.net.http async trace propagation#1214
FrankChen021 merged 5 commits into
masterfrom
codex/fix-java-net-http-sendasync-propagation

Conversation

@FrankChen021

Copy link
Copy Markdown
Owner

Summary

  • Keep sendAsync as an attached client span while JDK HttpClient schedules internal work.
  • Detach the client span after submission so it can be finished from the returned CompletableFuture without leaving it on the caller thread.
  • Propagate headers from HttpRequestImpl#getUserHeaders, so the remote server span is linked under the HTTP client worker/async-task context.

Root Cause

The previous sendAsync span used an async span that was not attached to the current trace context during the client executor submission path. Header propagation also happened at headers(), which could run before the worker context existed.

Validation

  • mvn -pl agent/agent-plugins-test -Dtest=JavaNetHttpClientThreadPropagationInterceptorTest,JavaNetHttpClientPluginInterceptorTest test
  • mvn -pl agent/agent-observability -Dtest=TraceSpanDetachTest test
  • mvn -pl agent/agent-distribution -am -DskipTests package
  • git diff --check

@FrankChen021 FrankChen021 marked this pull request as ready for review July 1, 2026 09:29
Copilot AI review requested due to automatic review settings July 1, 2026 09:29

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7f44867b53

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +62 to +63
.onMethod("getUserHeaders")
.interceptedBy("org.bithon.agent.plugin.httpclient.javanethttp.interceptor.HttpRequestImpl$GetUserHeaders")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep async header injection independent of executor context

For sendAsync with a user executor that is not instrumented to carry TraceContextHolder (for example an executor that starts a raw new thread), this new getUserHeaders hook can run on the HTTP worker with no current trace context, causing HttpRequestImpl$GetUserHeaders to return without injecting traceparent. The previous headers() hook was hit while HttpClientImpl copied the request on the caller thread, so propagation did not depend on executor instrumentation; this drops distributed tracing for those async clients.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. We evaluated this path and intentionally removed the fallback in e4a9ede. For now the expected model is that getUserHeaders propagates from the current worker TraceContextHolder, so the downstream server remains under the HTTP client's thread-pool/async-task span when executor context propagation is installed. Supporting uninstrumented executors would require extra request-copy handoff state on HttpRequestImpl, which we decided is too intrusive for the current scope.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the java.net.http client instrumentation to correctly propagate trace context across HttpClient.sendAsync scheduling/execution boundaries, ensuring outbound header injection occurs at send-time under the correct worker/async-task context.

Changes:

  • Switch sendAsync to create an attached client span and then detach it for safe async completion from the returned CompletableFuture.
  • Move header propagation interception from headers() to HttpRequestImpl#getUserHeaders() so injection happens at send-time.
  • Add/extend tests to validate async trace propagation and the new span-detach behavior.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
agent/agent-plugins/httpclient/java-net-http/src/main/java/org/bithon/agent/plugin/httpclient/javanethttp/JavaNetHttpClientPlugin.java Updates interceptor wiring to propagate headers via HttpRequestImpl#getUserHeaders() instead of headers().
agent/agent-plugins/httpclient/java-net-http/src/main/java/org/bithon/agent/plugin/httpclient/javanethttp/interceptor/HttpRequestImpl$GetUserHeaders.java Implements send-time header propagation by mutating returned HttpHeaders.
agent/agent-plugins/httpclient/java-net-http/src/main/java/org/bithon/agent/plugin/httpclient/javanethttp/interceptor/HttpClientImpl$SendAsync.java Changes span lifecycle for sendAsync: create attached span, detach after submission, finish via CompletableFuture completion.
agent/agent-plugins-test/src/test/java/org/bithon/agent/plugins/test/httpclient/JavaNetHttpClientThreadPropagationInterceptorTest.java Adds coverage to validate worker/async-task context propagation and correct parent/child span relationships.
agent/agent-observability/src/test/java/org/bithon/agent/observability/tracing/context/TraceSpanDetachTest.java Adds tests around ITraceSpan.detach() behavior.
agent/agent-observability/src/main/java/org/bithon/agent/observability/tracing/context/ITraceSpan.java Introduces detach() API on spans.
agent/agent-observability/src/main/java/org/bithon/agent/observability/tracing/context/impl/TracingSpan.java Implements detach() by creating an equivalent span in a copied context.
agent/agent-observability/src/main/java/org/bithon/agent/observability/tracing/context/impl/TracingContext.java Adds stack-detach support used by TracingSpan.detach().

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +60 to 64
// Intercept the JDK's send-time user headers access to propagate tracing headers
forClass("jdk.internal.net.http.HttpRequestImpl")
.onMethod("headers")
.interceptedBy("org.bithon.agent.plugin.httpclient.javanethttp.interceptor.HttpRequest$Headers")
.build(),
forClass("jdk.internal.net.http.ImmutableHttpRequest")
.onMethod("headers")
.interceptedBy("org.bithon.agent.plugin.httpclient.javanethttp.interceptor.HttpRequest$Headers")
.onMethod("getUserHeaders")
.interceptedBy("org.bithon.agent.plugin.httpclient.javanethttp.interceptor.HttpRequestImpl$GetUserHeaders")
.build()

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in de9e536: getUserHeaders now has an explicit zero-arg matcher via andArgsSize(0).

Comment on lines +162 to +166
LoggerFactory.getLogger(TracingContext.class).warn("Try to detach a span which is not in the stack. This IS a bug.\n"
+ "Current span: \n{}, Unfinished Spans:\n{}",
span,
spanStack);
return false;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in de9e536: the warning now says the span is not the current stack top and includes the current stack-top span in the log message.

Comment on lines +63 to +85
public void testDetachReturnsCopiedContextEvenWhenSpanIsNotStackTop() {
ITraceContext context = TraceContextFactory.newContext(SamplingMode.FULL);
ITraceSpan rootSpan = context.currentSpan().name("root");
ITraceSpan childSpan = rootSpan.newChildSpan("child");
ITraceSpan grandchildSpan = childSpan.newChildSpan("grandchild");

ITraceSpan detachedSpan = childSpan.detach();

Assertions.assertNotSame(childSpan, detachedSpan);
Assertions.assertNotSame(childSpan.context(), detachedSpan.context());
Assertions.assertEquals(childSpan.traceId(), detachedSpan.traceId());
Assertions.assertEquals(childSpan.spanId(), detachedSpan.spanId());
Assertions.assertEquals(childSpan.parentSpanId(), detachedSpan.parentSpanId());
Assertions.assertSame(detachedSpan, detachedSpan.context().currentSpan());

detachedSpan.finish();
detachedSpan.context().finish();

grandchildSpan.finish();
childSpan.finish();
rootSpan.finish();
context.finish();
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in de9e536: the test now detaches only the current child span and finishes the returned detached span, avoiding finishing the original child span after detach.

Comment on lines +149 to +152
/**
* Detach this span from its current trace context and return a span with the same identity
* in a copied context so it can be finished asynchronously.
*/

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in de9e536: the detach javadoc now documents the stack-top precondition and the lifecycle rule that callers should finish the returned span rather than the original reference.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Comment on lines +38 to +42
Object source = aopContext.getArgAs(0);
ITraceContext context = HttpRequestPropagationContext.get(source);
if (context != null) {
((IBithonObject) aopContext.getTarget()).setInjectedObject(context);
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hook was removed in e4a9ede along with the fallback propagation path, so the cast no longer exists.

@FrankChen021 FrankChen021 merged commit c2181d5 into master Jul 2, 2026
45 of 46 checks passed
@FrankChen021 FrankChen021 deleted the codex/fix-java-net-http-sendasync-propagation branch July 2, 2026 02:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants