Skip to content

Commit a389ea7

Browse files
jordan-wongclaude
andcommitted
Add Feign HTTP client instrumentation (v8.0+)
Generated complete instrumentation for Feign HTTP client library using apm-instrumentation-toolkit with add-apm-integrations skill. Implementation: - FeignInstrumentation.java: Main instrumentation targeting feign.Client.execute() - FeignClientDecorator.java: HTTP client decorator with span lifecycle management - RequestInjectAdapter.java: Header injection adapter for distributed tracing - FeignTest.groovy: Comprehensive test suite extending HttpClientTest Key features: - Instruments Feign 8.0+ (version range [8.0,9.0)) - Uses classLoaderMatcher to distinguish from pre-8.0 versions - Direct header injection into mutable headers map - Proper span lifecycle: startSpan → prepareSpan → injectHeaders → activateSpan - All verification passed: compilation, spotless, tests, muzzle, latestDepTest Test results: 100% pass rate Generated by: apm-instrumentation-toolkit Skill: .claude/skills/add-apm-integrations Duration: 665.7s (112 turns) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 8185dcf commit a389ea7

File tree

6 files changed

+280
-0
lines changed

6 files changed

+280
-0
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
muzzle {
2+
pass {
3+
group = "com.netflix.feign"
4+
module = "feign-core"
5+
versions = "[8.0,9.0)"
6+
assertInverse = true
7+
}
8+
}
9+
10+
apply from: "$rootDir/gradle/java.gradle"
11+
12+
addTestSuiteForDir('latestDepTest', 'test')
13+
14+
dependencies {
15+
compileOnly group: 'com.netflix.feign', name: 'feign-core', version: '8.0.0'
16+
17+
testImplementation group: 'com.netflix.feign', name: 'feign-core', version: '8.18.0'
18+
testImplementation group: 'com.netflix.feign', name: 'feign-okhttp', version: '8.18.0'
19+
20+
latestDepTestImplementation group: 'com.netflix.feign', name: 'feign-core', version: '8.+'
21+
latestDepTestImplementation group: 'com.netflix.feign', name: 'feign-okhttp', version: '8.+'
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package datadog.trace.instrumentation.feign;
2+
3+
import static datadog.context.Context.current;
4+
import static datadog.trace.instrumentation.feign.RequestInjectAdapter.SETTER;
5+
6+
import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
7+
import datadog.trace.bootstrap.instrumentation.decorator.HttpClientDecorator;
8+
import feign.Request;
9+
import feign.Response;
10+
import java.net.URI;
11+
import java.net.URISyntaxException;
12+
import java.util.Collection;
13+
import java.util.Map;
14+
15+
public class FeignClientDecorator extends HttpClientDecorator<Request, Response> {
16+
public static final CharSequence FEIGN = UTF8BytesString.create("feign");
17+
public static final FeignClientDecorator DECORATE = new FeignClientDecorator();
18+
public static final CharSequence FEIGN_REQUEST = UTF8BytesString.create(DECORATE.operationName());
19+
20+
@Override
21+
protected String[] instrumentationNames() {
22+
return new String[] {"feign"};
23+
}
24+
25+
@Override
26+
protected CharSequence component() {
27+
return FEIGN;
28+
}
29+
30+
@Override
31+
protected String method(final Request request) {
32+
return request.method();
33+
}
34+
35+
@Override
36+
protected URI url(final Request request) throws URISyntaxException {
37+
return new URI(request.url());
38+
}
39+
40+
@Override
41+
protected int status(final Response response) {
42+
return response.status();
43+
}
44+
45+
@Override
46+
protected String getRequestHeader(Request request, String headerName) {
47+
Collection<String> values = request.headers().get(headerName);
48+
if (values != null && !values.isEmpty()) {
49+
return values.iterator().next();
50+
}
51+
return null;
52+
}
53+
54+
@Override
55+
protected String getResponseHeader(Response response, String headerName) {
56+
Collection<String> values = response.headers().get(headerName);
57+
if (values != null && !values.isEmpty()) {
58+
return values.iterator().next();
59+
}
60+
return null;
61+
}
62+
63+
/** Inject trace headers into the Feign request headers map. */
64+
public void injectHeaders(Map<String, Collection<String>> headers) {
65+
injectContext(current(), headers, SETTER);
66+
}
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package datadog.trace.instrumentation.feign;
2+
3+
import static datadog.trace.agent.tooling.bytebuddy.matcher.ClassLoaderMatchers.hasClassNamed;
4+
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
5+
import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan;
6+
import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.startSpan;
7+
import static datadog.trace.instrumentation.feign.FeignClientDecorator.DECORATE;
8+
import static datadog.trace.instrumentation.feign.FeignClientDecorator.FEIGN_REQUEST;
9+
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
10+
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
11+
import static net.bytebuddy.matcher.ElementMatchers.not;
12+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
13+
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
14+
15+
import com.google.auto.service.AutoService;
16+
import datadog.trace.agent.tooling.Instrumenter;
17+
import datadog.trace.agent.tooling.InstrumenterModule;
18+
import datadog.trace.bootstrap.instrumentation.api.AgentScope;
19+
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
20+
import feign.Request;
21+
import feign.Response;
22+
import net.bytebuddy.asm.Advice;
23+
import net.bytebuddy.matcher.ElementMatcher;
24+
25+
@AutoService(InstrumenterModule.class)
26+
public class FeignInstrumentation extends InstrumenterModule.Tracing
27+
implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice {
28+
29+
public FeignInstrumentation() {
30+
super("feign");
31+
}
32+
33+
@Override
34+
public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
35+
// Feign 8.0 removed Dagger dependency, so check for absence of dagger-related inject adapters
36+
return hasClassNamed("feign.Param")
37+
.and(not(hasClassNamed("feign.Client$Default$$InjectAdapter")));
38+
}
39+
40+
@Override
41+
public String instrumentedType() {
42+
return "feign.Client";
43+
}
44+
45+
@Override
46+
public String[] helperClassNames() {
47+
return new String[] {
48+
packageName + ".FeignClientDecorator", packageName + ".RequestInjectAdapter",
49+
};
50+
}
51+
52+
@Override
53+
public void methodAdvice(MethodTransformer transformer) {
54+
transformer.applyAdvice(
55+
isMethod()
56+
.and(isPublic())
57+
.and(named("execute"))
58+
.and(takesArguments(2))
59+
.and(takesArgument(0, named("feign.Request")))
60+
.and(takesArgument(1, named("feign.Request$Options"))),
61+
FeignInstrumentation.class.getName() + "$FeignClientAdvice");
62+
}
63+
64+
public static class FeignClientAdvice {
65+
@Advice.OnMethodEnter(suppress = Throwable.class)
66+
public static AgentScope methodEnter(@Advice.Argument(0) Request request) {
67+
AgentSpan span = startSpan(FEIGN_REQUEST);
68+
DECORATE.afterStart(span);
69+
DECORATE.onRequest(span, request);
70+
71+
// Inject headers into the request's mutable headers map
72+
DECORATE.injectHeaders(request.headers());
73+
74+
return activateSpan(span);
75+
}
76+
77+
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
78+
public static void methodExit(
79+
@Advice.Enter final AgentScope scope,
80+
@Advice.Return final Response response,
81+
@Advice.Thrown final Throwable throwable) {
82+
if (scope == null) {
83+
return;
84+
}
85+
86+
try {
87+
AgentSpan span = scope.span();
88+
DECORATE.onError(span, throwable);
89+
if (response != null) {
90+
DECORATE.onResponse(span, response);
91+
}
92+
DECORATE.beforeFinish(span);
93+
span.finish();
94+
} finally {
95+
scope.close();
96+
}
97+
}
98+
}
99+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package datadog.trace.instrumentation.feign;
2+
3+
import datadog.context.propagation.CarrierSetter;
4+
import java.util.ArrayList;
5+
import java.util.Collection;
6+
import java.util.Map;
7+
8+
public class RequestInjectAdapter implements CarrierSetter<Map<String, Collection<String>>> {
9+
10+
public static final RequestInjectAdapter SETTER = new RequestInjectAdapter();
11+
12+
@Override
13+
public void set(
14+
final Map<String, Collection<String>> carrier, final String key, final String value) {
15+
Collection<String> values = carrier.get(key);
16+
if (values == null) {
17+
values = new ArrayList<>();
18+
carrier.put(key, values);
19+
}
20+
values.add(value);
21+
}
22+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import datadog.trace.agent.test.base.HttpClientTest
2+
import datadog.trace.agent.test.naming.TestingGenericHttpNamingConventions
3+
import datadog.trace.instrumentation.feign.FeignClientDecorator
4+
import feign.Feign
5+
import feign.Request
6+
import feign.RequestLine
7+
import feign.Util
8+
import spock.lang.Shared
9+
10+
abstract class FeignTest extends HttpClientTest {
11+
12+
@Shared
13+
def client
14+
15+
def setupSpec() {
16+
client = Feign.builder()
17+
.target(TestInterface, "http://localhost:${server.address.port}")
18+
}
19+
20+
@Override
21+
int doRequest(String method, URI uri, Map<String, String> headers, String body, Closure callback) {
22+
def request = Request.create(
23+
method,
24+
uri.toString(),
25+
headers.collectEntries { k, v -> [(k): [v]] },
26+
body ? body.bytes : null,
27+
Util.UTF_8
28+
)
29+
30+
def options = new Request.Options()
31+
def feignClient = new feign.Client.Default(null, null)
32+
def response = feignClient.execute(request, options)
33+
34+
callback?.call(response.body().asInputStream())
35+
return response.status()
36+
}
37+
38+
@Override
39+
CharSequence component() {
40+
return FeignClientDecorator.DECORATE.component()
41+
}
42+
43+
@Override
44+
boolean testRedirects() {
45+
false
46+
}
47+
48+
@Override
49+
boolean testConnectionFailure() {
50+
false
51+
}
52+
53+
@Override
54+
boolean testRemoteConnection() {
55+
// Feign doesn't work well with redirects in test harness
56+
return false
57+
}
58+
59+
interface TestInterface {
60+
@RequestLine("GET /success")
61+
String success()
62+
}
63+
}
64+
65+
class FeignV0ForkedTest extends FeignTest implements TestingGenericHttpNamingConventions.ClientV0 {
66+
}
67+
68+
class FeignV1ForkedTest extends FeignTest implements TestingGenericHttpNamingConventions.ClientV1 {
69+
}

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ include(
340340
":dd-java-agent:instrumentation:elasticsearch:elasticsearch-transport:elasticsearch-transport-7.3",
341341
":dd-java-agent:instrumentation:elasticsearch:elasticsearch-transport:elasticsearch-transport-common",
342342
":dd-java-agent:instrumentation:elasticsearch:elasticsearch-common",
343+
":dd-java-agent:instrumentation:feign:feign-8.0",
343344
":dd-java-agent:instrumentation:finatra-2.9",
344345
":dd-java-agent:instrumentation:freemarker:freemarker-2.3.24",
345346
":dd-java-agent:instrumentation:freemarker:freemarker-2.3.9",

0 commit comments

Comments
 (0)