Merge pull request #1327 netty-3.8

Netty 3.8-3.10 instrumentation
This commit is contained in:
Brian Devins-Suresh 2020-03-23 15:12:20 -04:00 committed by GitHub
commit 2b5037e2f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1571 additions and 8 deletions

View File

@ -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"
}
}
}
}

View File

@ -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<String, String> 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"
}
}

View File

@ -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<Channel> {
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"
}
}

View File

@ -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()));
}
}

View File

@ -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();
}
}

View File

@ -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<ClassLoader> classLoaderMatcher() {
// Optimization for expensive typeMatcher.
return hasClassesNamed("org.jboss.netty.channel.ChannelFutureListener");
}
@Override
public ElementMatcher<TypeDescription> 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<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(
isMethod()
.and(named("operationComplete"))
.and(takesArgument(0, named("org.jboss.netty.channel.ChannelFuture"))),
ChannelFutureListenerInstrumentation.class.getName() + "$OperationCompleteAdvice");
}
@Override
public Map<String, String> 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<Channel, ChannelTraceContext> 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();
}
}
}
}

View File

@ -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<ChannelTraceContext> {
public static final Factory INSTANCE = new Factory();
@Override
public ChannelTraceContext create() {
return new ChannelTraceContext();
}
}
TraceScope.Continuation connectionContinuation;
AgentSpan serverSpan;
AgentSpan clientSpan;
AgentSpan clientParentSpan;
}

View File

@ -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<ClassLoader> classLoaderMatcher() {
// Optimization for expensive typeMatcher.
return hasClassesNamed("org.jboss.netty.channel.Channel");
}
@Override
public ElementMatcher<TypeDescription> 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<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
final Map<ElementMatcher<? super MethodDescription>, 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<String, String> 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<Channel, ChannelTraceContext> contextStore =
InstrumentationContext.get(Channel.class, ChannelTraceContext.class);
if (contextStore
.putIfAbsent(channel, ChannelTraceContext.Factory.INSTANCE)
.getConnectionContinuation()
!= null) {
continuation.close();
} else {
contextStore.get(channel).setConnectionContinuation(continuation);
}
}
}
}
}
}

View File

@ -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<ClassLoader> classLoaderMatcher() {
// Optimization for expensive typeMatcher.
return hasClassesNamed("org.jboss.netty.channel.ChannelPipeline");
}
@Override
public ElementMatcher<TypeDescription> 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<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
final Map<ElementMatcher<? super MethodDescription>, 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<String, String> 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<Channel, ChannelTraceContext> 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<Channel, ChannelTraceContext> 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<Channel, ChannelTraceContext> contextStore =
InstrumentationContext.get(Channel.class, ChannelTraceContext.class);
ChannelPipelineAdviceUtil.wrapHandler(contextStore, pipeline, handler);
}
}
}

View File

@ -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<Channel, ChannelTraceContext> contextStore;
public HttpClientRequestTracingHandler(
final ContextStore<Channel, ChannelTraceContext> 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();
}
}
}
}

View File

@ -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<Channel, ChannelTraceContext> contextStore;
public HttpClientResponseTracingHandler(
final ContextStore<Channel, ChannelTraceContext> 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);
}
}
}

View File

@ -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<Channel, ChannelTraceContext> contextStore) {
super(
new HttpClientResponseTracingHandler(contextStore),
new HttpClientRequestTracingHandler(contextStore));
}
}

View File

@ -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<HttpRequest, HttpResponse> {
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();
}
}

View File

@ -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<HttpHeaders> {
public static final NettyResponseInjectAdapter SETTER = new NettyResponseInjectAdapter();
@Override
public void set(final HttpHeaders headers, final String key, final String value) {
headers.set(key, value);
}
}

View File

@ -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<Channel, ChannelTraceContext> contextStore;
public HttpServerRequestTracingHandler(
final ContextStore<Channel, ChannelTraceContext> 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;
}
}
}
}

View File

@ -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<Channel, ChannelTraceContext> contextStore;
public HttpServerResponseTracingHandler(
final ContextStore<Channel, ChannelTraceContext> 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
}
}

View File

@ -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<Channel, ChannelTraceContext> contextStore) {
super(
new HttpServerRequestTracingHandler(contextStore),
new HttpServerResponseTracingHandler(contextStore));
}
}

View File

@ -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<HttpRequest, Channel, HttpResponse> {
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();
}
}

View File

@ -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<HttpHeaders> {
public static final NettyRequestExtractAdapter GETTER = new NettyRequestExtractAdapter();
@Override
public Iterable<String> keys(final HttpHeaders headers) {
return headers.names();
}
@Override
public String get(final HttpHeaders headers, final String key) {
return headers.get(key);
}
}

View File

@ -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);
}
}

View File

@ -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<String, String> 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"
}
}

View File

@ -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<Channel> {
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"
}
}

View File

@ -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()));
}
}

View File

@ -60,10 +60,10 @@ public class HttpClientRequestTracingHandler extends ChannelOutboundHandlerAdapt
span.finish();
throw throwable;
}
}
if (null != parentScope) {
parentScope.close();
} finally {
if (null != parentScope) {
parentScope.close();
}
}
}
}

View File

@ -60,10 +60,10 @@ public class HttpClientRequestTracingHandler extends ChannelOutboundHandlerAdapt
span.finish();
throw throwable;
}
}
if (null != parentScope) {
parentScope.close();
} finally {
if (null != parentScope) {
parentScope.close();
}
}
}
}

View File

@ -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'