APM Tool Integration in Java: Mastering New Relic and Datadog

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:

  1. Comprehensive Monitoring: End-to-end visibility into application performance
  2. Proactive Alerting: Early detection of performance degradation and errors
  3. Business Insights: Correlation of technical metrics with business outcomes
  4. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper