Migrate servlet instrumentation to byte buddy.

This commit is contained in:
Tyler Benson 2017-11-06 11:28:06 -08:00
parent e21ed1052f
commit e5924b3fe9
12 changed files with 339 additions and 277 deletions

View File

@ -21,7 +21,8 @@ dependencies {
compile project(':dd-java-agent:tooling') compile project(':dd-java-agent:tooling')
compile project(':dd-trace-annotations') compile project(':dd-trace-annotations')
compile project(':dd-java-agent:integrations:servlet-2')
compile project(':dd-java-agent:integrations:servlet-3')
compile project(':dd-java-agent:integrations:spring-web') compile project(':dd-java-agent:integrations:spring-web')
compile deps.bytebuddy compile deps.bytebuddy

View File

@ -1,94 +0,0 @@
package com.datadoghq.agent.integration;
import io.opentracing.ActiveSpan;
import io.opentracing.NoopTracerFactory;
import io.opentracing.SpanContext;
import io.opentracing.Tracer;
import io.opentracing.contrib.web.servlet.filter.HttpServletRequestExtractAdapter;
import io.opentracing.contrib.web.servlet.filter.ServletFilterSpanDecorator;
import io.opentracing.propagation.Format;
import io.opentracing.tag.Tags;
import java.util.Collections;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.jboss.byteman.rule.Rule;
/** Please be very careful not to introduce any Servlet 3 dependencies into this class. */
@Slf4j
public class Servlet2Helper extends OpenTracingHelper {
/**
* Used as a key of {@link HttpServletRequest#setAttribute(String, Object)} to inject server span
* context
*/
public static final String SERVER_SPAN_CONTEXT =
Servlet2Helper.class.getName() + ".activeSpanContext";
public static final String SERVLET_OPERATION_NAME = "servlet.request";
protected final Tracer tracer;
public Servlet2Helper(final Rule rule) {
super(rule);
Tracer tracerResolved;
try {
tracerResolved = getTracer();
tracerResolved = tracerResolved == null ? NoopTracerFactory.create() : tracerResolved;
} catch (final Exception e) {
tracerResolved = NoopTracerFactory.create();
log.warn("Failed to retrieve the tracer, using a NoopTracer instead: {}", e.getMessage());
log.warn(e.getMessage(), e);
}
tracer = tracerResolved;
}
public void onRequest(final HttpServletRequest req, final HttpServletResponse resp) {
if (req.getAttribute(SERVER_SPAN_CONTEXT) != null) {
// Perhaps we're already tracing?
return;
}
final SpanContext extractedContext =
tracer.extract(Format.Builtin.HTTP_HEADERS, new HttpServletRequestExtractAdapter(req));
final ActiveSpan span =
tracer
.buildSpan(SERVLET_OPERATION_NAME)
.asChildOf(extractedContext)
.withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_SERVER)
.startActive();
req.setAttribute(SERVER_SPAN_CONTEXT, span.context());
ServletFilterSpanDecorator.STANDARD_TAGS.onRequest(req, span);
}
public void onError(
final HttpServletRequest req, final HttpServletResponse resp, final Throwable ex) {
if (req.getAttribute(SERVER_SPAN_CONTEXT) == null) {
// Doesn't look like an active span was started at the beginning
return;
}
final ActiveSpan span = tracer.activeSpan();
if (span != null) {
ServletFilterSpanDecorator.STANDARD_TAGS.onError(req, resp, ex, span);
span.log(Collections.singletonMap("error.object", ex));
span.deactivate();
}
}
public void onResponse(final HttpServletRequest req, final HttpServletResponse resp) {
if (req.getAttribute(SERVER_SPAN_CONTEXT) == null) {
// Doesn't look like an active span was started at the beginning
return;
}
final ActiveSpan span = tracer.activeSpan();
if (span != null) {
ServletFilterSpanDecorator.STANDARD_TAGS.onResponse(req, resp, span);
span.deactivate();
}
}
}

View File

@ -1,99 +0,0 @@
package com.datadoghq.agent.integration;
import io.opentracing.ActiveSpan;
import io.opentracing.contrib.web.servlet.filter.ServletFilterSpanDecorator;
import java.io.IOException;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.jboss.byteman.rule.Rule;
@Slf4j
public class Servlet3Helper extends Servlet2Helper {
public Servlet3Helper(final Rule rule) {
super(rule);
}
/**
* The distinction with this method compared with Servlet2Helper.onResponse is the addition of the
* async support.
*
* @param req
* @param resp
*/
@Override
public void onResponse(final HttpServletRequest req, final HttpServletResponse resp) {
if (req.getAttribute(SERVER_SPAN_CONTEXT) == null) {
// Doesn't look like an active span was started at the beginning
return;
}
final ActiveSpan span = tracer.activeSpan();
if (span != null) {
if (req.isAsyncStarted()) {
addAsyncListeners(req, resp, span);
} else {
ServletFilterSpanDecorator.STANDARD_TAGS.onResponse(req, resp, span);
}
span.deactivate();
}
}
private void addAsyncListeners(
final HttpServletRequest req, final HttpServletResponse resp, final ActiveSpan span) {
final ActiveSpan.Continuation cont = span.capture();
final AtomicBoolean activated = new AtomicBoolean(false);
// what if async is already finished? This would not be called
req.getAsyncContext()
.addListener(
new AsyncListener() {
@Override
public void onComplete(final AsyncEvent event) throws IOException {
if (activated.compareAndSet(false, true)) {
try (ActiveSpan activeSpan = cont.activate()) {
ServletFilterSpanDecorator.STANDARD_TAGS.onResponse(
(HttpServletRequest) event.getSuppliedRequest(),
(HttpServletResponse) event.getSuppliedResponse(),
span);
}
}
}
@Override
public void onTimeout(final AsyncEvent event) throws IOException {
if (activated.compareAndSet(false, true)) {
try (ActiveSpan activeSpan = cont.activate()) {
ServletFilterSpanDecorator.STANDARD_TAGS.onTimeout(
(HttpServletRequest) event.getSuppliedRequest(),
(HttpServletResponse) event.getSuppliedResponse(),
event.getAsyncContext().getTimeout(),
span);
}
}
}
@Override
public void onError(final AsyncEvent event) throws IOException {
if (activated.compareAndSet(false, true)) {
try (ActiveSpan activeSpan = cont.activate()) {
ServletFilterSpanDecorator.STANDARD_TAGS.onError(
(HttpServletRequest) event.getSuppliedRequest(),
(HttpServletResponse) event.getSuppliedResponse(),
event.getThrowable(),
span);
span.log(Collections.singletonMap("error.object", event.getThrowable()));
}
}
}
@Override
public void onStartAsync(final AsyncEvent event) throws IOException {}
});
}
}

View File

@ -9,3 +9,18 @@ versionScan {
"javax.servlet.FilterChain" : null, "javax.servlet.FilterChain" : null,
] ]
} }
apply from: "${rootDir}/gradle/java.gradle"
dependencies {
compileOnly group: 'javax.servlet', name: 'servlet-api', version: '2.3'
compile project(':dd-trace')
compile project(':dd-java-agent:tooling')
compile deps.bytebuddy
compile deps.opentracing
compile group: 'io.opentracing.contrib', name: 'opentracing-web-servlet-filter', version: '0.0.9'
compile group: 'com.google.auto.service', name: 'auto-service', version: '1.0-rc3'
}

View File

@ -0,0 +1,86 @@
package dd.inst.servlet2;
import static dd.trace.ClassLoaderHasClassMatcher.classLoaderHasClasses;
import static net.bytebuddy.matcher.ElementMatchers.isProtected;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.not;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import com.google.auto.service.AutoService;
import dd.trace.Instrumenter;
import io.opentracing.ActiveSpan;
import io.opentracing.SpanContext;
import io.opentracing.contrib.web.servlet.filter.HttpServletRequestExtractAdapter;
import io.opentracing.contrib.web.servlet.filter.ServletFilterSpanDecorator;
import io.opentracing.propagation.Format;
import io.opentracing.tag.Tags;
import io.opentracing.util.GlobalTracer;
import java.util.Collections;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
@AutoService(Instrumenter.class)
public final class HttpServlet2Instrumentation implements Instrumenter {
public static final String SERVLET_OPERATION_NAME = "servlet.request";
@Override
public AgentBuilder instrument(final AgentBuilder agentBuilder) {
return agentBuilder
.type(
named("javax.servlet.http.HttpServlet"),
not(classLoaderHasClasses("javax.servlet.AsyncEvent", "javax.servlet.AsyncListener"))
.and(
classLoaderHasClasses(
"javax.servlet.ServletContextEvent", "javax.servlet.FilterChain")))
.transform(
new AgentBuilder.Transformer.ForAdvice()
.advice(
named("service")
.and(takesArgument(0, named("javax.servlet.http.HttpServletRequest")))
.and(takesArgument(1, named("javax.servlet.http.HttpServletResponse")))
.and(isProtected()),
HttpServlet2Advice.class.getName()))
.asDecorator();
}
public static class HttpServlet2Advice {
@Advice.OnMethodEnter
public static ActiveSpan startSpan(@Advice.Argument(0) final HttpServletRequest req) {
final SpanContext extractedContext =
GlobalTracer.get()
.extract(Format.Builtin.HTTP_HEADERS, new HttpServletRequestExtractAdapter(req));
final ActiveSpan span =
GlobalTracer.get()
.buildSpan(SERVLET_OPERATION_NAME)
.asChildOf(extractedContext)
.withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_SERVER)
.startActive();
ServletFilterSpanDecorator.STANDARD_TAGS.onRequest(req, span);
return span;
}
@Advice.OnMethodExit(onThrowable = Throwable.class)
public static void stopSpan(
@Advice.Argument(0) final HttpServletRequest req,
@Advice.Argument(1) final HttpServletResponse resp,
@Advice.Enter final ActiveSpan span,
@Advice.Thrown final Throwable throwable) {
if (span != null) {
if (throwable != null) {
ServletFilterSpanDecorator.STANDARD_TAGS.onError(req, resp, throwable, span);
span.log(Collections.singletonMap("error.object", throwable));
} else {
ServletFilterSpanDecorator.STANDARD_TAGS.onResponse(req, resp, span);
}
span.deactivate();
}
}
}
}

View File

@ -10,3 +10,18 @@ versionScan {
"javax.servlet.AsyncListener": null, "javax.servlet.AsyncListener": null,
] ]
} }
apply from: "${rootDir}/gradle/java.gradle"
dependencies {
compileOnly group: 'javax.servlet', name: 'javax.servlet-api', version: '3.0.1'
compile project(':dd-trace')
compile project(':dd-java-agent:tooling')
compile deps.bytebuddy
compile deps.opentracing
compile group: 'io.opentracing.contrib', name: 'opentracing-web-servlet-filter', version: '0.0.9'
compile group: 'com.google.auto.service', name: 'auto-service', version: '1.0-rc3'
}

View File

@ -0,0 +1,148 @@
package dd.inst.servlet3;
import static dd.trace.ClassLoaderHasClassMatcher.classLoaderHasClasses;
import static net.bytebuddy.matcher.ElementMatchers.isProtected;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import com.google.auto.service.AutoService;
import dd.trace.Instrumenter;
import io.opentracing.ActiveSpan;
import io.opentracing.SpanContext;
import io.opentracing.contrib.web.servlet.filter.HttpServletRequestExtractAdapter;
import io.opentracing.contrib.web.servlet.filter.ServletFilterSpanDecorator;
import io.opentracing.propagation.Format;
import io.opentracing.tag.Tags;
import io.opentracing.util.GlobalTracer;
import java.io.IOException;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
@AutoService(Instrumenter.class)
public final class HttpServlet3Instrumentation implements Instrumenter {
public static final String SERVLET_OPERATION_NAME = "servlet.request";
@Override
public AgentBuilder instrument(final AgentBuilder agentBuilder) {
return agentBuilder
.type(
named("javax.servlet.http.HttpServlet"),
classLoaderHasClasses("javax.servlet.AsyncEvent", "javax.servlet.AsyncListener"))
.transform(
new AgentBuilder.Transformer.ForAdvice()
.advice(
named("service")
.and(takesArgument(0, named("javax.servlet.http.HttpServletRequest")))
.and(takesArgument(1, named("javax.servlet.http.HttpServletResponse")))
.and(isProtected()),
HttpServlet3Advice.class.getName()))
.asDecorator();
}
public static class HttpServlet3Advice {
@Advice.OnMethodEnter
public static ActiveSpan startSpan(@Advice.Argument(0) final HttpServletRequest req) {
final SpanContext extractedContext =
GlobalTracer.get()
.extract(Format.Builtin.HTTP_HEADERS, new HttpServletRequestExtractAdapter(req));
final ActiveSpan span =
GlobalTracer.get()
.buildSpan(SERVLET_OPERATION_NAME)
.asChildOf(extractedContext)
.withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_SERVER)
.startActive();
ServletFilterSpanDecorator.STANDARD_TAGS.onRequest(req, span);
return span;
}
@Advice.OnMethodExit(onThrowable = Throwable.class)
public static void stopSpan(
@Advice.Argument(0) final HttpServletRequest req,
@Advice.Argument(1) final HttpServletResponse resp,
@Advice.Enter final ActiveSpan span,
@Advice.Thrown final Throwable throwable) {
if (span != null) {
if (throwable != null) {
ServletFilterSpanDecorator.STANDARD_TAGS.onError(req, resp, throwable, span);
span.log(Collections.singletonMap("error.object", throwable));
} else if (req.isAsyncStarted()) {
final ActiveSpan.Continuation cont = span.capture();
final AtomicBoolean activated = new AtomicBoolean(false);
// what if async is already finished? This would not be called
req.getAsyncContext().addListener(new TagSettingAsyncListener(activated, cont, span));
} else {
ServletFilterSpanDecorator.STANDARD_TAGS.onResponse(req, resp, span);
}
span.deactivate();
}
}
public static class TagSettingAsyncListener implements AsyncListener {
private final AtomicBoolean activated;
private final ActiveSpan.Continuation cont;
private final ActiveSpan span;
public TagSettingAsyncListener(
final AtomicBoolean activated,
final ActiveSpan.Continuation cont,
final ActiveSpan span) {
this.activated = activated;
this.cont = cont;
this.span = span;
}
@Override
public void onComplete(final AsyncEvent event) throws IOException {
if (activated.compareAndSet(false, true)) {
try (ActiveSpan activeSpan = cont.activate()) {
ServletFilterSpanDecorator.STANDARD_TAGS.onResponse(
(HttpServletRequest) event.getSuppliedRequest(),
(HttpServletResponse) event.getSuppliedResponse(),
span);
}
}
}
@Override
public void onTimeout(final AsyncEvent event) throws IOException {
if (activated.compareAndSet(false, true)) {
try (ActiveSpan activeSpan = cont.activate()) {
ServletFilterSpanDecorator.STANDARD_TAGS.onTimeout(
(HttpServletRequest) event.getSuppliedRequest(),
(HttpServletResponse) event.getSuppliedResponse(),
event.getAsyncContext().getTimeout(),
span);
}
}
}
@Override
public void onError(final AsyncEvent event) throws IOException {
if (event.getThrowable() != null && activated.compareAndSet(false, true)) {
try (ActiveSpan activeSpan = cont.activate()) {
ServletFilterSpanDecorator.STANDARD_TAGS.onError(
(HttpServletRequest) event.getSuppliedRequest(),
(HttpServletResponse) event.getSuppliedResponse(),
event.getThrowable(),
span);
span.log(Collections.singletonMap("error.object", event.getThrowable()));
}
}
}
@Override
public void onStartAsync(final AsyncEvent event) throws IOException {}
}
}
}

View File

@ -1,5 +1,6 @@
package dd.inst.springweb; package dd.inst.springweb;
import static dd.trace.ClassLoaderHasClassWithFieldMatcher.classLoaderHasClassWithField;
import static net.bytebuddy.matcher.ElementMatchers.hasSuperType; import static net.bytebuddy.matcher.ElementMatchers.hasSuperType;
import static net.bytebuddy.matcher.ElementMatchers.isInterface; import static net.bytebuddy.matcher.ElementMatchers.isInterface;
import static net.bytebuddy.matcher.ElementMatchers.isMethod; import static net.bytebuddy.matcher.ElementMatchers.isMethod;
@ -31,7 +32,10 @@ public final class SpringWebInstrumentation implements Instrumenter {
return agentBuilder return agentBuilder
.type( .type(
not(isInterface()) not(isInterface())
.and(hasSuperType(named("org.springframework.web.servlet.HandlerAdapter")))) .and(hasSuperType(named("org.springframework.web.servlet.HandlerAdapter"))),
classLoaderHasClassWithField(
"org.springframework.web.servlet.HandlerMapping",
"BEST_MATCHING_PATTERN_ATTRIBUTE"))
.transform( .transform(
new AgentBuilder.Transformer.ForAdvice() new AgentBuilder.Transformer.ForAdvice()
.advice( .advice(

View File

@ -80,15 +80,3 @@ IF TRUE
DO DO
com.datadoghq.agent.InstrumentationRulesManager.registerClassLoad($0); com.datadoghq.agent.InstrumentationRulesManager.registerClassLoad($0);
ENDRULE ENDRULE
# Instrument Servlet
# ===========================
RULE HttpServlet-init
CLASS ^javax.servlet.http.HttpServlet
METHOD <init>
AT EXIT
IF TRUE
DO
com.datadoghq.agent.InstrumentationRulesManager.registerClassLoad($0);
ENDRULE

View File

@ -106,73 +106,3 @@ IF TRUE
DO DO
patch($0) patch($0)
ENDRULE ENDRULE
# Instrument Servlet 2
# ===========================
RULE HttpServlet-2.service-entry
CLASS ^javax.servlet.http.HttpServlet
METHOD service(HttpServletRequest, HttpServletResponse)
HELPER com.datadoghq.agent.integration.Servlet2Helper
COMPILE
AT ENTRY
IF TRUE
DO
onRequest($1, $2)
ENDRULE
RULE HttpServlet-2.service-exit
CLASS ^javax.servlet.http.HttpServlet
METHOD service(HttpServletRequest, HttpServletResponse)
HELPER com.datadoghq.agent.integration.Servlet2Helper
COMPILE
AT EXIT
IF TRUE
DO
onResponse($1, $2)
ENDRULE
RULE HttpServlet-2.service-error
CLASS ^javax.servlet.http.HttpServlet
METHOD service(HttpServletRequest, HttpServletResponse)
HELPER com.datadoghq.agent.integration.Servlet2Helper
COMPILE
AT EXCEPTION EXIT
IF TRUE
DO
onError($1, $2, $^)
ENDRULE
# Instrument Servlet 3
# ===========================
RULE HttpServlet-3.service-entry
CLASS ^javax.servlet.http.HttpServlet
METHOD service(HttpServletRequest, HttpServletResponse)
HELPER com.datadoghq.agent.integration.Servlet3Helper
COMPILE
AT ENTRY
IF TRUE
DO
onRequest($1, $2)
ENDRULE
RULE HttpServlet-3.service-exit
CLASS ^javax.servlet.http.HttpServlet
METHOD service(HttpServletRequest, HttpServletResponse)
HELPER com.datadoghq.agent.integration.Servlet3Helper
COMPILE
AT EXIT
IF TRUE
DO
onResponse($1, $2)
ENDRULE
RULE HttpServlet-3.service-error
CLASS ^javax.servlet.http.HttpServlet
METHOD service(HttpServletRequest, HttpServletResponse)
HELPER com.datadoghq.agent.integration.Servlet3Helper
COMPILE
AT EXCEPTION EXIT
IF TRUE
DO
onError($1, $2, $^)
ENDRULE

View File

@ -0,0 +1,32 @@
package dd.trace;
import net.bytebuddy.matcher.ElementMatcher;
public class ClassLoaderHasClassMatcher extends ElementMatcher.Junction.AbstractBase<ClassLoader> {
private final String[] names;
private ClassLoaderHasClassMatcher(final String... names) {
this.names = names;
}
public static ElementMatcher.Junction.AbstractBase<ClassLoader> classLoaderHasClasses(
final String... names) {
return new ClassLoaderHasClassMatcher(names);
}
@Override
public boolean matches(final ClassLoader target) {
try {
if (target != null) {
for (final String name : names) {
Class.forName(name, false, target);
}
return true;
}
return false;
} catch (final ClassNotFoundException e) {
return false;
}
}
}

View File

@ -0,0 +1,36 @@
package dd.trace;
import net.bytebuddy.matcher.ElementMatcher;
public class ClassLoaderHasClassWithFieldMatcher
extends ElementMatcher.Junction.AbstractBase<ClassLoader> {
private final String className;
private final String fieldName;
private ClassLoaderHasClassWithFieldMatcher(final String className, final String fieldName) {
this.className = className;
this.fieldName = fieldName;
}
public static AbstractBase<ClassLoader> classLoaderHasClassWithField(
final String className, final String fieldName) {
return new ClassLoaderHasClassWithFieldMatcher(className, fieldName);
}
@Override
public boolean matches(final ClassLoader target) {
try {
if (target != null) {
final Class<?> aClass = Class.forName(className, false, target);
aClass.getDeclaredField(fieldName);
return true;
}
return false;
} catch (final ClassNotFoundException e) {
return false;
} catch (final NoSuchFieldException e) {
return false;
}
}
}