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.