Merge pull request #521 from DataDog/tyler/apache-http

Refactor Apache HttpClient Instrumentation.
This commit is contained in:
Tyler Benson 2018-10-01 14:59:35 -04:00 committed by GitHub
commit 929bc3b690
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 504 additions and 499 deletions

View File

@ -1,86 +0,0 @@
package datadog.trace.instrumentation.apachehttpclient;
import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType;
import static io.opentracing.log.Fields.ERROR_OBJECT;
import static net.bytebuddy.matcher.ElementMatchers.isAbstract;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.not;
import com.google.auto.service.AutoService;
import datadog.trace.agent.tooling.Instrumenter;
import io.opentracing.Scope;
import io.opentracing.Span;
import io.opentracing.Tracer;
import io.opentracing.tag.Tags;
import io.opentracing.util.GlobalTracer;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
import org.apache.http.impl.execchain.ClientExecChain;
@AutoService(Instrumenter.class)
public class ApacheHttpClientInstrumentation extends Instrumenter.Default {
public ApacheHttpClientInstrumentation() {
super("httpclient");
}
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return named("org.apache.http.impl.client.HttpClientBuilder")
.or(safeHasSuperType(named("org.apache.http.impl.client.CloseableHttpClient")));
}
@Override
public String[] helperClassNames() {
return new String[] {
"datadog.trace.instrumentation.apachehttpclient.DDTracingClientExec",
"datadog.trace.instrumentation.apachehttpclient.DDTracingClientExec$HttpHeadersInjectAdapter"
};
}
@Override
public Map<ElementMatcher, String> transformers() {
final Map<ElementMatcher, String> transformers = new HashMap<>();
transformers.put(
isMethod().and(not(isAbstract())).and(named("doExecute")), ClientAdvice.class.getName());
transformers.put(
isMethod().and(named("decorateProtocolExec")), ClientExecAdvice.class.getName());
return transformers;
}
public static class ClientAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static Scope methodEnter() {
final Tracer.SpanBuilder spanBuilder =
GlobalTracer.get()
.buildSpan(DDTracingClientExec.OPERATION_NAME)
.withTag(Tags.COMPONENT.getKey(), DDTracingClientExec.COMPONENT_NAME);
return spanBuilder.startActive(true);
}
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void methodExit(
@Advice.Enter final Scope scope, @Advice.Thrown final Throwable throwable) {
final Span span = scope.span();
if (throwable != null) {
Tags.ERROR.set(span, Boolean.TRUE);
span.log(Collections.singletonMap(ERROR_OBJECT, throwable));
span.finish();
}
scope.close();
}
}
public static class ClientExecAdvice {
@Advice.OnMethodExit(suppress = Throwable.class)
public static void addTracingExec(@Advice.Return(readOnly = false) ClientExecChain execChain) {
execChain = new DDTracingClientExec(execChain, GlobalTracer.get());
}
}
}

View File

@ -1,132 +0,0 @@
package datadog.trace.instrumentation.apachehttpclient;
import static io.opentracing.log.Fields.ERROR_OBJECT;
import datadog.trace.api.DDSpanTypes;
import datadog.trace.api.DDTags;
import io.opentracing.Scope;
import io.opentracing.Span;
import io.opentracing.Tracer;
import io.opentracing.propagation.Format;
import io.opentracing.propagation.TextMap;
import io.opentracing.tag.Tags;
import java.io.IOException;
import java.net.URI;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpExecutionAware;
import org.apache.http.client.methods.HttpRequestWrapper;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.impl.execchain.ClientExecChain;
/**
* Tracing is added before {@link org.apache.http.impl.execchain.ProtocolExec} which is invoked as
* the next to last. Note that {@link org.apache.http.impl.execchain.RedirectExec} is invoked before
* so this exec has to deal with redirects.
*/
@Slf4j
public class DDTracingClientExec implements ClientExecChain {
static final String COMPONENT_NAME = "apache-httpclient";
static final String OPERATION_NAME = "http.request";
private final ClientExecChain requestExecutor;
private final Tracer tracer;
public DDTracingClientExec(final ClientExecChain clientExecChain, final Tracer tracer) {
requestExecutor = clientExecChain;
this.tracer = tracer;
}
@Override
public CloseableHttpResponse execute(
final HttpRoute route,
final HttpRequestWrapper request,
final HttpClientContext clientContext,
final HttpExecutionAware execAware)
throws IOException, HttpException {
Scope scope = null;
Span span = null;
try {
// This handlers runs untrapped in the client code
// so we must ensure any unexpected agent errors are caught.
try {
scope =
tracer
.buildSpan(OPERATION_NAME)
.withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT)
.withTag(DDTags.SPAN_TYPE, DDSpanTypes.HTTP_CLIENT)
.startActive(true);
span = scope.span();
final boolean awsClientCall = request.getHeaders("amz-sdk-invocation-id").length > 0;
// AWS calls are often signed, so we can't add headers without breaking the signature.
if (!awsClientCall) {
tracer.inject(
span.context(), Format.Builtin.HTTP_HEADERS, new HttpHeadersInjectAdapter(request));
}
// request tags
Tags.HTTP_METHOD.set(span, request.getRequestLine().getMethod());
Tags.HTTP_URL.set(span, request.getRequestLine().getUri());
final URI uri = request.getURI();
// zuul users have encountered cases where getURI returns null
if (null != uri) {
Tags.PEER_PORT.set(span, uri.getPort() == -1 ? 80 : uri.getPort());
Tags.PEER_HOSTNAME.set(span, uri.getHost());
}
} catch (final Exception e) {
log.debug("failed to create span", e);
}
final CloseableHttpResponse response =
requestExecutor.execute(route, request, clientContext, execAware);
try {
// response tags
if (null != span) {
Tags.HTTP_STATUS.set(span, response.getStatusLine().getStatusCode());
}
} catch (final Exception e) {
log.debug("failed to set span status", e);
}
return response;
} catch (final IOException | HttpException | RuntimeException e) {
// error tags
Tags.ERROR.set(span, Boolean.TRUE);
span.log(Collections.singletonMap(ERROR_OBJECT, e));
throw e;
} finally {
if (null != scope) {
scope.close();
}
}
}
public static class HttpHeadersInjectAdapter implements TextMap {
private final HttpRequest httpRequest;
public HttpHeadersInjectAdapter(final HttpRequest httpRequest) {
this.httpRequest = httpRequest;
}
@Override
public void put(final String key, final String value) {
httpRequest.addHeader(key, value);
}
@Override
public Iterator<Map.Entry<String, String>> iterator() {
throw new UnsupportedOperationException(
"This class should be used only with tracer#inject()");
}
}
}

View File

@ -4,12 +4,12 @@ muzzle {
fail {
group = "commons-httpclient"
module = "commons-httpclient"
versions = "[,4.3)"
versions = "[,4.0)"
}
pass {
group = "org.apache.httpcomponents"
module = "httpclient"
versions = "[4.3,)"
versions = "[4.0,)"
assertInverse = true
}
}
@ -17,13 +17,11 @@ muzzle {
apply plugin: 'org.unbroken-dome.test-sets'
testSets {
latestDepTest {
dirName = 'test'
}
latestDepTest
}
dependencies {
compileOnly group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.3'
compileOnly group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.0'
compile project(':dd-java-agent:agent-tooling')
@ -33,8 +31,7 @@ dependencies {
implementation deps.autoservice
testCompile project(':dd-java-agent:testing')
testCompile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.3'
testCompile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.0'
latestDepTestCompile group: 'org.apache.httpcomponents', name: 'httpclient', version: '+'
}

View File

@ -9,11 +9,13 @@ import org.apache.http.client.ClientProtocolException
import org.apache.http.client.HttpClient
import org.apache.http.client.config.RequestConfig
import org.apache.http.client.methods.HttpGet
import org.apache.http.impl.client.BasicResponseHandler
import org.apache.http.impl.client.HttpClientBuilder
import org.apache.http.message.BasicHeader
import spock.lang.AutoCleanup
import spock.lang.Shared
import static datadog.trace.agent.test.TestUtils.runUnderTrace
import static datadog.trace.agent.test.asserts.ListWriterAssert.assertTraces
import static datadog.trace.agent.test.server.http.TestHttpServer.httpServer
@ -25,7 +27,7 @@ class ApacheHttpClientTest extends AgentTestRunner {
handlers {
prefix("success") {
handleDistributedRequest()
String msg = "<html><body><h1>Hello test.</h1>\n"
String msg = "Hello."
response.status(200).send(msg)
}
prefix("redirect") {
@ -46,24 +48,35 @@ class ApacheHttpClientTest extends AgentTestRunner {
def redirectUrl = server.address.resolve("/redirect")
@Shared
def twoRedirectsUrl = server.address.resolve("/another-redirect")
@Shared
def handler = new BasicResponseHandler()
final HttpClientBuilder builder = HttpClientBuilder.create()
final HttpClient client = builder.build()
def "trace request with propagation"() {
when:
HttpResponse response = client.execute(new HttpGet(successUrl))
String response = runUnderTrace("parent") {
if (responseHandler) {
client.execute(new HttpGet(successUrl), responseHandler)
} else {
client.execute(new HttpGet(successUrl)).entity.content.text
}
}
then:
response.getStatusLine().getStatusCode() == 200
response == "Hello."
// one trace on the server, one trace on the client
assertTraces(TEST_WRITER, 2) {
server.distributedRequestTrace(it, 0, TEST_WRITER[1][1])
trace(1, 2) {
clientParentSpan(it, 0)
parentSpan(it, 0)
successClientSpan(it, 1, span(0))
}
}
where:
responseHandler << [null, handler]
}
def "trace redirected request with propagation many redirects allowed"() {
@ -75,18 +88,19 @@ class ApacheHttpClientTest extends AgentTestRunner {
request.setConfig(requestConfigBuilder.build())
when:
HttpResponse response = client.execute(request)
HttpResponse response = runUnderTrace("parent") {
client.execute(request)
}
then:
response.getStatusLine().getStatusCode() == 200
// two traces on the server, one trace on the client
assertTraces(TEST_WRITER, 3) {
server.distributedRequestTrace(it, 0, TEST_WRITER[2][2])
server.distributedRequestTrace(it, 0, TEST_WRITER[2][1])
server.distributedRequestTrace(it, 1, TEST_WRITER[2][1])
trace(2, 3) {
clientParentSpan(it, 0)
successClientSpan(it, 1, span(0))
redirectClientSpan(it, 2, span(0))
trace(2, 2) {
parentSpan(it, 0)
successClientSpan(it, 1, span(0), 200, "redirect")
}
}
}
@ -99,18 +113,19 @@ class ApacheHttpClientTest extends AgentTestRunner {
request.setConfig(requestConfigBuilder.build())
when:
HttpResponse response = client.execute(request)
HttpResponse response = runUnderTrace("parent") {
client.execute(request)
}
then:
response.getStatusLine().getStatusCode() == 200
// two traces on the server, one trace on the client
assertTraces(TEST_WRITER, 3) {
server.distributedRequestTrace(it, 0, TEST_WRITER[2][2])
server.distributedRequestTrace(it, 0, TEST_WRITER[2][1])
server.distributedRequestTrace(it, 1, TEST_WRITER[2][1])
trace(2, 3) {
clientParentSpan(it, 0)
successClientSpan(it, 1, span(0))
redirectClientSpan(it, 2, span(0))
trace(2, 2) {
parentSpan(it, 0)
successClientSpan(it, 1, span(0), 200, "redirect")
}
}
}
@ -124,18 +139,19 @@ class ApacheHttpClientTest extends AgentTestRunner {
request.setConfig(requestConfigBuilder.build())
when:
client.execute(request)
runUnderTrace("parent") {
client.execute(request)
}
then:
def exception = thrown(ClientProtocolException)
// two traces on the server, one trace on the client
assertTraces(TEST_WRITER, 3) {
server.distributedRequestTrace(it, 0, TEST_WRITER[2][2])
server.distributedRequestTrace(it, 0, TEST_WRITER[2][1])
server.distributedRequestTrace(it, 1, TEST_WRITER[2][1])
trace(2, 3) {
clientParentSpan(it, 0, exception)
redirectClientSpan(it, 1, span(0))
redirectClientSpan(it, 2, span(0), "another-redirect")
trace(2, 2) {
parentSpan(it, 0, exception)
successClientSpan(it, 1, span(0), null, "another-redirect", exception)
}
}
}
@ -146,29 +162,30 @@ class ApacheHttpClientTest extends AgentTestRunner {
request.addHeader(new BasicHeader("is-dd-server", "false"))
when:
HttpResponse response = client.execute(request)
HttpResponse response = runUnderTrace("parent") {
client.execute(request)
}
then:
response.getStatusLine().getStatusCode() == 200
// only one trace (client).
assertTraces(TEST_WRITER, 1) {
trace(0, 2) {
clientParentSpan(it, 0)
parentSpan(it, 0)
successClientSpan(it, 1, span(0))
}
}
}
def clientParentSpan(TraceAssert trace, int index, Throwable exception = null) {
def parentSpan(TraceAssert trace, int index, Throwable exception = null) {
trace.span(index) {
parent()
serviceName "unnamed-java-app"
operationName "apache.http"
resourceName "apache.http"
operationName "parent"
resourceName "parent"
errored exception != null
tags {
defaultTags()
"$Tags.COMPONENT.key" "apache-httpclient"
if (exception) {
errorTags(exception.class)
}
@ -176,15 +193,19 @@ class ApacheHttpClientTest extends AgentTestRunner {
}
}
def successClientSpan(TraceAssert trace, int index, DDSpan parent, status = 200, route = "success") {
def successClientSpan(TraceAssert trace, int index, DDSpan parent, status = 200, route = "success", Throwable exception = null) {
trace.span(index) {
childOf parent
serviceName "unnamed-java-app"
operationName "http.request"
resourceName "GET /$route"
errored false
errored exception != null
tags {
defaultTags()
if (exception) {
errorTags(exception.class)
}
"$Tags.COMPONENT.key" "apache-httpclient"
"$Tags.HTTP_STATUS.key" status
"$Tags.HTTP_URL.key" "http://localhost:$port/$route"
"$Tags.PEER_HOSTNAME.key" "localhost"
@ -195,8 +216,4 @@ class ApacheHttpClientTest extends AgentTestRunner {
}
}
}
def redirectClientSpan(TraceAssert trace, int index, DDSpan parent, route = "redirect") {
successClientSpan(trace, index, parent, 302, route)
}
}

View File

@ -0,0 +1,187 @@
package datadog.trace.instrumentation.apachehttpclient;
import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType;
import static io.opentracing.log.Fields.ERROR_OBJECT;
import static net.bytebuddy.matcher.ElementMatchers.isAbstract;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.not;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import com.google.auto.service.AutoService;
import datadog.trace.agent.tooling.Instrumenter;
import datadog.trace.api.DDSpanTypes;
import datadog.trace.api.DDTags;
import datadog.trace.bootstrap.CallDepthThreadLocalMap;
import io.opentracing.Scope;
import io.opentracing.Span;
import io.opentracing.Tracer;
import io.opentracing.propagation.Format;
import io.opentracing.propagation.TextMap;
import io.opentracing.tag.Tags;
import io.opentracing.util.GlobalTracer;
import java.io.IOException;
import java.net.URI;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.implementation.bytecode.assign.Assigner;
import net.bytebuddy.matcher.ElementMatcher;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpUriRequest;
@AutoService(Instrumenter.class)
public class ApacheHttpClientInstrumentation extends Instrumenter.Default {
public ApacheHttpClientInstrumentation() {
super("httpclient");
}
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return safeHasSuperType(named("org.apache.http.client.HttpClient"));
}
@Override
public String[] helperClassNames() {
return new String[] {
getClass().getName() + "$HttpHeadersInjectAdapter",
getClass().getName() + "$WrappingStatusSettingResponseHandler",
};
}
@Override
public Map<? extends ElementMatcher, String> transformers() {
return Collections.singletonMap(
isMethod()
.and(not(isAbstract()))
.and(named("execute"))
.and(takesArgument(0, named("org.apache.http.client.methods.HttpUriRequest"))),
ClientAdvice.class.getName());
}
public static class ClientAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static Scope methodEnter(
@Advice.Argument(0) final HttpUriRequest request,
// ResponseHandler could be either slot, but not both.
@Advice.Argument(
value = 1,
optional = true,
typing = Assigner.Typing.DYNAMIC,
readOnly = false)
Object handler1,
@Advice.Argument(
value = 2,
optional = true,
typing = Assigner.Typing.DYNAMIC,
readOnly = false)
Object handler2) {
final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(HttpClient.class);
if (callDepth > 0) {
return null;
}
final Tracer tracer = GlobalTracer.get();
final Scope scope =
tracer
.buildSpan("http.request")
.withTag(Tags.COMPONENT.getKey(), "apache-httpclient")
.withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT)
.withTag(DDTags.SPAN_TYPE, DDSpanTypes.HTTP_CLIENT)
.withTag(Tags.HTTP_METHOD.getKey(), request.getRequestLine().getMethod())
.withTag(Tags.HTTP_URL.getKey(), request.getRequestLine().getUri())
.startActive(true);
final Span span = scope.span();
// Wrap the handler so we capture the status code
if (handler1 instanceof ResponseHandler) {
handler1 = new WrappingStatusSettingResponseHandler(span, (ResponseHandler) handler1);
} else if (handler2 instanceof ResponseHandler) {
handler2 = new WrappingStatusSettingResponseHandler(span, (ResponseHandler) handler2);
}
final boolean awsClientCall = request.getHeaders("amz-sdk-invocation-id").length > 0;
// AWS calls are often signed, so we can't add headers without breaking the signature.
if (!awsClientCall) {
tracer.inject(
span.context(), Format.Builtin.HTTP_HEADERS, new HttpHeadersInjectAdapter(request));
}
final URI uri = request.getURI();
// zuul users have encountered cases where getURI returns null
if (null != uri) {
Tags.PEER_PORT.set(span, uri.getPort() == -1 ? 80 : uri.getPort());
Tags.PEER_HOSTNAME.set(span, uri.getHost());
}
return scope;
}
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void methodExit(
@Advice.Enter final Scope scope,
@Advice.Return final Object result,
@Advice.Thrown final Throwable throwable) {
if (scope != null) {
final Span span = scope.span();
if (result instanceof HttpResponse) {
Tags.HTTP_STATUS.set(span, ((HttpResponse) result).getStatusLine().getStatusCode());
} // else they probably provided a ResponseHandler.
if (throwable != null) {
Tags.ERROR.set(span, Boolean.TRUE);
span.log(Collections.singletonMap(ERROR_OBJECT, throwable));
span.finish();
}
scope.close();
CallDepthThreadLocalMap.reset(HttpClient.class);
}
}
}
public static class WrappingStatusSettingResponseHandler implements ResponseHandler {
final Span span;
final ResponseHandler handler;
public WrappingStatusSettingResponseHandler(final Span span, final ResponseHandler handler) {
this.span = span;
this.handler = handler;
}
@Override
public Object handleResponse(final HttpResponse response)
throws ClientProtocolException, IOException {
if (null != span) {
Tags.HTTP_STATUS.set(span, response.getStatusLine().getStatusCode());
}
return handler.handleResponse(response);
}
}
public static class HttpHeadersInjectAdapter implements TextMap {
private final HttpRequest httpRequest;
public HttpHeadersInjectAdapter(final HttpRequest httpRequest) {
this.httpRequest = httpRequest;
}
@Override
public void put(final String key, final String value) {
httpRequest.addHeader(key, value);
}
@Override
public Iterator<Map.Entry<String, String>> iterator() {
throw new UnsupportedOperationException(
"This class should be used only with tracer#inject()");
}
}
}

View File

@ -0,0 +1,139 @@
import datadog.opentracing.DDSpan
import datadog.trace.agent.test.AgentTestRunner
import datadog.trace.agent.test.asserts.TraceAssert
import datadog.trace.api.DDSpanTypes
import datadog.trace.api.DDTags
import io.opentracing.tag.Tags
import org.apache.http.HttpResponse
import org.apache.http.client.HttpClient
import org.apache.http.client.methods.HttpGet
import org.apache.http.impl.client.BasicResponseHandler
import org.apache.http.impl.client.DefaultHttpClient
import org.apache.http.message.BasicHeader
import spock.lang.AutoCleanup
import spock.lang.Shared
import static datadog.trace.agent.test.TestUtils.runUnderTrace
import static datadog.trace.agent.test.asserts.ListWriterAssert.assertTraces
import static datadog.trace.agent.test.server.http.TestHttpServer.httpServer
class ApacheHttpClientTest extends AgentTestRunner {
@AutoCleanup
@Shared
def server = httpServer {
handlers {
prefix("success") {
handleDistributedRequest()
String msg = "Hello."
response.status(200).send(msg)
}
prefix("redirect") {
handleDistributedRequest()
redirect(server.address.resolve("/success").toURL().toString())
}
prefix("another-redirect") {
handleDistributedRequest()
redirect(server.address.resolve("/redirect").toURL().toString())
}
}
}
@Shared
int port = server.address.port
@Shared
def successUrl = server.address.resolve("/success")
@Shared
def redirectUrl = server.address.resolve("/redirect")
@Shared
def twoRedirectsUrl = server.address.resolve("/another-redirect")
@Shared
def handler = new BasicResponseHandler()
final HttpClient client = new DefaultHttpClient()
def "trace request with propagation"() {
when:
String response = runUnderTrace("parent") {
if (responseHandler) {
client.execute(new HttpGet(successUrl), responseHandler)
} else {
client.execute(new HttpGet(successUrl)).entity.content.text
}
}
then:
response == "Hello."
// one trace on the server, one trace on the client
assertTraces(TEST_WRITER, 2) {
server.distributedRequestTrace(it, 0, TEST_WRITER[1][1])
trace(1, 2) {
parentSpan(it, 0)
successClientSpan(it, 1, span(0))
}
}
where:
responseHandler << [null, handler]
}
def "trace request without propagation"() {
setup:
HttpGet request = new HttpGet(successUrl)
request.addHeader(new BasicHeader("is-dd-server", "false"))
when:
HttpResponse response = runUnderTrace("parent") {
client.execute(request)
}
then:
response.getStatusLine().getStatusCode() == 200
// only one trace (client).
assertTraces(TEST_WRITER, 1) {
trace(0, 2) {
parentSpan(it, 0)
successClientSpan(it, 1, span(0))
}
}
}
def parentSpan(TraceAssert trace, int index, Throwable exception = null) {
trace.span(index) {
parent()
serviceName "unnamed-java-app"
operationName "parent"
resourceName "parent"
errored exception != null
tags {
defaultTags()
if (exception) {
errorTags(exception.class)
}
}
}
}
def successClientSpan(TraceAssert trace, int index, DDSpan parent, status = 200, route = "success", Throwable exception = null) {
trace.span(index) {
childOf parent
serviceName "unnamed-java-app"
operationName "http.request"
resourceName "GET /$route"
errored exception != null
tags {
defaultTags()
if (exception) {
errorTags(exception.class)
}
"$Tags.COMPONENT.key" "apache-httpclient"
"$Tags.HTTP_STATUS.key" status
"$Tags.HTTP_URL.key" "http://localhost:$port/$route"
"$Tags.PEER_HOSTNAME.key" "localhost"
"$Tags.PEER_PORT.key" server.address.port
"$Tags.HTTP_METHOD.key" "GET"
"$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT
"$DDTags.SPAN_TYPE" DDSpanTypes.HTTP_CLIENT
}
}
}
}

View File

@ -58,7 +58,7 @@ dependencies {
testCompile project(':dd-java-agent:testing')
// Include httpclient instrumentation for testing because it is a dependency for aws-sdk.
testCompile project(':dd-java-agent:instrumentation:apache-httpclient-4.3')
testCompile project(':dd-java-agent:instrumentation:apache-httpclient-4')
testCompile group: 'com.amazonaws', name: 'aws-java-sdk', version: '1.11.0'
}

View File

@ -13,21 +13,16 @@
*/
package datadog.trace.instrumentation.aws.v0;
import static io.opentracing.log.Fields.ERROR_KIND;
import static io.opentracing.log.Fields.ERROR_OBJECT;
import static io.opentracing.log.Fields.EVENT;
import static io.opentracing.log.Fields.MESSAGE;
import static io.opentracing.log.Fields.STACK;
import com.amazonaws.AmazonWebServiceResponse;
import com.amazonaws.Request;
import com.amazonaws.Response;
import datadog.trace.api.DDSpanTypes;
import datadog.trace.api.DDTags;
import io.opentracing.Span;
import io.opentracing.tag.Tags;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@ -50,9 +45,11 @@ class SpanDecorator {
span.setTag("aws.operation", awsOperation.getSimpleName());
span.setTag("aws.endpoint", request.getEndpoint().toString());
span.setTag(DDTags.SERVICE_NAME, COMPONENT_NAME);
span.setTag(
DDTags.RESOURCE_NAME,
remapServiceName(awsServiceName) + "." + remapOperationName(awsOperation));
span.setTag(DDTags.SPAN_TYPE, DDSpanTypes.HTTP_CLIENT);
}
static void onResponse(final Response response, final Span span) {
@ -65,22 +62,7 @@ class SpanDecorator {
static void onError(final Throwable throwable, final Span span) {
Tags.ERROR.set(span, Boolean.TRUE);
span.log(errorLogs(throwable));
}
private static Map<String, Object> errorLogs(final Throwable throwable) {
final Map<String, Object> errorLogs = new HashMap<>(4);
errorLogs.put(EVENT, Tags.ERROR.getKey());
errorLogs.put(ERROR_KIND, throwable.getClass().getName());
errorLogs.put(ERROR_OBJECT, throwable);
errorLogs.put(MESSAGE, throwable.getMessage());
final StringWriter sw = new StringWriter();
throwable.printStackTrace(new PrintWriter(sw));
errorLogs.put(STACK, sw.toString());
return errorLogs;
span.log(Collections.singletonMap(ERROR_OBJECT, throwable));
}
private static String remapServiceName(final String serviceName) {

View File

@ -7,6 +7,7 @@ import com.amazonaws.services.rds.model.DeleteOptionGroupRequest
import com.amazonaws.services.s3.AmazonS3Client
import com.amazonaws.services.s3.S3ClientOptions
import datadog.trace.agent.test.AgentTestRunner
import datadog.trace.api.DDSpanTypes
import datadog.trace.api.DDTags
import io.opentracing.tag.Tags
import spock.lang.AutoCleanup
@ -14,6 +15,7 @@ import spock.lang.Shared
import java.util.concurrent.atomic.AtomicReference
import static datadog.trace.agent.test.asserts.ListWriterAssert.assertTraces
import static datadog.trace.agent.test.server.http.TestHttpServer.httpServer
class AWSClientTest extends AgentTestRunner {
@ -72,81 +74,52 @@ class AWSClientTest extends AgentTestRunner {
client.requestHandler2s.size() == handlerCount
client.requestHandler2s.get(0).getClass().getSimpleName() == "TracingRequestHandler"
TEST_WRITER.size() == 2
def trace = TEST_WRITER.get(0)
trace.size() == 2
and: // span 0 - from apache-httpclient instrumentation
def span1 = trace[0]
span1.context().operationName == "apache.http"
span1.serviceName == "unnamed-java-app"
span1.resourceName == "apache.http"
span1.type == null
!span1.context().getErrorFlag()
span1.context().parentId == "0"
def tags1 = span1.context().tags
tags1["component"] == "apache-httpclient"
tags1["thread.name"] != null
tags1["thread.id"] != null
tags1.size() == 3
and: // span 1 - from apache-httpclient instrumentation
def span2 = trace[1]
span2.context().operationName == "http.request"
span2.serviceName == "unnamed-java-app"
span2.resourceName == "$method /$url"
span2.type == "http"
!span2.context().getErrorFlag()
span2.context().parentId == span1.spanId
def tags2 = span2.context().tags
tags2[Tags.SPAN_KIND.key] == Tags.SPAN_KIND_CLIENT
tags2[Tags.HTTP_METHOD.key] == "$method"
tags2[Tags.HTTP_URL.key] == "http://localhost:$server.address.port/$url"
tags2[Tags.PEER_HOSTNAME.key] == "localhost"
tags2[Tags.PEER_PORT.key] == server.address.port
tags2[DDTags.THREAD_NAME] != null
tags2[DDTags.THREAD_ID] != null
tags2.size() == 9
and:
def trace2 = TEST_WRITER.get(1)
trace2.size() == 1
and: // span 0 - from aws instrumentation
def span = trace2[0]
span.context().operationName == "aws.http"
span.serviceName == "java-aws-sdk"
span.resourceName == "$service.$operation"
span.type == "web"
!span.context().getErrorFlag()
span.context().parentId == "0"
def tags = span.context().tags
tags[Tags.COMPONENT.key] == "java-aws-sdk"
tags[Tags.SPAN_KIND.key] == Tags.SPAN_KIND_CLIENT
tags[Tags.HTTP_METHOD.key] == "$method"
tags[Tags.HTTP_URL.key] == "http://localhost:$server.address.port"
tags[Tags.HTTP_STATUS.key] == 200
tags["aws.service"] == "Amazon $service" || tags["aws.service"] == "Amazon$service"
tags["aws.endpoint"] == "http://localhost:$server.address.port"
tags["aws.operation"] == "${operation}Request"
tags["aws.agent"] == "java-aws-sdk"
tags["span.type"] == "web"
tags["thread.name"] != null
tags["thread.id"] != null
tags.size() == 12
server.lastRequest.headers.get("x-datadog-trace-id") == "$span.traceId"
server.lastRequest.headers.get("x-datadog-parent-id") == "$span.spanId"
assertTraces(TEST_WRITER, 2) {
trace(0, 1) {
span(0) {
operationName "http.request"
resourceName "$method /$url"
errored false
parent() // FIXME: This should be a child of the aws.http call.
tags {
"$Tags.COMPONENT.key" "apache-httpclient"
"$Tags.HTTP_STATUS.key" 200
"$Tags.HTTP_URL.key" "$server.address/$url"
"$Tags.PEER_HOSTNAME.key" "localhost"
"$Tags.PEER_PORT.key" server.address.port
"$Tags.HTTP_METHOD.key" "$method"
"$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT
"$DDTags.SPAN_TYPE" DDSpanTypes.HTTP_CLIENT
defaultTags()
}
}
}
trace(1, 1) {
span(0) {
serviceName "java-aws-sdk"
operationName "aws.http"
resourceName "$service.$operation"
errored false
parent()
tags {
"$Tags.COMPONENT.key" "java-aws-sdk"
"$Tags.HTTP_STATUS.key" 200
"$Tags.HTTP_URL.key" "$server.address"
"$Tags.HTTP_METHOD.key" "$method"
"$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT
"$DDTags.SPAN_TYPE" DDSpanTypes.HTTP_CLIENT
"aws.service" String
"aws.endpoint" "$server.address"
"aws.operation" "${operation}Request"
"aws.agent" "java-aws-sdk"
defaultTags()
}
}
}
}
// Not sure why these are children of the aws.http span:
server.lastRequest.headers.get("x-datadog-trace-id") == TEST_WRITER[1][0].traceId
server.lastRequest.headers.get("x-datadog-parent-id") == TEST_WRITER[1][0].spanId
where:
service | operation | method | url | handlerCount | call | body | client

View File

@ -31,7 +31,7 @@ dependencies {
testCompile project(':dd-java-agent:testing')
// Include httpclient instrumentation for testing because it is a dependency for aws-sdk.
testCompile project(':dd-java-agent:instrumentation:apache-httpclient-4.3')
testCompile project(':dd-java-agent:instrumentation:apache-httpclient-4')
testCompile group: 'com.amazonaws', name: 'aws-java-sdk', version: '1.11.106'
}

View File

@ -13,21 +13,16 @@
*/
package datadog.trace.instrumentation.aws.v106;
import static io.opentracing.log.Fields.ERROR_KIND;
import static io.opentracing.log.Fields.ERROR_OBJECT;
import static io.opentracing.log.Fields.EVENT;
import static io.opentracing.log.Fields.MESSAGE;
import static io.opentracing.log.Fields.STACK;
import com.amazonaws.AmazonWebServiceResponse;
import com.amazonaws.Request;
import com.amazonaws.Response;
import datadog.trace.api.DDSpanTypes;
import datadog.trace.api.DDTags;
import io.opentracing.Span;
import io.opentracing.tag.Tags;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@ -50,9 +45,11 @@ class SpanDecorator {
span.setTag("aws.operation", awsOperation.getSimpleName());
span.setTag("aws.endpoint", request.getEndpoint().toString());
span.setTag(DDTags.SERVICE_NAME, COMPONENT_NAME);
span.setTag(
DDTags.RESOURCE_NAME,
remapServiceName(awsServiceName) + "." + remapOperationName(awsOperation));
span.setTag(DDTags.SPAN_TYPE, DDSpanTypes.HTTP_CLIENT);
}
static void onResponse(final Response response, final Span span) {
@ -65,22 +62,7 @@ class SpanDecorator {
static void onError(final Throwable throwable, final Span span) {
Tags.ERROR.set(span, Boolean.TRUE);
span.log(errorLogs(throwable));
}
private static Map<String, Object> errorLogs(final Throwable throwable) {
final Map<String, Object> errorLogs = new HashMap<>(4);
errorLogs.put(EVENT, Tags.ERROR.getKey());
errorLogs.put(ERROR_KIND, throwable.getClass().getName());
errorLogs.put(ERROR_OBJECT, throwable);
errorLogs.put(MESSAGE, throwable.getMessage());
final StringWriter sw = new StringWriter();
throwable.printStackTrace(new PrintWriter(sw));
errorLogs.put(STACK, sw.toString());
return errorLogs;
span.log(Collections.singletonMap(ERROR_OBJECT, throwable));
}
private static String remapServiceName(final String serviceName) {

View File

@ -12,6 +12,7 @@ import com.amazonaws.services.rds.model.DeleteOptionGroupRequest
import com.amazonaws.services.s3.AmazonS3Client
import com.amazonaws.services.s3.AmazonS3ClientBuilder
import datadog.trace.agent.test.AgentTestRunner
import datadog.trace.api.DDSpanTypes
import datadog.trace.api.DDTags
import io.opentracing.tag.Tags
import spock.lang.AutoCleanup
@ -19,6 +20,7 @@ import spock.lang.Shared
import java.util.concurrent.atomic.AtomicReference
import static datadog.trace.agent.test.asserts.ListWriterAssert.assertTraces
import static datadog.trace.agent.test.server.http.TestHttpServer.httpServer
class AWSClientTest extends AgentTestRunner {
@ -101,81 +103,52 @@ class AWSClientTest extends AgentTestRunner {
client.requestHandler2s.size() == handlerCount
client.requestHandler2s.get(0).getClass().getSimpleName() == "TracingRequestHandler"
TEST_WRITER.size() == 2
def trace = TEST_WRITER.get(0)
trace.size() == 2
and: // span 0 - from apache-httpclient instrumentation
def span1 = trace[0]
span1.context().operationName == "apache.http"
span1.serviceName == "unnamed-java-app"
span1.resourceName == "apache.http"
span1.type == null
!span1.context().getErrorFlag()
span1.context().parentId == "0"
def tags1 = span1.context().tags
tags1["component"] == "apache-httpclient"
tags1["thread.name"] != null
tags1["thread.id"] != null
tags1.size() == 3
and: // span 1 - from apache-httpclient instrumentation
def span2 = trace[1]
span2.context().operationName == "http.request"
span2.serviceName == "unnamed-java-app"
span2.resourceName == "$method /$url"
span2.type == "http"
!span2.context().getErrorFlag()
span2.context().parentId == span1.spanId
def tags2 = span2.context().tags
tags2[Tags.SPAN_KIND.key] == Tags.SPAN_KIND_CLIENT
tags2[Tags.HTTP_METHOD.key] == "$method"
tags2[Tags.HTTP_URL.key] == "http://localhost:$server.address.port/$url"
tags2[Tags.PEER_HOSTNAME.key] == "localhost"
tags2[Tags.PEER_PORT.key] == server.address.port
tags2[DDTags.THREAD_NAME] != null
tags2[DDTags.THREAD_ID] != null
tags2.size() == 9
and:
def trace2 = TEST_WRITER.get(1)
trace2.size() == 1
and: // span 0 - from aws instrumentation
def span = trace2[0]
span.context().operationName == "aws.http"
span.serviceName == "java-aws-sdk"
span.resourceName == "$service.$operation"
span.type == "web"
!span.context().getErrorFlag()
span.context().parentId == "0"
def tags = span.context().tags
tags[Tags.COMPONENT.key] == "java-aws-sdk"
tags[Tags.SPAN_KIND.key] == Tags.SPAN_KIND_CLIENT
tags[Tags.HTTP_METHOD.key] == "$method"
tags[Tags.HTTP_URL.key] == "http://localhost:$server.address.port"
tags[Tags.HTTP_STATUS.key] == 200
tags["aws.service"] == "Amazon $service" || tags["aws.service"] == "Amazon$service"
tags["aws.endpoint"] == "http://localhost:$server.address.port"
tags["aws.operation"] == "${operation}Request"
tags["aws.agent"] == "java-aws-sdk"
tags["span.type"] == "web"
tags["thread.name"] != null
tags["thread.id"] != null
tags.size() == 12
server.lastRequest.headers.get("x-datadog-trace-id") == "$span.traceId"
server.lastRequest.headers.get("x-datadog-parent-id") == "$span.spanId"
assertTraces(TEST_WRITER, 2) {
trace(0, 1) {
span(0) {
operationName "http.request"
resourceName "$method /$url"
errored false
parent() // FIXME: This should be a child of the aws.http call.
tags {
"$Tags.COMPONENT.key" "apache-httpclient"
"$Tags.HTTP_STATUS.key" 200
"$Tags.HTTP_URL.key" "$server.address/$url"
"$Tags.PEER_HOSTNAME.key" "localhost"
"$Tags.PEER_PORT.key" server.address.port
"$Tags.HTTP_METHOD.key" "$method"
"$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT
"$DDTags.SPAN_TYPE" DDSpanTypes.HTTP_CLIENT
defaultTags()
}
}
}
trace(1, 1) {
span(0) {
serviceName "java-aws-sdk"
operationName "aws.http"
resourceName "$service.$operation"
errored false
parent()
tags {
"$Tags.COMPONENT.key" "java-aws-sdk"
"$Tags.HTTP_STATUS.key" 200
"$Tags.HTTP_URL.key" "$server.address"
"$Tags.HTTP_METHOD.key" "$method"
"$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT
"$DDTags.SPAN_TYPE" DDSpanTypes.HTTP_CLIENT
"aws.service" String
"aws.endpoint" "$server.address"
"aws.operation" "${operation}Request"
"aws.agent" "java-aws-sdk"
defaultTags()
}
}
}
}
// Not sure why these are children of the aws.http span:
server.lastRequest.headers.get("x-datadog-trace-id") == TEST_WRITER[1][0].traceId
server.lastRequest.headers.get("x-datadog-parent-id") == TEST_WRITER[1][0].spanId
where:
service | operation | method | url | handlerCount | call | body | client

View File

@ -34,7 +34,7 @@ dependencies {
// Include httpclient instrumentation for testing because it is a dependency for elasticsearch-rest-client.
// It doesn't actually work though. They use HttpAsyncClient, which isn't currently instrumented.
// TODO: add HttpAsyncClient instrumentation when that is complete.
testCompile project(':dd-java-agent:instrumentation:apache-httpclient-4.3')
testCompile project(':dd-java-agent:instrumentation:apache-httpclient-4')
// TODO: add netty instrumentation when that is complete.
testCompile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.11.0'

View File

@ -41,7 +41,7 @@ dependencies {
// Include httpclient instrumentation for testing because it is a dependency for elasticsearch-rest-client.
// It doesn't actually work though. They use HttpAsyncClient, which isn't currently instrumented.
// TODO: add HttpAsyncClient instrumentation when that is complete.
testCompile project(':dd-java-agent:instrumentation:apache-httpclient-4.3')
testCompile project(':dd-java-agent:instrumentation:apache-httpclient-4')
// TODO: add netty instrumentation when that is complete.
testCompile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.11.0'

View File

@ -41,7 +41,7 @@ dependencies {
// Include httpclient instrumentation for testing because it is a dependency for elasticsearch-rest-client.
// It doesn't actually work though. They use HttpAsyncClient, which isn't currently instrumented.
// TODO: add HttpAsyncClient instrumentation when that is complete.
testCompile project(':dd-java-agent:instrumentation:apache-httpclient-4.3')
testCompile project(':dd-java-agent:instrumentation:apache-httpclient-4')
// TODO: add netty instrumentation when that is complete.
testCompile group: 'org.elasticsearch.plugin', name: 'transport-netty4-client', version: '6.0.0'

View File

@ -6,15 +6,11 @@ import java.util.List;
/** Create DDSpanDecorators */
public class DDDecoratorsFactory {
public static List<AbstractDecorator> createBuiltinDecorators() {
final HTTPComponent httpDecorator = new HTTPComponent();
httpDecorator.setMatchingTag("component");
httpDecorator.setMatchingValue("java-aws-sdk");
return Arrays.asList(
new DBStatementAsResourceName(),
new DBTypeDecorator(),
new ErrorFlag(),
httpDecorator,
new OperationDecorator(),
new PeerServiceDecorator(),
new ResourceNameDecorator(),

View File

@ -1,29 +0,0 @@
package datadog.opentracing.decorators;
import datadog.opentracing.DDSpanContext;
import datadog.trace.api.DDTags;
import io.opentracing.tag.Tags;
/**
* This span decorator leverages HTTP tags. It allows the dev to define a custom service name and
* retrieves some HTTP meta such as the request path
*/
public class HTTPComponent extends AbstractDecorator {
public HTTPComponent() {
super();
this.setMatchingTag(Tags.COMPONENT.getKey());
this.setReplacementTag(DDTags.SERVICE_NAME);
}
@Override
public boolean shouldSetTag(final DDSpanContext context, final String tag, final Object value) {
if (getMatchingValue().equals(value)) {
// Assign service name
super.shouldSetTag(context, tag, value);
// Assign span type to WEB
context.setSpanType("web");
}
return true;
}
}

View File

@ -17,7 +17,6 @@ public class OperationDecorator extends AbstractDecorator {
static {
final Map<String, String> mappings = new HashMap<>();
// Component name <> Operation name
mappings.put("apache-httpclient", "apache.http");
mappings.put("java-aws-sdk", "aws.http");
// FIXME: JMS ops card is low (jms-send or jms-receive), may be this mapping is useless
mappings.put("java-jms", "jms");
@ -28,7 +27,7 @@ public class OperationDecorator extends AbstractDecorator {
public OperationDecorator() {
super();
this.setMatchingTag(Tags.COMPONENT.getKey());
setMatchingTag(Tags.COMPONENT.getKey());
}
@Override

View File

@ -17,8 +17,8 @@ public class URLAsResourceName extends AbstractDecorator {
public URLAsResourceName() {
super();
this.setMatchingTag(Tags.HTTP_URL.getKey());
this.setReplacementTag(DDTags.RESOURCE_NAME);
setMatchingTag(Tags.HTTP_URL.getKey());
setReplacementTag(DDTags.RESOURCE_NAME);
}
@Override
@ -59,6 +59,10 @@ public class URLAsResourceName extends AbstractDecorator {
norm = QUERYSTRING.matcher(norm).replaceAll("");
norm = PATH_MIXED_ALPHANUMERICS.matcher(norm).replaceAll("?");
if (norm.trim().isEmpty()) {
norm = "/";
}
return norm;
}
}

View File

@ -25,6 +25,9 @@ class URLAsResourceNameTest extends Specification {
where:
input | output
"" | "/"
" " | "/"
"\t" | "/"
"/" | "/"
"/?asdf" | "/"
"/search" | "/search"

View File

@ -10,7 +10,7 @@ include ':dd-trace-api'
// instrumentation:
include ':dd-java-agent:instrumentation:akka-http-10.0'
include ':dd-java-agent:instrumentation:apache-httpclient-4.3'
include ':dd-java-agent:instrumentation:apache-httpclient-4'
include ':dd-java-agent:instrumentation:aws-java-sdk-1.11.0'
include ':dd-java-agent:instrumentation:aws-java-sdk-1.11.106'
include ':dd-java-agent:instrumentation:couchbase-2.0'