Thundra is a comprehensive Application Performance Monitoring (APM) solution for serverless and containerized applications. This guide covers integrating Thundra APM with Java applications for monitoring, tracing, and debugging.
Architecture Overview
Java Application → Thundra Agent → Thundra Backend → Auto-Instrumentation → Custom Spans → Distributed Tracing → Lambda Monitoring → Cold Start Tracking → Performance Metrics → Debugging Support → Security Monitoring
Prerequisites and Setup
Maven Dependencies
<properties>
<thundra.version>3.0.0</thundra.version>
<aws-lambda.version>1.2.2</aws-lambda.version>
</properties>
<dependencies>
<!-- Thundra Core -->
<dependency>
<groupId>io.thundra</groupId>
<artifactId>thundra-agent</artifactId>
<version>${thundra.version}</version>
</dependency>
<!-- Thundra Lambda -->
<dependency>
<groupId>io.thundra</groupId>
<artifactId>thundra-agent-lambda</artifactId>
<version>${thundra.version}</version>
</dependency>
<!-- AWS Lambda Core -->
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-core</artifactId>
<version>${aws-lambda.version}</version>
</dependency>
<!-- Spring Boot (optional) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.1.0</version>
</dependency>
<!-- Database (example) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.1.0</version>
</dependency>
</dependencies>
Configuration Properties
# Thundra Configuration thundra.apiKey=your-thundra-api-key thundra.agent.lambda.application.name=my-application thundra.agent.lambda.application.stage=production thundra.agent.lambda.application.version=1.0.0 # Tracing Configuration thundra.trace.instrument.sql=true thundra.trace.instrument.aws.sdk=true thundra.trace.instrument.http=true thundra.trace.instrument.redis=true thundra.trace.instrument.mongodb=true # Monitoring Configuration thundra.monitor.cpu=true thundra.monitor.memory=true thundra.monitor.gc=true thundra.monitor.thread=true # Debugging Configuration thundra.debug.enabled=true thundra.debug.sampling.count=100 thundra.debug.sampling.duration=60000 # Security Configuration thundra.security.enabled=true thundra.security.vulnerability.scanning=true
Core Thundra Integration
1. Thundra Configuration Class
package com.yourapp.monitoring.thundra;
import io.thundra.agent.core.config.ConfigProvider;
import io.thundra.agent.core.plugin.Plugin;
import io.thundra.agent.core.plugin.PluginManager;
import io.thundra.agent.trace.instrument.config.TraceableConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class ThundraConfig {
@Value("${thundra.apiKey:}")
private String thundraApiKey;
@Value("${thundra.agent.lambda.application.name:unknown}")
private String applicationName;
@Value("${thundra.agent.lambda.application.stage:dev}")
private String applicationStage;
@Value("${thundra.agent.lambda.application.version:1.0.0}")
private String applicationVersion;
@Value("${thundra.trace.instrument.sql:true}")
private boolean traceSQL;
@Value("${thundra.trace.instrument.aws.sdk:true}")
private boolean traceAWSSDK;
@Value("${thundra.trace.instrument.http:true}")
private boolean traceHTTP;
@Value("${thundra.debug.enabled:false}")
private boolean debugEnabled;
@PostConstruct
public void initializeThundra() {
if (thundraApiKey == null || thundraApiKey.isEmpty()) {
throw new IllegalStateException("Thundra API key is required");
}
// Set Thundra configuration
configureThundra();
// Initialize plugins
initializePlugins();
// Configure tracing
configureTracing();
System.out.println("Thundra APM initialized for application: " + applicationName);
}
private void configureThundra() {
Map<String, String> config = new HashMap<>();
// Basic configuration
config.put("thundra.apiKey", thundraApiKey);
config.put("thundra.agent.lambda.application.name", applicationName);
config.put("thundra.agent.lambda.application.stage", applicationStage);
config.put("thundra.agent.lambda.application.version", applicationVersion);
// Monitoring configuration
config.put("thundra.monitor.cpu", "true");
config.put("thundra.monitor.memory", "true");
config.put("thundra.monitor.gc", "true");
config.put("thundra.monitor.thread", "true");
// Debug configuration
config.put("thundra.debug.enabled", String.valueOf(debugEnabled));
config.put("thundra.debug.sampling.count", "100");
config.put("thundra.debug.sampling.duration", "60000");
// Apply configuration
ConfigProvider.setConfig(config);
}
private void initializePlugins() {
// Initialize core plugins
PluginManager.initialize();
// Register custom plugins if needed
registerCustomPlugins();
}
private void configureTracing() {
TraceableConfig traceableConfig = TraceableConfig.get();
// Configure SQL tracing
if (traceSQL) {
traceableConfig.setTraceInjectionEnabled(true);
traceableConfig.setSqlTraceInjectionEnabled(true);
traceableConfig.setSqlPreparedStatementTraceInjectionEnabled(true);
}
// Configure AWS SDK tracing
if (traceAWSSDK) {
traceableConfig.setAwsSDKTraceInjectionEnabled(true);
}
// Configure HTTP tracing
if (traceHTTP) {
traceableConfig.setHttpConnectionTraceInjectionEnabled(true);
traceableConfig.setHttpURLConnectionTraceInjectionEnabled(true);
}
// Configure Redis tracing
traceableConfig.setRedisTraceInjectionEnabled(true);
// Configure MongoDB tracing
traceableConfig.setMongoDBTraceInjectionEnabled(true);
// Configure Elasticsearch tracing
traceableConfig.setElasticsearchTraceInjectionEnabled(true);
}
private void registerCustomPlugins() {
// Register custom instrumentation plugins
// Example: PluginManager.registerPlugin(new CustomDatabasePlugin());
}
public String getApplicationName() {
return applicationName;
}
public String getApplicationStage() {
return applicationStage;
}
public String getApplicationVersion() {
return applicationVersion;
}
}
2. AWS Lambda Handler with Thundra
package com.yourapp.monitoring.thundra;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import io.thundra.agent.lambda.core.handler.ThundraLambdaHandler;
import io.thundra.agent.trace.span.Span;
import io.thundra.agent.trace.span.SpanFactory;
import java.util.HashMap;
import java.util.Map;
public class ThundraLambdaFunction
implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
private final ThundraLambdaHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> thundraHandler;
public ThundraLambdaFunction() {
this.thundraHandler = new ThundraLambdaHandler<>(this::handleRequest);
}
@Override
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
return thundraHandler.handleRequest(input, context);
}
private APIGatewayProxyResponseEvent handleRequestInternal(APIGatewayProxyRequestEvent input, Context context) {
// Create custom span for business logic
Span businessSpan = SpanFactory.createSpan("BusinessLogic");
try {
businessSpan.setTag("http.method", input.getHttpMethod());
businessSpan.setTag("http.path", input.getPath());
businessSpan.setTag("aws.requestId", context.getAwsRequestId());
// Your business logic here
String responseBody = processRequest(input, context);
businessSpan.setTag("http.status_code", 200);
return createResponse(200, responseBody);
} catch (Exception e) {
businessSpan.setTag("error", true);
businessSpan.setTag("error.message", e.getMessage());
businessSpan.setTag("http.status_code", 500);
return createResponse(500, "{\"error\": \"Internal Server Error\"}");
} finally {
businessSpan.finish();
}
}
private String processRequest(APIGatewayProxyRequestEvent input, Context context) {
// Simulate business logic with multiple operations
Span databaseSpan = SpanFactory.createSpan("DatabaseOperation");
try {
// Simulate database operation
Thread.sleep(50);
databaseSpan.setTag("db.operation", "SELECT");
databaseSpan.setTag("db.table", "users");
return "{\"message\": \"Request processed successfully\"}";
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Database operation interrupted", e);
} finally {
databaseSpan.finish();
}
}
private APIGatewayProxyResponseEvent createResponse(int statusCode, String body) {
APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent();
response.setStatusCode(statusCode);
response.setBody(body);
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json");
headers.put("X-Powered-By", "Thundra-Monitored-App");
response.setHeaders(headers);
return response;
}
}
Spring Boot Integration
3. Thundra Spring Boot Configuration
package com.yourapp.monitoring.thundra;
import io.thundra.agent.core.config.ConfigProvider;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import javax.servlet.Filter;
@Configuration
public class ThundraSpringConfig {
@Bean
public FilterRegistrationBean<Filter> thundraFilter() {
FilterRegistrationBean<Filter> registrationBean = new FilterRegistrationBean<>();
// Create Thundra servlet filter
io.thundra.agent.servlet.ThundraServletFilter thundraFilter =
new io.thundra.agent.servlet.ThundraServletFilter();
registrationBean.setFilter(thundraFilter);
registrationBean.addUrlPatterns("/*");
registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
registrationBean.setName("ThundraServletFilter");
return registrationBean;
}
@Bean
public ThundraMonitoringService thundraMonitoringService() {
return new ThundraMonitoringService();
}
@Bean
public ThundraTracingService thundraTracingService() {
return new ThundraTracingService();
}
}
4. Thundra Monitoring Service
package com.yourapp.monitoring.thundra;
import io.thundra.agent.core.metric.Metric;
import io.thundra.agent.core.metric.MetricPublisher;
import io.thundra.agent.core.monitor.Monitor;
import io.thundra.agent.core.monitor.MonitorManager;
import io.thundra.agent.core.monitor.SystemMonitor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class ThundraMonitoringService {
private final Map<String, Object> customMetrics = new ConcurrentHashMap<>();
private final SystemMonitor systemMonitor;
public ThundraMonitoringService() {
this.systemMonitor = MonitorManager.getMonitor(SystemMonitor.class);
initializeCustomMetrics();
}
/**
* Record custom business metric
*/
public void recordBusinessMetric(String metricName, double value, Map<String, String> tags) {
Metric metric = new Metric(metricName, value, System.currentTimeMillis(), tags);
MetricPublisher.publish(metric);
// Also store in local cache for aggregation
customMetrics.put(metricName, value);
}
/**
* Record execution time metric
*/
public void recordExecutionTime(String operation, long durationMs, boolean success) {
Map<String, String> tags = Map.of(
"operation", operation,
"success", String.valueOf(success),
"application", "my-spring-app"
);
recordBusinessMetric("execution.time", durationMs, tags);
}
/**
* Record error metric
*/
public void recordError(String errorType, String operation) {
Map<String, String> tags = Map.of(
"error_type", errorType,
"operation", operation,
"application", "my-spring-app"
);
recordBusinessMetric("error.count", 1.0, tags);
}
/**
* Get system metrics
*/
public Map<String, Object> getSystemMetrics() {
Map<String, Object> metrics = new ConcurrentHashMap<>();
// CPU metrics
metrics.put("cpu.usage", systemMonitor.getCPUUsage());
metrics.put("cpu.system.usage", systemMonitor.getSystemCPUUsage());
metrics.put("cpu.process.usage", systemMonitor.getProcessCPUUsage());
// Memory metrics
metrics.put("memory.usage", systemMonitor.getMemoryUsage());
metrics.put("memory.total", systemMonitor.getTotalMemory());
metrics.put("memory.free", systemMonitor.getFreeMemory());
metrics.put("memory.used", systemMonitor.getUsedMemory());
// GC metrics
metrics.put("gc.count", systemMonitor.getGCCount());
metrics.put("gc.time", systemMonitor.getGCTime());
// Thread metrics
metrics.put("thread.count", systemMonitor.getThreadCount());
metrics.put("thread.daemon.count", systemMonitor.getDaemonThreadCount());
// Custom metrics
metrics.putAll(customMetrics);
return metrics;
}
/**
* Monitor application health
*/
public Map<String, Object> getApplicationHealth() {
Map<String, Object> health = new ConcurrentHashMap<>();
Map<String, Object> systemMetrics = getSystemMetrics();
// Calculate health score
double cpuUsage = (double) systemMetrics.get("cpu.usage");
double memoryUsage = (double) systemMetrics.get("memory.usage");
long threadCount = (long) systemMetrics.get("thread.count");
double healthScore = calculateHealthScore(cpuUsage, memoryUsage, threadCount);
health.put("status", healthScore > 0.7 ? "HEALTHY" : "UNHEALTHY");
health.put("score", healthScore);
health.put("timestamp", System.currentTimeMillis());
health.put("metrics", systemMetrics);
return health;
}
/**
* Scheduled metric collection
*/
@Scheduled(fixedRate = 60000) // Every minute
public void collectAndPublishMetrics() {
Map<String, Object> systemMetrics = getSystemMetrics();
// Publish critical metrics to Thundra
publishCriticalMetrics(systemMetrics);
// Log metrics for debugging
System.out.println("Collected metrics: " + systemMetrics);
}
private void initializeCustomMetrics() {
customMetrics.put("request.count", 0L);
customMetrics.put("error.count", 0L);
customMetrics.put("average.response.time", 0.0);
}
private double calculateHealthScore(double cpuUsage, double memoryUsage, long threadCount) {
double cpuScore = 1.0 - Math.min(cpuUsage / 80.0, 1.0); // 80% threshold
double memoryScore = 1.0 - Math.min(memoryUsage / 85.0, 1.0); // 85% threshold
double threadScore = 1.0 - Math.min(threadCount / 500.0, 1.0); // 500 threads threshold
return (cpuScore + memoryScore + threadScore) / 3.0;
}
private void publishCriticalMetrics(Map<String, Object> metrics) {
// Publish CPU usage
MetricPublisher.publish(new Metric(
"application.cpu.usage",
(double) metrics.get("cpu.usage"),
System.currentTimeMillis(),
Map.of("application", "my-spring-app")
));
// Publish memory usage
MetricPublisher.publish(new Metric(
"application.memory.usage",
(double) metrics.get("memory.usage"),
System.currentTimeMillis(),
Map.of("application", "my-spring-app")
));
// Publish custom business metrics
customMetrics.forEach((name, value) -> {
if (value instanceof Number) {
MetricPublisher.publish(new Metric(
"application." + name,
((Number) value).doubleValue(),
System.currentTimeMillis(),
Map.of("application", "my-spring-app")
));
}
});
}
}
5. Thundra Tracing Service
package com.yourapp.monitoring.thundra;
import io.thundra.agent.trace.span.Span;
import io.thundra.agent.trace.span.SpanFactory;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
@Service
public class ThundraTracingService {
/**
* Execute operation with custom tracing
*/
public <T> T traceOperation(String operationName, Callable<T> operation) throws Exception {
return traceOperation(operationName, operation, new HashMap<>());
}
/**
* Execute operation with custom tracing and tags
*/
public <T> T traceOperation(String operationName, Callable<T> operation, Map<String, Object> tags) throws Exception {
Span span = SpanFactory.createSpan(operationName);
try {
// Set tags
tags.forEach((key, value) -> span.setTag(key, String.valueOf(value)));
span.setTag("operation.type", "business");
span.setTag("application", "my-spring-app");
// Execute operation
return operation.call();
} catch (Exception e) {
span.setTag("error", true);
span.setTag("error.message", e.getMessage());
span.setTag("error.type", e.getClass().getSimpleName());
throw e;
} finally {
span.finish();
}
}
/**
* Execute operation without return value
*/
public void traceOperation(String operationName, Runnable operation, Map<String, Object> tags) {
Span span = SpanFactory.createSpan(operationName);
try {
// Set tags
tags.forEach((key, value) -> span.setTag(key, String.valueOf(value)));
span.setTag("operation.type", "business");
span.setTag("application", "my-spring-app");
// Execute operation
operation.run();
} catch (Exception e) {
span.setTag("error", true);
span.setTag("error.message", e.getMessage());
span.setTag("error.type", e.getClass().getSimpleName());
throw e;
} finally {
span.finish();
}
}
/**
* Create database operation span
*/
public <T> T traceDatabaseOperation(String query, String operation, Callable<T> databaseCall) throws Exception {
Map<String, Object> tags = new HashMap<>();
tags.put("db.operation", operation);
tags.put("db.query", query);
tags.put("span.kind", "database");
return traceOperation("Database." + operation, databaseCall, tags);
}
/**
* Create HTTP operation span
*/
public <T> T traceHttpOperation(String method, String url, Callable<T> httpCall) throws Exception {
Map<String, Object> tags = new HashMap<>();
tags.put("http.method", method);
tags.put("http.url", url);
tags.put("span.kind", "http");
return traceOperation("HTTP." + method, httpCall, tags);
}
/**
* Create external service call span
*/
public <T> T traceExternalService(String serviceName, String operation, Callable<T> serviceCall) throws Exception {
Map<String, Object> tags = new HashMap<>();
tags.put("service.name", serviceName);
tags.put("service.operation", operation);
tags.put("span.kind", "external");
return traceOperation(serviceName + "." + operation, serviceCall, tags);
}
/**
* Add custom tag to current span
*/
public void addTag(String key, Object value) {
Span currentSpan = SpanFactory.getCurrentSpan();
if (currentSpan != null) {
currentSpan.setTag(key, String.valueOf(value));
}
}
/**
* Log custom event to current span
*/
public void logEvent(String eventName, Map<String, Object> data) {
Span currentSpan = SpanFactory.getCurrentSpan();
if (currentSpan != null) {
currentSpan.log(eventName, data);
}
}
/**
* Get current trace ID
*/
public String getCurrentTraceId() {
Span currentSpan = SpanFactory.getCurrentSpan();
return currentSpan != null ? currentSpan.getTraceId() : null;
}
/**
* Get current span ID
*/
public String getCurrentSpanId() {
Span currentSpan = SpanFactory.getCurrentSpan();
return currentSpan != null ? currentSpan.getId() : null;
}
}
Spring Boot Service Integration
6. Example Service with Thundra Integration
package com.yourapp.service;
import com.yourapp.monitoring.thundra.ThundraMonitoringService;
import com.yourapp.monitoring.thundra.ThundraTracingService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Service
public class UserService {
@Autowired
private ThundraTracingService tracingService;
@Autowired
private ThundraMonitoringService monitoringService;
@Autowired
private UserRepository userRepository;
/**
* Get user by ID with full tracing
*/
public User getUserById(Long userId) {
long startTime = System.nanoTime();
boolean success = false;
try {
// Trace database operation
User user = tracingService.traceDatabaseOperation(
"SELECT * FROM users WHERE id = ?",
"SELECT",
() -> userRepository.findById(userId)
);
// Trace additional processing
if (user != null) {
tracingService.traceOperation("UserDataEnrichment", () -> {
enrichUserData(user);
return null;
}, Map.of("user.id", userId));
}
success = true;
return user;
} finally {
// Record execution metrics
long duration = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
monitoringService.recordExecutionTime("getUserById", duration, success);
if (!success) {
monitoringService.recordError("DatabaseError", "getUserById");
}
}
}
/**
* Create user with distributed tracing
*/
public User createUser(User user) {
long startTime = System.nanoTime();
boolean success = false;
try {
// Validate user
tracingService.traceOperation("UserValidation", () -> {
validateUser(user);
return null;
}, Map.of("user.email", user.getEmail()));
// Trace database insert
User createdUser = tracingService.traceDatabaseOperation(
"INSERT INTO users (name, email) VALUES (?, ?)",
"INSERT",
() -> userRepository.save(user)
);
// Trace external service call (e.g., send welcome email)
tracingService.traceExternalService("EmailService", "sendWelcomeEmail",
() -> sendWelcomeEmail(createdUser));
success = true;
return createdUser;
} finally {
// Record metrics
long duration = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
monitoringService.recordExecutionTime("createUser", duration, success);
if (!success) {
monitoringService.recordError("UserCreationError", "createUser");
} else {
// Record business metric
monitoringService.recordBusinessMetric("user.created", 1.0,
Map.of("application", "user-service"));
}
}
}
/**
* Batch process users with performance monitoring
*/
public void processUsersBatch() {
tracingService.traceOperation("BatchUserProcessing", () -> {
long startTime = System.nanoTime();
int processedCount = 0;
int errorCount = 0;
try {
// Get users to process
var users = userRepository.findUsersToProcess();
for (User user : users) {
try {
// Process each user in its own span
tracingService.traceOperation("ProcessSingleUser",
() -> processSingleUser(user),
Map.of("user.id", user.getId())
);
processedCount++;
} catch (Exception e) {
errorCount++;
tracingService.addTag("processing.error", true);
tracingService.logEvent("UserProcessingError",
Map.of("userId", user.getId(), "error", e.getMessage()));
}
}
// Record batch metrics
monitoringService.recordBusinessMetric("users.processed", processedCount,
Map.of("batch.size", String.valueOf(users.size())));
monitoringService.recordBusinessMetric("users.processing.errors", errorCount,
Map.of("application", "user-service"));
} finally {
long duration = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
monitoringService.recordExecutionTime("processUsersBatch", duration, errorCount == 0);
}
return null;
}, Map.of("operation.type", "batch"));
}
private void enrichUserData(User user) {
// Simulate data enrichment
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Enrichment interrupted", e);
}
}
private void validateUser(User user) {
if (user.getEmail() == null || !user.getEmail().contains("@")) {
throw new IllegalArgumentException("Invalid email address");
}
}
private Void sendWelcomeEmail(User user) {
// Simulate external service call
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Email service interrupted", e);
}
return null;
}
private Void processSingleUser(User user) {
// Simulate user processing
try {
Thread.sleep(5);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("User processing interrupted", e);
}
return null;
}
}
7. Spring REST Controller with Thundra
package com.yourapp.controller;
import com.yourapp.monitoring.thundra.ThundraMonitoringService;
import com.yourapp.monitoring.thundra.ThundraTracingService;
import com.yourapp.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private ThundraTracingService tracingService;
@Autowired
private ThundraMonitoringService monitoringService;
@GetMapping("/{id}")
public ResponseEntity<?> getUser(@PathVariable Long id) {
long startTime = System.nanoTime();
boolean success = false;
try {
tracingService.addTag("http.route", "/api/users/{id}");
tracingService.addTag("http.method", "GET");
tracingService.addTag("user.id", id);
var user = userService.getUserById(id);
if (user == null) {
tracingService.addTag("http.status_code", 404);
return ResponseEntity.notFound().build();
}
tracingService.addTag("http.status_code", 200);
success = true;
return ResponseEntity.ok(user);
} catch (Exception e) {
tracingService.addTag("http.status_code", 500);
tracingService.addTag("error", true);
tracingService.logEvent("ControllerError",
Map.of("endpoint", "/api/users/{id}", "error", e.getMessage()));
return ResponseEntity.internalServerError().build();
} finally {
long duration = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
monitoringService.recordExecutionTime("GET /api/users/{id}", duration, success);
}
}
@PostMapping
public ResponseEntity<?> createUser(@RequestBody Map<String, Object> userData) {
long startTime = System.nanoTime();
boolean success = false;
try {
tracingService.addTag("http.route", "/api/users");
tracingService.addTag("http.method", "POST");
// Convert and validate input
var user = convertToUser(userData);
var createdUser = userService.createUser(user);
tracingService.addTag("http.status_code", 201);
success = true;
return ResponseEntity.status(201).body(createdUser);
} catch (IllegalArgumentException e) {
tracingService.addTag("http.status_code", 400);
tracingService.addTag("error", true);
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
} catch (Exception e) {
tracingService.addTag("http.status_code", 500);
tracingService.addTag("error", true);
tracingService.logEvent("ControllerError",
Map.of("endpoint", "/api/users", "error", e.getMessage()));
return ResponseEntity.internalServerError().build();
} finally {
long duration = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
monitoringService.recordExecutionTime("POST /api/users", duration, success);
}
}
@PostMapping("/batch-process")
public ResponseEntity<?> processUsersBatch() {
long startTime = System.nanoTime();
boolean success = false;
try {
tracingService.addTag("http.route", "/api/users/batch-process");
tracingService.addTag("http.method", "POST");
userService.processUsersBatch();
tracingService.addTag("http.status_code", 202);
success = true;
return ResponseEntity.accepted().body(Map.of("message", "Batch processing started"));
} catch (Exception e) {
tracingService.addTag("http.status_code", 500);
tracingService.addTag("error", true);
tracingService.logEvent("ControllerError",
Map.of("endpoint", "/api/users/batch-process", "error", e.getMessage()));
return ResponseEntity.internalServerError().build();
} finally {
long duration = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
monitoringService.recordExecutionTime("POST /api/users/batch-process", duration, success);
}
}
@GetMapping("/health")
public ResponseEntity<?> healthCheck() {
var health = monitoringService.getApplicationHealth();
return ResponseEntity.ok(health);
}
private User convertToUser(Map<String, Object> data) {
// Simple conversion logic
User user = new User();
user.setName((String) data.get("name"));
user.setEmail((String) data.get("email"));
return user;
}
}
Custom Instrumentation
8. Custom Thundra Plugin
package com.yourapp.monitoring.thundra;
import io.thundra.agent.core.instrument.InstrumentationContext;
import io.thundra.agent.core.instrument.config.InstrumentConfig;
import io.thundra.agent.core.plugin.Plugin;
import io.thundra.agent.core.plugin.PluginContext;
public class CustomDatabasePlugin implements Plugin {
@Override
public void initialize(PluginContext context) {
System.out.println("Initializing CustomDatabasePlugin");
}
@Override
public void onPreDestroy() {
System.out.println("Destroying CustomDatabasePlugin");
}
@Override
public InstrumentConfig getInstrumentConfig() {
return new InstrumentConfig.Builder()
.addClass("com.yourapp.repository.UserRepository")
.addMethod("findById")
.addMethod("save")
.addMethod("delete")
.build();
}
public static class UserRepositoryInterceptor {
public static void onMethodEnter(InstrumentationContext context) {
String methodName = context.getMethodName();
Object[] args = context.getArgs();
// Create span for database operation
io.thundra.agent.trace.span.Span span =
io.thundra.agent.trace.span.SpanFactory.createSpan("CustomDatabase." + methodName);
span.setTag("db.operation", methodName);
span.setTag("db.class", "UserRepository");
if (args != null && args.length > 0) {
span.setTag("db.parameters.count", args.length);
}
context.setLocal("customSpan", span);
}
public static void onMethodExit(InstrumentationContext context) {
io.thundra.agent.trace.span.Span span =
(io.thundra.agent.trace.span.Span) context.getLocal("customSpan");
if (span != null) {
Object result = context.getReturnValue();
if (result != null) {
span.setTag("db.result.type", result.getClass().getSimpleName());
}
Throwable throwable = context.getThrowable();
if (throwable != null) {
span.setTag("db.error", true);
span.setTag("db.error.message", throwable.getMessage());
}
span.finish();
}
}
}
}
9. Thundra Aspect for Custom Monitoring
```java
package com.yourapp.monitoring.thundra;
import io.thundra.agent.trace.span.Span;
import io.thundra.agent.trace.span.SpanFactory;
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.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Aspect
@Component
public class ThundraMonitoringAspect {
@Around("@annotation(com.yourapp.monitoring.thundra.Monitored)")
public Object monitorMethod(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String methodName = signature.getMethod().getDeclaringClass().getSimpleName() + "." + signature.getMethod().getName();
Span span = SpanFactory.createSpan("Aspect." + methodName);
long startTime = System.nanoTime();
boolean success = false;
try {
// Set method context
span.setTag("method.name", methodName);
span.setTag("class.name", signature.getDeclaringTypeName());
span.setTag("aspect.monitored", true);
// Log method arguments
if (joinPoint.getArgs() != null && joinPoint.getArgs().length > 0) {
Map<String, Object> args = new HashMap<>();
for (int i = 0; i < joinPoint.getArgs().length; i++) {
args.put("arg" + i, String.valueOf(joinPoint.getArgs()[i]));
}
span.log("MethodArguments", args);
}
// Execute method
Object result = joinPoint.proceed();
success = true;
return result;
} catch (Throwable throwable) {
span.setTag("error", true);
span.setTag("error.type", throwable.getClass().getSimpleName());
span.setTag("error.message", throwable.getMessage());
throw throwable;