Java Custom Instrumentation using the Datadog API
This page details common use cases for adding and customizing observability with Datadog APM. If you have not read the setup instructions for automatic instrumentation, start with the Java Setup Instructions.
The Datadog Java tracer is built on OpenTracing. Although OpenTracing is deprecated in favor of OpenTelemetry, the following examples correctly import the opentracing
library.
Add custom span tags to your spans to customize your observability within Datadog. The span tags are applied to your incoming traces, allowing you to correlate observed behavior with code-level information such as merchant tier, checkout amount, or user ID.
Add custom tags to your spans corresponding to any dynamic value within your application code such as customer.id
.
import org.apache.cxf.transport.servlet.AbstractHTTPServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import io.opentracing.Tracer;
import io.opentracing.util.GlobalTracer;
@WebServlet
class ShoppingCartServlet extends AbstractHttpServlet {
@Override
void doGet(HttpServletRequest req, HttpServletResponse resp) {
// Get the active span
final Span span = GlobalTracer.get().activeSpan();
if (span != null) {
// customer_id -> 254889
// customer_tier -> platinum
// cart_value -> 867
span.setTag("customer.id", customer_id);
span.setTag("customer.tier", customer_tier);
span.setTag("cart.value", cart_value);
}
// [...]
}
}
The dd.tags
property allows setting tags across all generated spans for an application. This can be useful for grouping stats for your applications, datacenters, or any other tags you would like to see within the Datadog UI.
java -javaagent:<DD-JAVA-AGENT-PATH>.jar \
-Ddd.tags=datacenter:njc,<TAG_KEY>:<TAG_VALUE> \
-jar <YOUR_APPLICATION_PATH>.jar
Set errors on a span
To customize an error associated with one of your spans, set the error tag on the span and use Span.log()
to set an “error event”. The error event is a Map<String,Object>
containing a Fields.ERROR_OBJECT->Throwable
entry, a Fields.MESSAGE->String
, or both.
import io.opentracing.Span;
import io.opentracing.tag.Tags;
import io.opentracing.util.GlobalTracer;
import io.opentracing.log.Fields;
...
// Get active span if not available in current method
final Span span = GlobalTracer.get().activeSpan();
if (span != null) {
span.setTag(Tags.ERROR, true);
span.log(Collections.singletonMap(Fields.ERROR_OBJECT, ex));
}
Note: Span.log()
is a generic OpenTracing mechanism for associating events to the current timestamp. The Java Tracer only supports logging error events.
Alternatively, you can set error tags directly on the span without log()
:
import io.opentracing.Span;
import io.opentracing.tag.Tags;
import io.opentracing.util.GlobalTracer;
import datadog.trace.api.DDTags;
import java.io.PrintWriter;
import java.io.StringWriter;
...
final Span span = GlobalTracer.get().activeSpan();
if (span != null) {
span.setTag(Tags.ERROR, true);
span.setTag(DDTags.ERROR_MSG, ex.getMessage());
span.setTag(DDTags.ERROR_TYPE, ex.getClass().getName());
final StringWriter errorString = new StringWriter();
ex.printStackTrace(new PrintWriter(errorString));
span.setTag(DDTags.ERROR_STACK, errorString.toString());
}
Note: You can add any relevant error metadata listed in the trace view docs. If the current span isn’t the root span, mark it as an error by using the dd-trace-api
library to grab the root span with MutableSpan
, then use setError(true)
. See the setting tags & errors on a root span section for more details.
When an event or condition happens downstream, you may want that behavior or value reflected as a tag on the top level or root span. This can be useful to count an error or for measuring performance, or setting a dynamic tag for observability.
import java.util.Collections;
import io.opentracing.Span;
import io.opentracing.Scope;
import datadog.trace.api.interceptor.MutableSpan;
import io.opentracing.log.Fields;
import io.opentracing.util.GlobalTracer;
import io.opentracing.util.Tracer;
Tracer tracer = GlobalTracer.get();
final Span span = tracer.buildSpan("<OPERATION_NAME>").start();
// Note: The scope in the try with resource block below
// will be automatically closed at the end of the code block.
// If you do not use a try with resource statement, you need
// to call scope.close().
try (final Scope scope = tracer.activateSpan(span)) {
// exception thrown here
} catch (final Exception e) {
// Set error tag on span as normal
span.log(Collections.singletonMap(Fields.ERROR_OBJECT, e));
// Set error on root span
if (span instanceof MutableSpan) {
MutableSpan localRootSpan = ((MutableSpan) span).getLocalRootSpan();
localRootSpan.setError(true);
localRootSpan.setTag("some.other.tag", "value");
}
} finally {
// Close span in a finally block
span.finish();
}
If you are not manually creating a span, you can still access the root span through the GlobalTracer
:
import io.opentracing.Span;
import io.opentracing.util.GlobalTracer;
import datadog.trace.api.interceptor.MutableSpan;
...
final Span span = GlobalTracer.get().activeSpan();
if (span != null && (span instanceof MutableSpan)) {
MutableSpan localRootSpan = ((MutableSpan) span).getLocalRootSpan();
// do stuff with root span
}
Note: Although MutableSpan
and Span
share many similar methods, they are distinct types. MutableSpan
is Datadog specific and not part of the OpenTracing API.
Adding spans
If you aren’t using a supported framework instrumentation, or you would like additional depth in your application’s traces, you may want to add custom instrumentation to your code for complete flame graphs or to measure execution times for pieces of code.
If modifying application code is not possible, use the environment variable dd.trace.methods
to detail these methods.
If you have existing @Trace
or similar annotations, or prefer to use annotations to complete any incomplete traces within Datadog, use Trace Annotations.
Datadog trace methods
Using the dd.trace.methods
system property, you can get visibility into unsupported frameworks without changing application code.
java -javaagent:/path/to/dd-java-agent.jar -Ddd.env=prod -Ddd.service.name=db-app -Ddd.trace.methods=store.db.SessionManager[saveSession] -jar path/to/application.jar
To trace several functions within the same class, use the following syntax:
java -javaagent:/path/to/dd-java-agent.jar -Ddd.env=prod -Ddd.service.name=db-app -Ddd.trace.methods=store.db.SessionManager[saveSession,loadSession] -jar path/to/application.jar
The only difference between this approach and using @Trace
annotations is the customization options for the operation and resource names. With DD Trace Methods, operationName
is trace.annotation
and resourceName
is SessionManager.saveSession
.
Trace annotations
Add @Trace
to methods to have them be traced when running with dd-java-agent.jar
. If the Agent is not attached, this annotation has no effect on your application.
Datadog’s Trace annotation is provided by the dd-trace-api dependency.
The available arguments for the @Trace
annotation are:
operationName
: Set the operation name for the trace (default: The method’s name).resourceName
: Set the resource name for the trace (default: The same value as operationName
).noParent
: Set to true
to always start a new trace at that method. Supported from v1.22.0+ of dd-trace-java
(default: false
).
import datadog.trace.api.Trace;
public class SessionManager {
@Trace(operationName = "database.persist", resourceName = "SessionManager.saveSession")
public static void saveSession() {
// your method implementation here
}
}
Note: Through the dd.trace.annotations
system property, other tracing method annotations can be recognized by Datadog as @Trace
. You can find a list in TraceAnnotationsInstrumentation.java if you have previously decorated your code.
Manually creating a new span
In addition to automatic instrumentation, the @Trace
annotation, and dd.trace.methods
configurations , you can customize your observability by programmatically creating spans around any block of code. Spans created in this manner integrate with other tracing mechanisms automatically. In other words, if a trace has already started, the manual span will have its caller as its parent span. Similarly, any traced methods called from the wrapped block of code will have the manual span as its parent.
import datadog.trace.api.DDTags;
import io.opentracing.Scope;
import io.opentracing.Span;
import io.opentracing.Tracer;
import io.opentracing.util.GlobalTracer;
class SomeClass {
void someMethod() {
Tracer tracer = GlobalTracer.get();
// Service and resource name tags are required.
// You can set them when creating the span:
Span span = tracer.buildSpan("<OPERATION_NAME>")
.withTag(DDTags.SERVICE_NAME, "<SERVICE_NAME>")
.withTag(DDTags.RESOURCE_NAME, "<RESOURCE_NAME>")
.start();
// Note: The scope in the try with resource block below
// will be automatically closed at the end of the code block.
// If you do not use a try with resource statement, you need
// to call scope.close().
try (Scope scope = tracer.activateSpan(span)) {
// Alternatively, set tags after creation
span.setTag("my.tag", "value");
// The code you're tracing
} catch (Exception e) {
// Set error on span
} finally {
// Close span in a finally block
span.finish();
}
}
}
Extending tracers
The tracing libraries are designed to be extensible. Customers may consider writing a custom post-processor called a TraceInterceptor
to intercept Spans then adjust or discard them accordingly (for example, based on regular expressions). The following example implements two interceptors to achieve complex post-processing logic.
import java.util.List;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import datadog.trace.api.interceptor.TraceInterceptor;
import datadog.trace.api.interceptor.MutableSpan;
class FilteringInterceptor implements TraceInterceptor {
@Override
public Collection<? extends MutableSpan> onTraceComplete(
Collection<? extends MutableSpan> trace) {
List<MutableSpan> filteredTrace = new ArrayList<>();
for (final MutableSpan span : trace) {
String orderId = (String) span.getTags().get("order.id");
// Drop spans when the order id starts with "TEST-"
if (orderId == null || !orderId.startsWith("TEST-")) {
filteredTrace.add(span);
}
}
return filteredTrace;
}
@Override
public int priority() {
// some high unique number so this interceptor is last
return 100;
}
}
class PricingInterceptor implements TraceInterceptor {
@Override
public Collection<? extends MutableSpan> onTraceComplete(
Collection<? extends MutableSpan> trace) {
for (final MutableSpan span : trace) {
Map<String, Object> tags = span.getTags();
Double originalPrice = (Double) tags.get("order.price");
Double discount = (Double) tags.get("order.discount");
// Set a tag from a calculation from other tags
if (originalPrice != null && discount != null) {
span.setTag("order.value", originalPrice - discount);
}
}
return trace;
}
@Override
public int priority() {
return 20; // some unique number
}
}
Near the start of your application, register the interceptors with the following:
datadog.trace.api.GlobalTracer.get().addTraceInterceptor(new FilteringInterceptor());
datadog.trace.api.GlobalTracer.get().addTraceInterceptor(new PricingInterceptor());
Trace client and Agent configuration
There are additional configurations possible for both the tracing client and Datadog Agent for context propagation, as well as to exclude specific Resources from sending traces to Datadog in the event these traces are not wanted to count in metrics calculated, such as Health Checks.
Propagating context with headers extraction and injection
You can configure the propagation of context for distributed traces by injecting and extracting headers. Read Trace Context Propagation for information.
Resource filtering
Traces can be excluded based on their resource name, to remove synthetic traffic such as health checks from reporting traces to Datadog. This and other security and fine-tuning configurations can be found on the Security page or in Ignoring Unwanted Resources.
Further Reading
Additional helpful documentation, links, and articles: