Around 120 integrations consisting of about 200 instrumentations are currently provided with the Datadog Java Trace
Agent. An auto-instrumentation allows compiled Java applications to be instrumented at runtime by a Java agent. This
happens when compiled classes matching rules defined in the instrumentation undergo bytecode manipulation to accomplish
some of what could be done by a developer instrumenting the code manually. Instrumentations are maintained
in /dd-java-agent/instrumentation/
Instrumentations are in the directory:
/dd-java-agent/instrumentation/$framework/$framework-$minVersion
where $framework
is the framework name, and $minVersion
is the minimum version of the framework supported by the
instrumentation. For example:
$ tree dd-java-agent/instrumentation/couchbase -L 2
dd-java-agent/instrumentation/couchbase
├── build.gradle
├── couchbase-2.0
│ ├── build.gradle
│ └── src
├── couchbase-2.6
│ ├── build.gradle
│ └── src
├── couchbase-3.1
│ ├── build.gradle
│ └── src
└── couchbase-3.2
├── build.gradle
└── src
In some cases, such as Hibernate, there is a submodule containing different version-specific instrumentations, but typically a version-specific module is enough when there is only one instrumentation implemented ( e.g. Akka-HTTP)
Instrumentations included when building the Datadog java trace agent are defined
in /settings.gradle
in alphabetical order with
the other instrumentations in this format:
include ':dd-java-agent:instrumentation:$framework?:$framework-$minVersion'
Dependencies specific to a particular instrumentation are added to the build.gradle
file in that instrumentation’s
directory. Add necessary dependencies as compileOnly
so they do not leak into the tracer.
Muzzle directives are applied at build time from the build.gradle
file. OpenTelemetry provides
some Muzzle documentation.
Muzzle directives check for a range of framework versions that are safe to load the instrumentation.
See this excerpt as an example from rediscala
muzzle {
pass {
group = "com.github.etaty"
module = "rediscala_2.11"
versions = "[1.5.0,)"
assertInverse = true
}
pass {
group = "com.github.etaty"
module = "rediscala_2.12"
versions = "[1.8.0,)"
assertInverse = true
}
}
This means that the instrumentation should be safe with rediscala_2.11
from version 1.5.0
and all later versions,
but should fail (and so will not be loaded), for older versions (see assertInverse
).
A similar range of versions is specified for rediscala_2.12
.
When the agent is built, the muzzle plugin will download versions of the framework and check these directives hold.
To run muzzle on your instrumentation, run:
./gradlew :dd-java-agent:instrumentation:rediscala-1.8.0:muzzle
⚠️ Muzzle does not run tests. It checks that the types and methods used by the instrumentation are present in particular versions of libraries. It can be subverted withMethodHandle
and reflection, so muzzle passing is not the end of the story.
By default, all the muzzle directives are checked against all the instrumentations included in a module.
However, there can be situations in which it’s only needed to check one specific directive on an instrumentation.
At this point the instrumentation should override the method muzzleDirective()
by returning the name of the directive to execute.
The Instrumentation class is where the Instrumentation begins. It will:
- Use Matchers to choose target types (i.e., classes)
- From only those target types, use Matchers to select the members (i.e., methods) to instrument.
- Apply instrumentation code from an Advice class to those members.
Instrumentation classes:
- Must be annotated with
@AutoService(InstrumenterModule.class)
- Should extend one of the six abstract TargetSystem
InstrumenterModule
classes - Should implement one of the
Instrumenter
interfaces
For example:
import datadog.trace.agent.tooling.Instrumenter;
import datadog.trace.agent.tooling.InstrumenterModule;
@AutoService(InstrumenterModule.class)
public class RabbitChannelInstrumentation extends InstrumenterModule.Tracing
implements Instrumenter.ForTypeHierarchy {/* */
}
TargetSystem | Usage |
InstrumenterModule. Tracing |
An Instrumentation class should extend an appropriate provided TargetSystem class when possible. |
InstrumenterModule. Profiling |
|
InstrumenterModule. AppSec |
|
InstrumenterModule. Iast |
|
InstrumenterModule. CiVisibility |
|
InstrumenterModule. Usm |
|
InstrumenterModule |
Avoid extending InstrumenterModule directly. When no other TargetGroup is applicable we generally default to InstrumenterModule.Tracing |
Instrumentation classes should implement an appropriate Instrumenter interface that specifies how target types will be selected for instrumentation.
Instrumenter Interface | Method(s) | Usage(Example) |
ForSingleType |
String instrumentedType() |
Instruments only a single class name known at compile time.(see Json2FactoryInstrumentation) |
ForKnownTypes |
String[] knownMatchingTypes() |
Instruments multiple class names known at compile time. |
ForTypeHierarchy |
String hierarchyMarkerType()``ElementMatcher<TypeDescription> hierarchyMatcher() |
Composes more complex matchers using chained HierarchyMatchers methods. The hierarchyMarkerType() method should return a type name. Classloaders without this type can skip the more expensive hierarchyMatcher() method. (see HttpClientInstrumentation) |
ForConfiguredType |
Collection<String> configuredMatchingTypes() |
Do not implement this interface_._Use ForKnownType instead. ForConfiguredType is only used for last minute additions in the field - such as when a customer has a new JDBC driver that's not in the allowed list and we need to test it and provide a workaround until the next release. |
ForConfiguredTypes |
String configuredMatchingType(); |
Do not implement this interface. __Like ForConfiguredType, for multiple classes |
When matching your instrumentation against target types, prefer ForSingleType or ForKnownTypes over more expensive ForTypeHierarchy matching.
Consider adding an appropriate ClassLoaderMatcher so the Instrumentation only activates when that class is loaded. For example:
@Override
public ElementMatcher<ClassLoader> classLoaderMatcher() {
return hasClassNamed("java.net.http.HttpClient");
}
The Instrumenter.ForBootstrap
interface is a hint that this instrumenter works on bootstrap types and there is no
classloader present to interrogate. Use it when instrumenting something from the JDK that will be on the bootstrap
classpath. For
example, ShutdownInstrumentation
or UrlInstrumentation
.
Note
Without classloader available, helper classes for bootstrap instrumentation must be place into the
:dd-java-agent:agent-bootstrap
module rather than loaded using the default mechanism.
After the type is selected, the type’s target members(e.g., methods) must next be selected using the Instrumentation
class’s adviceTransformations()
method.
ByteBuddy’s ElementMatchers
are used to describe the target members to be instrumented.
Datadog’s DDElementMatchers
class also provides these 10 additional matchers:
- implementsInterface
- hasInterface
- hasSuperType
- declaresMethod
- extendsClass
- concreteClass
- declaresField
- declaresContextField
- declaresAnnotation
- hasSuperMethod
Here, any public execute()
method taking no arguments will have PreparedStatementAdvice
applied:
@Override
public void adviceTransformations(AdviceTransformation transformation) {
transformation.applyAdvice(
nameStartsWith("execute")
.and(takesArguments(0))
.and(isPublic()),
getClass().getName() + "$PreparedStatementAdvice"
);
}
Here, any matching connect()
method will have DriverAdvice
applied:
@Override
public void adviceTransformations(AdviceTransformation transformation) {
transformation.applyAdvice(
nameStartsWith("connect")
.and(takesArgument(0, String.class))
.and(takesArgument(1, Properties.class))
.and(returns(named("java.sql.Connection"))),
getClass().getName() + "$DriverAdvice");
}
Be precise in matching to avoid inadvertently instrumenting something unintended in a current or future version of the target class. Having multiple precise matchers is preferable to one more vague catch-all matcher which leaves some method characteristics undefined.
Instrumentation class names should end in Instrumentation.
Classes referenced by Advice that are not provided on the bootclasspath must be defined in Helper Classes otherwise they will not be loaded at runtime. This included any decorators, extractors/injectors or wrapping classes such as tracing listeners that extend or implement types provided by the library being instrumented. Also watch out for implicit types such as anonymous/nested classes because they must be listed alongside the main helper class.
If an instrumentation is producing no results it may be that a required class is missing. Running muzzle
./gradlew muzzle
can quickly tell you if you missed a required helper class. Messages like this in debug logs also indicate that classes are missing:
[MSC service thread 1-3] DEBUG datadog.trace.agent.tooling.muzzle.MuzzleCheck - Muzzled mismatch - instrumentation.names=[jakarta-mdb] instrumentation.class=datadog.trace.instrumentation.jakarta.jms.MDBMessageConsumerInstrumentation instrumentation.target.classloader=ModuleClassLoader for Module "deployment.cmt.war" from Service Module Loader muzzle.mismatch="datadog.trace.instrumentation.jakarta.jms.MessageExtractAdapter:20 Missing class datadog.trace.instrumentation.jakarta.jms.MessageExtractAdapter$1"
The missing class must be added in the helperClassNames method, for example:
@Override
public String[] helperClassNames() {
return new String[]{
"datadog.trace.instrumentation.jakarta.jms.MessageExtractAdapter",
"datadog.trace.instrumentation.jakarta.jms.JMSDecorator",
"datadog.trace.instrumentation.jakarta.jms.MessageExtractAdapter$1"
};
}
Use care when deciding to include enums in your Advice and Decorator classes because each element of the enum will need
to be added to the helper classes individually. For example not just MyDecorator.MyEnum
but
also MyDecorator.MyEnum$1, MyDecorator.MyEnum$2
, etc.
Decorators contain extra code that will be injected into the instrumented methods.
These provided Decorator classes sit in dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator
Decorator | Parent Class | Usage(see JavaDoc for more detail) |
AppSecUserEventDecorator |
- |
Provides mostly login-related functions to the Spring Security instrumentation. |
AsyncResultDecorator |
BaseDecorator |
Handles asynchronous result types, finishing spans only when the async calls are complete. |
BaseDecorator |
- |
Provides many convenience methods related to span naming and error handling. New Decorators should extend BaseDecorator or one of its child classes. |
ClientDecorator |
BaseDecorator |
Parent of many Client Decorators. Used to set client specific tags, serviceName, etc |
DBTypeProcessingDatabaseClientDecorator |
DatabaseClientDecorator |
Adds automatic processDatabaseType() call to DatabaseClientDecorator. |
DatabaseClientDecorator |
ClientDecorator |
Provides general db-related methods. |
HttpClientDecorator |
UriBasedClientDecorator |
Mostly adds span tags to HTTP client requests and responses. |
HttpServerDecorator |
ServerDecorator |
Adds connection and HTTP response tagging often used for server frameworks. |
MessagingClientDecorator |
ClientDecorator |
Adds e2e (end-to-end) duration monitoring. |
OrmClientDecorator |
DatabaseClientDecorator |
Set the span’s resourceName to the entityName value. |
ServerDecorator |
BaseDecorator |
Adding server and language tags to the span. |
UriBasedClientDecorator |
ClientDecorator |
Adds hostname, port and service values from URIs to HttpClient spans. |
UrlConnectionDecorator |
UriBasedClientDecorator |
Sets some tags based on URI and URL values. Also provides some caching. Only used by UrlInstrumentation . |
Instrumentations often include their own Decorators which extend those classes, for example:
Instrumentation | Decorator | Parent Class |
JDBC | DataSourceDecorator |
BaseDecorator |
RabbitMQ | RabbitDecorator |
MessagingClientDecorator |
All HTTP Server frameworks | various | HttpServerDecorator |
Decorator class names must be in the instrumentation's helper classes since Decorators need to be loaded with the instrumentation.
Decorator class names should end in Decorator.
Byte Buddy injects compiled bytecode at runtime to wrap existing methods, so they communicate with Datadog at entry or exit. These modifications are referred to as advice transformation or just advice.
Instrumenters register advice transformations by calling
AdviceTransformation.applyAdvice(ElementMatcher, String)
and Methods are matched by the
instrumentation's adviceTransformations()
method.
The Advice is injected into the type so Advice can only refer to those classes on the bootstrap class-path or helpers injected into the application class-loader. Advice must not refer to any methods in the instrumentation class or even other methods in the same advice class because the advice is really only a template of bytecode to be inserted into the target class. It is only the advice bytecode (plus helpers) that is copied over. The rest of the instrumenter and advice class is ignored. Do not place code in the Advice constructor because the constructor is never called.
You can not use methods like InstrumentationContext.get()
outside of the instrumentation advice because the tracer
currently patches the method stub with the real call at runtime. But you can pass the ContextStore into a
helper/decorator like
in DatadogMessageListener.
This could reduce duplication if you re-used the helper. But unlike most applications, some duplication can be the
better choice in the tracer if it simplifies things and reduces overhead. You might end up with very similar code
scattered around, but it will be simple to maintain. Trying to find an abstraction that works well across
instrumentations can take time and may introduce extra indirection.
Advice classes provide the code to be executed before and/or after a matched method. The classes use a static method
annotated by @Advice.OnMethodEnter
and/or @Advice.OnMethodExit
to provide the code. The method name is irrelevant.
A method that is annotated with @Advice.OnMethodEnter
can annotate its parameters
with @Advice.Argument
. @Advice.Argument
will substitute this parameter with the corresponding argument of the
instrumented method. This allows the @Advice.OnMethodEnter
code to see and modify the parameters that would be passed
to the target method.
Alternatively, a parameter can be annotated by Advice.This
where the this
reference of the instrumented method is
assigned to the new parameter. This can also be used to assign a new value to the this
reference of an instrumented
method.
If no annotation is used on a parameter, it is assigned the n-th parameter of the instrumented method for the n-th parameter of the advice method. Explicitly specifying which parameter is intended is recommended to be more clear, for example:
@Advice.Argument(0) final HttpUriRequest request
All parameters must declare the exact same type as the parameters of the instrumented type or the method's declaring
type for Advice.This
. If they are marked as read-only, then the parameter type may be a super type of the original.
A method that is annotated with Advice.OnMethodExit
can also annotate its parameters with Advice.Argument
and Advice.This
. It can also annotate a parameter with Advice.Return
to receive the original method's return value.
By reassigning the return value, it can replace the returned value. If an instrumented method does not return a value,
this annotation must not be used. If a method throws an exception, the parameter is set to its default value (0 for
primitive types and to null for reference types). The parameter's type must equal the instrumented method's return type
if it is not set to read-only. If the parameter is read-only it may be a super type of the instrumented method's return
type.
Advice class names should end in Advice.
Advice methods are typically annotated like
@Advice.OnMethodEnter(suppress = Throwable.class)
and
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
Using suppress = Throwable.class
is considered our default for both unless there is a reason not to. It means the
exception handler is triggered on any exception thrown within the Advice and the Advice method terminates. The opposite
would be either no suppress
annotation or equivalently suppress = NoExceptionHandler.class
which would both allow
exceptions in Advice code to surface and is usually undesirable.
If
the Advice.OnMethodEnter
method throws an exception,
the Advice.OnMethodExit
method is not invoked.
The Advice.Thrown
annotation passes any thrown exception from the instrumented method to
the Advice.OnMethodExit
advice
method. Advice.Thrown
****
should annotate at most one parameter on the exit advice.
If the instrumented method throws an exception, the Advice.OnMethodExit method is still invoked unless the Advice.OnMethodExit.onThrowable() property is set to false. If this property is set to false, the Advice.Thrown annotation must not be used on any parameter.
If an instrumented method throws an exception, the return parameter is set to its default of 0 for primitive types or null for reference types. An exception can be read by annotating an exit method’s Throwable parameter with Advice.Thrown which is assigned the thrown Throwable or null if a method returns normally. This allows exchanging a thrown exception with any checked or unchecked exception. For example, either the result or the exception will be passed to the helper method here:
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void methodExit(
@Advice.Return final Object result,
@Advice.Thrown final Throwable throwable
) {
HelperMethods.doMethodExit(result, throwable);
}
Custom Inject Adapter static instances typically named SETTER
implement the AgentPropagation.Setter
interface and
are used to normalize setting shared context values such as in HTTP headers.
Custom inject adapter static instances typically named GETTER
implement the AgentPropagation.Getter
interface and
are used to normalize extracting shared context values such as from HTTP headers.
For example google-http-client
sets its header values using:
com.google.api.client.http.HttpRequest.getHeaders().put(key,value)
package datadog.trace.instrumentation.googlehttpclient;
import com.google.api.client.http.HttpRequest;
import datadog.trace.bootstrap.instrumentation.api.AgentPropagation;
public class HeadersInjectAdapter implements AgentPropagation.Setter<HttpRequest> {
public static final HeadersInjectAdapter SETTER = new HeadersInjectAdapter();
@Override
public void set(final HttpRequest carrier, final String key, final String value) {
carrier.getHeaders().put(key, value);
}
}
But notice apache-http-client5
sets its header values using:
org.apache.hc.core5.http.HttpRequest.setHeader(key,value)
package datadog.trace.instrumentation.apachehttpclient5;
import datadog.trace.bootstrap.instrumentation.api.AgentPropagation;
import org.apache.hc.core5.http.HttpRequest;
public class HttpHeadersInjectAdapter implements AgentPropagation.Setter<HttpRequest> {
public static final HttpHeadersInjectAdapter SETTER = new HttpHeadersInjectAdapter();
@Override
public void set(final HttpRequest carrier, final String key, final String value) {
carrier.setHeader(key, value);
}
}
These implementation-specific methods are both wrapped in a standard set(...) method by the SETTER.
Typically, an instrumentation will use ByteBuddy to apply new code from an Advice class before and/or after the targeted
code using @Advice.OnMethodEnter
and @Advice.OnMethodExit.
Alternatively, you can replace the call to the target method with your own code which wraps the original method call. An
example is the JMS Instrumentation which replaces the MessageListener.onMessage()
method
with DatadogMessageListener.onMessage().
The DatadogMessageListener
then calls the original onMessage()
method.
Note that this style is not recommended because it can cause datadog packages to appear in stack traces generated
by errors in user code. This has created confusion in the past.
Context stores pass information between instrumented methods, using library objects that both methods have access to. They can be used to attach data to a request when the request is received, and read that data where the request is deserialized. Context stores work internally by dynamically adding a field to the “carrier” object by manipulating the bytecode. Since they manipulate bytecode, context stores can only be created within Advice classes. For example:
ContextStore<X> store = InstrumentationContext.get(
"com.amazonaws.services.sqs.model.ReceiveMessageResult", "java.lang.String");
It’s also possible to pass the types as class objects, but this is only possible for classes that are in the bootstrap
classpath. Basic types like String
would work and the usual datadog types like AgentSpan
are OK too, but classes
from the library you are instrumenting are not.
In the example above, that context store is used to store an arbitrary String
in a ReceiveMessageResult
class. It is
used like a Map:
store.put(response, "my string");
and/or
String stored = store.get(response); // "my string"
Context stores also need to be pre-declared in the Advice by overriding the contextStore()
method otherwise, using
them throws exceptions.
@Override
public Map<String, String> contextStore() {
return singletonMap(
"com.amazonaws.services.sqs.model.ReceiveMessageResult",
"java.lang.String"
);
}
It is important to understand that even though they look like maps, since the value is stored in the key, you can only
retrieve a value if you use the exact same key object as when it was set. Using a different object that is “.equals()
”
to the first will yield nothing.
Since ContextStore
does not support null keys, null checks must be enforced before using an object as a key.
In order to avoid activating new spans on recursive calls to the same method a CallDepthThreadLocalMap is often used to determine if a call is recursive by using a counter. It is incremented with each call to the method and decremented ( or reset) when exiting.
This only works if the methods are called on the same thread since the counter is a ThreadLocal variable.
In Advice classes, the @Advice.OnMethodEnter
methods typically start spans and @Advice.OnMethodExit
methods
typically finish spans.
Starting the span may be done directly or with helper methods which eventually make a call to one of the
various AgentTracer.startSpan(...)
methods.
Finishing the span is normally done by calling span.finish()
in the exit method;
The basic span lifecycle in an Advice class looks like:
-
Start the span
-
Decorate the span
-
Activate the span and get the AgentScope
-
Run the instrumented target method
-
Close the Agent Scope
-
Finish the span
@Advice.OnMethodEnter(suppress = Throwable.class)
public static AgentScope begin() {
final AgentSpan span = startSpan(/* */);
DECORATE.afterStart(span);
return activateSpan(span);
}
@Advice.OnMethodExit(suppress = Throwable.class)
public static void end(@Advice.Enter final AgentScope scope) {
AgentSpan span = scope.span();
DECORATE.beforeFinish(span);
scope.close();
span.finish();
}
For example,
the HttpUrlConnectionInstrumentation
class contains
the HttpUrlConnectionAdvice
class which calls
the HttpUrlState.
start
()
and HttpUrlState.
finishSpan
()
methods.
AgentScope.Continuation
is used to pass context between threads.- Continuations must be either activated or canceled.
- If a Continuation is activated it returns a TraceScope which must eventually be closed.
- Only after all TraceScopes are closed and any non-activated Continuations are canceled may the Trace finally close.
Notice
in HttpClientRequestTracingHandler
how the AgentScope.Continuation is used to obtain the parentScope
which is
finally closed.
- Instrumentation names use kebab case. For example:
google-http-client
- Instrumentation module name and package name should be consistent. For example, the
instrumentation
google-http-client
contains theGoogleHttpClientInstrumentation
class in the packagedatadog.trace.instrumentation.googlehttpclient.
- As usual, class names should be nouns, in camel case with the first letter of each internal word capitalized. Use whole words-avoid acronyms and abbreviations (unless the abbreviation is much more widely used than the long form, such as URL or HTML).
- Advice class names should end in Advice.
- Instrumentation class names should end in Instrumentation.
- Decorator class names should end in Decorator.
The file ignored_class_name.trie lists classes that are to be globally ignored by matchers because they are unsafe, pointless or expensive to transform. If you notice an expected class is not being transformed, it may be covered by an entry in this list.
Instrumentations running on GraalVM should avoid using reflection if possible. If reflection must be used the reflection usage should be added to
dd-java-agent/agent-bootstrap/src/main/resources/META-INF/native-image/com.datadoghq/dd-java-agent/reflect-config.json
See GraalVM configuration docs.
Tests are written in Groovy using the Spock framework. For
instrumentations, AgentTestRunner
must be extended. For example, HTTP server frameworks use base tests which enforce
consistency between different implementations (
see HttpServerTest).
When writing an instrumentation it is much faster to test just the instrumentation rather than build the entire project,
for example:
./gradlew :dd-java-agent:instrumentation:play-ws-2.1:test
Sometimes it is necessary to force Gradle to discard cached test results and rerun all tasks.
./gradle test --rerun-tasks
Running tests that require JDK-21 will require the JAVA_21_HOME
env var set and can be done like this:
./gradlew :dd-java-agent:instrumentation:aerospike-4:allLatestDepTests -PtestJvm=21
Adding a directive to the build file gives early warning when breaking changes are released by framework maintainers. For example, for Play 2.5, we download the latest dependency and run tests against it:
latestDepTestCompile group: 'com.typesafe.play', name: 'play-java_2.11', version: '2.5.+'
latestDepTestCompile group: 'com.typesafe.play', name: 'play-java-ws_2.11', version: '2.5.+'
latestDepTestCompile(group: 'com.typesafe.play', name: 'play-test_2.11', version: '2.5.+') {
exclude group: 'org.eclipse.jetty.websocket', module: 'websocket-client'
}
Dependency tests can be run like:
./gradlew :dd-java-agent:instrumentation:play-ws-2.1:latestDepTest
The
file dd-trace-java/gradle/test-suites.gradle
contains these macros for adding different test suites to individual instrumentation builds. Notice how addTestSuite
and addTestSuiteForDir
pass values
to addTestSuiteExtendingForDir
which configures the tests.
ext.addTestSuite = (String testSuiteName) -> {
ext.addTestSuiteForDir(testSuiteName, testSuiteName)
}
ext.addTestSuiteForDir = (String testSuiteName, String dirName) -> {
ext.addTestSuiteExtendingForDir(testSuiteName, 'test', dirName)
}
ext.addTestSuiteExtendingForDir = (String testSuiteName, String parentSuiteName, String dirName) -> { /* */ }
For example:
addTestSuite('latestDepTest')
Also, the forked test for latestDep is not run by default without declaring something like:
addTestSuiteExtendingForDir('latestDepForkedTest', 'latestDepTest', 'test')
(also
example vertx-web-3.5/build.gradle
)
In addition to unit tests, Smoke tests may be
needed. Smoke tests run with a real agent jar file set as the javaagent
. These are optional and not all frameworks
have them, but contributions are very welcome.
Integrations have evolved over time. Newer examples of integrations such as Spring and JDBC illustrate current best practices.
- Datadog Instrumentations rely heavily on ByteBuddy. You may find the ByteBuddy tutorial useful.
- The Groovy docs.
- Spock Framework Reference Documentation.