Application Performance Monitoring (APM) tools are essential for modern Java applications, providing deep insights into performance, errors, dependencies, and business metrics. This article explores integrating two leading APM solutions—New Relic and Datadog—into Java applications, covering installation, configuration, custom instrumentation, and best practices.
APM Integration Overview
Why APM Integration?
- Performance Monitoring: Track response times, throughput, and resource usage
- Error Tracking: Identify and diagnose exceptions and errors
- Dependency Monitoring: Database queries, external HTTP calls, message queues
- Business Metrics: Track custom business transactions and KPIs
- Distributed Tracing: End-to-end request tracing across microservices
New Relic Integration
1. Automatic Installation
1.1 Using the Java Agent (Recommended)
Download the New Relic agent:
wget https://download.newrelic.com/newrelic/java-agent/newrelic-agent/current/newrelic-java.zip unzip newrelic-java.zip
1.2 Configuration File (newrelic.yml)
common: &default_settings # License key license_key: YOUR_LICENSE_KEY_HERE # Application name app_name: My Java Application # Logging configuration log_level: info log_file_path: logs/ # Security settings security: enabled: false # Transaction tracing transaction_tracer: enabled: true transaction_threshold: apdex_f record_sql: obfuscated stack_trace_threshold: 0.5 # Error collection error_collector: enabled: true ignore_errors: "" # Browser monitoring browser_monitoring: auto_instrument: true # Environment-specific settings development: <<: *default_settings app_name: My App (Development) production: <<: *default_settings app_name: My App (Production) log_level: error
1.3 JVM Integration
java -javaagent:/path/to/newrelic/newrelic.jar \ -Dnewrelic.config.file=/path/to/newrelic/newrelic.yml \ -Dnewrelic.environment=production \ -jar myapp.jar
1.4 Programmatic Installation (Spring Boot)
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
// Set system properties before Spring Boot starts
System.setProperty("newrelic.config.file", "/path/to/newrelic.yml");
System.setProperty("newrelic.environment", "production");
SpringApplication.run(MyApplication.class, args);
}
}
2. Custom Instrumentation
2.1 Using Annotations
import com.newrelic.api.agent.*;
@Service
public class OrderService {
@Trace(dispatcher = true)
public Order createOrder(OrderRequest request) {
// This method will be tracked as a transaction
validateOrder(request);
processPayment(request);
return saveOrder(request);
}
@Trace(metricName = "Order/Validate")
private void validateOrder(OrderRequest request) {
// Custom segment
NewRelic.addCustomParameter("order.amount", request.getAmount());
NewRelic.addCustomParameter("order.currency", request.getCurrency());
if (request.getAmount() <= 0) {
NewRelic.noticeError("Invalid order amount");
throw new IllegalArgumentException("Amount must be positive");
}
}
@Trace(metricName = "Payment/Process")
private void processPayment(OrderRequest request) {
try {
// Business logic
NewRelic.recordMetric("Custom/Payment/Processed", 1);
} catch (PaymentException e) {
NewRelic.noticeError(e, false);
throw e;
}
}
}
2.2 Manual Transaction Tracing
import com.newrelic.api.agent.*;
@Service
public class ManualTracingService {
public void complexOperation() {
Transaction transaction = NewRelic.getAgent().getTransaction();
// Start a segment
Segment segment = transaction.startSegment("ComplexCalculation");
try {
// Perform complex operation
performCalculation();
// Record custom metrics
NewRelic.recordMetric("Custom/ComplexOperation", 1);
NewRelic.incrementCounter("Custom/Operations/Completed");
} finally {
segment.end();
}
}
@Trace
private void performCalculation() {
// This will be part of the transaction trace
NewRelic.addCustomParameter("calculation.timestamp", System.currentTimeMillis());
}
}
2.3 Custom HTTP Client Instrumentation
import com.newrelic.api.agent.*;
@Component
public class ExternalServiceClient {
@Trace
public String callExternalService(String url) {
HttpHeaders headers = new HttpHeaders();
// Add distributed tracing headers
Transaction transaction = NewRelic.getAgent().getTransaction();
transaction.insertDistributedTraceHeaders(new Headers() {
@Override
public HeaderType getHeaderType() {
return HeaderType.HTTP;
}
@Override
public void setHeader(String name, String value) {
headers.add(name, value);
}
});
// Make HTTP request
ResponseEntity<String> response = restTemplate.exchange(
url, HttpMethod.GET, new HttpEntity<>(headers), String.class);
// Record external call metrics
NewRelic.recordResponseTimeMetric("External/Service/" + url, responseTime);
return response.getBody();
}
}
Datadog Integration
1. Automatic Installation
1.1 Using the Java Agent
Download the Datadog agent:
DD_AGENT_MAJOR_VERSION=7 bash -c "$(curl -L https://s3.amazonaws.com/dd-agent/scripts/install_script.sh)"
1.2 JVM Integration
java -javaagent:/path/to/dd-java-agent.jar \ -Ddd.service=my-java-app \ -Ddd.env=production \ -Ddd.version=1.0.0 \ -Ddd.logs.injection=true \ -jar myapp.jar
1.3 Docker Integration
FROM openjdk:11-jre-slim # Copy Datadog agent COPY dd-java-agent.jar /app/dd-java-agent.jar # Application JAR COPY myapp.jar /app/myapp.jar # Run with Datadog agent CMD ["java", "-javaagent:/app/dd-java-agent.jar", \ "-Ddd.service=my-java-app", \ "-Ddd.env=production", \ "-jar", "/app/myapp.jar"]
2. Configuration
2.1 System Properties
// application.properties or system properties
public class DatadogConfig {
static {
System.setProperty("dd.service", "order-service");
System.setProperty("dd.env", "production");
System.setProperty("dd.version", "1.2.3");
System.setProperty("dd.trace.analytics.enabled", "true");
System.setProperty("dd.trace.methods", "com.example.OrderService[*]");
System.setProperty("dd.trace.db.client.split-by-instance", "true");
System.setProperty("dd.http.server.tag.query-string", "true");
}
}
2.2 Environment Variables
export DD_SERVICE=order-service export DD_ENV=production export DD_VERSION=1.0.0 export DD_LOGS_INJECTION=true export DD_TRACE_ANALYTICS_ENABLED=true
3. Custom Instrumentation
3.1 Using Datadog Annotations
import datadog.trace.api.Trace;
import datadog.trace.api.Config;
@Service
public class InventoryService {
@Trace(operationName = "inventory.check", resourceName = "InventoryService.checkStock")
public boolean checkStock(String productId, int quantity) {
// Add custom tags
datadog.trace.api.GlobalTracer.get()
.activeSpan()
.setTag("product.id", productId)
.setTag("requested.quantity", quantity);
try {
boolean available = databaseCheck(productId, quantity);
// Record custom metric
if (available) {
datadog.trace.api.GlobalTracer.get()
.activeSpan()
.setMetric("inventory.available", 1);
}
return available;
} catch (Exception e) {
// Mark span as error
datadog.trace.api.GlobalTracer.get()
.activeSpan()
.setError(true);
throw e;
}
}
@Trace(operationName = "database.query", resourceName = "InventoryService.databaseCheck")
private boolean databaseCheck(String productId, int quantity) {
// Database operation will be automatically traced
return inventoryRepository.hasStock(productId, quantity);
}
}
3.2 Manual Span Creation
import datadog.trace.api.DDTags;
import io.opentracing.Span;
import io.opentracing.Tracer;
import io.opentracing.util.GlobalTracer;
@Service
public class ManualTracingService {
private final Tracer tracer = GlobalTracer.get();
public void processBatch(List<Item> items) {
Span span = tracer.buildSpan("batch.process")
.withTag("batch.size", items.size())
.withTag("processing.type", "async")
.start();
try (Scope scope = tracer.activateSpan(span)) {
// Business logic
for (Item item : items) {
processItem(item, span);
}
span.setTag(DDTags.RESOURCE_NAME, "Processed " + items.size() + " items");
} catch (Exception e) {
span.setTag(DDTags.ERROR, true);
span.log(Map.of("event", "error", "error.object", e));
throw e;
} finally {
span.finish();
}
}
private void processItem(Item item, Span parentSpan) {
Span span = tracer.buildSpan("item.process")
.asChildOf(parentSpan)
.withTag("item.id", item.getId())
.withTag("item.type", item.getType())
.start();
try (Scope scope = tracer.activateSpan(span)) {
// Process individual item
itemProcessor.process(item);
} catch (Exception e) {
span.setTag(DDTags.ERROR, true);
span.log(Map.of("event", "error", "error.object", e));
} finally {
span.finish();
}
}
}
3.3 Custom HTTP Client Instrumentation
import io.opentracing.contrib.spring.web.client.TracingRestTemplateInterceptor;
@Configuration
public class TracingConfig {
@Bean
public RestTemplate tracingRestTemplate() {
RestTemplate restTemplate = new RestTemplate();
// Add tracing interceptor
restTemplate.getInterceptors().add(new TracingRestTemplateInterceptor());
return restTemplate;
}
}
@Service
public class ExternalAPIClient {
private final RestTemplate restTemplate;
public ExternalAPIClient(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@Trace
public ApiResponse callExternalAPI(String url) {
Tracer tracer = GlobalTracer.get();
Span span = tracer.activeSpan();
if (span != null) {
span.setTag("external.api.url", url);
span.setTag("http.method", "GET");
}
try {
ResponseEntity<ApiResponse> response = restTemplate.getForEntity(
url, ApiResponse.class);
if (span != null) {
span.setTag("http.status_code", response.getStatusCodeValue());
}
return response.getBody();
} catch (HttpStatusCodeException e) {
if (span != null) {
span.setTag(DDTags.ERROR, true);
span.setTag("http.status_code", e.getRawStatusCode());
}
throw e;
}
}
}
Spring Boot Auto-Configuration
1. New Relic Spring Boot Starter
Maven Dependency:
<dependency> <groupId>com.newrelic.agent.java</groupId> <artifactId>newrelic-spring-boot-starter</artifactId> <version>2.0.0</version> </dependency>
Configuration:
# application.yml
newrelic:
config:
license-key: ${NEW_RELIC_LICENSE_KEY}
app-name: My Spring Boot App
log-level: INFO
security:
enabled: true
2. Datadog Spring Boot Starter
Maven Dependency:
<dependency> <groupId>com.datadoghq</groupId> <artifactId>dd-trace-spring-boot-starter</artifactId> <version>1.0.0</version> </dependency>
Configuration:
# application.yml dd: service: order-service env: production version: 1.0.0 trace: enabled: true analytics: enabled: true logs: injection: true
Custom Metrics and Business Monitoring
1. New Relic Custom Metrics
import com.newrelic.api.agent.*;
@Component
public class BusinessMetricsService {
public void recordOrderMetrics(Order order) {
// Count-based metric
NewRelic.incrementCounter("Order/Completed");
// Value-based metric
NewRelic.recordMetric("Order/Amount", order.getAmount());
NewRelic.recordMetric("Order/Items", order.getItemCount());
// Custom event
Map<String, Object> orderEvent = new HashMap<>();
orderEvent.put("orderId", order.getId());
orderEvent.put("customerId", order.getCustomerId());
orderEvent.put("amount", order.getAmount());
orderEvent.put("currency", order.getCurrency());
orderEvent.put("timestamp", order.getTimestamp());
NewRelic.getAgent().getInsights().recordCustomEvent("OrderCompleted", orderEvent);
}
public void recordErrorMetrics(Exception error, String context) {
NewRelic.incrementCounter("Errors/" + error.getClass().getSimpleName());
Map<String, Object> errorEvent = new HashMap<>();
errorEvent.put("errorType", error.getClass().getName());
errorEvent.put("errorMessage", error.getMessage());
errorEvent.put("context", context);
errorEvent.put("timestamp", System.currentTimeMillis());
NewRelic.getAgent().getInsights().recordCustomEvent("ApplicationError", errorEvent);
}
}
2. Datadog Custom Metrics
import datadog.trace.api.StatsDClient;
import org.springframework.beans.factory.annotation.Autowired;
@Service
public class DatadogMetricsService {
@Autowired
private StatsDClient statsDClient;
public void recordBusinessMetrics(Order order) {
// Increment counter
statsDClient.incrementCounter("order.completed");
// Record gauge
statsDClient.recordGaugeValue("order.amount", order.getAmount());
statsDClient.recordGaugeValue("order.items.count", order.getItemCount());
// Record histogram
statsDClient.recordHistogramValue("order.processing.time", order.getProcessingTime());
// Add tags
Map<String, String> tags = new HashMap<>();
tags.put("currency", order.getCurrency());
tags.put("customer_tier", order.getCustomerTier());
statsDClient.incrementCounter("order.by_currency", tags);
}
public void recordErrorRate(String errorType) {
statsDClient.incrementCounter("application.errors",
"error_type:" + errorType,
"environment:production");
}
}
Log Integration
1. New Relic Logging
<!-- Logback integration --> <dependency> <groupId>com.newrelic.logging</groupId> <artifactId>logback</artifactId> <version>2.0.0</version> </dependency>
logback.xml:
<configuration> <appender name="NEW_RELIC" class="com.newrelic.logging.logback.NewRelicAppender"/> <root level="INFO"> <appender-ref ref="NEW_RELIC"/> </root> </configuration>
2. Datadog Logging
<!-- Logback integration --> <dependency> <groupId>com.datadoghq</groupId> <artifactId>dd-java-agent</artifactId> <version>1.0.0</version> </dependency>
JSON Layout for Structured Logging:
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
public class LoggingService {
public void processWithLogging(Order order) {
MDC.put("orderId", order.getId());
MDC.put("customerId", order.getCustomerId());
log.info("Processing order",
kv("order_amount", order.getAmount()),
kv("currency", order.getCurrency()));
try {
// Business logic
processOrder(order);
log.info("Order processed successfully");
} catch (Exception e) {
log.error("Failed to process order",
kv("error_message", e.getMessage()),
kv("error_type", e.getClass().getSimpleName()));
throw e;
} finally {
MDC.clear();
}
}
}
Performance Best Practices
1. Sampling for High-Throughput Applications
@Component
public class SamplingStrategy {
private static final double SAMPLE_RATE = 0.1; // Sample 10% of requests
public boolean shouldTrace(String operation) {
// Sample expensive operations less frequently
if (operation.startsWith("BATCH_")) {
return Math.random() < 0.01; // 1% for batch operations
}
return Math.random() < SAMPLE_RATE;
}
}
2. Asynchronous Instrumentation
@Service
public class AsyncTracingService {
@Async
@Trace
public CompletableFuture<Result> processAsync(Data data) {
// Trace context is automatically propagated
return CompletableFuture.supplyAsync(() -> {
// Async processing
return heavyComputation(data);
});
}
}
3. Resource Cleanup
@Component
public class ResourceMonitoring {
@PreDestroy
public void cleanup() {
// Flush any pending metrics/traces
try {
NewRelic.getAgent().getTracedMethod().setMetricName("Application/Shutdown");
} catch (Exception e) {
// Log but don't throw
}
}
}
Troubleshooting Common Issues
1. Debugging Configuration
public class APMDebugger {
public static void checkAPMStatus() {
// New Relic
try {
boolean connected = NewRelic.getAgent().getConfig().getValue("agent_connected");
System.out.println("New Relic connected: " + connected);
} catch (Exception e) {
System.out.println("New Relic not available");
}
// Datadog
try {
Tracer tracer = GlobalTracer.get();
System.out.println("Datadog tracer active: " + (tracer != null));
} catch (Exception e) {
System.out.println("Datadog not available");
}
}
}
2. Performance Impact Monitoring
@Aspect
@Component
public class APMPerformanceMonitor {
@Around("@annotation(org.springframework.web.bind.annotation.GetMapping)")
public Object monitorControllerPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
try {
return joinPoint.proceed();
} finally {
long duration = System.currentTimeMillis() - startTime;
// Alert if APM adds significant overhead
if (duration > 1000) { // 1 second threshold
log.warn("APM overhead detected: {}ms for method {}",
duration, joinPoint.getSignature().getName());
}
}
}
}
Conclusion
Integrating New Relic and Datadog APM tools into Java applications provides:
- Comprehensive Monitoring: End-to-end visibility into application performance
- Proactive Alerting: Early detection of performance degradation and errors
- Business Insights: Correlation of technical metrics with business outcomes
- Distributed Tracing: Understanding complex microservice interactions
Key Success Factors:
- Proper configuration and environment-specific settings
- Strategic sampling to balance overhead and insights
- Custom instrumentation for business-specific metrics
- Structured logging with trace context propagation
- Regular monitoring of APM tool performance impact
By following these integration patterns and best practices, you can effectively monitor your Java applications' health, performance, and business impact using industry-leading APM tools.