Add tracing for other annotations and config

Allows for specifying additional traced methods via env var or sys prop config.
This commit is contained in:
Tyler Benson 2018-03-01 09:25:53 +10:00 committed by Tyler Benson
parent c1ac920e97
commit 441bf59e80
8 changed files with 283 additions and 90 deletions

View File

@ -44,7 +44,10 @@ def includeShadowJar(subproject, jarname) {
relocate 'datadog.trace.common', 'datadog.trace.agent.common'
relocate 'datadog.opentracing', 'datadog.trace.agent.ot'
relocate 'io.opentracing.contrib', 'datadog.trace.agent.opentracing.contrib'
relocate('io.opentracing.contrib', 'datadog.trace.agent.opentracing.contrib') {
// Don't want to change the annotation we're looking for.
exclude 'io.opentracing.contrib.dropwizard.Trace'
}
relocate 'org.yaml', 'datadog.trace.agent.deps.yaml'
relocate 'org.msgpack', 'datadog.trace.agent.deps.msgpack'

View File

@ -0,0 +1,48 @@
package datadog.trace.instrumentation.trace_annotation;
import static io.opentracing.log.Fields.ERROR_OBJECT;
import datadog.trace.api.Trace;
import io.opentracing.Scope;
import io.opentracing.Span;
import io.opentracing.tag.Tags;
import io.opentracing.util.GlobalTracer;
import java.lang.reflect.Method;
import java.util.Collections;
import net.bytebuddy.asm.Advice;
public class TraceAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static Scope startSpan(@Advice.Origin final Method method) {
final Trace trace = method.getAnnotation(Trace.class);
String operationName = trace == null ? null : trace.operationName();
if (operationName == null || operationName.isEmpty()) {
final Class<?> declaringClass = method.getDeclaringClass();
String className = declaringClass.getSimpleName();
if (className.isEmpty()) {
className = declaringClass.getName();
if (declaringClass.getPackage() != null) {
final String pkgName = declaringClass.getPackage().getName();
if (!pkgName.isEmpty()) {
className = declaringClass.getName().replace(pkgName, "").substring(1);
}
}
}
operationName = className + "." + method.getName();
}
return GlobalTracer.get().buildSpan(operationName).startActive(true);
}
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void stopSpan(
@Advice.Enter final Scope scope, @Advice.Thrown final Throwable throwable) {
if (throwable != null) {
final Span span = scope.span();
Tags.ERROR.set(span, true);
span.log(Collections.singletonMap(ERROR_OBJECT, throwable));
}
scope.close();
}
}

View File

@ -1,75 +1,51 @@
package datadog.trace.instrumentation.trace_annotation;
import static io.opentracing.log.Fields.ERROR_OBJECT;
import static net.bytebuddy.matcher.ElementMatchers.declaresMethod;
import static net.bytebuddy.matcher.ElementMatchers.failSafe;
import static net.bytebuddy.matcher.ElementMatchers.hasSuperType;
import static net.bytebuddy.matcher.ElementMatchers.is;
import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith;
import static net.bytebuddy.matcher.ElementMatchers.named;
import com.google.auto.service.AutoService;
import datadog.trace.agent.tooling.DDAdvice;
import datadog.trace.agent.tooling.DDTransformers;
import datadog.trace.agent.tooling.Instrumenter;
import datadog.trace.api.Trace;
import io.opentracing.Scope;
import io.opentracing.Span;
import io.opentracing.tag.Tags;
import io.opentracing.util.GlobalTracer;
import java.lang.reflect.Method;
import java.util.Collections;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.NamedElement;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
@AutoService(Instrumenter.class)
public final class TraceAnnotationInstrumentation extends Instrumenter.Configurable {
private static final String[] ADDITIONAL_ANNOTATIONS =
new String[] {
"com.newrelic.api.agent.Trace",
"kamon.annotation.Trace",
"com.tracelytics.api.ext.LogMethod",
"io.opentracing.contrib.dropwizard.Trace",
"org.springframework.cloud.sleuth.annotation.NewSpan"
};
public TraceAnnotationInstrumentation() {
super("trace", "trace-annotation");
}
@Override
public AgentBuilder apply(final AgentBuilder agentBuilder) {
ElementMatcher.Junction<NamedElement> methodTraceMatcher =
is(new TypeDescription.ForLoadedType(Trace.class));
for (final String annotationName : ADDITIONAL_ANNOTATIONS) {
methodTraceMatcher = methodTraceMatcher.or(named(annotationName));
}
return agentBuilder
.type(failSafe(hasSuperType(declaresMethod(isAnnotatedWith(Trace.class)))))
.type(failSafe(hasSuperType(declaresMethod(isAnnotatedWith(methodTraceMatcher)))))
.transform(DDTransformers.defaultTransformers())
.transform(
DDAdvice.create().advice(isAnnotatedWith(Trace.class), TraceAdvice.class.getName()))
DDAdvice.create()
.advice(isAnnotatedWith(methodTraceMatcher), TraceAdvice.class.getName()))
.asDecorator();
}
public static class TraceAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static Scope startSpan(@Advice.Origin final Method method) {
final Trace trace = method.getAnnotation(Trace.class);
String operationName = trace == null ? null : trace.operationName();
if (operationName == null || operationName.isEmpty()) {
final Class<?> declaringClass = method.getDeclaringClass();
String className = declaringClass.getSimpleName();
if (className.isEmpty()) {
className = declaringClass.getName();
if (declaringClass.getPackage() != null) {
final String pkgName = declaringClass.getPackage().getName();
if (!pkgName.isEmpty()) {
className = declaringClass.getName().replace(pkgName, "").substring(1);
}
}
}
operationName = className + "." + method.getName();
}
return GlobalTracer.get().buildSpan(operationName).startActive(true);
}
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void stopSpan(
@Advice.Enter final Scope scope, @Advice.Thrown final Throwable throwable) {
if (throwable != null) {
final Span span = scope.span();
Tags.ERROR.set(span, true);
span.log(Collections.singletonMap(ERROR_OBJECT, throwable));
}
scope.close();
}
}
}

View File

@ -0,0 +1,87 @@
package datadog.trace.instrumentation.trace_annotation;
import static net.bytebuddy.matcher.ElementMatchers.hasSuperType;
import static net.bytebuddy.matcher.ElementMatchers.named;
import com.google.auto.service.AutoService;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import datadog.trace.agent.tooling.DDAdvice;
import datadog.trace.agent.tooling.Instrumenter;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.matcher.ElementMatcher;
@Slf4j
@AutoService(Instrumenter.class)
public class TraceConfigInstrumentation extends Instrumenter.Configurable {
private static final String CONFIG_NAME = "dd.trace.methods";
private final Map<String, Set<String>> classMethodsToTrace;
public TraceConfigInstrumentation() {
super("trace", "trace-config");
final String configString = getPropOrEnv(CONFIG_NAME);
if (configString == null || configString.trim().isEmpty()) {
classMethodsToTrace = Collections.emptyMap();
} else if (!configString.matches(
"(?:([\\w.\\$]+)\\[((?:\\w+,)*(?:\\w+,?))\\];)*([\\w.\\$]+)\\[((?:\\w+,)*(?:\\w+,?))\\];?")) {
log.warn(
"Invalid config '{}'. Must match 'package.Class$Name[method1,method2];*'.", configString);
classMethodsToTrace = Collections.emptyMap();
} else {
final Map<String, Set<String>> toTrace = Maps.newHashMap();
final String[] classMethods = configString.split(";");
for (final String classMethod : classMethods) {
final String[] splitClassMethod = classMethod.split("\\[");
final String className = splitClassMethod[0];
final String methodNames =
splitClassMethod[1].substring(0, splitClassMethod[1].length() - 1);
final String[] splitMethodNames = methodNames.split(",");
toTrace.put(className, Sets.newHashSet(splitMethodNames));
}
classMethodsToTrace = Collections.unmodifiableMap(toTrace);
}
}
@Override
public AgentBuilder apply(final AgentBuilder agentBuilder) {
if (classMethodsToTrace.isEmpty()) {
return agentBuilder;
}
AgentBuilder builder = agentBuilder;
for (final Map.Entry<String, Set<String>> entry : classMethodsToTrace.entrySet()) {
ElementMatcher.Junction<MethodDescription> methodMatchers = null;
for (final String methodName : entry.getValue()) {
if (methodMatchers == null) {
methodMatchers = named(methodName);
} else {
methodMatchers = methodMatchers.or(named(methodName));
}
}
builder =
builder
.type(hasSuperType(named(entry.getKey())))
.transform(DDAdvice.create().advice(methodMatchers, TraceAdvice.class.getName()))
.asDecorator();
}
return builder;
}
private String getPropOrEnv(final String name) {
return System.getProperty(name, System.getenv(propToEnvName(name)));
}
static String propToEnvName(final String name) {
return name.toUpperCase().replace(".", "_");
}
}

View File

@ -1,12 +1,12 @@
import datadog.opentracing.DDSpan
import datadog.opentracing.decorators.ErrorFlag
import datadog.trace.agent.test.AgentTestRunner
import datadog.trace.api.Trace
import dd.test.trace.annotation.SayTracedHello
import org.assertj.core.api.Assertions
import java.util.concurrent.Callable
import static datadog.trace.agent.test.ListWriterAssert.assertTraces
class TraceAnnotationsTest extends AgentTestRunner {
def "test simple case annotations"() {
@ -14,34 +14,63 @@ class TraceAnnotationsTest extends AgentTestRunner {
// Test single span in new trace
SayTracedHello.sayHello()
Assertions.assertThat(TEST_WRITER.firstTrace().size()).isEqualTo(1)
Assertions.assertThat(TEST_WRITER.firstTrace().get(0).getOperationName())
.isEqualTo("SayTracedHello.sayHello")
Assertions.assertThat(TEST_WRITER.firstTrace().get(0).getServiceName()).isEqualTo("test")
expect:
assertTraces(TEST_WRITER, 1) {
trace(0, 1) {
span(0) {
serviceName "test"
resourceName "SayTracedHello.sayHello"
operationName "SayTracedHello.sayHello"
parent()
errored false
tags {
defaultTags()
}
}
}
}
}
def "test complex case annotations"() {
setup:
when:
// Test new trace with 2 children spans
SayTracedHello.sayHELLOsayHA()
Assertions.assertThat(TEST_WRITER.firstTrace().size()).isEqualTo(3)
final long parentId = TEST_WRITER.firstTrace().get(0).context().getSpanId()
Assertions.assertThat(TEST_WRITER.firstTrace().get(0).getOperationName()).isEqualTo("NEW_TRACE")
Assertions.assertThat(TEST_WRITER.firstTrace().get(0).getParentId())
.isEqualTo(0) // ROOT / no parent
Assertions.assertThat(TEST_WRITER.firstTrace().get(0).context().getParentId()).isEqualTo(0)
Assertions.assertThat(TEST_WRITER.firstTrace().get(0).getServiceName()).isEqualTo("test2")
Assertions.assertThat(TEST_WRITER.firstTrace().get(1).getOperationName()).isEqualTo("SAY_HA")
Assertions.assertThat(TEST_WRITER.firstTrace().get(1).getParentId()).isEqualTo(parentId)
Assertions.assertThat(TEST_WRITER.firstTrace().get(1).context().getSpanType()).isEqualTo("DB")
Assertions.assertThat(TEST_WRITER.firstTrace().get(2).getOperationName())
.isEqualTo("SayTracedHello.sayHello")
Assertions.assertThat(TEST_WRITER.firstTrace().get(2).getServiceName()).isEqualTo("test")
Assertions.assertThat(TEST_WRITER.firstTrace().get(2).getParentId()).isEqualTo(parentId)
then:
assertTraces(TEST_WRITER, 1) {
trace(0, 3) {
span(0) {
resourceName "NEW_TRACE"
operationName "NEW_TRACE"
parent()
errored false
tags {
defaultTags()
}
}
span(1) {
resourceName "SAY_HA"
operationName "SAY_HA"
spanType "DB"
childOf span(0)
errored false
tags {
"span.type" "DB"
defaultTags()
}
}
span(2) {
serviceName "test"
resourceName "SayTracedHello.sayHello"
operationName "SayTracedHello.sayHello"
childOf span(0)
errored false
tags {
defaultTags()
}
}
}
}
}
def "test exception exit"() {
@ -56,16 +85,20 @@ class TraceAnnotationsTest extends AgentTestRunner {
error = ex
}
final StringWriter errorString = new StringWriter()
error.printStackTrace(new PrintWriter(errorString))
final DDSpan span = TEST_WRITER.firstTrace().get(0)
Assertions.assertThat(span.getOperationName()).isEqualTo("ERROR")
Assertions.assertThat(span.getTags().get("error")).isEqualTo(true)
Assertions.assertThat(span.getTags().get("error.msg")).isEqualTo(error.getMessage())
Assertions.assertThat(span.getTags().get("error.type")).isEqualTo(error.getClass().getName())
Assertions.assertThat(span.getTags().get("error.stack")).isEqualTo(errorString.toString())
Assertions.assertThat(span.getError()).isEqualTo(1)
expect:
assertTraces(TEST_WRITER, 1) {
trace(0, 1) {
span(0) {
resourceName "ERROR"
operationName "ERROR"
errored true
tags {
errorTags(error.class)
defaultTags()
}
}
}
}
}
def "test annonymous class annotations"() {
@ -73,10 +106,15 @@ class TraceAnnotationsTest extends AgentTestRunner {
// Test anonymous classes with package.
SayTracedHello.fromCallable()
Assertions.assertThat(TEST_WRITER.size()).isEqualTo(1)
Assertions.assertThat(TEST_WRITER.firstTrace().size()).isEqualTo(1)
Assertions.assertThat(TEST_WRITER.firstTrace().get(0).getOperationName())
.isEqualTo("SayTracedHello\$1.call")
expect:
assertTraces(TEST_WRITER, 1) {
trace(0, 1) {
span(0) {
resourceName "SayTracedHello\$1.call"
operationName "SayTracedHello\$1.call"
}
}
}
when:
// Test anonymous classes with no package.
@ -90,9 +128,48 @@ class TraceAnnotationsTest extends AgentTestRunner {
TEST_WRITER.waitForTraces(2)
then:
Assertions.assertThat(TEST_WRITER.size()).isEqualTo(2)
Assertions.assertThat(TEST_WRITER.get(1).size()).isEqualTo(1)
Assertions.assertThat(TEST_WRITER.get(1).get(0).getOperationName())
.isEqualTo("TraceAnnotationsTest\$1.call")
assertTraces(TEST_WRITER, 2) {
trace(0, 1) {
span(0) {
resourceName "SayTracedHello\$1.call"
operationName "SayTracedHello\$1.call"
}
trace(1, 1) {
span(0) {
resourceName "TraceAnnotationsTest\$1.call"
operationName "TraceAnnotationsTest\$1.call"
}
}
}
}
}
def "test configuration based trace"() {
expect:
new ConfigTracedCallable().call() == "Hello!"
when:
TEST_WRITER.waitForTraces(1)
then:
assertTraces(TEST_WRITER, 1) {
trace(0, 1) {
span(0) {
resourceName "ConfigTracedCallable.call"
operationName "ConfigTracedCallable.call"
}
}
}
}
static {
System.setProperty("dd.trace.methods", "package.ClassName[method1,method2];${ConfigTracedCallable.name}[call]")
}
class ConfigTracedCallable implements Callable<String> {
@Override
String call() throws Exception {
return "Hello!"
}
}
}

View File

@ -37,7 +37,7 @@ public class SayTracedHello {
public static String fromCallable() throws Exception {
return new Callable<String>() {
@Trace
@com.newrelic.api.agent.Trace
@Override
public String call() throws Exception {
return "Howdy!";

View File

@ -10,4 +10,5 @@ dependencies {
implementation deps.autoservice
testCompile project(':dd-java-agent:testing')
testCompile group: 'com.newrelic.agent.java', name: 'newrelic-api', version: '+'
}

View File

@ -43,6 +43,7 @@ class TagsAssert {
if (args.length > 1) {
throw new IllegalArgumentException(args)
}
assertedTags.add(name)
assert tags[name] == args[0]
}