Skip to content

Commit c9da16f

Browse files
committed
Add resource naming instrumentation for jax-rs
1 parent 859b93b commit c9da16f

File tree

15 files changed

+363
-16
lines changed

15 files changed

+363
-16
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
apply plugin: 'version-scan'
2+
3+
versionScan {
4+
group = "javax.ws.rs"
5+
module = "jsr311-api"
6+
versions = "(,)"
7+
}
8+
9+
apply from: "${rootDir}/gradle/java.gradle"
10+
11+
dependencies {
12+
compileOnly group: 'javax.ws.rs', name: 'jsr311-api', version: '1.1.1'
13+
14+
compile deps.bytebuddy
15+
compile deps.opentracing
16+
compile deps.autoservice
17+
18+
compile project(':dd-trace-ot')
19+
compile project(':dd-java-agent:tooling')
20+
21+
testCompile project(':dd-java-agent:testing')
22+
testCompile group: 'com.sun.jersey', name: 'jersey-core', version: '1.19.4'
23+
testCompile group: 'com.sun.jersey', name: 'jersey-servlet', version: '1.19.4'
24+
testCompile group: 'io.dropwizard', name: 'dropwizard-testing', version: '0.7.1'
25+
testCompile group: 'javax.xml.bind', name: 'jaxb-api', version: '2.2.3'
26+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package datadog.trace.instrumentation.jaxrs;
2+
3+
import static net.bytebuddy.matcher.ElementMatchers.declaresMethod;
4+
import static net.bytebuddy.matcher.ElementMatchers.hasSuperType;
5+
import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith;
6+
import static net.bytebuddy.matcher.ElementMatchers.named;
7+
8+
import com.google.auto.service.AutoService;
9+
import datadog.trace.agent.tooling.DDAdvice;
10+
import datadog.trace.agent.tooling.Instrumenter;
11+
import datadog.trace.api.DDTags;
12+
import io.opentracing.Scope;
13+
import io.opentracing.tag.Tags;
14+
import io.opentracing.util.GlobalTracer;
15+
import java.lang.annotation.Annotation;
16+
import java.lang.reflect.Method;
17+
import java.util.LinkedList;
18+
import javax.ws.rs.HttpMethod;
19+
import javax.ws.rs.Path;
20+
import net.bytebuddy.agent.builder.AgentBuilder;
21+
import net.bytebuddy.asm.Advice;
22+
23+
@AutoService(Instrumenter.class)
24+
public final class JaxRsInstrumentation extends Instrumenter.Configurable {
25+
26+
public JaxRsInstrumentation() {
27+
super("jax-rs", "jaxrs");
28+
}
29+
30+
@Override
31+
protected boolean defaultEnabled() {
32+
return false;
33+
}
34+
35+
@Override
36+
protected AgentBuilder apply(final AgentBuilder agentBuilder) {
37+
return agentBuilder
38+
.type(
39+
hasSuperType(
40+
isAnnotatedWith(named("javax.ws.rs.Path"))
41+
.or(hasSuperType(declaresMethod(isAnnotatedWith(named("javax.ws.rs.Path")))))))
42+
.transform(
43+
DDAdvice.create()
44+
.advice(
45+
isAnnotatedWith(
46+
named("javax.ws.rs.Path")
47+
.or(named("javax.ws.rs.DELETE"))
48+
.or(named("javax.ws.rs.GET"))
49+
.or(named("javax.ws.rs.HEAD"))
50+
.or(named("javax.ws.rs.OPTIONS"))
51+
.or(named("javax.ws.rs.POST"))
52+
.or(named("javax.ws.rs.PUT"))),
53+
JaxRsAdvice.class.getName()))
54+
.asDecorator();
55+
}
56+
57+
public static class JaxRsAdvice {
58+
59+
@Advice.OnMethodEnter(suppress = Throwable.class)
60+
public static void nameSpan(@Advice.This final Object obj, @Advice.Origin final Method method) {
61+
// TODO: do we need caching for this?
62+
63+
final LinkedList<Path> classPaths = new LinkedList<>();
64+
Class<?> target = obj.getClass();
65+
while (target != Object.class) {
66+
final Path annotation = target.getAnnotation(Path.class);
67+
if (annotation != null) {
68+
classPaths.push(annotation);
69+
}
70+
target = target.getSuperclass();
71+
}
72+
final Path methodPath = method.getAnnotation(Path.class);
73+
String httpMethod = null;
74+
for (final Annotation ann : method.getDeclaredAnnotations()) {
75+
if (ann.annotationType().getAnnotation(HttpMethod.class) != null) {
76+
httpMethod = ann.annotationType().getSimpleName();
77+
}
78+
}
79+
80+
final StringBuilder resourceNameBuilder = new StringBuilder();
81+
if (httpMethod != null) {
82+
resourceNameBuilder.append(httpMethod);
83+
resourceNameBuilder.append(" ");
84+
}
85+
for (final Path classPath : classPaths) {
86+
resourceNameBuilder.append(classPath.value());
87+
}
88+
if (methodPath != null) {
89+
resourceNameBuilder.append(methodPath.value());
90+
}
91+
final String resourceName = resourceNameBuilder.toString().trim();
92+
93+
final Scope scope = GlobalTracer.get().scopeManager().active();
94+
if (scope != null && !resourceName.isEmpty()) {
95+
scope.span().setTag(DDTags.RESOURCE_NAME, resourceName);
96+
Tags.COMPONENT.set(scope.span(), "jax-rs");
97+
}
98+
}
99+
}
100+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import datadog.opentracing.DDSpanContext
2+
import datadog.trace.agent.test.AgentTestRunner
3+
import io.opentracing.util.GlobalTracer
4+
import spock.lang.Unroll
5+
6+
import javax.ws.rs.*
7+
8+
class JaxRsInstrumentationTest extends AgentTestRunner {
9+
10+
static {
11+
System.setProperty("dd.integration.jax-rs.enabled", "true")
12+
}
13+
14+
@Unroll
15+
def "span named '#resourceName' from annotations on class"() {
16+
setup:
17+
def scope = GlobalTracer.get().buildSpan("test").startActive(false)
18+
DDSpanContext spanContext = scope.span().context()
19+
obj.call()
20+
21+
expect:
22+
spanContext.resourceName == resourceName
23+
24+
cleanup:
25+
scope.close()
26+
27+
where:
28+
resourceName | obj
29+
"test" | new Jax() {
30+
// invalid because no annotations
31+
void call() {}
32+
}
33+
"/a" | new Jax() {
34+
@Path("/a")
35+
void call() {}
36+
}
37+
"GET /b" | new Jax() {
38+
@GET
39+
@Path("/b")
40+
void call() {}
41+
}
42+
"test" | new InterfaceWithPath() {
43+
// invalid because no annotations
44+
void call() {}
45+
}
46+
"POST /c" | new InterfaceWithPath() {
47+
@POST
48+
@Path("/c")
49+
void call() {}
50+
}
51+
"HEAD" | new InterfaceWithPath() {
52+
@HEAD
53+
void call() {}
54+
}
55+
"test" | new AbstractClassWithPath() {
56+
// invalid because no annotations
57+
void call() {}
58+
}
59+
"POST /abstract/d" | new AbstractClassWithPath() {
60+
@POST
61+
@Path("/d")
62+
void call() {}
63+
}
64+
"PUT /abstract" | new AbstractClassWithPath() {
65+
@PUT
66+
void call() {}
67+
}
68+
"test" | new ChildClassWithPath() {
69+
// invalid because no annotations
70+
void call() {}
71+
}
72+
"OPTIONS /abstract/child/e" | new ChildClassWithPath() {
73+
@OPTIONS
74+
@Path("/e")
75+
void call() {}
76+
}
77+
"DELETE /abstract/child" | new ChildClassWithPath() {
78+
@DELETE
79+
void call() {}
80+
}
81+
"POST /abstract/child" | new ChildClassWithPath()
82+
}
83+
84+
interface Jax {
85+
void call()
86+
}
87+
88+
@Path("/interface")
89+
interface InterfaceWithPath extends Jax {
90+
@GET
91+
void call()
92+
}
93+
94+
@Path("/abstract")
95+
abstract class AbstractClassWithPath implements Jax {
96+
@PUT
97+
abstract void call()
98+
}
99+
100+
@Path("/child")
101+
class ChildClassWithPath extends AbstractClassWithPath {
102+
@POST
103+
void call() {}
104+
}
105+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import datadog.trace.agent.test.AgentTestRunner
2+
import io.dropwizard.testing.junit.ResourceTestRule
3+
import org.junit.ClassRule
4+
import spock.lang.Shared
5+
6+
class JerseyTest extends AgentTestRunner {
7+
8+
static {
9+
System.setProperty("dd.integration.jax-rs.enabled", "true")
10+
}
11+
12+
@Shared
13+
@ClassRule
14+
ResourceTestRule resources = ResourceTestRule.builder().addResource(new TestResource()).build()
15+
16+
def "test resource"() {
17+
setup:
18+
// start a trace because the test doesn't go through any servlet or other instrumentation.
19+
def scope = TEST_TRACER.buildSpan("test.span").startActive(true)
20+
def response = resources.client().resource("/test/hello/bob").post(String)
21+
scope.close()
22+
23+
expect:
24+
response == "Hello bob!"
25+
TEST_WRITER.waitForTraces(1)
26+
TEST_WRITER.size() == 1
27+
28+
def trace = TEST_WRITER.firstTrace()
29+
def span = trace[0]
30+
span.resourceName == "POST /test/hello/{name}"
31+
span.tags["component"] == "jax-rs"
32+
}
33+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import javax.ws.rs.POST;
2+
import javax.ws.rs.Path;
3+
import javax.ws.rs.PathParam;
4+
5+
// Originally had this as a groovy class but was getting some weird errors.
6+
@Path("/test")
7+
public class TestResource {
8+
@POST
9+
@Path("/hello/{name}")
10+
public String addBook(@PathParam("name") final String name) {
11+
return "Hello " + name + "!";
12+
}
13+
}

dd-java-agent/instrumentation/kafka-clients-0.11/kafka-clients-0.11.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,5 @@ dependencies {
2525
testCompile group: 'org.apache.kafka', name: 'kafka-clients', version: '0.11.0.0'
2626
testCompile group: 'org.springframework.kafka', name: 'spring-kafka', version: '1.3.3.RELEASE'
2727
testCompile group: 'org.springframework.kafka', name: 'spring-kafka-test', version: '1.3.3.RELEASE'
28-
testCompile group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.0'
28+
testCompile group: 'javax.xml.bind', name: 'jaxb-api', version: '2.2.3'
2929
}

dd-java-agent/instrumentation/kafka-clients-0.11/src/test/groovy/KafkaClientTest.groovy

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
import ch.qos.logback.classic.Level
2-
import ch.qos.logback.classic.Logger
31
import datadog.trace.agent.test.AgentTestRunner
42
import org.apache.kafka.clients.consumer.ConsumerRecord
53
import org.junit.ClassRule
6-
import org.slf4j.LoggerFactory
74
import org.springframework.kafka.core.DefaultKafkaConsumerFactory
85
import org.springframework.kafka.core.DefaultKafkaProducerFactory
96
import org.springframework.kafka.core.KafkaTemplate
@@ -22,8 +19,6 @@ class KafkaClientTest extends AgentTestRunner {
2219
static final SHARED_TOPIC = "shared.topic"
2320

2421
static {
25-
((Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME)).setLevel(Level.WARN)
26-
((Logger) LoggerFactory.getLogger("datadog")).setLevel(Level.DEBUG)
2722
System.setProperty("dd.integration.kafka.enabled", "true")
2823
}
2924

dd-java-agent/instrumentation/kafka-streams-0.11/kafka-streams-0.11.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,5 @@ dependencies {
2929
testCompile group: 'org.apache.kafka', name: 'kafka-streams', version: '0.11.0.0'
3030
testCompile group: 'org.springframework.kafka', name: 'spring-kafka', version: '1.3.3.RELEASE'
3131
testCompile group: 'org.springframework.kafka', name: 'spring-kafka-test', version: '1.3.3.RELEASE'
32-
testCompile group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.0'
32+
testCompile group: 'javax.xml.bind', name: 'jaxb-api', version: '2.2.3'
3333
}

dd-java-agent/instrumentation/kafka-streams-0.11/src/test/groovy/KafkaStreamsTest.groovy

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import ch.qos.logback.classic.Level
22
import ch.qos.logback.classic.Logger
33
import datadog.trace.agent.test.AgentTestRunner
4-
import io.opentracing.util.GlobalTracer
54
import org.apache.kafka.clients.consumer.ConsumerRecord
65
import org.apache.kafka.common.serialization.Serdes
76
import org.apache.kafka.streams.KafkaStreams
@@ -61,7 +60,7 @@ class KafkaStreamsTest extends AgentTestRunner {
6160
@Override
6261
void onMessage(ConsumerRecord<String, String> record) {
6362
WRITER_PHASER.arriveAndAwaitAdvance() // ensure consistent ordering of traces
64-
GlobalTracer.get().activeSpan().setTag("testing", 123)
63+
TEST_TRACER.activeSpan().setTag("testing", 123)
6564
records.add(record)
6665
}
6766
})
@@ -80,7 +79,7 @@ class KafkaStreamsTest extends AgentTestRunner {
8079
@Override
8180
String apply(String textLine) {
8281
WRITER_PHASER.arriveAndAwaitAdvance() // ensure consistent ordering of traces
83-
GlobalTracer.get().activeSpan().setTag("asdf", "testing")
82+
TEST_TRACER.activeSpan().setTag("asdf", "testing")
8483
return textLine.toLowerCase()
8584
}
8685
})

dd-java-agent/src/test/resources/logback.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@
1313
<appender-ref ref="console"/>
1414
</root>
1515

16-
<logger name="com.datadoghq" level="debug"/>
16+
<logger name="datadog" level="debug"/>
1717

1818
</configuration>

0 commit comments

Comments
 (0)