Skip to content

DRAFT: APM AI TOOLKIT New Integration: Feign Client Instrumentation#10579

Draft
wconti27 wants to merge 1 commit intomasterfrom
conti/feign-instrumentation
Draft

DRAFT: APM AI TOOLKIT New Integration: Feign Client Instrumentation#10579
wconti27 wants to merge 1 commit intomasterfrom
conti/feign-instrumentation

Conversation

@wconti27
Copy link
Contributor

@wconti27 wconti27 commented Feb 12, 2026

THIS IS A TEST RUN FOR TRYING TO INSTRUMENT JAVA CODE WITH THE AI TOOLKIT, @mcculls to review

Add instrumentation for OpenFeign (feign-core) HTTP client library. Covers both sync (Client) and async (AsyncClient) execution paths with distributed tracing context propagation.

Files:

  • FeignClientInstrumentation: ByteBuddy type/method matchers for Client.execute
  • FeignAsyncClientInstrumentation: Matchers for AsyncClient.execute
  • FeignClientAdvice: Sync span lifecycle (enter/exit)
  • FeignAsyncClientAdvice: Async span lifecycle via CompletableFuture
  • FeignDecorator: HttpClientDecorator with operation names and tags
  • FeignHeadersInjectAdapter: CarrierSetter for trace context propagation
  • FeignClientTest: Spock tests using HttpClientTest base (70 tests pass)

Generated by APM AI Integration Toolkit (anubis)

What Does This Do

Motivation

Additional Notes

Contributor Checklist

Jira ticket: [PROJ-IDENT]

Note: Once your PR is ready to merge, add it to the merge queue by commenting /merge. /merge -c cancels the queue request. /merge -f --reason "reason" skips all merge queue checks; please use this judiciously, as some checks do not run at the PR-level. For more information, see this doc.

Add instrumentation for OpenFeign (feign-core) HTTP client library.
Covers both sync (Client) and async (AsyncClient) execution paths
with distributed tracing context propagation.

Files:
- FeignClientInstrumentation: ByteBuddy type/method matchers for Client.execute
- FeignAsyncClientInstrumentation: Matchers for AsyncClient.execute
- FeignClientAdvice: Sync span lifecycle (enter/exit)
- FeignAsyncClientAdvice: Async span lifecycle via CompletableFuture
- FeignDecorator: HttpClientDecorator with operation names and tags
- FeignHeadersInjectAdapter: CarrierSetter for trace context propagation
- FeignClientTest: Spock tests using HttpClientTest base (70 tests pass)

Generated by APM Instrumentation Toolkit (anubis)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@mcculls mcculls self-requested a review February 12, 2026 17:01
@mcculls mcculls added the AI label Feb 12, 2026
@jbachorik jbachorik removed the AI label Feb 19, 2026
import java.util.concurrent.CompletableFuture;
import net.bytebuddy.asm.Advice;

public class FeignAsyncClientAdvice {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice to see the advice separated out into its own class, rather than nested inside the instrumentation class. This makes it more clear the advice is actually decoupled from the instrumentation declaration.

(basically the bytecode here is lifted and patched into the target method, this class is never actually linked and run)


@Advice.OnMethodEnter(suppress = Throwable.class)
public static AgentScope onEnter(@Advice.Argument(0) final Request request) {
final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(Client.class);
Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting to see it use CallDepthThreadLocalMap - this will stop additional spans being created when requests are reentrant (i.e. loop back into itself) - not always required, but easy enough to add from the start.

Note CallDepthThreadLocalMap can be tricky to use - if you get it wrong and have a path where the thread doesn't reset it when the request is unwound then it can mean a thread will not create spans, because it thinks it is still in a request. The use here looks good.

return activateSpan(span);
}

@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
Copy link
Contributor

Choose a reason for hiding this comment

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

+1 for use of onThrowable = Throwable.class so the advice is always run, even when the patched method throws an exception (important to clean up the CallDepthThreadLocalMap, and suppress = Throwable.class to stop instrumentation errors from leaking out.

DECORATE.beforeFinish(span);
} finally {
span.finish();
CallDepthThreadLocalMap.reset(Client.class);
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure about this use of CallDepthThreadLocalMap.reset - if the future completes on a different thread then it could leave the original thread still thinking that the request is in-progress (from the perspective of its CallDepthThreadLocalMap)


if (future != null) {
future.whenComplete(
(response, error) -> {
Copy link
Contributor

Choose a reason for hiding this comment

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

This compiles, but will result in an invokedynamic opcode in the bytecode where the target of the invokedynamic (the lambda) has not been prepared by the class being patched. In such cases we should either move the lambda use to a helper where it can be setup properly, or convert the lambda to a named class which is then injected as a helper.

Note this is something which we could have a linting rule for, because we've had developers make the same mistake. (And I was also confused why muzzle was failing before I realized that it was caused by the use of a lambda in the advice, which led to a confusing message about the advice class - I'll look into fixing this to make the message clearer.)

scope.close();

if (future != null) {
future.whenComplete(
Copy link
Contributor

Choose a reason for hiding this comment

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

future.whenComplete will return a new future - at the moment this is thrown away, so it has no effect.

To tell byte-buddy to pass the 'traced' future back to the patched method you would normally use @Advice.Return(readOnly=false) future and then assign the updated value to the future parameter.

Note this is very specific to byte-buddy as normally assigning a new value to a parameter has no side-effect outside of the method, but here the bytecode is patched into the target method so the store opcode actually does have a side-effect.

This is where clear examples of what you should (and shouldn't) do in advice classes would help, because the rules are subtly different to usual Java development.

Copy link
Contributor

Choose a reason for hiding this comment

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

It's also where standard ways of wrapping a future to continue and close out a trace would help.

import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;

@AutoService(InstrumenterModule.class)
Copy link
Contributor

Choose a reason for hiding this comment

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

Most instrumentations at the moment follow this pattern, but we want to move towards having a single InstrumenterModule per integration which then lists the different instrumentations, in this case the 'sync' and 'async' cases.

For example: https://github.com/DataDog/dd-trace-java/blob/master/dd-java-agent/instrumentation/reactor-core-3.1/src/main/java/datadog/trace/instrumentation/reactor/core/ReactorCoreModule.java#L54

group = "io.github.openfeign"
module = "feign-core"
versions = "[10.0.0,)"
assertInverse = true
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice - this assertInverse is something people often forget :)

":dd-java-agent:instrumentation:elasticsearch:elasticsearch-transport:elasticsearch-transport-7.3",
":dd-java-agent:instrumentation:elasticsearch:elasticsearch-transport:elasticsearch-transport-common",
":dd-java-agent:instrumentation:elasticsearch:elasticsearch-common",
":dd-java-agent:instrumentation:feign-core",
Copy link
Contributor

Choose a reason for hiding this comment

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

Note this fails our more recent conventions:

  Instrumentation naming convention violations found:

  • feign-core
    Module name 'feign-core' must end with a version (e.g., '2.0', '3.1.0') or one of: '-common', '-stubs', '-iast'
  Naming rules:
    1. Module name must end with a version (e.g., '2.0', '3.1') OR one of: '-common', '-stubs', '-iast'
    2. Module name must include the parent directory name
       Example: 'couchbase/couchbase-2.0' ✓ (contains 'couchbase')

@mcculls
Copy link
Contributor

mcculls commented Feb 24, 2026

Minor notes:

  • currently fails spotless - run ./gradlew spotlessApply to fix
  • currently fails 'codeNarc' due to an unused import in the test

These are trivial though :)

@jordan-wong
Copy link
Contributor

jordan-wong commented Feb 24, 2026

Ideas:

  • Testing: can be useful to look at the libraries official tests, add tracer, compare
  • can we do a PR comparing existing instrumentation to Toolkit generated?
  • async in general is a bit trickier, there are a few helper methods to point to specifically vs sync instrumentations ex. futures

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.

4 participants