Article: Comprehensive Datadog APM Implementation for Java Applications
Datadog Application Performance Monitoring (APM) provides distributed tracing, performance metrics, and error tracking for modern applications. This article covers comprehensive integration strategies for Java applications.
Core Datadog APM Configuration
package com.titliel.apm;
import datadog.trace.api.DDTags;
import datadog.trace.api.GlobalTracer;
import datadog.trace.api.Trace;
import datadog.trace.api.interceptor.MutableSpan;
import io.opentracing.Span;
import io.opentracing.SpanContext;
import io.opentracing.Tracer;
import io.opentracing.propagation.Format;
import io.opentracing.propagation.TextMapAdapter;
import io.opentracing.util.GlobalTracer;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* Titliel Datadog APM Manager
* Advanced APM integration with custom metrics and tracing
*/
public class TitlielDatadogApm {
private final Tracer tracer;
private final ApmConfig config;
private final Map<String, ServiceMetrics> serviceMetrics;
public TitlielDatadogApm() {
this.tracer = GlobalTracer.get();
this.config = ApmConfig.getInstance();
this.serviceMetrics = new ConcurrentHashMap<>();
}
/**
* Start a new trace for business transaction
*/
public Span startBusinessTrace(String operationName, String serviceName) {
Tracer.SpanBuilder spanBuilder = tracer.buildSpan(operationName)
.withTag(DDTags.SERVICE_NAME, serviceName)
.withTag(DDTags.SPAN_TYPE, "web")
.withTag(DDTags.RESOURCE_NAME, operationName);
Span span = spanBuilder.start();
// Add custom tags
span.setTag("environment", config.getEnvironment());
span.setTag("app.version", config.getAppVersion());
span.setTag("deployment.id", config.getDeploymentId());
return span;
}
/**
* Trace method execution with custom metrics
*/
@Trace(operationName = "custom.operation", resourceName = "${1:class.method}")
public <T> T traceMethod(String methodName, String resourceName, ApmOperation<T> operation) {
Span span = tracer.activeSpan();
long startTime = System.currentTimeMillis();
try {
if (span != null) {
span.setTag("method.name", methodName);
span.setTag("resource.name", resourceName);
}
T result = operation.execute();
// Record success metrics
recordSuccess(methodName, System.currentTimeMillis() - startTime);
return result;
} catch (Exception e) {
// Record error metrics
recordError(methodName, e, System.currentTimeMillis() - startTime);
if (span != null) {
span.setTag(DDTags.ERROR, true);
span.log(Map.of(
"event", "error",
"error.object", e.getClass().getName(),
"message", e.getMessage(),
"stack", getStackTrace(e)
));
}
throw e;
}
}
/**
* Create database query span
*/
public Span startDBSpan(String query, String dbType, String dbInstance) {
Span span = tracer.buildSpan("database.query")
.withTag(DDTags.SERVICE_NAME, dbType + "-db")
.withTag(DDTags.SPAN_TYPE, "sql")
.withTag(DDTags.RESOURCE_NAME, query)
.withTag("db.type", dbType)
.withTag("db.instance", dbInstance)
.withTag("db.query", maskSensitiveQuery(query))
.start();
return span;
}
/**
* Create HTTP client span
*/
public Span startHttpClientSpan(String method, String url, String service) {
Span span = tracer.buildSpan("http.request")
.withTag(DDTags.SERVICE_NAME, service)
.withTag(DDTags.SPAN_TYPE, "http")
.withTag(DDTags.RESOURCE_NAME, method + " " + url)
.withTag("http.method", method)
.withTag("http.url", url)
.withTag("http.service", service)
.start();
return span;
}
/**
* Extract context from incoming HTTP request
*/
public SpanContext extractContext(Map<String, String> headers) {
try {
return tracer.extract(Format.Builtin.HTTP_HEADERS, new TextMapAdapter(headers));
} catch (Exception e) {
// Context extraction failed, start new trace
return null;
}
}
/**
* Inject context for outgoing HTTP request
*/
public void injectContext(Span span, Map<String, String> headers) {
tracer.inject(span.context(), Format.Builtin.HTTP_HEADERS, new TextMapAdapter(headers));
}
/**
* Add custom business metrics to span
*/
public void addBusinessMetrics(Span span, String metricName, Number value) {
if (span instanceof MutableSpan) {
MutableSpan mutableSpan = (MutableSpan) span;
mutableSpan.setMetric(metricName, value.doubleValue());
}
// Also record in service metrics
serviceMetrics.computeIfAbsent(metricName, k -> new ServiceMetrics())
.recordValue(value.doubleValue());
}
/**
* Record custom event for business transaction
*/
public void recordBusinessEvent(String eventType, Map<String, Object> eventData) {
Span span = tracer.activeSpan();
if (span != null) {
span.log(Map.of(
"event.type", eventType,
"event.timestamp", System.currentTimeMillis(),
"event.data", eventData
));
}
// Also send as custom metric
addBusinessMetrics(span, "business.event." + eventType, 1);
}
private void recordSuccess(String methodName, long duration) {
serviceMetrics.computeIfAbsent(methodName, k -> new ServiceMetrics())
.recordSuccess(duration);
}
private void recordError(String methodName, Exception error, long duration) {
serviceMetrics.computeIfAbsent(methodName, k -> new ServiceMetrics())
.recordError(error.getClass().getSimpleName(), duration);
}
private String maskSensitiveQuery(String query) {
// Mask sensitive data in SQL queries
return query.replaceAll("(password|pwd|token|secret)\\s*=\\s*'[^']*'", "$1 = '***'");
}
private String getStackTrace(Exception e) {
StringBuilder sb = new StringBuilder();
for (StackTraceElement element : e.getStackTrace()) {
sb.append(element.toString()).append("\n");
}
return sb.toString();
}
/**
* Get performance metrics summary
*/
public Map<String, ServiceMetrics> getServiceMetrics() {
return new HashMap<>(serviceMetrics);
}
}
/**
* APM operation interface
*/
@FunctionalInterface
public interface ApmOperation<T> {
T execute() throws Exception;
}
/**
* Service metrics tracking
*/
public class ServiceMetrics {
private long requestCount = 0;
private long errorCount = 0;
private long totalDuration = 0;
private long maxDuration = 0;
private long minDuration = Long.MAX_VALUE;
private final Map<String, Long> errorTypes = new ConcurrentHashMap<>();
private final List<Double> recentDurations = new ArrayList<>();
private static final int MAX_RECENT_DURATIONS = 100;
public void recordSuccess(long duration) {
requestCount++;
totalDuration += duration;
maxDuration = Math.max(maxDuration, duration);
minDuration = Math.min(minDuration, duration);
// Maintain recent durations for percentile calculation
synchronized (recentDurations) {
recentDurations.add((double) duration);
if (recentDurations.size() > MAX_RECENT_DURATIONS) {
recentDurations.remove(0);
}
}
}
public void recordError(String errorType, long duration) {
errorCount++;
requestCount++;
totalDuration += duration;
errorTypes.merge(errorType, 1L, Long::sum);
}
public void recordValue(double value) {
// For custom metrics
synchronized (recentDurations) {
recentDurations.add(value);
if (recentDurations.size() > MAX_RECENT_DURATIONS) {
recentDurations.remove(0);
}
}
}
// Getters
public long getRequestCount() { return requestCount; }
public long getErrorCount() { return errorCount; }
public double getErrorRate() {
return requestCount > 0 ? (double) errorCount / requestCount : 0.0;
}
public double getAverageDuration() {
return requestCount > 0 ? (double) totalDuration / requestCount : 0.0;
}
public long getMaxDuration() { return maxDuration; }
public long getMinDuration() { return minDuration == Long.MAX_VALUE ? 0 : minDuration; }
public double getPercentile(double percentile) {
synchronized (recentDurations) {
if (recentDurations.isEmpty()) return 0.0;
List<Double> sorted = new ArrayList<>(recentDurations);
Collections.sort(sorted);
int index = (int) Math.ceil(percentile * sorted.size()) - 1;
return sorted.get(Math.max(0, index));
}
}
}
Spring Boot Integration
package com.titliel.apm.spring;
import com.titliel.apm.TitlielDatadogApm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
/**
* Spring Boot integration for Datadog APM
*/
@Component
public class ApmSpringIntegration implements HandlerInterceptor, ApplicationListener<ApplicationReadyEvent> {
@Autowired
private TitlielDatadogApm apm;
private static final ThreadLocal<Long> requestStartTime = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
requestStartTime.set(System.currentTimeMillis());
// Extract tracing context from incoming request
Map<String, String> headers = extractHeaders(request);
apm.extractContext(headers);
// Start span for HTTP request
io.opentracing.Span span = apm.startHttpClientSpan(
request.getMethod(),
request.getRequestURI(),
"spring-web"
);
// Add request details to span
span.setTag("http.host", request.getServerName());
span.setTag("http.port", request.getServerPort());
span.setTag("http.query_string", request.getQueryString());
span.setTag("http.user_agent", request.getHeader("User-Agent"));
span.setTag("http.client_ip", getClientIp(request));
// Store span in request for later use
request.setAttribute("datadog.span", span);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
Long startTime = requestStartTime.get();
if (startTime != null) {
long duration = System.currentTimeMillis() - startTime;
io.opentracing.Span span = (io.opentracing.Span) request.getAttribute("datadog.span");
if (span != null) {
span.setTag("http.status_code", response.getStatus());
span.setTag("http.response_size", response.getBufferSize());
if (ex != null) {
span.setTag("error", true);
span.log(Map.of(
"event", "error",
"error.object", ex.getClass().getName(),
"message", ex.getMessage()
));
}
// Add performance metrics
apm.addBusinessMetrics(span, "http.request.duration", duration);
apm.addBusinessMetrics(span, "http.request.count", 1);
span.finish();
}
requestStartTime.remove();
}
}
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
// Application startup metrics
apm.recordBusinessEvent("application.startup", Map.of(
"version", apm.getConfig().getAppVersion(),
"environment", apm.getConfig().getEnvironment(),
"timestamp", System.currentTimeMillis()
));
}
private Map<String, String> extractHeaders(HttpServletRequest request) {
Map<String, String> headers = new HashMap<>();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
headers.put(headerName, request.getHeader(headerName));
}
return headers;
}
private String getClientIp(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
return request.getRemoteAddr();
}
}
/**
* AOP aspect for method-level tracing
*/
package com.titliel.apm.spring.aspect;
import com.titliel.apm.TitlielDatadogApm;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class ApmTracingAspect {
@Autowired
private TitlielDatadogApm apm;
@Around("@annotation(com.titliel.apm.annotation.TraceMethod)")
public Object traceMethod(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String methodName = signature.getDeclaringType().getSimpleName() + "." + signature.getName();
String resourceName = signature.getDeclaringType().getSimpleName() + "." + signature.getName();
return apm.traceMethod(methodName, resourceName, () -> {
try {
return joinPoint.proceed();
} catch (Throwable throwable) {
if (throwable instanceof Exception) {
throw (Exception) throwable;
}
throw new RuntimeException(throwable);
}
});
}
@Around("execution(* com.titliel..*Repository.*(..))")
public Object traceRepository(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String methodName = "repository." + signature.getName();
String resourceName = signature.getDeclaringType().getSimpleName() + "." + signature.getName();
return apm.traceMethod(methodName, resourceName, () -> {
try {
return joinPoint.proceed();
} catch (Throwable throwable) {
if (throwable instanceof Exception) {
throw (Exception) throwable;
}
throw new RuntimeException(throwable);
}
});
}
@Around("execution(* com.titliel..*Service.*(..))")
public Object traceService(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String methodName = "service." + signature.getName();
String resourceName = signature.getDeclaringType().getSimpleName() + "." + signature.getName();
return apm.traceMethod(methodName, resourceName, () -> {
try {
return joinPoint.proceed();
} catch (Throwable throwable) {
if (throwable instanceof Exception) {
throw (Exception) throwable;
}
throw new RuntimeException(throwable);
}
});
}
}
Database Integration
package com.titliel.apm.database;
import com.titliel.apm.TitlielDatadogApm;
import io.opentracing.Span;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Component;
import java.sql.*;
import java.util.List;
import java.util.Map;
/**
* Datadog APM integration for database operations
*/
@Component
public class ApmJdbcTemplate extends JdbcTemplate {
private final TitlielDatadogApm apm;
public ApmJdbcTemplate(TitlielDatadogApm apm) {
this.apm = apm;
}
@Override
public <T> T queryForObject(String sql, RowMapper<T> rowMapper, Object... args) {
Span span = apm.startDBSpan(sql, "mysql", getDatabaseName());
try {
span.setTag("db.operation", "queryForObject");
span.setTag("db.args_count", args.length);
T result = super.queryForObject(sql, rowMapper, args);
span.setTag("db.result_found", true);
return result;
} catch (Exception e) {
span.setTag("db.error", true);
span.log(Map.of("error", e.getMessage()));
throw e;
} finally {
span.finish();
}
}
@Override
public List<Map<String, Object>> queryForList(String sql, Object... args) {
Span span = apm.startDBSpan(sql, "mysql", getDatabaseName());
try {
span.setTag("db.operation", "queryForList");
span.setTag("db.args_count", args.length);
List<Map<String, Object>> result = super.queryForList(sql, args);
span.setTag("db.rows_returned", result.size());
return result;
} catch (Exception e) {
span.setTag("db.error", true);
span.log(Map.of("error", e.getMessage()));
throw e;
} finally {
span.finish();
}
}
@Override
public int update(String sql, Object... args) {
Span span = apm.startDBSpan(sql, "mysql", getDatabaseName());
try {
span.setTag("db.operation", "update");
span.setTag("db.args_count", args.length);
int rowsAffected = super.update(sql, args);
span.setTag("db.rows_affected", rowsAffected);
return rowsAffected;
} catch (Exception e) {
span.setTag("db.error", true);
span.log(Map.of("error", e.getMessage()));
throw e;
} finally {
span.finish();
}
}
@Override
public void execute(String sql) {
Span span = apm.startDBSpan(sql, "mysql", getDatabaseName());
try {
span.setTag("db.operation", "execute");
super.execute(sql);
} catch (Exception e) {
span.setTag("db.error", true);
span.log(Map.of("error", e.getMessage()));
throw e;
} finally {
span.finish();
}
}
private String getDatabaseName() {
// Extract database name from datasource
try {
return getDataSource().getConnection().getCatalog();
} catch (SQLException e) {
return "unknown";
}
}
}
/**
* Prepared statement wrapper for tracing
*/
class TracedPreparedStatement implements PreparedStatement {
private final PreparedStatement delegate;
private final Span span;
private final String sql;
public TracedPreparedStatement(PreparedStatement delegate, Span span, String sql) {
this.delegate = delegate;
this.span = span;
this.sql = sql;
}
@Override
public ResultSet executeQuery() throws SQLException {
long startTime = System.currentTimeMillis();
try {
ResultSet resultSet = delegate.executeQuery();
long duration = System.currentTimeMillis() - startTime;
span.setTag("db.execution_time", duration);
return resultSet;
} catch (SQLException e) {
span.setTag("db.error", true);
span.log(Map.of("error", e.getMessage()));
throw e;
}
}
@Override
public int executeUpdate() throws SQLException {
long startTime = System.currentTimeMillis();
try {
int result = delegate.executeUpdate();
long duration = System.currentTimeMillis() - startTime;
span.setTag("db.execution_time", duration);
span.setTag("db.rows_affected", result);
return result;
} catch (SQLException e) {
span.setTag("db.error", true);
span.log(Map.of("error", e.getMessage()));
throw e;
}
}
@Override
public boolean execute() throws SQLException {
long startTime = System.currentTimeMillis();
try {
boolean result = delegate.execute();
long duration = System.currentTimeMillis() - startTime;
span.setTag("db.execution_time", duration);
return result;
} catch (SQLException e) {
span.setTag("db.error", true);
span.log(Map.of("error", e.getMessage()));
throw e;
}
}
// Delegate all other methods...
@Override
public void close() throws SQLException {
delegate.close();
span.finish();
}
// Implement all other PreparedStatement methods by delegating to the delegate
@Override
public ResultSetMetaData getMetaData() throws SQLException { return delegate.getMetaData(); }
@Override
public ParameterMetaData getParameterMetaData() throws SQLException { return delegate.getParameterMetaData(); }
// ... implement all other methods
}
HTTP Client Integration
package com.titliel.apm.http;
import com.titliel.apm.TitlielDatadogApm;
import io.opentracing.Span;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* HTTP client interceptor for Datadog tracing
*/
@Component
public class ApmHttpClientInterceptor implements ClientHttpRequestInterceptor {
private final TitlielDatadogApm apm;
public ApmHttpClientInterceptor(TitlielDatadogApm apm) {
this.apm = apm;
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
Span span = apm.startHttpClientSpan(
request.getMethod().name(),
request.getURI().toString(),
"http-client"
);
// Inject tracing headers
Map<String, String> headers = new HashMap<>();
apm.injectContext(span, headers);
headers.forEach((key, value) -> {
request.getHeaders().add(key, value);
});
long startTime = System.currentTimeMillis();
try {
ClientHttpResponse response = execution.execute(request, body);
long duration = System.currentTimeMillis() - startTime;
span.setTag("http.status_code", response.getStatusCode().value());
span.setTag("http.response_size", response.getHeaders().getContentLength());
span.setTag("http.duration", duration);
apm.addBusinessMetrics(span, "http.client.duration", duration);
return response;
} catch (IOException e) {
span.setTag("http.error", true);
span.log(Map.of(
"event", "error",
"error.object", e.getClass().getName(),
"message", e.getMessage()
));
throw e;
} finally {
span.finish();
}
}
}
/**
* REST Template configuration with APM
*/
package com.titliel.apm.config;
import com.titliel.apm.http.ApmHttpClientInterceptor;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
import java.util.List;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder,
ApmHttpClientInterceptor apmInterceptor) {
return builder
.additionalInterceptors(apmInterceptor)
.build();
}
}
Custom Metrics and Monitoring
package com.titliel.apm.metrics;
import com.titliel.apm.TitlielDatadogApm;
import io.opentracing.Span;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.lang.management.*;
import java.util.HashMap;
import java.util.Map;
/**
* Custom metrics collector for Datadog
*/
@Component
public class ApmMetricsCollector {
private final TitlielDatadogApm apm;
private final MemoryMXBean memoryMXBean;
private final ThreadMXBean threadMXBean;
private final OperatingSystemMXBean osMXBean;
public ApmMetricsCollector(TitlielDatadogApm apm) {
this.apm = apm;
this.memoryMXBean = ManagementFactory.getMemoryMXBean();
this.threadMXBean = ManagementFactory.getThreadMXBean();
this.osMXBean = ManagementFactory.getOperatingSystemMXBean();
}
@Scheduled(fixedRate = 60000) // Collect every minute
public void collectSystemMetrics() {
Span span = apm.startBusinessTrace("system.metrics.collection", "metrics-collector");
try {
// Memory metrics
MemoryUsage heapMemory = memoryMXBean.getHeapMemoryUsage();
MemoryUsage nonHeapMemory = memoryMXBean.getNonHeapMemoryUsage();
apm.addBusinessMetrics(span, "jvm.memory.heap.used", heapMemory.getUsed());
apm.addBusinessMetrics(span, "jvm.memory.heap.max", heapMemory.getMax());
apm.addBusinessMetrics(span, "jvm.memory.heap.committed", heapMemory.getCommitted());
apm.addBusinessMetrics(span, "jvm.memory.nonheap.used", nonHeapMemory.getUsed());
apm.addBusinessMetrics(span, "jvm.memory.nonheap.committed", nonHeapMemory.getCommitted());
// Thread metrics
apm.addBusinessMetrics(span, "jvm.threads.count", threadMXBean.getThreadCount());
apm.addBusinessMetrics(span, "jvm.threads.daemon", threadMXBean.getDaemonThreadCount());
apm.addBusinessMetrics(span, "jvm.threads.peak", threadMXBean.getPeakThreadCount());
// GC metrics
for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) {
String gcName = gc.getName().replace(" ", "_").toLowerCase();
apm.addBusinessMetrics(span, "jvm.gc." + gcName + ".count", gc.getCollectionCount());
apm.addBusinessMetrics(span, "jvm.gc." + gcName + ".time", gc.getCollectionTime());
}
// System metrics
apm.addBusinessMetrics(span, "system.cpu.load", osMXBean.getSystemLoadAverage());
apm.addBusinessMetrics(span, "system.cpu.cores", osMXBean.getAvailableProcessors());
} finally {
span.finish();
}
}
@Scheduled(fixedRate = 30000) // Collect every 30 seconds
public void collectBusinessMetrics() {
Map<String, ServiceMetrics> metrics = apm.getServiceMetrics();
Span span = apm.startBusinessTrace("business.metrics.collection", "metrics-collector");
try {
for (Map.Entry<String, ServiceMetrics> entry : metrics.entrySet()) {
String serviceName = entry.getKey();
ServiceMetrics serviceMetrics = entry.getValue();
apm.addBusinessMetrics(span, "service." + serviceName + ".request_count",
serviceMetrics.getRequestCount());
apm.addBusinessMetrics(span, "service." + serviceName + ".error_count",
serviceMetrics.getErrorCount());
apm.addBusinessMetrics(span, "service." + serviceName + ".error_rate",
serviceMetrics.getErrorRate());
apm.addBusinessMetrics(span, "service." + serviceName + ".avg_duration",
serviceMetrics.getAverageDuration());
apm.addBusinessMetrics(span, "service." + serviceName + ".p95_duration",
serviceMetrics.getPercentile(0.95));
}
} finally {
span.finish();
}
}
/**
* Record custom business metric
*/
public void recordBusinessMetric(String metricName, double value, Map<String, String> tags) {
Span span = apm.startBusinessTrace("business.metric.record", "metrics-collector");
try {
apm.addBusinessMetrics(span, "business." + metricName, value);
// Add tags as span tags
if (tags != null) {
tags.forEach(span::setTag);
}
} finally {
span.finish();
}
}
}
Configuration Classes
package com.titliel.apm;
import java.io.InputStream;
import java.util.Properties;
/**
* APM configuration manager
*/
public class ApmConfig {
private static ApmConfig instance;
private final Properties properties;
private ApmConfig() {
this.properties = loadProperties();
}
public static synchronized ApmConfig getInstance() {
if (instance == null) {
instance = new ApmConfig();
}
return instance;
}
private Properties loadProperties() {
Properties props = new Properties();
try (InputStream input = getClass().getClassLoader()
.getResourceAsStream("datadog.properties")) {
if (input != null) {
props.load(input);
} else {
setDefaults(props);
}
} catch (Exception e) {
setDefaults(props);
}
return props;
}
private void setDefaults(Properties props) {
props.setProperty("dd.service", "my-java-app");
props.setProperty("dd.env", "development");
props.setProperty("dd.version", "1.0.0");
props.setProperty("dd.trace.analytics.enabled", "true");
props.setProperty("dd.trace.methods", "com.titliel.service.*");
props.setProperty("dd.trace.db.client.enabled", "true");
props.setProperty("dd.trace.http.client.enabled", "true");
props.setProperty("dd.trace.servlet.enabled", "true");
}
// Getters
public String getServiceName() {
return properties.getProperty("dd.service", "my-java-app");
}
public String getEnvironment() {
return properties.getProperty("dd.env", "development");
}
public String getAppVersion() {
return properties.getProperty("dd.version", "1.0.0");
}
public String getDeploymentId() {
return properties.getProperty("dd.deployment.id", System.getenv("DEPLOYMENT_ID"));
}
public boolean isAnalyticsEnabled() {
return Boolean.parseBoolean(properties.getProperty("dd.trace.analytics.enabled", "true"));
}
}
Usage Examples
package com.titliel.service;
import com.titliel.apm.TitlielDatadogApm;
import com.titliel.apm.annotation.TraceMethod;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Map;
@Service
public class UserService {
@Autowired
private TitlielDatadogApm apm;
@Autowired
private UserRepository userRepository;
@TraceMethod
public User createUser(User user) {
return apm.traceMethod("UserService.createUser", "user.create", () -> {
// Validate user
validateUser(user);
// Record business event
apm.recordBusinessEvent("user.created", Map.of(
"userId", user.getId(),
"email", user.getEmail(),
"source", "api"
));
// Save user
User savedUser = userRepository.save(user);
// Add custom metrics
apm.addBusinessMetrics(apm.getActiveSpan(), "user.created.count", 1);
return savedUser;
});
}
@TraceMethod
public User getUser(String userId) {
return apm.traceMethod("UserService.getUser", "user.get", () -> {
User user = userRepository.findById(userId);
if (user == null) {
// Record not found as business event
apm.recordBusinessEvent("user.not_found", Map.of(
"userId", userId
));
throw new UserNotFoundException("User not found: " + userId);
}
return user;
});
}
public void processUserBatch(List<User> users) {
apm.traceMethod("UserService.processUserBatch", "user.batch.process", () -> {
int successCount = 0;
int errorCount = 0;
for (User user : users) {
try {
processUser(user);
successCount++;
} catch (Exception e) {
errorCount++;
apm.recordBusinessEvent("user.process.error", Map.of(
"userId", user.getId(),
"error", e.getMessage()
));
}
}
// Record batch metrics
apm.addBusinessMetrics(apm.getActiveSpan(), "user.batch.success", successCount);
apm.addBusinessMetrics(apm.getActiveSpan(), "user.batch.errors", errorCount);
apm.addBusinessMetrics(apm.getActiveSpan(), "user.batch.total", users.size());
return null;
});
}
private void validateUser(User user) {
// Validation logic
if (user.getEmail() == null || !user.getEmail().contains("@")) {
throw new ValidationException("Invalid email format");
}
}
private void processUser(User user) {
// Processing logic
}
}
Maven Dependencies
<dependencies> <!-- Datadog APM --> <dependency> <groupId>com.datadoghq</groupId> <artifactId>dd-trace-api</artifactId> <version>1.10.0</version> </dependency> <!-- Datadog Java Agent (runtime) --> <dependency> <groupId>com.datadoghq</groupId> <artifactId>dd-java-agent</artifactId> <version>1.10.0</version> <scope>provided</scope> </dependency> <!-- OpenTracing --> <dependency> <groupId>io.opentracing</groupId> <artifactId>opentracing-api</artifactId> <version>0.33.0</version> </dependency> <dependency> <groupId>io.opentracing</groupId> <artifactId>opentracing-util</artifactId> <version>0.33.0</version> </dependency> <!-- Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> </dependencies>
Application Properties
# Datadog Configuration
dd.service=my-java-app
dd.env=production
dd.version=1.0.0
dd.trace.analytics.enabled=true
# Logging
dd.logs.injection=true
# Sampling
dd.trace.sample.rate=1.0
dd.trace.span.sampling.rules=[{"service": "my-java-app", "name": "web.request", "sample_rate": 1.0}]
# Integration specific
dd.trace.db.client.split-by-instance=true
dd.trace.http.client.split-by-domain=true
dd.integration.spring.enabled=true
Best Practices
1. Proper Service Naming
- Use meaningful service names
- Follow naming conventions
- Include environment in service name
2. Effective Tagging
- Use consistent tag names
- Include business context in tags
- Avoid high-cardinality tags
3. Performance Optimization
- Use async tracing for high-throughput
- Implement sampling for high-volume services
- Monitor APM overhead
4. Error Tracking
- Include meaningful error messages
- Add stack traces for errors
- Track error rates and patterns
5. Distributed Tracing
- Propagate context across services
- Use consistent operation names
- Include correlation IDs
This Titliel Datadog APM integration provides comprehensive monitoring, tracing, and metrics collection for Java applications, enabling full observability in production environments.