Datadog APM provides distributed tracing and performance monitoring for Java applications. It helps you identify bottlenecks, track requests across microservices, and monitor application health.
Setup and Dependencies
1. Maven Dependencies
<properties> <dd-trace-java.version>1.10.0</dd-trace-java.version> <spring-boot.version>3.1.0</spring-boot.version> </properties> <dependencies> <!-- Datadog APM Tracer --> <dependency> <groupId>com.datadoghq</groupId> <artifactId>dd-trace-api</artifactId> <version>${dd-trace-java.version}</version> </dependency> <!-- Datadog Java Agent (for auto-instrumentation) --> <!-- This is typically added as a Java agent, not as a dependency --> <!-- Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> <version>${spring-boot.version}</version> </dependency> </dependencies> 2. Docker Compose for Local Development
version: '3.8' services: datadog-agent: image: gcr.io/datadoghq/agent:latest environment: - DD_API_KEY=${DATADOG_API_KEY} - DD_APM_ENABLED=true - DD_APM_NON_LOCAL_TRAFFIC=true - DD_LOGS_ENABLED=true - DD_LOGS_CONFIG_CONTAINER_COLLECT_ALL=true - DD_PROCESS_AGENT_ENABLED=true ports: - "8126:8126" # APM - "8125:8125/udp" # StatsD volumes: - /var/run/docker.sock:/var/run/docker.sock - /proc/:/host/proc/:ro - /sys/fs/cgroup/:/host/sys/fs/cgroup:ro Configuration
1. Application Properties
# application.yml spring: application: name: user-service management: endpoints: web: exposure: include: health,metrics,info,prometheus metrics: export: datadog: enabled: true api-key: ${DATADOG_API_KEY} step: 1m logging: level: com.datadog: DEBUG 2. Java Agent Configuration
# JVM arguments for Datadog agent java -javaagent:/path/to/dd-java-agent.jar \ -Ddd.service=user-service \ -Ddd.env=production \ -Ddd.version=1.0.0 \ -Ddd.trace.sample.rate=1.0 \ -Ddd.logs.injection=true \ -jar your-application.jar
3. Environment Variables
export DD_SERVICE_NAME=user-service export DD_ENV=production export DD_VERSION=1.0.0 export DD_AGENT_HOST=localhost export DD_TRACE_AGENT_PORT=8126 export DD_LOGS_INJECTION=true export DD_TRACE_SAMPLE_RATE=1.0 export DD_PROFILING_ENABLED=true export DD_RUNTIME_METRICS_ENABLED=true
Manual Instrumentation
1. Basic Tracing
import datadog.trace.api.Trace; import datadog.trace.api.DDTags; import io.opentracing.Span; import io.opentracing.Tracer; import io.opentracing.util.GlobalTracer; @Service public class UserService { private final Tracer tracer = GlobalTracer.get(); private final UserRepository userRepository; @Trace(operationName = "user.create", resourceName = "UserService.createUser") public User createUser(CreateUserRequest request) { Span span = tracer.activeSpan(); try { // Add custom tags to the span span.setTag("user.email", request.getEmail()); span.setTag("user.role", request.getRole()); span.setTag(DDTags.SERVICE_NAME, "user-service"); // Business logic validateUserRequest(request); User user = userRepository.save(mapToUser(request)); // Log custom event span.log(Map.of("event", "user_created", "user_id", user.getId())); return user; } catch (Exception e) { // Mark span as error span.setTag(DDTags.ERROR, true); span.log(Map.of("error.object", e)); throw e; } } @Trace(operationName = "user.batch_processing", resourceName = "UserService.processBatch") public void processUserBatch(List<CreateUserRequest> requests) { Span parentSpan = tracer.activeSpan(); for (int i = 0; i < requests.size(); i++) { CreateUserRequest request = requests.get(i); // Create child span for each user Span childSpan = tracer.buildSpan("user.process_single") .asChildOf(parentSpan) .withTag("batch.index", i) .start(); try (Scope scope = tracer.activateSpan(childSpan)) { processSingleUser(request); } catch (Exception e) { childSpan.setTag(DDTags.ERROR, true); childSpan.log(Map.of("error.object", e)); } finally { childSpan.finish(); } } } } 2. Custom Span Creation
@Component public class DatabaseService { private final Tracer tracer = GlobalTracer.get(); public User findUserById(String userId) { Span span = tracer.buildSpan("database.query") .withTag("db.operation", "SELECT") .withTag("db.table", "users") .withTag("db.user_id", userId) .start(); try (Scope scope = tracer.activateSpan(span)) { // Simulate database query Thread.sleep(100); return userRepository.findById(userId); } catch (Exception e) { span.setTag(DDTags.ERROR, true); span.log(Map.of("error.message", e.getMessage())); throw new RuntimeException("Database error", e); } finally { span.finish(); } } public List<User> findUsersByCriteria(UserCriteria criteria) { return tracer.trace("database.complex_query", () -> { Span span = tracer.activeSpan(); span.setTag("db.operation", "SELECT"); span.setTag("db.table", "users"); span.setTag("db.criteria", criteria.toString()); // Complex query logic return executeComplexQuery(criteria); }); } } 3. Async Tracing
@Service public class AsyncUserService { private final Tracer tracer = GlobalTracer.get(); private final ExecutorService executorService; @Async @Trace(operationName = "async.user.processing") public CompletableFuture<User> processUserAsync(String userId) { Span span = tracer.activeSpan(); return CompletableFuture.supplyAsync(() -> { // Activate the span in the async thread try (Scope scope = tracer.activateSpan(span)) { span.setTag("user.id", userId); span.setTag("thread.name", Thread.currentThread().getName()); // Simulate async processing User user = userRepository.findById(userId); enrichUserData(user); return user; } catch (Exception e) { span.setTag(DDTags.ERROR, true); span.log(Map.of("error.object", e)); throw new CompletionException(e); } }, executorService); } @Trace(operationName = "user.parallel.processing") public List<User> processUsersInParallel(List<String> userIds) { Span parentSpan = tracer.activeSpan(); List<CompletableFuture<User>> futures = userIds.stream() .map(userId -> { return CompletableFuture.supplyAsync(() -> { // Create child span for each async operation Span childSpan = tracer.buildSpan("user.async.process") .asChildOf(parentSpan) .withTag("user.id", userId) .start(); try (Scope scope = tracer.activateSpan(childSpan)) { return processSingleUser(userId); } catch (Exception e) { childSpan.setTag(DDTags.ERROR, true); throw e; } finally { childSpan.finish(); } }, executorService); }) .collect(Collectors.toList()); return futures.stream() .map(CompletableFuture::join) .collect(Collectors.toList()); } } 4. HTTP Client Tracing
@Component public class TracingHttpClient { private final Tracer tracer = GlobalTracer.get(); private final RestTemplate restTemplate; @Trace(operationName = "http.client.call") public <T> T executeWithTracing(String url, HttpMethod method, Object request, Class<T> responseType) { Span span = tracer.activeSpan(); try { // Add HTTP headers for distributed tracing HttpHeaders headers = new HttpHeaders(); tracer.inject(span.context(), Format.Builtin.HTTP_HEADERS, new TextMap() { @Override public void put(String key, String value) { headers.add(key, value); } @Override public Iterator<Map.Entry<String, String>> iterator() { throw new UnsupportedOperationException(); } }); // Add custom headers headers.add("X-Correlation-ID", span.context().toTraceId()); HttpEntity<Object> entity = new HttpEntity<>(request, headers); ResponseEntity<T> response = restTemplate.exchange(url, method, entity, responseType); // Add response tags to span span.setTag("http.status_code", response.getStatusCodeValue()); span.setTag("http.url", url); span.setTag("http.method", method.name()); return response.getBody(); } catch (Exception e) { span.setTag(DDTags.ERROR, true); span.setTag("http.error", e.getMessage()); throw e; } } } Spring Boot Integration
1. Configuration Class
@Configuration public class DatadogConfig { @Bean public Tracer tracer() { return GlobalTracer.get(); } @Bean public Filter datadogFilter() { return new TracingFilter(); } @Bean public RestTemplate tracingRestTemplate(Tracer tracer) { RestTemplate restTemplate = new RestTemplate(); // Add tracing interceptor restTemplate.setInterceptors(Collections.singletonList( new TracingClientHttpRequestInterceptor(tracer) )); return restTemplate; } } @Component class TracingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor { private final Tracer tracer; public TracingClientHttpRequestInterceptor(Tracer tracer) { this.tracer = tracer; } @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { Span span = tracer.buildSpan("http.client") .withTag("http.method", request.getMethod().name()) .withTag("http.url", request.getURI().toString()) .start(); // Inject tracing headers tracer.inject(span.context(), Format.Builtin.HTTP_HEADERS, new TextMap() { @Override public void put(String key, String value) { request.getHeaders().add(key, value); } @Override public Iterator<Map.Entry<String, String>> iterator() { throw new UnsupportedOperationException(); } }); try (Scope scope = tracer.activateSpan(span)) { ClientHttpResponse response = execution.execute(request, body); span.setTag("http.status_code", response.getStatusCode().value()); return response; } catch (Exception e) { span.setTag(DDTags.ERROR, true); span.log(Map.of("error.object", e)); throw e; } finally { span.finish(); } } } 2. Controller with Custom Tracing
@RestController @RequestMapping("/api/users") public class UserController { private final Tracer tracer = GlobalTracer.get(); private final UserService userService; private static final Logger logger = LoggerFactory.getLogger(UserController.class); @PostMapping @Trace(operationName = "http.request", resourceName = "POST /api/users") public ResponseEntity<User> createUser(@RequestBody CreateUserRequest request) { Span span = tracer.activeSpan(); try { // Add custom tags span.setTag("http.route", "/api/users"); span.setTag("user.email", request.getEmail()); span.setTag("request.size", request.toString().length()); logger.info("Creating user: {}", request.getEmail()); User user = userService.createUser(request); span.setTag("user.id", user.getId()); span.setTag("http.status_code", 201); return ResponseEntity.status(HttpStatus.CREATED).body(user); } catch (Exception e) { span.setTag(DDTags.ERROR, true); span.setTag("http.status_code", 500); span.log(Map.of("error.message", e.getMessage())); throw e; } } @GetMapping("/{userId}") @Trace(operationName = "http.request", resourceName = "GET /api/users/{userId}") public ResponseEntity<User> getUser(@PathVariable String userId) { Span span = tracer.activeSpan(); span.setTag("user.id", userId); try { User user = userService.findUserById(userId); if (user == null) { span.setTag("http.status_code", 404); return ResponseEntity.notFound().build(); } span.setTag("http.status_code", 200); return ResponseEntity.ok(user); } catch (Exception e) { span.setTag(DDTags.ERROR, true); span.setTag("http.status_code", 500); throw e; } } } 3. AOP for Method Tracing
@Aspect @Component public class TracingAspect { private final Tracer tracer = GlobalTracer.get(); @Around("@annotation(Traced)") public Object traceMethod(ProceedingJoinPoint joinPoint) throws Throwable { String methodName = joinPoint.getSignature().toShortString(); Span span = tracer.buildSpan(methodName) .withTag("class", joinPoint.getTarget().getClass().getSimpleName()) .withTag("method", joinPoint.getSignature().getName()) .start(); try (Scope scope = tracer.activateSpan(span)) { Object result = joinPoint.proceed(); span.setTag("result.type", result != null ? result.getClass().getSimpleName() : "void"); return result; } catch (Exception e) { span.setTag(DDTags.ERROR, true); span.log(Map.of("error.object", e)); throw e; } finally { span.finish(); } } } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Traced { String value() default ""; } 4. Database Query Tracing
@Aspect @Component public class DatabaseTracingAspect { private final Tracer tracer = GlobalTracer.get(); @Around("execution(* org.springframework.data.repository.Repository+.*(..))") public Object traceRepositoryMethods(ProceedingJoinPoint joinPoint) throws Throwable { String methodName = joinPoint.getSignature().getName(); String repositoryName = joinPoint.getTarget().getClass().getSimpleName(); Span span = tracer.buildSpan("database.repository") .withTag("db.operation", methodName) .withTag("db.repository", repositoryName) .start(); long startTime = System.currentTimeMillis(); try (Scope scope = tracer.activateSpan(span)) { Object result = joinPoint.proceed(); long duration = System.currentTimeMillis() - startTime; span.setTag("db.duration_ms", duration); span.setTag("db.result_count", getResultCount(result)); return result; } catch (Exception e) { span.setTag(DDTags.ERROR, true); span.setTag("db.error", e.getMessage()); throw e; } finally { span.finish(); } } private int getResultCount(Object result) { if (result == null) return 0; if (result instanceof Collection) return ((Collection<?>) result).size(); if (result instanceof Optional) return ((Optional<?>) result).isPresent() ? 1 : 0; return 1; } } Custom Metrics and Business Monitoring
1. Custom Metrics Service
@Component public class DatadogMetricsService { private final StatsDClient statsDClient; public DatadogMetricsService() { this.statsDClient = new NonBlockingStatsDClientBuilder() .prefix("user.service") .hostname("localhost") .port(8125) .build(); } public void recordUserCreation(String userId, String role, long duration) { // Increment counter statsDClient.incrementCounter("user.created", "role:" + role, "status:success"); // Record histogram for duration statsDClient.recordHistogramValue("user.creation.duration", duration, "role:" + role); // Record gauge for active users statsDClient.recordGaugeValue("users.active", getActiveUsersCount()); } public void recordUserLogin(String userId, boolean success) { String status = success ? "success" : "failure"; statsDClient.incrementCounter("user.login", "user_id:" + userId, "status:" + status); } public void recordBusinessEvent(String eventName, Map<String, String> tags) { statsDClient.incrementCounter("business.event." + eventName, tags.entrySet().stream() .map(e -> e.getKey() + ":" + e.getValue()) .toArray(String[]::new)); } } 2. Business Metrics Integration
@Service public class OrderService { private final DatadogMetricsService metricsService; @Trace(operationName = "order.process") public Order processOrder(OrderRequest request) { long startTime = System.currentTimeMillis(); try { Order order = createOrder(request); processPayment(order); updateInventory(order); long duration = System.currentTimeMillis() - startTime; // Record business metrics metricsService.recordHistogramValue("order.processing.duration", duration, "currency:" + order.getCurrency(), "payment_method:" + order.getPaymentMethod()); metricsService.incrementCounter("order.completed", "status:success", "currency:" + order.getCurrency()); return order; } catch (Exception e) { metricsService.incrementCounter("order.completed", "status:failure"); throw e; } } } Error Tracking and Monitoring
1. Custom Error Handler
@ControllerAdvice public class GlobalExceptionHandler { private final Tracer tracer = GlobalTracer.get(); @ExceptionHandler(Exception.class) public ResponseEntity<ErrorResponse> handleException(Exception e, HttpServletRequest request) { Span span = tracer.activeSpan(); if (span != null) { span.setTag(DDTags.ERROR, true); span.log(Map.of( "error.message", e.getMessage(), "error.stack", getStackTrace(e), "http.url", request.getRequestURL().toString(), "http.method", request.getMethod() )); } // Send custom metric for errors recordErrorMetric(e, request); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(new ErrorResponse("Internal server error")); } private void recordErrorMetric(Exception e, HttpServletRequest request) { // This would integrate with Datadog metrics System.err.printf("Error occurred: %s at %s %s%n", e.getMessage(), request.getMethod(), request.getRequestURI()); } private String getStackTrace(Exception e) { StringWriter sw = new StringWriter(); e.printStackTrace(new PrintWriter(sw)); return sw.toString(); } } 2. Health Check with Metrics
@Component public class DatabaseHealthIndicator implements HealthIndicator { private final DatadogMetricsService metricsService; private final UserRepository userRepository; @Override public Health health() { long startTime = System.currentTimeMillis(); try { // Simple database check userRepository.count(); long duration = System.currentTimeMillis() - startTime; metricsService.recordGaugeValue("health.database.duration", duration); metricsService.incrementCounter("health.check", "component:database", "status:healthy"); return Health.up() .withDetail("database", "connected") .withDetail("response_time", duration + "ms") .build(); } catch (Exception e) { long duration = System.currentTimeMillis() - startTime; metricsService.incrementCounter("health.check", "component:database", "status:unhealthy"); metricsService.recordGaugeValue("health.database.duration", duration); return Health.down(e) .withDetail("database", "disconnected") .withDetail("response_time", duration + "ms") .build(); } } } Testing and Verification
1. Test Configuration
@SpringBootTest @TestPropertySource(properties = { "dd.trace.enabled=false", "dd.env=test", "dd.service.name=user-service-test" }) public class UserServiceTest { @Test public void testUserCreationWithTracing() { // Test without actual tracing in test environment UserService userService = new UserService(); User user = userService.createUser(createTestRequest()); assertNotNull(user); assertNotNull(user.getId()); } } 2. Tracing Verification Utility
@Component @Profile("dev") public class TracingVerifier { private final Tracer tracer = GlobalTracer.get(); public void verifyTrace(String operationName) { Span span = tracer.activeSpan(); if (span != null) { System.out.printf("Active span: %s, Trace ID: %s%n", span.getOperationName(), span.context().toTraceId()); } else { System.out.println("No active span"); } } } Best Practices
- Consistent Naming: Use consistent operation names and resource names
- Reasonable Sampling: Adjust sampling rate based on traffic
- Tag Wisely: Avoid high-cardinality tags that can impact performance
- Error Handling: Always mark spans as errors when exceptions occur
- Resource Cleanup: Ensure spans are properly finished
- Async Context: Properly propagate context to async operations
- Security: Avoid storing sensitive data in tags
// Example of secure tagging public void setSecureTags(Span span, User user) { span.setTag("user.id", user.getId()); span.setTag("user.role", user.getRole()); // Don't include sensitive information // span.setTag("user.email", user.getEmail()); // Avoid // span.setTag("user.password_hash", user.getPasswordHash()); // Avoid } Conclusion
Datadog APM in Java provides:
- Distributed tracing across microservices
- Performance monitoring and bottleneck identification
- Custom metrics for business monitoring
- Error tracking and alerting
- Integration with existing Spring Boot applications
By implementing the patterns shown above, you can gain deep visibility into your Java applications, identify performance issues, and monitor your system's health in production environments. The combination of automatic instrumentation and custom tracing gives you complete control over what you monitor and how you track your application's performance.