diff --git a/dd-java-agent/instrumentation/netty-3.8/netty-3.8.gradle b/dd-java-agent/instrumentation/netty-3.8/netty-3.8.gradle new file mode 100644 index 0000000000..23984df4ba --- /dev/null +++ b/dd-java-agent/instrumentation/netty-3.8/netty-3.8.gradle @@ -0,0 +1,49 @@ +// Set properties before any plugins get loaded +ext { + minJavaVersionForTests = JavaVersion.VERSION_1_8 + maxJavaVersionForTests = JavaVersion.VERSION_1_8 +} + +apply from: "${rootDir}/gradle/java.gradle" + +muzzle { + pass { + group = "io.netty" + module = "netty" + versions = "[3.8.0.Final,4)" + assertInverse = true + } + fail { + group = "io.netty" + module = "netty-all" + versions = "[,]" + } +} + +apply plugin: 'org.unbroken-dome.test-sets' + +testSets { + latestDepTest +} + +dependencies { + compileOnly group: 'io.netty', name: 'netty', version: '3.8.0.Final' + + testCompile group: 'io.netty', name: 'netty', version: '3.8.0.Final' + testCompile group: 'com.ning', name: 'async-http-client', version: '1.8.0' + + latestDepTestCompile group: 'io.netty', name: 'netty', version: '3.10.+' + latestDepTestCompile group: 'com.ning', name: 'async-http-client', version: '1.9.+' +} + +// We need to force the dependency to the earliest supported version because other libraries declare newer versions. +configurations.testCompile { + resolutionStrategy { + eachDependency { DependencyResolveDetails details -> + //specifying a fixed version for all libraries with io.netty' group + if (details.requested.group == 'io.netty') { + details.useVersion "3.8.0.Final" + } + } + } +} diff --git a/dd-java-agent/instrumentation/netty-3.8/src/latestDepTest/groovy/Netty38ClientTest.groovy b/dd-java-agent/instrumentation/netty-3.8/src/latestDepTest/groovy/Netty38ClientTest.groovy new file mode 100644 index 0000000000..9c26e09b90 --- /dev/null +++ b/dd-java-agent/instrumentation/netty-3.8/src/latestDepTest/groovy/Netty38ClientTest.groovy @@ -0,0 +1,106 @@ +import com.ning.http.client.AsyncCompletionHandler +import com.ning.http.client.AsyncHttpClient +import com.ning.http.client.AsyncHttpClientConfig +import com.ning.http.client.Response +import datadog.trace.agent.test.base.HttpClientTest +import datadog.trace.bootstrap.instrumentation.api.Tags +import datadog.trace.instrumentation.netty38.client.NettyHttpClientDecorator +import spock.lang.AutoCleanup +import spock.lang.Shared + +import java.util.concurrent.ExecutionException +import java.util.concurrent.TimeUnit + +import static datadog.trace.agent.test.utils.PortUtils.UNUSABLE_PORT +import static datadog.trace.agent.test.utils.TraceUtils.basicSpan +import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace + +class Netty38ClientTest extends HttpClientTest { + + @Shared + def clientConfig = new AsyncHttpClientConfig.Builder() + .setRequestTimeout(TimeUnit.SECONDS.toMillis(10).toInteger()) + .build() + + @Shared + @AutoCleanup + AsyncHttpClient asyncHttpClient = new AsyncHttpClient(clientConfig) + + @Override + int doRequest(String method, URI uri, Map headers, Closure callback) { + def methodName = "prepare" + method.toLowerCase().capitalize() + def requestBuilder = asyncHttpClient."$methodName"(uri.toString()) + headers.each { requestBuilder.setHeader(it.key, it.value) } + def response = requestBuilder.execute(new AsyncCompletionHandler() { + @Override + Object onCompleted(Response response) throws Exception { + callback?.call() + return response + } + }).get() + blockUntilChildSpansFinished(1) + return response.statusCode + } + + @Override + String component() { + return NettyHttpClientDecorator.DECORATE.component() + } + + @Override + String expectedOperationName() { + return "netty.client.request" + } + + @Override + boolean testRedirects() { + false + } + + @Override + boolean testConnectionFailure() { + false + } + + def "connection error (unopened port)"() { + given: + def uri = new URI("http://localhost:$UNUSABLE_PORT/") + + when: + runUnderTrace("parent") { + doRequest(method, uri) + } + + then: + def ex = thrown(Exception) + def thrownException = ex instanceof ExecutionException ? ex.cause : ex + + and: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent", null, thrownException) + + span(1) { + operationName "netty.connect" + resourceName "netty.connect" + childOf span(0) + errored true + tags { + "$Tags.COMPONENT" "netty" + Class errorClass = ConnectException + try { + errorClass = Class.forName('io.netty.channel.AbstractChannel$AnnotatedConnectException') + } catch (ClassNotFoundException e) { + // Older versions use 'java.net.ConnectException' and do not have 'io.netty.channel.AbstractChannel$AnnotatedConnectException' + } + errorTags errorClass, "Connection refused: localhost/127.0.0.1:$UNUSABLE_PORT" + defaultTags() + } + } + } + } + + where: + method = "GET" + } +} diff --git a/dd-java-agent/instrumentation/netty-3.8/src/latestDepTest/groovy/Netty38ServerTest.groovy b/dd-java-agent/instrumentation/netty-3.8/src/latestDepTest/groovy/Netty38ServerTest.groovy new file mode 100644 index 0000000000..e7b0794e0b --- /dev/null +++ b/dd-java-agent/instrumentation/netty-3.8/src/latestDepTest/groovy/Netty38ServerTest.groovy @@ -0,0 +1,111 @@ +import datadog.trace.agent.test.base.HttpServerTest +import datadog.trace.instrumentation.netty38.server.NettyHttpServerDecorator +import org.jboss.netty.bootstrap.ServerBootstrap +import org.jboss.netty.buffer.ChannelBuffer +import org.jboss.netty.buffer.ChannelBuffers +import org.jboss.netty.channel.* +import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory +import org.jboss.netty.handler.codec.http.* +import org.jboss.netty.handler.logging.LoggingHandler +import org.jboss.netty.logging.InternalLogLevel +import org.jboss.netty.util.CharsetUtil + +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.* +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.* +import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1 + +class Netty38ServerTest extends HttpServerTest { + + ChannelPipeline channelPipeline() { + ChannelPipeline channelPipeline = new DefaultChannelPipeline() + + channelPipeline.addLast("http-codec", new HttpServerCodec()) + channelPipeline.addLast("controller", new SimpleChannelHandler() { + @Override + void messageReceived(ChannelHandlerContext ctx, MessageEvent msg) throws Exception { + if (msg.getMessage() instanceof HttpRequest) { + def uri = URI.create((msg.getMessage() as HttpRequest).getUri()) + HttpServerTest.ServerEndpoint endpoint = forPath(uri.path) + ctx.sendDownstream controller(endpoint) { + HttpResponse response + ChannelBuffer responseContent = null + switch (endpoint) { + case SUCCESS: + case ERROR: + responseContent = ChannelBuffers.copiedBuffer(endpoint.body, CharsetUtil.UTF_8) + response = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.valueOf(endpoint.status)) + response.setContent(responseContent) + break + case QUERY_PARAM: + responseContent = ChannelBuffers.copiedBuffer(uri.query, CharsetUtil.UTF_8) + response = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.valueOf(endpoint.status)) + response.setContent(responseContent) + break + case REDIRECT: + response = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.valueOf(endpoint.status)) + response.headers().set(LOCATION, endpoint.body) + break + case EXCEPTION: + throw new Exception(endpoint.body) + default: + responseContent = ChannelBuffers.copiedBuffer(NOT_FOUND.body, CharsetUtil.UTF_8) + response = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.valueOf(endpoint.status)) + response.setContent(responseContent) + break + } + response.headers().set(CONTENT_TYPE, "text/plain") + if (responseContent) { + response.headers().set(CONTENT_LENGTH, responseContent.readableBytes()) + } + return new DownstreamMessageEvent( + ctx.getChannel(), + new SucceededChannelFuture(ctx.getChannel()), + response, + ctx.getChannel().getRemoteAddress()) + } + } + } + + @Override + void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent ex) throws Exception { + ChannelBuffer buffer = ChannelBuffers.copiedBuffer(ex.getCause().getMessage(), CharsetUtil.UTF_8) + HttpResponse response = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.INTERNAL_SERVER_ERROR) + response.setContent(buffer) + response.headers().set(CONTENT_TYPE, "text/plain") + response.headers().set(CONTENT_LENGTH, buffer.readableBytes()) + ctx.sendDownstream(new DownstreamMessageEvent( + ctx.getChannel(), + new FailedChannelFuture(ctx.getChannel(), ex.getCause()), + response, + ctx.getChannel().getRemoteAddress())) + } + }) + + return channelPipeline + } + + @Override + Channel startServer(int port) { + ServerBootstrap bootstrap = new ServerBootstrap(new NioServerSocketChannelFactory()) + bootstrap.setParentHandler(new LoggingHandler(InternalLogLevel.INFO)) + bootstrap.setPipeline(channelPipeline()) + + InetSocketAddress address = new InetSocketAddress(port) + return bootstrap.bind(address) + } + + @Override + void stopServer(Channel server) { + server?.disconnect() + } + + @Override + String component() { + NettyHttpServerDecorator.DECORATE.component() + } + + @Override + String expectedOperationName() { + "netty.request" + } +} diff --git a/dd-java-agent/instrumentation/netty-3.8/src/latestDepTest/groovy/NettyServerTestInstrumentation.java b/dd-java-agent/instrumentation/netty-3.8/src/latestDepTest/groovy/NettyServerTestInstrumentation.java new file mode 100644 index 0000000000..9657cea79c --- /dev/null +++ b/dd-java-agent/instrumentation/netty-3.8/src/latestDepTest/groovy/NettyServerTestInstrumentation.java @@ -0,0 +1,21 @@ +import static net.bytebuddy.matcher.ElementMatchers.named; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.test.base.HttpServerTestAdvice; +import datadog.trace.agent.tooling.Instrumenter; +import net.bytebuddy.agent.builder.AgentBuilder; + +@AutoService(Instrumenter.class) +public class NettyServerTestInstrumentation implements Instrumenter { + + @Override + public AgentBuilder instrument(final AgentBuilder agentBuilder) { + return agentBuilder + .type(named("org.jboss.netty.handler.codec.http.HttpRequestDecoder")) + .transform( + new AgentBuilder.Transformer.ForAdvice() + .advice( + named("createMessage"), + HttpServerTestAdvice.ServerEntryAdvice.class.getName())); + } +} diff --git a/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/AbstractNettyAdvice.java b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/AbstractNettyAdvice.java new file mode 100644 index 0000000000..97845db559 --- /dev/null +++ b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/AbstractNettyAdvice.java @@ -0,0 +1,10 @@ +package datadog.trace.instrumentation.netty38; + +import org.jboss.netty.handler.codec.http.HttpHeaders; +import org.jboss.netty.handler.codec.http.HttpRequest; + +public class AbstractNettyAdvice { + public static void muzzleCheck(final HttpRequest httpRequest) { + final HttpHeaders headers = httpRequest.headers(); + } +} diff --git a/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/ChannelFutureListenerInstrumentation.java b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/ChannelFutureListenerInstrumentation.java new file mode 100644 index 0000000000..1e25fd4a7b --- /dev/null +++ b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/ChannelFutureListenerInstrumentation.java @@ -0,0 +1,119 @@ +package datadog.trace.instrumentation.netty38; + +import static datadog.trace.agent.tooling.ClassLoaderMatcher.hasClassesNamed; +import static datadog.trace.agent.tooling.bytebuddy.matcher.DDElementMatchers.implementsInterface; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.startSpan; +import static datadog.trace.instrumentation.netty38.server.NettyHttpServerDecorator.DECORATE; +import static java.util.Collections.singletonMap; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.bootstrap.ContextStore; +import datadog.trace.bootstrap.InstrumentationContext; +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.context.TraceScope; +import java.util.Collections; +import java.util.Map; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelFuture; + +@AutoService(Instrumenter.class) +public class ChannelFutureListenerInstrumentation extends Instrumenter.Default { + + public ChannelFutureListenerInstrumentation() { + super( + NettyChannelPipelineInstrumentation.INSTRUMENTATION_NAME, + NettyChannelPipelineInstrumentation.ADDITIONAL_INSTRUMENTATION_NAMES); + } + + @Override + public ElementMatcher classLoaderMatcher() { + // Optimization for expensive typeMatcher. + return hasClassesNamed("org.jboss.netty.channel.ChannelFutureListener"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("org.jboss.netty.channel.ChannelFutureListener")); + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".AbstractNettyAdvice", + packageName + ".ChannelTraceContext", + packageName + ".ChannelTraceContext$Factory", + packageName + ".server.NettyHttpServerDecorator", + packageName + ".server.NettyRequestExtractAdapter" + }; + } + + @Override + public Map, String> transformers() { + return singletonMap( + isMethod() + .and(named("operationComplete")) + .and(takesArgument(0, named("org.jboss.netty.channel.ChannelFuture"))), + ChannelFutureListenerInstrumentation.class.getName() + "$OperationCompleteAdvice"); + } + + @Override + public Map contextStore() { + return Collections.singletonMap( + "org.jboss.netty.channel.Channel", packageName + ".ChannelTraceContext"); + } + + public static class OperationCompleteAdvice extends AbstractNettyAdvice { + @Advice.OnMethodEnter + public static TraceScope activateScope(@Advice.Argument(0) final ChannelFuture future) { + /* + Idea here is: + - To return scope only if we have captured it. + - To capture scope only in case of error. + */ + final Throwable cause = future.getCause(); + if (cause == null) { + return null; + } + + final ContextStore contextStore = + InstrumentationContext.get(Channel.class, ChannelTraceContext.class); + + final TraceScope.Continuation continuation = + contextStore + .putIfAbsent(future.getChannel(), ChannelTraceContext.Factory.INSTANCE) + .getConnectionContinuation(); + contextStore.get(future.getChannel()).setConnectionContinuation(null); + if (continuation == null) { + return null; + } + final TraceScope parentScope = continuation.activate(); + + final AgentSpan errorSpan = startSpan("netty.connect").setTag(Tags.COMPONENT, "netty"); + try (final AgentScope scope = activateSpan(errorSpan, false)) { + DECORATE.onError(errorSpan, cause); + DECORATE.beforeFinish(errorSpan); + errorSpan.finish(); + } + + return parentScope; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void deactivateScope(@Advice.Enter final TraceScope scope) { + if (scope != null) { + scope.close(); + } + } + } +} diff --git a/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/ChannelTraceContext.java b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/ChannelTraceContext.java new file mode 100644 index 0000000000..812ad71f6f --- /dev/null +++ b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/ChannelTraceContext.java @@ -0,0 +1,23 @@ +package datadog.trace.instrumentation.netty38; + +import datadog.trace.bootstrap.ContextStore; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.context.TraceScope; +import lombok.Data; + +@Data +public class ChannelTraceContext { + public static class Factory implements ContextStore.Factory { + public static final Factory INSTANCE = new Factory(); + + @Override + public ChannelTraceContext create() { + return new ChannelTraceContext(); + } + } + + TraceScope.Continuation connectionContinuation; + AgentSpan serverSpan; + AgentSpan clientSpan; + AgentSpan clientParentSpan; +} diff --git a/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/NettyChannelInstrumentation.java b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/NettyChannelInstrumentation.java new file mode 100644 index 0000000000..d43eb15ad9 --- /dev/null +++ b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/NettyChannelInstrumentation.java @@ -0,0 +1,91 @@ +package datadog.trace.instrumentation.netty38; + +import static datadog.trace.agent.tooling.ClassLoaderMatcher.hasClassesNamed; +import static datadog.trace.agent.tooling.bytebuddy.matcher.DDElementMatchers.implementsInterface; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeScope; +import static datadog.trace.instrumentation.netty38.NettyChannelPipelineInstrumentation.ADDITIONAL_INSTRUMENTATION_NAMES; +import static datadog.trace.instrumentation.netty38.NettyChannelPipelineInstrumentation.INSTRUMENTATION_NAME; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.bootstrap.ContextStore; +import datadog.trace.bootstrap.InstrumentationContext; +import datadog.trace.context.TraceScope; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.jboss.netty.channel.Channel; + +@AutoService(Instrumenter.class) +public class NettyChannelInstrumentation extends Instrumenter.Default { + public NettyChannelInstrumentation() { + super(INSTRUMENTATION_NAME, ADDITIONAL_INSTRUMENTATION_NAMES); + } + + @Override + public ElementMatcher classLoaderMatcher() { + // Optimization for expensive typeMatcher. + return hasClassesNamed("org.jboss.netty.channel.Channel"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("org.jboss.netty.channel.Channel")); + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".AbstractNettyAdvice", + packageName + ".ChannelTraceContext", + packageName + ".ChannelTraceContext$Factory" + }; + } + + @Override + public Map, String> transformers() { + final Map, String> transformers = new HashMap<>(); + transformers.put( + isMethod() + .and(named("connect")) + .and(returns(named("org.jboss.netty.channel.ChannelFuture"))), + NettyChannelInstrumentation.class.getName() + "$ChannelConnectAdvice"); + return transformers; + } + + @Override + public Map contextStore() { + return Collections.singletonMap( + "org.jboss.netty.channel.Channel", ChannelTraceContext.class.getName()); + } + + public static class ChannelConnectAdvice extends AbstractNettyAdvice { + @Advice.OnMethodEnter + public static void addConnectContinuation(@Advice.This final Channel channel) { + final TraceScope scope = activeScope(); + if (scope != null) { + final TraceScope.Continuation continuation = scope.capture(); + if (continuation != null) { + final ContextStore contextStore = + InstrumentationContext.get(Channel.class, ChannelTraceContext.class); + + if (contextStore + .putIfAbsent(channel, ChannelTraceContext.Factory.INSTANCE) + .getConnectionContinuation() + != null) { + continuation.close(); + } else { + contextStore.get(channel).setConnectionContinuation(continuation); + } + } + } + } + } +} diff --git a/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/NettyChannelPipelineInstrumentation.java b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/NettyChannelPipelineInstrumentation.java new file mode 100644 index 0000000000..d592fb9264 --- /dev/null +++ b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/NettyChannelPipelineInstrumentation.java @@ -0,0 +1,207 @@ +package datadog.trace.instrumentation.netty38; + +import static datadog.trace.agent.tooling.ClassLoaderMatcher.hasClassesNamed; +import static datadog.trace.agent.tooling.bytebuddy.matcher.DDElementMatchers.implementsInterface; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.bootstrap.CallDepthThreadLocalMap; +import datadog.trace.bootstrap.ContextStore; +import datadog.trace.bootstrap.InstrumentationContext; +import datadog.trace.instrumentation.netty38.client.HttpClientRequestTracingHandler; +import datadog.trace.instrumentation.netty38.client.HttpClientResponseTracingHandler; +import datadog.trace.instrumentation.netty38.client.HttpClientTracingHandler; +import datadog.trace.instrumentation.netty38.server.HttpServerRequestTracingHandler; +import datadog.trace.instrumentation.netty38.server.HttpServerResponseTracingHandler; +import datadog.trace.instrumentation.netty38.server.HttpServerTracingHandler; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelHandler; +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.handler.codec.http.HttpClientCodec; +import org.jboss.netty.handler.codec.http.HttpRequestDecoder; +import org.jboss.netty.handler.codec.http.HttpRequestEncoder; +import org.jboss.netty.handler.codec.http.HttpResponseDecoder; +import org.jboss.netty.handler.codec.http.HttpResponseEncoder; +import org.jboss.netty.handler.codec.http.HttpServerCodec; + +@AutoService(Instrumenter.class) +public class NettyChannelPipelineInstrumentation extends Instrumenter.Default { + + static final String INSTRUMENTATION_NAME = "netty"; + static final String[] ADDITIONAL_INSTRUMENTATION_NAMES = {"netty-3.9"}; + + public NettyChannelPipelineInstrumentation() { + super(INSTRUMENTATION_NAME, ADDITIONAL_INSTRUMENTATION_NAMES); + } + + @Override + public ElementMatcher classLoaderMatcher() { + // Optimization for expensive typeMatcher. + return hasClassesNamed("org.jboss.netty.channel.ChannelPipeline"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("org.jboss.netty.channel.ChannelPipeline")); + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".AbstractNettyAdvice", + packageName + ".ChannelTraceContext", + packageName + ".ChannelTraceContext$Factory", + NettyChannelPipelineInstrumentation.class.getName() + "$ChannelPipelineAdviceUtil", + // Util + packageName + ".util.CombinedSimpleChannelHandler", + // client helpers + packageName + ".client.NettyHttpClientDecorator", + packageName + ".client.NettyResponseInjectAdapter", + packageName + ".client.HttpClientRequestTracingHandler", + packageName + ".client.HttpClientResponseTracingHandler", + packageName + ".client.HttpClientTracingHandler", + // server helpers + packageName + ".server.NettyHttpServerDecorator", + packageName + ".server.NettyRequestExtractAdapter", + packageName + ".server.HttpServerRequestTracingHandler", + packageName + ".server.HttpServerResponseTracingHandler", + packageName + ".server.HttpServerTracingHandler" + }; + } + + @Override + public Map, String> transformers() { + final Map, String> transformers = new HashMap<>(); + transformers.put( + isMethod() + .and(nameStartsWith("add")) + .and(takesArgument(1, named("org.jboss.netty.channel.ChannelHandler"))), + NettyChannelPipelineInstrumentation.class.getName() + "$ChannelPipelineAdd2ArgsAdvice"); + transformers.put( + isMethod() + .and(nameStartsWith("add")) + .and(takesArgument(2, named("org.jboss.netty.channel.ChannelHandler"))), + NettyChannelPipelineInstrumentation.class.getName() + "$ChannelPipelineAdd3ArgsAdvice"); + return transformers; + } + + @Override + public Map contextStore() { + return Collections.singletonMap( + "org.jboss.netty.channel.Channel", ChannelTraceContext.class.getName()); + } + + /** + * When certain handlers are added to the pipeline, we want to add our corresponding tracing + * handlers. If those handlers are later removed, we may want to remove our handlers. That is not + * currently implemented. + */ + public static class ChannelPipelineAdviceUtil { + public static void wrapHandler( + final ContextStore contextStore, + final ChannelPipeline pipeline, + final ChannelHandler handler) { + try { + // Server pipeline handlers + if (handler instanceof HttpServerCodec) { + pipeline.addLast( + HttpServerTracingHandler.class.getName(), new HttpServerTracingHandler(contextStore)); + } else if (handler instanceof HttpRequestDecoder) { + pipeline.addLast( + HttpServerRequestTracingHandler.class.getName(), + new HttpServerRequestTracingHandler(contextStore)); + } else if (handler instanceof HttpResponseEncoder) { + pipeline.addLast( + HttpServerResponseTracingHandler.class.getName(), + new HttpServerResponseTracingHandler(contextStore)); + } else + // Client pipeline handlers + if (handler instanceof HttpClientCodec) { + pipeline.addLast( + HttpClientTracingHandler.class.getName(), new HttpClientTracingHandler(contextStore)); + } else if (handler instanceof HttpRequestEncoder) { + pipeline.addLast( + HttpClientRequestTracingHandler.class.getName(), + new HttpClientRequestTracingHandler(contextStore)); + } else if (handler instanceof HttpResponseDecoder) { + pipeline.addLast( + HttpClientResponseTracingHandler.class.getName(), + new HttpClientResponseTracingHandler(contextStore)); + } + } finally { + CallDepthThreadLocalMap.reset(ChannelPipeline.class); + } + } + } + + public static class ChannelPipelineAdd2ArgsAdvice extends AbstractNettyAdvice { + @Advice.OnMethodEnter + public static int checkDepth( + @Advice.This final ChannelPipeline pipeline, + @Advice.Argument(1) final ChannelHandler handler) { + // Pipelines are created once as a factory and then copied multiple times using the same add + // methods as we are hooking. If our handler has already been added we need to remove it so we + // don't end up with duplicates (this throws an exception) + if (pipeline.get(handler.getClass().getName()) != null) { + pipeline.remove(handler.getClass().getName()); + } + return CallDepthThreadLocalMap.incrementCallDepth(ChannelPipeline.class); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void addHandler( + @Advice.Enter final int depth, + @Advice.This final ChannelPipeline pipeline, + @Advice.Argument(1) final ChannelHandler handler) { + if (depth > 0) { + return; + } + + final ContextStore contextStore = + InstrumentationContext.get(Channel.class, ChannelTraceContext.class); + + ChannelPipelineAdviceUtil.wrapHandler(contextStore, pipeline, handler); + } + } + + public static class ChannelPipelineAdd3ArgsAdvice extends AbstractNettyAdvice { + @Advice.OnMethodEnter + public static int checkDepth( + @Advice.This final ChannelPipeline pipeline, + @Advice.Argument(2) final ChannelHandler handler) { + // Pipelines are created once as a factory and then copied multiple times using the same add + // methods as we are hooking. If our handler has already been added we need to remove it so we + // don't end up with duplicates (this throws an exception) + if (pipeline.get(handler.getClass().getName()) != null) { + pipeline.remove(handler.getClass().getName()); + } + return CallDepthThreadLocalMap.incrementCallDepth(ChannelPipeline.class); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void addHandler( + @Advice.Enter final int depth, + @Advice.This final ChannelPipeline pipeline, + @Advice.Argument(2) final ChannelHandler handler) { + if (depth > 0) { + return; + } + + final ContextStore contextStore = + InstrumentationContext.get(Channel.class, ChannelTraceContext.class); + + ChannelPipelineAdviceUtil.wrapHandler(contextStore, pipeline, handler); + } + } +} diff --git a/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/client/HttpClientRequestTracingHandler.java b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/client/HttpClientRequestTracingHandler.java new file mode 100644 index 0000000000..df488fbef6 --- /dev/null +++ b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/client/HttpClientRequestTracingHandler.java @@ -0,0 +1,79 @@ +package datadog.trace.instrumentation.netty38.client; + +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.propagate; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.startSpan; +import static datadog.trace.instrumentation.netty38.client.NettyHttpClientDecorator.DECORATE; +import static datadog.trace.instrumentation.netty38.client.NettyResponseInjectAdapter.SETTER; + +import datadog.trace.bootstrap.ContextStore; +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.context.TraceScope; +import datadog.trace.instrumentation.netty38.ChannelTraceContext; +import java.net.InetSocketAddress; +import lombok.extern.slf4j.Slf4j; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelDownstreamHandler; +import org.jboss.netty.handler.codec.http.HttpRequest; + +@Slf4j +public class HttpClientRequestTracingHandler extends SimpleChannelDownstreamHandler { + + private final ContextStore contextStore; + + public HttpClientRequestTracingHandler( + final ContextStore contextStore) { + this.contextStore = contextStore; + } + + @Override + public void writeRequested(final ChannelHandlerContext ctx, final MessageEvent msg) + throws Exception { + if (!(msg.getMessage() instanceof HttpRequest)) { + ctx.sendDownstream(msg); + return; + } + + final ChannelTraceContext channelTraceContext = + contextStore.putIfAbsent(ctx.getChannel(), ChannelTraceContext.Factory.INSTANCE); + + TraceScope parentScope = null; + final TraceScope.Continuation continuation = channelTraceContext.getConnectionContinuation(); + if (continuation != null) { + parentScope = continuation.activate(); + channelTraceContext.setConnectionContinuation(null); + } + + final HttpRequest request = (HttpRequest) msg.getMessage(); + + channelTraceContext.setClientParentSpan(activeSpan()); + + final AgentSpan span = startSpan("netty.client.request"); + try (final AgentScope scope = activateSpan(span, false)) { + DECORATE.afterStart(span); + DECORATE.onRequest(span, request); + DECORATE.onPeerConnection(span, (InetSocketAddress) ctx.getChannel().getRemoteAddress()); + + propagate().inject(span, request.headers(), SETTER); + + channelTraceContext.setClientSpan(span); + + try { + ctx.sendDownstream(msg); + } catch (final Throwable throwable) { + DECORATE.onError(span, throwable); + DECORATE.beforeFinish(span); + span.finish(); + throw throwable; + } + } finally { + if (parentScope != null) { + parentScope.close(); + } + } + } +} diff --git a/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/client/HttpClientResponseTracingHandler.java b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/client/HttpClientResponseTracingHandler.java new file mode 100644 index 0000000000..156af2ec6c --- /dev/null +++ b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/client/HttpClientResponseTracingHandler.java @@ -0,0 +1,55 @@ +package datadog.trace.instrumentation.netty38.client; + +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.noopSpan; +import static datadog.trace.instrumentation.netty38.client.NettyHttpClientDecorator.DECORATE; + +import datadog.trace.bootstrap.ContextStore; +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.instrumentation.netty38.ChannelTraceContext; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelUpstreamHandler; +import org.jboss.netty.handler.codec.http.HttpResponse; + +public class HttpClientResponseTracingHandler extends SimpleChannelUpstreamHandler { + + private final ContextStore contextStore; + + public HttpClientResponseTracingHandler( + final ContextStore contextStore) { + this.contextStore = contextStore; + } + + @Override + public void messageReceived(final ChannelHandlerContext ctx, final MessageEvent msg) + throws Exception { + final ChannelTraceContext channelTraceContext = + contextStore.putIfAbsent(ctx.getChannel(), ChannelTraceContext.Factory.INSTANCE); + + AgentSpan parent = channelTraceContext.getClientParentSpan(); + if (parent == null) { + parent = noopSpan(); + channelTraceContext.setClientParentSpan(noopSpan()); + } + final AgentSpan span = channelTraceContext.getClientSpan(); + + final boolean finishSpan = msg.getMessage() instanceof HttpResponse; + + if (span != null && finishSpan) { + try (final AgentScope scope = activateSpan(span, false)) { + DECORATE.onResponse(span, (HttpResponse) msg.getMessage()); + DECORATE.beforeFinish(span); + span.finish(); + } + } + + // We want the callback in the scope of the parent, not the client span + try (final AgentScope scope = activateSpan(parent, false)) { + scope.setAsyncPropagation(true); + ctx.sendUpstream(msg); + } + } +} diff --git a/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/client/HttpClientTracingHandler.java b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/client/HttpClientTracingHandler.java new file mode 100644 index 0000000000..ebc8ebc0fd --- /dev/null +++ b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/client/HttpClientTracingHandler.java @@ -0,0 +1,17 @@ +package datadog.trace.instrumentation.netty38.client; + +import datadog.trace.bootstrap.ContextStore; +import datadog.trace.instrumentation.netty38.ChannelTraceContext; +import datadog.trace.instrumentation.netty38.util.CombinedSimpleChannelHandler; +import org.jboss.netty.channel.Channel; + +public class HttpClientTracingHandler + extends CombinedSimpleChannelHandler< + HttpClientResponseTracingHandler, HttpClientRequestTracingHandler> { + + public HttpClientTracingHandler(final ContextStore contextStore) { + super( + new HttpClientResponseTracingHandler(contextStore), + new HttpClientRequestTracingHandler(contextStore)); + } +} diff --git a/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/client/NettyHttpClientDecorator.java b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/client/NettyHttpClientDecorator.java new file mode 100644 index 0000000000..f1f90a206e --- /dev/null +++ b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/client/NettyHttpClientDecorator.java @@ -0,0 +1,45 @@ +package datadog.trace.instrumentation.netty38.client; + +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.HOST; + +import datadog.trace.bootstrap.instrumentation.decorator.HttpClientDecorator; +import java.net.URI; +import java.net.URISyntaxException; +import lombok.extern.slf4j.Slf4j; +import org.jboss.netty.handler.codec.http.HttpRequest; +import org.jboss.netty.handler.codec.http.HttpResponse; + +@Slf4j +public class NettyHttpClientDecorator extends HttpClientDecorator { + public static final NettyHttpClientDecorator DECORATE = new NettyHttpClientDecorator(); + + @Override + protected String[] instrumentationNames() { + return new String[] {"netty", "netty-3.9"}; + } + + @Override + protected String component() { + return "netty-client"; + } + + @Override + protected String method(final HttpRequest httpRequest) { + return httpRequest.getMethod().getName(); + } + + @Override + protected URI url(final HttpRequest request) throws URISyntaxException { + final URI uri = new URI(request.getUri()); + if ((uri.getHost() == null || uri.getHost().equals("")) && request.headers().contains(HOST)) { + return new URI("http://" + request.headers().get(HOST) + request.getUri()); + } else { + return uri; + } + } + + @Override + protected Integer status(final HttpResponse httpResponse) { + return httpResponse.getStatus().getCode(); + } +} diff --git a/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/client/NettyResponseInjectAdapter.java b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/client/NettyResponseInjectAdapter.java new file mode 100644 index 0000000000..7f4dc69580 --- /dev/null +++ b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/client/NettyResponseInjectAdapter.java @@ -0,0 +1,14 @@ +package datadog.trace.instrumentation.netty38.client; + +import datadog.trace.bootstrap.instrumentation.api.AgentPropagation; +import org.jboss.netty.handler.codec.http.HttpHeaders; + +public class NettyResponseInjectAdapter implements AgentPropagation.Setter { + + public static final NettyResponseInjectAdapter SETTER = new NettyResponseInjectAdapter(); + + @Override + public void set(final HttpHeaders headers, final String key, final String value) { + headers.set(key, value); + } +} diff --git a/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/server/HttpServerRequestTracingHandler.java b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/server/HttpServerRequestTracingHandler.java new file mode 100644 index 0000000000..67cda84d9b --- /dev/null +++ b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/server/HttpServerRequestTracingHandler.java @@ -0,0 +1,72 @@ +package datadog.trace.instrumentation.netty38.server; + +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.propagate; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.startSpan; +import static datadog.trace.instrumentation.netty38.server.NettyHttpServerDecorator.DECORATE; +import static datadog.trace.instrumentation.netty38.server.NettyRequestExtractAdapter.GETTER; + +import datadog.trace.bootstrap.ContextStore; +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan.Context; +import datadog.trace.instrumentation.netty38.ChannelTraceContext; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelUpstreamHandler; +import org.jboss.netty.handler.codec.http.HttpRequest; + +public class HttpServerRequestTracingHandler extends SimpleChannelUpstreamHandler { + + private final ContextStore contextStore; + + public HttpServerRequestTracingHandler( + final ContextStore contextStore) { + this.contextStore = contextStore; + } + + @Override + public void messageReceived(final ChannelHandlerContext ctx, final MessageEvent msg) + throws Exception { + final ChannelTraceContext channelTraceContext = + contextStore.putIfAbsent(ctx.getChannel(), ChannelTraceContext.Factory.INSTANCE); + + if (!(msg.getMessage() instanceof HttpRequest)) { + final AgentSpan span = channelTraceContext.getServerSpan(); + if (span == null) { + ctx.sendUpstream(msg); // superclass does not throw + } else { + try (final AgentScope scope = activateSpan(span, false)) { + scope.setAsyncPropagation(true); + ctx.sendUpstream(msg); // superclass does not throw + } + } + return; + } + + final HttpRequest request = (HttpRequest) msg.getMessage(); + + final Context context = propagate().extract(request.headers(), GETTER); + + final AgentSpan span = startSpan("netty.request", context); + try (final AgentScope scope = activateSpan(span, false)) { + DECORATE.afterStart(span); + DECORATE.onConnection(span, ctx.getChannel()); + DECORATE.onRequest(span, request); + + scope.setAsyncPropagation(true); + + channelTraceContext.setServerSpan(span); + + try { + ctx.sendUpstream(msg); + } catch (final Throwable throwable) { + DECORATE.onError(span, throwable); + DECORATE.beforeFinish(span); + span.finish(); // Finish the span manually since finishSpanOnClose was false + throw throwable; + } + } + } +} diff --git a/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/server/HttpServerResponseTracingHandler.java b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/server/HttpServerResponseTracingHandler.java new file mode 100644 index 0000000000..d3b511b2b8 --- /dev/null +++ b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/server/HttpServerResponseTracingHandler.java @@ -0,0 +1,50 @@ +package datadog.trace.instrumentation.netty38.server; + +import static datadog.trace.instrumentation.netty38.server.NettyHttpServerDecorator.DECORATE; + +import datadog.trace.bootstrap.ContextStore; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.instrumentation.netty38.ChannelTraceContext; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelDownstreamHandler; +import org.jboss.netty.handler.codec.http.HttpResponse; + +public class HttpServerResponseTracingHandler extends SimpleChannelDownstreamHandler { + + private final ContextStore contextStore; + + public HttpServerResponseTracingHandler( + final ContextStore contextStore) { + this.contextStore = contextStore; + } + + @Override + public void writeRequested(final ChannelHandlerContext ctx, final MessageEvent msg) + throws Exception { + final ChannelTraceContext channelTraceContext = + contextStore.putIfAbsent(ctx.getChannel(), ChannelTraceContext.Factory.INSTANCE); + + final AgentSpan span = channelTraceContext.getServerSpan(); + if (span == null || !(msg.getMessage() instanceof HttpResponse)) { + ctx.sendDownstream(msg); + return; + } + + final HttpResponse response = (HttpResponse) msg.getMessage(); + + try { + ctx.sendDownstream(msg); + } catch (final Throwable throwable) { + DECORATE.onError(span, throwable); + span.setTag(Tags.HTTP_STATUS, 500); + span.finish(); // Finish the span manually since finishSpanOnClose was false + throw throwable; + } + DECORATE.onResponse(span, response); + DECORATE.beforeFinish(span); + span.finish(); // Finish the span manually since finishSpanOnClose was false + } +} diff --git a/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/server/HttpServerTracingHandler.java b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/server/HttpServerTracingHandler.java new file mode 100644 index 0000000000..c4827a1fb7 --- /dev/null +++ b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/server/HttpServerTracingHandler.java @@ -0,0 +1,17 @@ +package datadog.trace.instrumentation.netty38.server; + +import datadog.trace.bootstrap.ContextStore; +import datadog.trace.instrumentation.netty38.ChannelTraceContext; +import datadog.trace.instrumentation.netty38.util.CombinedSimpleChannelHandler; +import org.jboss.netty.channel.Channel; + +public class HttpServerTracingHandler + extends CombinedSimpleChannelHandler< + HttpServerRequestTracingHandler, HttpServerResponseTracingHandler> { + + public HttpServerTracingHandler(final ContextStore contextStore) { + super( + new HttpServerRequestTracingHandler(contextStore), + new HttpServerResponseTracingHandler(contextStore)); + } +} diff --git a/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/server/NettyHttpServerDecorator.java b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/server/NettyHttpServerDecorator.java new file mode 100644 index 0000000000..b4baa2dbd1 --- /dev/null +++ b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/server/NettyHttpServerDecorator.java @@ -0,0 +1,67 @@ +package datadog.trace.instrumentation.netty38.server; + +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.HOST; + +import datadog.trace.bootstrap.instrumentation.decorator.HttpServerDecorator; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import lombok.extern.slf4j.Slf4j; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.handler.codec.http.HttpRequest; +import org.jboss.netty.handler.codec.http.HttpResponse; + +@Slf4j +public class NettyHttpServerDecorator + extends HttpServerDecorator { + public static final NettyHttpServerDecorator DECORATE = new NettyHttpServerDecorator(); + + @Override + protected String[] instrumentationNames() { + return new String[] {"netty", "netty-3.9"}; + } + + @Override + protected String component() { + return "netty"; + } + + @Override + protected String method(final HttpRequest httpRequest) { + return httpRequest.getMethod().getName(); + } + + @Override + protected URI url(final HttpRequest request) throws URISyntaxException { + final URI uri = new URI(request.getUri()); + if ((uri.getHost() == null || uri.getHost().equals("")) && request.headers().contains(HOST)) { + return new URI("http://" + request.headers().get(HOST) + request.getUri()); + } else { + return uri; + } + } + + @Override + protected String peerHostIP(final Channel channel) { + final SocketAddress socketAddress = channel.getRemoteAddress(); + if (socketAddress instanceof InetSocketAddress) { + return ((InetSocketAddress) socketAddress).getAddress().getHostAddress(); + } + return null; + } + + @Override + protected Integer peerPort(final Channel channel) { + final SocketAddress socketAddress = channel.getRemoteAddress(); + if (socketAddress instanceof InetSocketAddress) { + return ((InetSocketAddress) socketAddress).getPort(); + } + return null; + } + + @Override + protected Integer status(final HttpResponse httpResponse) { + return httpResponse.getStatus().getCode(); + } +} diff --git a/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/server/NettyRequestExtractAdapter.java b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/server/NettyRequestExtractAdapter.java new file mode 100644 index 0000000000..39eedcc226 --- /dev/null +++ b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/server/NettyRequestExtractAdapter.java @@ -0,0 +1,19 @@ +package datadog.trace.instrumentation.netty38.server; + +import datadog.trace.bootstrap.instrumentation.api.AgentPropagation; +import org.jboss.netty.handler.codec.http.HttpHeaders; + +public class NettyRequestExtractAdapter implements AgentPropagation.Getter { + + public static final NettyRequestExtractAdapter GETTER = new NettyRequestExtractAdapter(); + + @Override + public Iterable keys(final HttpHeaders headers) { + return headers.names(); + } + + @Override + public String get(final HttpHeaders headers, final String key) { + return headers.get(key); + } +} diff --git a/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/util/CombinedSimpleChannelHandler.java b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/util/CombinedSimpleChannelHandler.java new file mode 100644 index 0000000000..d33442e4b7 --- /dev/null +++ b/dd-java-agent/instrumentation/netty-3.8/src/main/java/datadog/trace/instrumentation/netty38/util/CombinedSimpleChannelHandler.java @@ -0,0 +1,152 @@ +package datadog.trace.instrumentation.netty38.util; + +import org.jboss.netty.channel.ChannelEvent; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.ChannelStateEvent; +import org.jboss.netty.channel.ChildChannelStateEvent; +import org.jboss.netty.channel.ExceptionEvent; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelDownstreamHandler; +import org.jboss.netty.channel.SimpleChannelHandler; +import org.jboss.netty.channel.SimpleChannelUpstreamHandler; +import org.jboss.netty.channel.WriteCompletionEvent; + +public class CombinedSimpleChannelHandler< + Upstream extends SimpleChannelUpstreamHandler, + Downstream extends SimpleChannelDownstreamHandler> + extends SimpleChannelHandler { + + private final Upstream upstream; + private final Downstream downstream; + + public CombinedSimpleChannelHandler(final Upstream upstream, final Downstream downstream) { + this.upstream = upstream; + this.downstream = downstream; + } + + @Override + public void handleUpstream(final ChannelHandlerContext ctx, final ChannelEvent e) + throws Exception { + upstream.handleUpstream(ctx, e); + } + + @Override + public void messageReceived(final ChannelHandlerContext ctx, final MessageEvent e) + throws Exception { + upstream.messageReceived(ctx, e); + } + + @Override + public void exceptionCaught(final ChannelHandlerContext ctx, final ExceptionEvent e) + throws Exception { + upstream.exceptionCaught(ctx, e); + } + + @Override + public void channelOpen(final ChannelHandlerContext ctx, final ChannelStateEvent e) + throws Exception { + upstream.channelOpen(ctx, e); + } + + @Override + public void channelBound(final ChannelHandlerContext ctx, final ChannelStateEvent e) + throws Exception { + upstream.channelBound(ctx, e); + } + + @Override + public void channelConnected(final ChannelHandlerContext ctx, final ChannelStateEvent e) + throws Exception { + upstream.channelConnected(ctx, e); + } + + @Override + public void channelInterestChanged(final ChannelHandlerContext ctx, final ChannelStateEvent e) + throws Exception { + upstream.channelInterestChanged(ctx, e); + } + + @Override + public void channelDisconnected(final ChannelHandlerContext ctx, final ChannelStateEvent e) + throws Exception { + upstream.channelDisconnected(ctx, e); + } + + @Override + public void channelUnbound(final ChannelHandlerContext ctx, final ChannelStateEvent e) + throws Exception { + upstream.channelUnbound(ctx, e); + } + + @Override + public void channelClosed(final ChannelHandlerContext ctx, final ChannelStateEvent e) + throws Exception { + upstream.channelClosed(ctx, e); + } + + @Override + public void writeComplete(final ChannelHandlerContext ctx, final WriteCompletionEvent e) + throws Exception { + upstream.writeComplete(ctx, e); + } + + @Override + public void childChannelOpen(final ChannelHandlerContext ctx, final ChildChannelStateEvent e) + throws Exception { + upstream.childChannelOpen(ctx, e); + } + + @Override + public void childChannelClosed(final ChannelHandlerContext ctx, final ChildChannelStateEvent e) + throws Exception { + upstream.childChannelClosed(ctx, e); + } + + @Override + public void handleDownstream(final ChannelHandlerContext ctx, final ChannelEvent e) + throws Exception { + downstream.handleDownstream(ctx, e); + } + + @Override + public void writeRequested(final ChannelHandlerContext ctx, final MessageEvent e) + throws Exception { + downstream.writeRequested(ctx, e); + } + + @Override + public void bindRequested(final ChannelHandlerContext ctx, final ChannelStateEvent e) + throws Exception { + downstream.bindRequested(ctx, e); + } + + @Override + public void connectRequested(final ChannelHandlerContext ctx, final ChannelStateEvent e) + throws Exception { + downstream.connectRequested(ctx, e); + } + + @Override + public void setInterestOpsRequested(final ChannelHandlerContext ctx, final ChannelStateEvent e) + throws Exception { + downstream.setInterestOpsRequested(ctx, e); + } + + @Override + public void disconnectRequested(final ChannelHandlerContext ctx, final ChannelStateEvent e) + throws Exception { + downstream.disconnectRequested(ctx, e); + } + + @Override + public void unbindRequested(final ChannelHandlerContext ctx, final ChannelStateEvent e) + throws Exception { + downstream.unbindRequested(ctx, e); + } + + @Override + public void closeRequested(final ChannelHandlerContext ctx, final ChannelStateEvent e) + throws Exception { + downstream.closeRequested(ctx, e); + } +} diff --git a/dd-java-agent/instrumentation/netty-3.8/src/test/groovy/Netty38ClientTest.groovy b/dd-java-agent/instrumentation/netty-3.8/src/test/groovy/Netty38ClientTest.groovy new file mode 100644 index 0000000000..c56d227845 --- /dev/null +++ b/dd-java-agent/instrumentation/netty-3.8/src/test/groovy/Netty38ClientTest.groovy @@ -0,0 +1,106 @@ +import com.ning.http.client.AsyncCompletionHandler +import com.ning.http.client.AsyncHttpClient +import com.ning.http.client.AsyncHttpClientConfig +import com.ning.http.client.Response +import datadog.trace.agent.test.base.HttpClientTest +import datadog.trace.bootstrap.instrumentation.api.Tags +import datadog.trace.instrumentation.netty38.client.NettyHttpClientDecorator +import spock.lang.AutoCleanup +import spock.lang.Shared + +import java.util.concurrent.ExecutionException +import java.util.concurrent.TimeUnit + +import static datadog.trace.agent.test.utils.PortUtils.UNUSABLE_PORT +import static datadog.trace.agent.test.utils.TraceUtils.basicSpan +import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace + +class Netty38ClientTest extends HttpClientTest { + + @Shared + def clientConfig = new AsyncHttpClientConfig.Builder() + .setRequestTimeoutInMs(TimeUnit.SECONDS.toMillis(10).toInteger()) + .build() + + @Shared + @AutoCleanup + AsyncHttpClient asyncHttpClient = new AsyncHttpClient(clientConfig) + + @Override + int doRequest(String method, URI uri, Map headers, Closure callback) { + def methodName = "prepare" + method.toLowerCase().capitalize() + def requestBuilder = asyncHttpClient."$methodName"(uri.toString()) + headers.each { requestBuilder.setHeader(it.key, it.value) } + def response = requestBuilder.execute(new AsyncCompletionHandler() { + @Override + Object onCompleted(Response response) throws Exception { + callback?.call() + return response + } + }).get() + blockUntilChildSpansFinished(1) + return response.statusCode + } + + @Override + String component() { + return NettyHttpClientDecorator.DECORATE.component() + } + + @Override + String expectedOperationName() { + return "netty.client.request" + } + + @Override + boolean testRedirects() { + false + } + + @Override + boolean testConnectionFailure() { + false + } + + def "connection error (unopened port)"() { + given: + def uri = new URI("http://localhost:$UNUSABLE_PORT/") + + when: + runUnderTrace("parent") { + doRequest(method, uri) + } + + then: + def ex = thrown(Exception) + def thrownException = ex instanceof ExecutionException ? ex.cause : ex + + and: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent", null, thrownException) + + span(1) { + operationName "netty.connect" + resourceName "netty.connect" + childOf span(0) + errored true + tags { + "$Tags.COMPONENT" "netty" + Class errorClass = ConnectException + try { + errorClass = Class.forName('io.netty.channel.AbstractChannel$AnnotatedConnectException') + } catch (ClassNotFoundException e) { + // Older versions use 'java.net.ConnectException' and do not have 'io.netty.channel.AbstractChannel$AnnotatedConnectException' + } + errorTags errorClass, "Connection refused: localhost/127.0.0.1:$UNUSABLE_PORT" + defaultTags() + } + } + } + } + + where: + method = "GET" + } +} diff --git a/dd-java-agent/instrumentation/netty-3.8/src/test/groovy/Netty38ServerTest.groovy b/dd-java-agent/instrumentation/netty-3.8/src/test/groovy/Netty38ServerTest.groovy new file mode 100644 index 0000000000..e7b0794e0b --- /dev/null +++ b/dd-java-agent/instrumentation/netty-3.8/src/test/groovy/Netty38ServerTest.groovy @@ -0,0 +1,111 @@ +import datadog.trace.agent.test.base.HttpServerTest +import datadog.trace.instrumentation.netty38.server.NettyHttpServerDecorator +import org.jboss.netty.bootstrap.ServerBootstrap +import org.jboss.netty.buffer.ChannelBuffer +import org.jboss.netty.buffer.ChannelBuffers +import org.jboss.netty.channel.* +import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory +import org.jboss.netty.handler.codec.http.* +import org.jboss.netty.handler.logging.LoggingHandler +import org.jboss.netty.logging.InternalLogLevel +import org.jboss.netty.util.CharsetUtil + +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.* +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.* +import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1 + +class Netty38ServerTest extends HttpServerTest { + + ChannelPipeline channelPipeline() { + ChannelPipeline channelPipeline = new DefaultChannelPipeline() + + channelPipeline.addLast("http-codec", new HttpServerCodec()) + channelPipeline.addLast("controller", new SimpleChannelHandler() { + @Override + void messageReceived(ChannelHandlerContext ctx, MessageEvent msg) throws Exception { + if (msg.getMessage() instanceof HttpRequest) { + def uri = URI.create((msg.getMessage() as HttpRequest).getUri()) + HttpServerTest.ServerEndpoint endpoint = forPath(uri.path) + ctx.sendDownstream controller(endpoint) { + HttpResponse response + ChannelBuffer responseContent = null + switch (endpoint) { + case SUCCESS: + case ERROR: + responseContent = ChannelBuffers.copiedBuffer(endpoint.body, CharsetUtil.UTF_8) + response = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.valueOf(endpoint.status)) + response.setContent(responseContent) + break + case QUERY_PARAM: + responseContent = ChannelBuffers.copiedBuffer(uri.query, CharsetUtil.UTF_8) + response = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.valueOf(endpoint.status)) + response.setContent(responseContent) + break + case REDIRECT: + response = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.valueOf(endpoint.status)) + response.headers().set(LOCATION, endpoint.body) + break + case EXCEPTION: + throw new Exception(endpoint.body) + default: + responseContent = ChannelBuffers.copiedBuffer(NOT_FOUND.body, CharsetUtil.UTF_8) + response = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.valueOf(endpoint.status)) + response.setContent(responseContent) + break + } + response.headers().set(CONTENT_TYPE, "text/plain") + if (responseContent) { + response.headers().set(CONTENT_LENGTH, responseContent.readableBytes()) + } + return new DownstreamMessageEvent( + ctx.getChannel(), + new SucceededChannelFuture(ctx.getChannel()), + response, + ctx.getChannel().getRemoteAddress()) + } + } + } + + @Override + void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent ex) throws Exception { + ChannelBuffer buffer = ChannelBuffers.copiedBuffer(ex.getCause().getMessage(), CharsetUtil.UTF_8) + HttpResponse response = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.INTERNAL_SERVER_ERROR) + response.setContent(buffer) + response.headers().set(CONTENT_TYPE, "text/plain") + response.headers().set(CONTENT_LENGTH, buffer.readableBytes()) + ctx.sendDownstream(new DownstreamMessageEvent( + ctx.getChannel(), + new FailedChannelFuture(ctx.getChannel(), ex.getCause()), + response, + ctx.getChannel().getRemoteAddress())) + } + }) + + return channelPipeline + } + + @Override + Channel startServer(int port) { + ServerBootstrap bootstrap = new ServerBootstrap(new NioServerSocketChannelFactory()) + bootstrap.setParentHandler(new LoggingHandler(InternalLogLevel.INFO)) + bootstrap.setPipeline(channelPipeline()) + + InetSocketAddress address = new InetSocketAddress(port) + return bootstrap.bind(address) + } + + @Override + void stopServer(Channel server) { + server?.disconnect() + } + + @Override + String component() { + NettyHttpServerDecorator.DECORATE.component() + } + + @Override + String expectedOperationName() { + "netty.request" + } +} diff --git a/dd-java-agent/instrumentation/netty-3.8/src/test/groovy/NettyServerTestInstrumentation.java b/dd-java-agent/instrumentation/netty-3.8/src/test/groovy/NettyServerTestInstrumentation.java new file mode 100644 index 0000000000..9657cea79c --- /dev/null +++ b/dd-java-agent/instrumentation/netty-3.8/src/test/groovy/NettyServerTestInstrumentation.java @@ -0,0 +1,21 @@ +import static net.bytebuddy.matcher.ElementMatchers.named; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.test.base.HttpServerTestAdvice; +import datadog.trace.agent.tooling.Instrumenter; +import net.bytebuddy.agent.builder.AgentBuilder; + +@AutoService(Instrumenter.class) +public class NettyServerTestInstrumentation implements Instrumenter { + + @Override + public AgentBuilder instrument(final AgentBuilder agentBuilder) { + return agentBuilder + .type(named("org.jboss.netty.handler.codec.http.HttpRequestDecoder")) + .transform( + new AgentBuilder.Transformer.ForAdvice() + .advice( + named("createMessage"), + HttpServerTestAdvice.ServerEntryAdvice.class.getName())); + } +} diff --git a/dd-java-agent/instrumentation/netty-4.0/src/main/java/datadog/trace/instrumentation/netty40/client/HttpClientRequestTracingHandler.java b/dd-java-agent/instrumentation/netty-4.0/src/main/java/datadog/trace/instrumentation/netty40/client/HttpClientRequestTracingHandler.java index cf2a5cf9ad..a34c269521 100644 --- a/dd-java-agent/instrumentation/netty-4.0/src/main/java/datadog/trace/instrumentation/netty40/client/HttpClientRequestTracingHandler.java +++ b/dd-java-agent/instrumentation/netty-4.0/src/main/java/datadog/trace/instrumentation/netty40/client/HttpClientRequestTracingHandler.java @@ -60,10 +60,10 @@ public class HttpClientRequestTracingHandler extends ChannelOutboundHandlerAdapt span.finish(); throw throwable; } - } - - if (null != parentScope) { - parentScope.close(); + } finally { + if (null != parentScope) { + parentScope.close(); + } } } } diff --git a/dd-java-agent/instrumentation/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/client/HttpClientRequestTracingHandler.java b/dd-java-agent/instrumentation/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/client/HttpClientRequestTracingHandler.java index 86b8294486..38ba4d9cbd 100644 --- a/dd-java-agent/instrumentation/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/client/HttpClientRequestTracingHandler.java +++ b/dd-java-agent/instrumentation/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/client/HttpClientRequestTracingHandler.java @@ -60,10 +60,10 @@ public class HttpClientRequestTracingHandler extends ChannelOutboundHandlerAdapt span.finish(); throw throwable; } - } - - if (null != parentScope) { - parentScope.close(); + } finally { + if (null != parentScope) { + parentScope.close(); + } } } } diff --git a/settings.gradle b/settings.gradle index 75240bee25..e184365078 100644 --- a/settings.gradle +++ b/settings.gradle @@ -112,6 +112,7 @@ include ':dd-java-agent:instrumentation:log4j2' include ':dd-java-agent:instrumentation:mongo' include ':dd-java-agent:instrumentation:mongo:driver-3.1' include ':dd-java-agent:instrumentation:mongo:driver-async-3.3' +include ':dd-java-agent:instrumentation:netty-3.8' include ':dd-java-agent:instrumentation:netty-4.0' include ':dd-java-agent:instrumentation:netty-4.1' include ':dd-java-agent:instrumentation:okhttp-3'