diff --git a/dd-java-agent/instrumentation/build.gradle b/dd-java-agent/instrumentation/build.gradle index 5e36217ad42..1dcf6aee6ee 100644 --- a/dd-java-agent/instrumentation/build.gradle +++ b/dd-java-agent/instrumentation/build.gradle @@ -159,7 +159,11 @@ TaskProvider registerIndexTask(String indexTaskName, String indexer, S } instrumentationNaming { - exclusions = ["org-json-20230227"] // org-json does not use semver + exclusions = [ + "org-json-20230227", + // org-json does not use semver + "feign-10.8-generated", // research convention: toolkit-generated side-by-side eval module + ] suffixes = ["-common", "-stubs", "-iast"] } diff --git a/dd-java-agent/instrumentation/feign/feign-10.8-generated/build.gradle b/dd-java-agent/instrumentation/feign/feign-10.8-generated/build.gradle new file mode 100644 index 00000000000..0758ba775f4 --- /dev/null +++ b/dd-java-agent/instrumentation/feign/feign-10.8-generated/build.gradle @@ -0,0 +1,19 @@ +muzzle { + pass { + group = "io.github.openfeign" + module = "feign-core" + versions = "[10.8,)" + } +} + +apply from: "$rootDir/gradle/java.gradle" + +addTestSuiteForDir('latestDepTest', 'test') + +dependencies { + compileOnly(group: 'io.github.openfeign', name: 'feign-core', version: '10.8') + + testImplementation(group: 'io.github.openfeign', name: 'feign-core', version: '10.8') + + latestDepTestImplementation group: 'io.github.openfeign', name: 'feign-core', version: '10+' +} diff --git a/dd-java-agent/instrumentation/feign/feign-10.8-generated/src/main/java/datadog/trace/instrumentation/feign/FeignAsyncClientInstrumentation.java b/dd-java-agent/instrumentation/feign/feign-10.8-generated/src/main/java/datadog/trace/instrumentation/feign/FeignAsyncClientInstrumentation.java new file mode 100644 index 00000000000..02b4feac734 --- /dev/null +++ b/dd-java-agent/instrumentation/feign/feign-10.8-generated/src/main/java/datadog/trace/instrumentation/feign/FeignAsyncClientInstrumentation.java @@ -0,0 +1,124 @@ +package datadog.trace.instrumentation.feign; + +import static datadog.context.Context.current; +import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.implementsInterface; +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.startSpan; +import static datadog.trace.instrumentation.feign.FeignClientDecorator.DECORATE; +import static datadog.trace.instrumentation.feign.FeignClientDecorator.FEIGN; +import static datadog.trace.instrumentation.feign.FeignClientDecorator.HTTP_REQUEST; +import static datadog.trace.instrumentation.feign.RequestHeaderInjectAdapter.SETTER; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import datadog.trace.bootstrap.CallDepthThreadLocalMap; +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import feign.AsyncClient; +import feign.Request; +import feign.Response; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumenterModule.class) +public class FeignAsyncClientInstrumentation extends InstrumenterModule.Tracing + implements Instrumenter.ForTypeHierarchy, Instrumenter.HasMethodAdvice { + + public FeignAsyncClientInstrumentation() { + super("feign", "feign-10.8"); + } + + @Override + public String hierarchyMarkerType() { + return "feign.AsyncClient"; + } + + @Override + public ElementMatcher hierarchyMatcher() { + return implementsInterface(named(hierarchyMarkerType())); + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".FeignClientDecorator", + packageName + ".RequestHeaderInjectAdapter", + packageName + ".SpanFinishingCallback", + }; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + isMethod() + .and(named("execute")) + .and(isPublic()) + .and(takesArguments(3)) + .and(takesArgument(0, named("feign.Request"))) + .and(takesArgument(1, named("feign.Request$Options"))), + FeignAsyncClientInstrumentation.class.getName() + "$AsyncExecuteAdvice"); + } + + public static class AsyncExecuteAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope methodEnter( + @Advice.Argument(value = 0, readOnly = false) Request request) { + final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(AsyncClient.class); + if (callDepth > 0) { + return null; + } + + final AgentSpan span = startSpan(FEIGN.toString(), HTTP_REQUEST); + DECORATE.afterStart(span); + DECORATE.onRequest(span, request); + + final AgentScope scope = activateSpan(span); + + // Inject trace context into request headers + Map> injectedHeaders = new LinkedHashMap<>(); + DECORATE.injectContext(current(), injectedHeaders, SETTER); + request = RequestHeaderInjectAdapter.inject(request, injectedHeaders); + + return scope; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Enter final AgentScope scope, + @Advice.Return(readOnly = false) CompletableFuture future, + @Advice.Thrown final Throwable throwable) { + if (scope == null) { + return; + } + final AgentSpan span = scope.span(); + scope.close(); + CallDepthThreadLocalMap.reset(AsyncClient.class); + + if (throwable != null) { + DECORATE.onError(span, throwable); + DECORATE.beforeFinish(span); + span.finish(); + return; + } + + if (future != null) { + future = future.whenComplete(new SpanFinishingCallback(span)); + } else { + DECORATE.beforeFinish(span); + span.finish(); + } + } + } +} diff --git a/dd-java-agent/instrumentation/feign/feign-10.8-generated/src/main/java/datadog/trace/instrumentation/feign/FeignClientDecorator.java b/dd-java-agent/instrumentation/feign/feign-10.8-generated/src/main/java/datadog/trace/instrumentation/feign/FeignClientDecorator.java new file mode 100644 index 00000000000..98b27fbbb34 --- /dev/null +++ b/dd-java-agent/instrumentation/feign/feign-10.8-generated/src/main/java/datadog/trace/instrumentation/feign/FeignClientDecorator.java @@ -0,0 +1,63 @@ +package datadog.trace.instrumentation.feign; + +import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; +import datadog.trace.bootstrap.instrumentation.decorator.HttpClientDecorator; +import feign.Request; +import feign.Response; +import java.net.URI; +import java.net.URISyntaxException; + +public class FeignClientDecorator extends HttpClientDecorator { + + public static final CharSequence FEIGN = UTF8BytesString.create("feign"); + public static final FeignClientDecorator DECORATE = new FeignClientDecorator(); + + public static final CharSequence HTTP_REQUEST = UTF8BytesString.create(DECORATE.operationName()); + + @Override + protected String[] instrumentationNames() { + return new String[] {"feign", "feign-10.8"}; + } + + @Override + protected CharSequence component() { + return FEIGN; + } + + @Override + protected String method(final Request request) { + return request.httpMethod().name(); + } + + @Override + protected URI url(final Request request) { + try { + return new URI(request.url()); + } catch (URISyntaxException e) { + return null; + } + } + + @Override + protected int status(final Response response) { + return response.status(); + } + + @Override + protected String getRequestHeader(Request request, String headerName) { + java.util.Collection values = request.headers().get(headerName); + if (values != null && !values.isEmpty()) { + return values.iterator().next(); + } + return null; + } + + @Override + protected String getResponseHeader(Response response, String headerName) { + java.util.Collection values = response.headers().get(headerName); + if (values != null && !values.isEmpty()) { + return values.iterator().next(); + } + return null; + } +} diff --git a/dd-java-agent/instrumentation/feign/feign-10.8-generated/src/main/java/datadog/trace/instrumentation/feign/FeignClientInstrumentation.java b/dd-java-agent/instrumentation/feign/feign-10.8-generated/src/main/java/datadog/trace/instrumentation/feign/FeignClientInstrumentation.java new file mode 100644 index 00000000000..f5e177c881e --- /dev/null +++ b/dd-java-agent/instrumentation/feign/feign-10.8-generated/src/main/java/datadog/trace/instrumentation/feign/FeignClientInstrumentation.java @@ -0,0 +1,116 @@ +package datadog.trace.instrumentation.feign; + +import static datadog.context.Context.current; +import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.implementsInterface; +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.startSpan; +import static datadog.trace.instrumentation.feign.FeignClientDecorator.DECORATE; +import static datadog.trace.instrumentation.feign.FeignClientDecorator.FEIGN; +import static datadog.trace.instrumentation.feign.FeignClientDecorator.HTTP_REQUEST; +import static datadog.trace.instrumentation.feign.RequestHeaderInjectAdapter.SETTER; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import datadog.trace.bootstrap.CallDepthThreadLocalMap; +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import feign.Client; +import feign.Request; +import feign.Response; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumenterModule.class) +public class FeignClientInstrumentation extends InstrumenterModule.Tracing + implements Instrumenter.ForTypeHierarchy, Instrumenter.HasMethodAdvice { + + public FeignClientInstrumentation() { + super("feign", "feign-10.8"); + } + + @Override + public String hierarchyMarkerType() { + return "feign.Client"; + } + + @Override + public ElementMatcher hierarchyMatcher() { + return implementsInterface(named(hierarchyMarkerType())); + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".FeignClientDecorator", packageName + ".RequestHeaderInjectAdapter", + }; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + isMethod() + .and(named("execute")) + .and(isPublic()) + .and(takesArguments(2)) + .and(takesArgument(0, named("feign.Request"))) + .and(takesArgument(1, named("feign.Request$Options"))), + FeignClientInstrumentation.class.getName() + "$ExecuteAdvice"); + } + + public static class ExecuteAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope methodEnter( + @Advice.Argument(value = 0, readOnly = false) Request request) { + final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(Client.class); + if (callDepth > 0) { + return null; + } + + final AgentSpan span = startSpan(FEIGN.toString(), HTTP_REQUEST); + DECORATE.afterStart(span); + DECORATE.onRequest(span, request); + + final AgentScope scope = activateSpan(span); + + // Inject trace context into request headers + Map> injectedHeaders = new LinkedHashMap<>(); + DECORATE.injectContext(current(), injectedHeaders, SETTER); + request = RequestHeaderInjectAdapter.inject(request, injectedHeaders); + + return scope; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Enter final AgentScope scope, + @Advice.Return final Response response, + @Advice.Thrown final Throwable throwable) { + if (scope == null) { + return; + } + final AgentSpan span = scope.span(); + try { + if (response != null) { + DECORATE.onResponse(span, response); + } + DECORATE.onError(span, throwable); + DECORATE.beforeFinish(span); + } finally { + scope.close(); + span.finish(); + CallDepthThreadLocalMap.reset(Client.class); + } + } + } +} diff --git a/dd-java-agent/instrumentation/feign/feign-10.8-generated/src/main/java/datadog/trace/instrumentation/feign/RequestHeaderInjectAdapter.java b/dd-java-agent/instrumentation/feign/feign-10.8-generated/src/main/java/datadog/trace/instrumentation/feign/RequestHeaderInjectAdapter.java new file mode 100644 index 00000000000..1625a4d94cd --- /dev/null +++ b/dd-java-agent/instrumentation/feign/feign-10.8-generated/src/main/java/datadog/trace/instrumentation/feign/RequestHeaderInjectAdapter.java @@ -0,0 +1,36 @@ +package datadog.trace.instrumentation.feign; + +import datadog.context.propagation.CarrierSetter; +import feign.Request; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import javax.annotation.ParametersAreNonnullByDefault; + +@ParametersAreNonnullByDefault +public class RequestHeaderInjectAdapter implements CarrierSetter>> { + + public static final RequestHeaderInjectAdapter SETTER = new RequestHeaderInjectAdapter(); + + @Override + public void set( + final Map> carrier, final String key, final String value) { + Collection values = new ArrayList<>(1); + values.add(value); + carrier.put(key, values); + } + + /** + * Feign Request objects are immutable — headers cannot be modified after creation. This method + * creates a new Request with the trace context headers injected. + */ + public static Request inject( + final Request original, final Map> injectedHeaders) { + Map> merged = new LinkedHashMap<>(original.headers()); + merged.putAll(injectedHeaders); + return Request.create( + original.httpMethod(), original.url(), merged, original.body(), StandardCharsets.UTF_8); + } +} diff --git a/dd-java-agent/instrumentation/feign/feign-10.8-generated/src/main/java/datadog/trace/instrumentation/feign/SpanFinishingCallback.java b/dd-java-agent/instrumentation/feign/feign-10.8-generated/src/main/java/datadog/trace/instrumentation/feign/SpanFinishingCallback.java new file mode 100644 index 00000000000..9066336e5e8 --- /dev/null +++ b/dd-java-agent/instrumentation/feign/feign-10.8-generated/src/main/java/datadog/trace/instrumentation/feign/SpanFinishingCallback.java @@ -0,0 +1,32 @@ +package datadog.trace.instrumentation.feign; + +import static datadog.trace.instrumentation.feign.FeignClientDecorator.DECORATE; + +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import feign.Response; +import java.util.function.BiConsumer; + +/** Callback to finish a span when a {@link java.util.concurrent.CompletableFuture} completes. */ +public class SpanFinishingCallback implements BiConsumer { + + private final AgentSpan span; + + public SpanFinishingCallback(final AgentSpan span) { + this.span = span; + } + + @Override + public void accept(final Response response, final Throwable error) { + try { + if (response != null) { + DECORATE.onResponse(span, response); + } + if (error != null) { + DECORATE.onError(span, error); + } + DECORATE.beforeFinish(span); + } finally { + span.finish(); + } + } +} diff --git a/dd-java-agent/instrumentation/feign/feign-10.8-generated/src/test/java/datadog/trace/instrumentation/feign/FeignAsyncClientTest.java b/dd-java-agent/instrumentation/feign/feign-10.8-generated/src/test/java/datadog/trace/instrumentation/feign/FeignAsyncClientTest.java new file mode 100644 index 00000000000..871351cf6e8 --- /dev/null +++ b/dd-java-agent/instrumentation/feign/feign-10.8-generated/src/test/java/datadog/trace/instrumentation/feign/FeignAsyncClientTest.java @@ -0,0 +1,526 @@ +package datadog.trace.instrumentation.feign; + +import static datadog.trace.agent.test.assertions.Matchers.is; +import static datadog.trace.agent.test.assertions.Matchers.isNonNull; +import static datadog.trace.agent.test.assertions.Matchers.matches; +import static datadog.trace.agent.test.assertions.Matchers.validates; +import static datadog.trace.agent.test.assertions.SpanMatcher.span; +import static datadog.trace.agent.test.assertions.TagsMatcher.defaultTags; +import static datadog.trace.agent.test.assertions.TagsMatcher.tag; +import static datadog.trace.agent.test.assertions.TraceMatcher.SORT_BY_START_TIME; +import static datadog.trace.agent.test.assertions.TraceMatcher.trace; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.startSpan; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.sun.net.httpserver.HttpServer; +import datadog.trace.agent.test.AbstractInstrumentationTest; +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.core.DDSpan; +import datadog.trace.junit.utils.config.WithConfig; +import feign.AsyncClient; +import feign.Client; +import feign.Request; +import feign.Response; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.regex.Pattern; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@WithConfig(key = "trace.enabled", value = "true") +public class FeignAsyncClientTest extends AbstractInstrumentationTest { + + private HttpServer httpServer; + private int port; + + @BeforeAll + void setupServer() throws IOException { + httpServer = HttpServer.create(new InetSocketAddress(0), 0); + + httpServer.createContext( + "/api/users", + exchange -> { + byte[] body = "{\"id\":1,\"name\":\"Alice\"}".getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().add("Content-Type", "application/json"); + exchange.sendResponseHeaders(200, body.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(body); + } + }); + + httpServer.createContext( + "/api/error", + exchange -> { + byte[] body = "{\"error\":\"Internal Server Error\"}".getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().add("Content-Type", "application/json"); + exchange.sendResponseHeaders(500, body.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(body); + } + }); + + httpServer.setExecutor(null); + httpServer.start(); + port = httpServer.getAddress().getPort(); + } + + @AfterAll + void tearDownServer() { + if (httpServer != null) { + httpServer.stop(0); + } + } + + @Test + void asyncGetCreatesClientSpan() + throws ExecutionException, TimeoutException, InterruptedException { + AgentSpan parentSpan = startSpan("test", "parent"); + AgentScope parentScope = activateSpan(parentSpan); + Response response; + try { + AsyncClient asyncClient = new AsyncClient.Pseudo<>(new Client.Default(null, null)); + Request request = buildRequest(Request.HttpMethod.GET, "/api/users", null); + CompletableFuture future = + asyncClient.execute(request, defaultOptions(), Optional.empty()); + response = future.get(10, TimeUnit.SECONDS); + } finally { + parentScope.close(); + parentSpan.finish(); + } + + assertEquals(200, response.status()); + response.close(); + + // AsyncClient.Pseudo delegates to Client.Default, which is also instrumented, + // resulting in two client spans: one from the async instrumentation and one from the sync + assertTraces( + trace( + SORT_BY_START_TIME, + span().root().operationName(Pattern.compile("parent")), + span() + .operationName(Pattern.compile("http\\.request")) + .type("http") + .error(false) + .childOfPrevious() + .resourceName(r -> r != null && r.toString().startsWith("GET")) + .tags( + defaultTags(), + tag("span.kind", matches("client")), + tag("component", matches("feign")), + tag("http.method", matches("GET")), + tag( + "http.url", + validates(v -> v != null && v.toString().contains("/api/users"))), + tag("http.status_code", is(200)), + tag("peer.hostname", matches("localhost")), + tag("peer.port", is(port))), + span() + .operationName(Pattern.compile("http\\.request")) + .type("http") + .error(false) + .childOfPrevious() + .tags( + defaultTags(), + tag("span.kind", matches("client")), + tag("component", matches("feign")), + tag("http.method", matches("GET")), + tag( + "http.url", + validates(v -> v != null && v.toString().contains("/api/users"))), + tag("http.status_code", is(200)), + tag("peer.hostname", matches("localhost")), + tag("peer.port", is(port))))); + } + + @Test + void asyncPostCreatesClientSpanWithCorrectMethod() + throws ExecutionException, TimeoutException, InterruptedException { + AgentSpan parentSpan = startSpan("test", "parent"); + AgentScope parentScope = activateSpan(parentSpan); + Response response; + try { + AsyncClient asyncClient = new AsyncClient.Pseudo<>(new Client.Default(null, null)); + byte[] body = "{\"name\":\"Bob\"}".getBytes(StandardCharsets.UTF_8); + Request request = buildRequest(Request.HttpMethod.POST, "/api/users", body); + CompletableFuture future = + asyncClient.execute(request, defaultOptions(), Optional.empty()); + response = future.get(10, TimeUnit.SECONDS); + } finally { + parentScope.close(); + parentSpan.finish(); + } + + assertEquals(200, response.status()); + response.close(); + + // AsyncClient.Pseudo delegates to Client.Default, which is also instrumented, + // resulting in two client spans: one from the async instrumentation and one from the sync + assertTraces( + trace( + SORT_BY_START_TIME, + span().root().operationName(Pattern.compile("parent")), + span() + .operationName(Pattern.compile("http\\.request")) + .type("http") + .error(false) + .childOfPrevious() + .resourceName(r -> r != null && r.toString().startsWith("POST")) + .tags( + defaultTags(), + tag("span.kind", matches("client")), + tag("component", matches("feign")), + tag("http.method", matches("POST")), + tag( + "http.url", + validates(v -> v != null && v.toString().contains("/api/users"))), + tag("http.status_code", is(200)), + tag("peer.hostname", matches("localhost")), + tag("peer.port", is(port))), + span() + .operationName(Pattern.compile("http\\.request")) + .type("http") + .error(false) + .childOfPrevious() + .tags( + defaultTags(), + tag("span.kind", matches("client")), + tag("component", matches("feign")), + tag("http.method", matches("POST")), + tag( + "http.url", + validates(v -> v != null && v.toString().contains("/api/users"))), + tag("http.status_code", is(200)), + tag("peer.hostname", matches("localhost")), + tag("peer.port", is(port))))); + } + + @Test + void asyncErrorResponseSetsStatusCodeAndErrorFlag() + throws ExecutionException, TimeoutException, InterruptedException { + AgentSpan parentSpan = startSpan("test", "parent"); + AgentScope parentScope = activateSpan(parentSpan); + Response response; + try { + AsyncClient asyncClient = new AsyncClient.Pseudo<>(new Client.Default(null, null)); + Request request = buildRequest(Request.HttpMethod.GET, "/api/error", null); + CompletableFuture future = + asyncClient.execute(request, defaultOptions(), Optional.empty()); + response = future.get(10, TimeUnit.SECONDS); + } finally { + parentScope.close(); + parentSpan.finish(); + } + + assertEquals(500, response.status()); + response.close(); + + // 500 is a server error, not in dd-trace-java's default HTTP client error statuses (400-499) + // AsyncClient.Pseudo delegates to Client.Default, producing two client spans + assertTraces( + trace( + SORT_BY_START_TIME, + span().root().operationName(Pattern.compile("parent")), + span() + .operationName(Pattern.compile("http\\.request")) + .type("http") + .error(false) + .childOfPrevious() + .tags( + defaultTags(), + tag("span.kind", matches("client")), + tag("component", matches("feign")), + tag("http.method", matches("GET")), + tag( + "http.url", + validates(v -> v != null && v.toString().contains("/api/error"))), + tag("http.status_code", is(500)), + tag("peer.hostname", matches("localhost")), + tag("peer.port", is(port))), + span() + .operationName(Pattern.compile("http\\.request")) + .type("http") + .error(false) + .childOfPrevious() + .tags( + defaultTags(), + tag("span.kind", matches("client")), + tag("component", matches("feign")), + tag("http.method", matches("GET")), + tag( + "http.url", + validates(v -> v != null && v.toString().contains("/api/error"))), + tag("http.status_code", is(500)), + tag("peer.hostname", matches("localhost")), + tag("peer.port", is(port))))); + } + + @Test + void asyncConnectionExceptionSetsErrorTags() throws TimeoutException, InterruptedException { + AgentSpan parentSpan = startSpan("test", "parent"); + AgentScope parentScope = activateSpan(parentSpan); + try { + AsyncClient asyncClient = new AsyncClient.Pseudo<>(new Client.Default(null, null)); + // Use a port that is not listening to force a connection error + Request request = + Request.create( + Request.HttpMethod.GET, + "http://localhost:1/not-listening", + Collections.emptyMap(), + null, + StandardCharsets.UTF_8); + CompletableFuture future = + asyncClient.execute(request, defaultOptions(), Optional.empty()); + try { + future.get(10, TimeUnit.SECONDS); + } catch (ExecutionException expected) { + // expected - connection refused wrapped in ExecutionException + } + } finally { + parentScope.close(); + parentSpan.finish(); + } + + // AsyncClient.Pseudo delegates to Client.Default, producing two client spans (both with error) + assertTraces( + trace( + SORT_BY_START_TIME, + span().root().operationName(Pattern.compile("parent")), + span() + .operationName(Pattern.compile("http\\.request")) + .type("http") + .error(true) + .childOfPrevious() + .tags( + defaultTags(), + tag("span.kind", matches("client")), + tag("component", matches("feign")), + tag("http.method", matches("GET")), + tag( + "http.url", + validates(v -> v != null && v.toString().contains("/not-listening"))), + tag("error.type", isNonNull()), + tag("error.message", isNonNull()), + tag("error.stack", isNonNull()), + tag("peer.hostname", matches("localhost")), + tag("peer.port", is(1))), + span() + .operationName(Pattern.compile("http\\.request")) + .type("http") + .error(true) + .childOfPrevious() + .tags( + defaultTags(), + tag("span.kind", matches("client")), + tag("component", matches("feign")), + tag("http.method", matches("GET")), + tag( + "http.url", + validates(v -> v != null && v.toString().contains("/not-listening"))), + tag("error.type", isNonNull()), + tag("error.message", isNonNull()), + tag("error.stack", isNonNull()), + tag("peer.hostname", matches("localhost")), + tag("peer.port", is(1))))); + } + + @Test + void asyncContextPropagationInjectsHeaders() + throws ExecutionException, TimeoutException, InterruptedException { + // Capture headers received by the server + Map> receivedHeaders = new HashMap<>(); + httpServer.createContext( + "/api/headers", + exchange -> { + for (Map.Entry> entry : exchange.getRequestHeaders().entrySet()) { + receivedHeaders.put(entry.getKey().toLowerCase(), entry.getValue()); + } + exchange.sendResponseHeaders(200, -1); + exchange.close(); + }); + + AgentSpan parentSpan = startSpan("test", "parent"); + AgentScope parentScope = activateSpan(parentSpan); + Response response; + try { + AsyncClient asyncClient = new AsyncClient.Pseudo<>(new Client.Default(null, null)); + Request request = buildRequest(Request.HttpMethod.GET, "/api/headers", null); + CompletableFuture future = + asyncClient.execute(request, defaultOptions(), Optional.empty()); + response = future.get(10, TimeUnit.SECONDS); + } finally { + parentScope.close(); + parentSpan.finish(); + } + + assertEquals(200, response.status()); + response.close(); + + // AsyncClient.Pseudo delegates to Client.Default, producing two client spans + assertTraces( + trace( + SORT_BY_START_TIME, + span().root().operationName(Pattern.compile("parent")), + span() + .operationName(Pattern.compile("http\\.request")) + .type("http") + .error(false) + .childOfPrevious() + .tags( + defaultTags(), + tag("span.kind", matches("client")), + tag("component", matches("feign")), + tag("http.method", matches("GET")), + tag( + "http.url", + validates(v -> v != null && v.toString().contains("/api/headers"))), + tag("http.status_code", is(200)), + tag("peer.hostname", matches("localhost")), + tag("peer.port", is(port))), + span() + .operationName(Pattern.compile("http\\.request")) + .type("http") + .error(false) + .childOfPrevious() + .tags( + defaultTags(), + tag("span.kind", matches("client")), + tag("component", matches("feign")), + tag("http.method", matches("GET")), + tag( + "http.url", + validates(v -> v != null && v.toString().contains("/api/headers"))), + tag("http.status_code", is(200)), + tag("peer.hostname", matches("localhost")), + tag("peer.port", is(port))))); + + // Verify context propagation headers were injected + assertTrue( + receivedHeaders.containsKey("x-datadog-trace-id"), + "Server should receive x-datadog-trace-id header. Received headers: " + + receivedHeaders.keySet()); + assertTrue( + receivedHeaders.containsKey("x-datadog-parent-id"), + "Server should receive x-datadog-parent-id header"); + assertTrue( + receivedHeaders.containsKey("x-datadog-sampling-priority"), + "Server should receive x-datadog-sampling-priority header"); + + String traceId = receivedHeaders.get("x-datadog-trace-id").get(0); + String parentId = receivedHeaders.get("x-datadog-parent-id").get(0); + assertNotNull(traceId, "x-datadog-trace-id should not be null"); + assertNotNull(parentId, "x-datadog-parent-id should not be null"); + assertFalse(traceId.isEmpty(), "x-datadog-trace-id should not be empty"); + assertFalse(parentId.isEmpty(), "x-datadog-parent-id should not be empty"); + + // Verify the trace ID matches the active trace + List allSpans = flattenTraces(); + DDSpan clientSpan = findSpanByKind(allSpans, "client"); + assertNotNull(clientSpan, "Expected an HTTP client span from feign async"); + assertEquals( + String.valueOf(clientSpan.getTraceId().toLong()), + traceId, + "Injected trace ID should match the client span's trace ID"); + } + + @Test + void asyncSpanFinishesAfterFutureCompletes() + throws ExecutionException, TimeoutException, InterruptedException { + // Verify the span lifecycle: span should start at execute() and finish when the future + // completes, capturing the response status code from the completed future + AsyncClient asyncClient = new AsyncClient.Pseudo<>(new Client.Default(null, null)); + Request request = buildRequest(Request.HttpMethod.GET, "/api/users", null); + CompletableFuture future = + asyncClient.execute(request, defaultOptions(), Optional.empty()); + Response response = future.get(10, TimeUnit.SECONDS); + response.close(); + + // AsyncClient.Pseudo delegates to Client.Default, producing two client spans + assertTraces( + trace( + SORT_BY_START_TIME, + span() + .operationName(Pattern.compile("http\\.request")) + .type("http") + .error(false) + .root() + .tags( + defaultTags(), + tag("span.kind", matches("client")), + tag("component", matches("feign")), + tag("http.method", matches("GET")), + tag( + "http.url", + validates(v -> v != null && v.toString().contains("/api/users"))), + tag("http.status_code", is(200)), + tag("peer.hostname", matches("localhost")), + tag("peer.port", is(port))), + span() + .operationName(Pattern.compile("http\\.request")) + .type("http") + .error(false) + .childOfPrevious() + .tags( + defaultTags(), + tag("span.kind", matches("client")), + tag("component", matches("feign")), + tag("http.method", matches("GET")), + tag( + "http.url", + validates(v -> v != null && v.toString().contains("/api/users"))), + tag("http.status_code", is(200)), + tag("peer.hostname", matches("localhost")), + tag("peer.port", is(port))))); + } + + // ---- Helper methods ---- + + private Request buildRequest(Request.HttpMethod method, String path, byte[] requestBody) { + String url = "http://localhost:" + port + path; + Map> headers = new HashMap<>(); + headers.put("Accept", Collections.singletonList("application/json")); + headers.put("Content-Type", Collections.singletonList("application/json")); + return Request.create(method, url, headers, requestBody, StandardCharsets.UTF_8); + } + + private Request.Options defaultOptions() { + return new Request.Options(5, TimeUnit.SECONDS, 5, TimeUnit.SECONDS, true); + } + + private List flattenTraces() { + List result = new ArrayList<>(); + for (List trace : writer) { + result.addAll(trace); + } + return result; + } + + private DDSpan findSpanByKind(List spans, String spanKind) { + for (DDSpan span : spans) { + if (spanKind.equals(String.valueOf(span.getTag("span.kind")))) { + return span; + } + } + return null; + } +} diff --git a/dd-java-agent/instrumentation/feign/feign-10.8-generated/src/test/java/datadog/trace/instrumentation/feign/FeignClientTest.java b/dd-java-agent/instrumentation/feign/feign-10.8-generated/src/test/java/datadog/trace/instrumentation/feign/FeignClientTest.java new file mode 100644 index 00000000000..2cc3a40915a --- /dev/null +++ b/dd-java-agent/instrumentation/feign/feign-10.8-generated/src/test/java/datadog/trace/instrumentation/feign/FeignClientTest.java @@ -0,0 +1,403 @@ +package datadog.trace.instrumentation.feign; + +import static datadog.trace.agent.test.assertions.Matchers.is; +import static datadog.trace.agent.test.assertions.Matchers.isNonNull; +import static datadog.trace.agent.test.assertions.Matchers.matches; +import static datadog.trace.agent.test.assertions.Matchers.validates; +import static datadog.trace.agent.test.assertions.SpanMatcher.span; +import static datadog.trace.agent.test.assertions.TagsMatcher.defaultTags; +import static datadog.trace.agent.test.assertions.TagsMatcher.tag; +import static datadog.trace.agent.test.assertions.TraceMatcher.SORT_BY_START_TIME; +import static datadog.trace.agent.test.assertions.TraceMatcher.trace; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.startSpan; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.sun.net.httpserver.HttpServer; +import datadog.trace.agent.test.AbstractInstrumentationTest; +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.core.DDSpan; +import datadog.trace.junit.utils.config.WithConfig; +import feign.Client; +import feign.Request; +import feign.Response; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@WithConfig(key = "trace.enabled", value = "true") +public class FeignClientTest extends AbstractInstrumentationTest { + + private HttpServer httpServer; + private int port; + + @BeforeAll + void setupServer() throws IOException { + httpServer = HttpServer.create(new InetSocketAddress(0), 0); + + httpServer.createContext( + "/api/users", + exchange -> { + byte[] body = "{\"id\":1,\"name\":\"Alice\"}".getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().add("Content-Type", "application/json"); + exchange.sendResponseHeaders(200, body.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(body); + } + }); + + httpServer.createContext( + "/api/error", + exchange -> { + byte[] body = "{\"error\":\"Internal Server Error\"}".getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().add("Content-Type", "application/json"); + exchange.sendResponseHeaders(500, body.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(body); + } + }); + + httpServer.setExecutor(null); + httpServer.start(); + port = httpServer.getAddress().getPort(); + } + + @AfterAll + void tearDownServer() { + if (httpServer != null) { + httpServer.stop(0); + } + } + + @Test + void syncGetCreatesClientSpan() throws IOException { + AgentSpan parentSpan = startSpan("test", "parent"); + AgentScope parentScope = activateSpan(parentSpan); + Response response; + try { + Client client = new Client.Default(null, null); + Request request = buildRequest(Request.HttpMethod.GET, "/api/users", null); + response = client.execute(request, defaultOptions()); + } finally { + parentScope.close(); + parentSpan.finish(); + } + + assertEquals(200, response.status()); + response.close(); + + assertTraces( + trace( + SORT_BY_START_TIME, + span().root().operationName(Pattern.compile("parent")), + span() + .operationName(Pattern.compile("http\\.request")) + .type("http") + .error(false) + .childOfPrevious() + .resourceName(r -> r != null && r.toString().startsWith("GET")) + .tags( + defaultTags(), + tag("span.kind", matches("client")), + tag("component", matches("feign")), + tag("http.method", matches("GET")), + tag( + "http.url", + validates(v -> v != null && v.toString().contains("/api/users"))), + tag("http.status_code", is(200)), + tag("peer.hostname", matches("localhost")), + tag("peer.port", is(port))))); + } + + @Test + void syncPostCreatesClientSpanWithCorrectMethod() throws IOException { + AgentSpan parentSpan = startSpan("test", "parent"); + AgentScope parentScope = activateSpan(parentSpan); + Response response; + try { + Client client = new Client.Default(null, null); + byte[] body = "{\"name\":\"Bob\"}".getBytes(StandardCharsets.UTF_8); + Request request = buildRequest(Request.HttpMethod.POST, "/api/users", body); + response = client.execute(request, defaultOptions()); + } finally { + parentScope.close(); + parentSpan.finish(); + } + + assertEquals(200, response.status()); + response.close(); + + assertTraces( + trace( + SORT_BY_START_TIME, + span().root().operationName(Pattern.compile("parent")), + span() + .operationName(Pattern.compile("http\\.request")) + .type("http") + .error(false) + .childOfPrevious() + .resourceName(r -> r != null && r.toString().startsWith("POST")) + .tags( + defaultTags(), + tag("span.kind", matches("client")), + tag("component", matches("feign")), + tag("http.method", matches("POST")), + tag( + "http.url", + validates(v -> v != null && v.toString().contains("/api/users"))), + tag("http.status_code", is(200)), + tag("peer.hostname", matches("localhost")), + tag("peer.port", is(port))))); + } + + @Test + void syncErrorResponseSetsStatusCodeAndErrorFlag() throws IOException { + AgentSpan parentSpan = startSpan("test", "parent"); + AgentScope parentScope = activateSpan(parentSpan); + Response response; + try { + Client client = new Client.Default(null, null); + Request request = buildRequest(Request.HttpMethod.GET, "/api/error", null); + response = client.execute(request, defaultOptions()); + } finally { + parentScope.close(); + parentSpan.finish(); + } + + assertEquals(500, response.status()); + response.close(); + + // 500 is a server error, not in dd-trace-java's default HTTP client error statuses (400-499) + assertTraces( + trace( + SORT_BY_START_TIME, + span().root().operationName(Pattern.compile("parent")), + span() + .operationName(Pattern.compile("http\\.request")) + .type("http") + .error(false) + .childOfPrevious() + .tags( + defaultTags(), + tag("span.kind", matches("client")), + tag("component", matches("feign")), + tag("http.method", matches("GET")), + tag( + "http.url", + validates(v -> v != null && v.toString().contains("/api/error"))), + tag("http.status_code", is(500)), + tag("peer.hostname", matches("localhost")), + tag("peer.port", is(port))))); + } + + @Test + void syncConnectionExceptionSetsErrorTags() { + AgentSpan parentSpan = startSpan("test", "parent"); + AgentScope parentScope = activateSpan(parentSpan); + try { + Client client = new Client.Default(null, null); + // Use a port that is not listening to force a connection error + Request request = + Request.create( + Request.HttpMethod.GET, + "http://localhost:1/not-listening", + Collections.emptyMap(), + null, + StandardCharsets.UTF_8); + try { + client.execute(request, defaultOptions()); + } catch (IOException expected) { + // expected - connection refused + } + } finally { + parentScope.close(); + parentSpan.finish(); + } + + assertTraces( + trace( + SORT_BY_START_TIME, + span().root().operationName(Pattern.compile("parent")), + span() + .operationName(Pattern.compile("http\\.request")) + .type("http") + .error(true) + .childOfPrevious() + .tags( + defaultTags(), + tag("span.kind", matches("client")), + tag("component", matches("feign")), + tag("http.method", matches("GET")), + tag( + "http.url", + validates(v -> v != null && v.toString().contains("/not-listening"))), + tag("error.type", isNonNull()), + tag("error.message", isNonNull()), + tag("error.stack", isNonNull()), + tag("peer.hostname", matches("localhost")), + tag("peer.port", is(1))))); + } + + @Test + void syncContextPropagationInjectsHeaders() throws IOException { + // Capture headers received by the server + Map> receivedHeaders = new HashMap<>(); + httpServer.createContext( + "/api/headers", + exchange -> { + for (Map.Entry> entry : exchange.getRequestHeaders().entrySet()) { + receivedHeaders.put(entry.getKey().toLowerCase(), entry.getValue()); + } + exchange.sendResponseHeaders(200, -1); + exchange.close(); + }); + + AgentSpan parentSpan = startSpan("test", "parent"); + AgentScope parentScope = activateSpan(parentSpan); + Response response; + try { + Client client = new Client.Default(null, null); + Request request = buildRequest(Request.HttpMethod.GET, "/api/headers", null); + response = client.execute(request, defaultOptions()); + } finally { + parentScope.close(); + parentSpan.finish(); + } + + assertEquals(200, response.status()); + response.close(); + + assertTraces( + trace( + SORT_BY_START_TIME, + span().root().operationName(Pattern.compile("parent")), + span() + .operationName(Pattern.compile("http\\.request")) + .type("http") + .error(false) + .childOfPrevious() + .tags( + defaultTags(), + tag("span.kind", matches("client")), + tag("component", matches("feign")), + tag("http.method", matches("GET")), + tag( + "http.url", + validates(v -> v != null && v.toString().contains("/api/headers"))), + tag("http.status_code", is(200)), + tag("peer.hostname", matches("localhost")), + tag("peer.port", is(port))))); + + // Verify context propagation headers were injected + assertTrue( + receivedHeaders.containsKey("x-datadog-trace-id"), + "Server should receive x-datadog-trace-id header. Received headers: " + + receivedHeaders.keySet()); + assertTrue( + receivedHeaders.containsKey("x-datadog-parent-id"), + "Server should receive x-datadog-parent-id header"); + assertTrue( + receivedHeaders.containsKey("x-datadog-sampling-priority"), + "Server should receive x-datadog-sampling-priority header"); + + String traceId = receivedHeaders.get("x-datadog-trace-id").get(0); + String parentId = receivedHeaders.get("x-datadog-parent-id").get(0); + assertNotNull(traceId, "x-datadog-trace-id should not be null"); + assertNotNull(parentId, "x-datadog-parent-id should not be null"); + assertFalse(traceId.isEmpty(), "x-datadog-trace-id should not be empty"); + assertFalse(parentId.isEmpty(), "x-datadog-parent-id should not be empty"); + + // Verify the trace ID matches the active trace + List allSpans = flattenTraces(); + DDSpan clientSpan = findSpanByKind(allSpans, "client"); + assertNotNull(clientSpan, "Expected an HTTP client span from feign"); + assertEquals( + String.valueOf(clientSpan.getTraceId().toLong()), + traceId, + "Injected trace ID should match the client span's trace ID"); + // Verify the parent ID in the header matches the client span's span ID + // This ensures the downstream service will correctly link its span as a child of this client + // span + assertEquals( + String.valueOf(clientSpan.getSpanId()), + parentId, + "Injected parent ID should match the client span's span ID"); + } + + @Test + void syncClientSpanWithoutParentIsRootSpan() throws IOException { + // Execute without an active parent span to verify span is still created + Client client = new Client.Default(null, null); + Request request = buildRequest(Request.HttpMethod.GET, "/api/users", null); + Response response = client.execute(request, defaultOptions()); + response.close(); + + assertTraces( + trace( + span() + .operationName(Pattern.compile("http\\.request")) + .type("http") + .error(false) + .root() + .tags( + defaultTags(), + tag("span.kind", matches("client")), + tag("component", matches("feign")), + tag("http.method", matches("GET")), + tag( + "http.url", + validates(v -> v != null && v.toString().contains("/api/users"))), + tag("http.status_code", is(200)), + tag("peer.hostname", matches("localhost")), + tag("peer.port", is(port))))); + } + + // ---- Helper methods ---- + + private Request buildRequest(Request.HttpMethod method, String path, byte[] requestBody) { + String url = "http://localhost:" + port + path; + Map> headers = new HashMap<>(); + headers.put("Accept", Collections.singletonList("application/json")); + headers.put("Content-Type", Collections.singletonList("application/json")); + return Request.create(method, url, headers, requestBody, StandardCharsets.UTF_8); + } + + private Request.Options defaultOptions() { + return new Request.Options(5, TimeUnit.SECONDS, 5, TimeUnit.SECONDS, true); + } + + private List flattenTraces() { + List result = new ArrayList<>(); + for (List trace : writer) { + result.addAll(trace); + } + return result; + } + + private DDSpan findSpanByKind(List spans, String spanKind) { + for (DDSpan span : spans) { + if (spanKind.equals(String.valueOf(span.getTag("span.kind")))) { + return span; + } + } + return null; + } +} diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 09498b2beda..fbb1d45e6ef 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -5657,6 +5657,54 @@ "aliases": [] } ], + "DD_TRACE_FEIGN_10_8_ANALYTICS_ENABLED": [ + { + "version": "A", + "type": "boolean", + "default": "false", + "aliases": ["DD_FEIGN_10_8_ANALYTICS_ENABLED"] + } + ], + "DD_TRACE_FEIGN_10_8_ANALYTICS_SAMPLE_RATE": [ + { + "version": "A", + "type": "decimal", + "default": "1.0", + "aliases": ["DD_FEIGN_10_8_ANALYTICS_SAMPLE_RATE"] + } + ], + "DD_TRACE_FEIGN_10_8_ENABLED": [ + { + "version": "A", + "type": "boolean", + "default": "false", + "aliases": ["DD_TRACE_INTEGRATION_FEIGN_10_8_ENABLED", "DD_INTEGRATION_FEIGN_10_8_ENABLED"] + } + ], + "DD_TRACE_FEIGN_ANALYTICS_ENABLED": [ + { + "version": "A", + "type": "boolean", + "default": "false", + "aliases": ["DD_FEIGN_ANALYTICS_ENABLED"] + } + ], + "DD_TRACE_FEIGN_ANALYTICS_SAMPLE_RATE": [ + { + "version": "A", + "type": "decimal", + "default": "1.0", + "aliases": ["DD_FEIGN_ANALYTICS_SAMPLE_RATE"] + } + ], + "DD_TRACE_FEIGN_ENABLED": [ + { + "version": "A", + "type": "boolean", + "default": "false", + "aliases": ["DD_TRACE_INTEGRATION_FEIGN_ENABLED", "DD_INTEGRATION_FEIGN_ENABLED"] + } + ], "DD_TRACE_FILEITEMITERATOR_ENABLED": [ { "version": "A", diff --git a/settings.gradle.kts b/settings.gradle.kts index 0647382920a..78d6ade802b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -356,6 +356,7 @@ include( ":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:feign-10.8-generated", ":dd-java-agent:instrumentation:finatra-2.9", ":dd-java-agent:instrumentation:freemarker:freemarker-2.3.24", ":dd-java-agent:instrumentation:freemarker:freemarker-2.3.9",