Bugsnag provides powerful error monitoring and reporting for Java applications. It helps you detect, diagnose, and resolve exceptions and errors in production environments.
Dependencies and Setup
1. Maven Dependencies
<properties>
<bugsnag.version>3.6.4</bugsnag.version>
<spring-boot.version>3.1.0</spring-boot.version>
</properties>
<dependencies>
<!-- Bugsnag Core -->
<dependency>
<groupId>com.bugsnag</groupId>
<artifactId>bugsnag</artifactId>
<version>${bugsnag.version}</version>
</dependency>
<!-- Spring Boot Integration -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- For web applications -->
<dependency>
<groupId>com.bugsnag</groupId>
<artifactId>bugsnag-spring</artifactId>
<version>${bugsnag.version}</version>
</dependency>
</dependencies>
2. Application Properties
# application.yml
bugsnag:
api-key: ${BUGSNAG_API_KEY:your-api-key-here}
enabled: true
release-stage: ${APP_ENV:production}
app-version: 1.0.0
notify-release-stages: production,staging
project-packages: com.example,org.example
send-uncaught-exceptions: true
filters: password,secret,token,authorization
management:
endpoints:
web:
exposure:
include: health,metrics,info
endpoint:
health:
show-details: always
Basic Configuration
1. Bugsnag Configuration Class
package com.example.config;
import com.bugsnag.Bugsnag;
import com.bugsnag.BugsnagSpringConfiguration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import(BugsnagSpringConfiguration.class)
public class BugsnagConfig {
@Value("${bugsnag.api-key}")
private String bugsnagApiKey;
@Value("${bugsnag.release-stage:production}")
private String releaseStage;
@Value("${bugsnag.app-version:1.0.0}")
private String appVersion;
@Value("${bugsnag.enabled:true}")
private boolean enabled;
@Bean
public Bugsnag bugsnag() {
Bugsnag bugsnag = new Bugsnag(bugsnagApiKey);
// Configure Bugsnag
bugsnag.setReleaseStage(releaseStage);
bugsnag.setAppVersion(appVersion);
bugsnag.setNotifyReleaseStages("production", "staging");
bugsnag.setProjectPackages("com.example");
bugsnag.setSendUncaughtExceptions(true);
// Add filters for sensitive data
bugsnag.addCallback(report -> {
// Filter sensitive data from metadata
report.getMetaData().addToTab("user", "password", "[FILTERED]");
report.getMetaData().addToTab("request", "authorization", "[FILTERED]");
report.getMetaData().addToTab("request", "cookie", "[FILTERED]");
});
return bugsnag;
}
}
2. Enhanced Configuration with Custom Callbacks
package com.example.config;
import com.bugsnag.Bugsnag;
import com.bugsnag.Report;
import com.bugsnag.callbacks.Callback;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;
@Component
public class BugsnagInitializer {
@Autowired
private Bugsnag bugsnag;
@Autowired
private Environment environment;
@PostConstruct
public void initialize() {
// Add global metadata
bugsnag.addCallback(new Callback() {
@Override
public void beforeNotify(Report report) {
// Add application metadata
report.addToTab("App", "Environment", environment.getActiveProfiles());
report.addToTab("App", "Spring Version", getSpringVersion());
report.addToTab("App", "Java Version", System.getProperty("java.version"));
// Add system information
report.addToTab("System", "Available Processors", Runtime.getRuntime().availableProcessors());
report.addToTab("System", "Free Memory", Runtime.getRuntime().freeMemory());
report.addToTab("System", "Max Memory", Runtime.getRuntime().maxMemory());
report.addToTab("System", "Total Memory", Runtime.getRuntime().totalMemory());
// Add thread information
report.addToTab("Thread", "Name", Thread.currentThread().getName());
report.addToTab("Thread", "State", Thread.currentThread().getState().toString());
// Filter sensitive headers
filterSensitiveData(report);
}
});
// Add callback for grouping errors
bugsnag.addCallback(report -> {
String errorClass = report.getException().getClass().getSimpleName();
String message = report.getException().getMessage();
// Custom grouping for specific error types
if (errorClass.equals("DatabaseConnectionException")) {
report.setGroupingHash("database-connection-issues");
} else if (message != null && message.contains("timeout")) {
report.setGroupingHash("timeout-errors");
}
});
}
private String getSpringVersion() {
try {
return org.springframework.core.SpringVersion.getVersion();
} catch (Exception e) {
return "unknown";
}
}
private void filterSensitiveData(Report report) {
// Filter sensitive data from request metadata
if (report.getMetaData().getTab("request") != null) {
Map<String, Object> requestData = report.getMetaData().getTab("request");
filterMap(requestData);
}
// Filter sensitive data from environment metadata
if (report.getMetaData().getTab("environment") != null) {
Map<String, Object> envData = report.getMetaData().getTab("environment");
filterMap(envData);
}
}
private void filterMap(Map<String, Object> data) {
String[] sensitiveKeys = {"password", "secret", "token", "authorization", "cookie", "key"};
for (String sensitiveKey : sensitiveKeys) {
if (data.containsKey(sensitiveKey)) {
data.put(sensitiveKey, "[FILTERED]");
}
}
}
}
Spring Boot Integration
1. Service Layer with Error Reporting
package com.example.service;
import com.bugsnag.Bugsnag;
import com.bugsnag.Report;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class UserService {
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
@Autowired
private Bugsnag bugsnag;
@Autowired
private UserRepository userRepository;
public User getUserById(String userId) {
try {
logger.info("Fetching user with ID: {}", userId);
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException("User not found: " + userId));
logger.debug("Successfully fetched user: {}", user.getEmail());
return user;
} catch (UserNotFoundException e) {
// This is an expected error - log but don't report to Bugsnag
logger.warn("User not found: {}", userId);
throw e;
} catch (Exception e) {
// Unexpected error - report to Bugsnag
reportErrorToBugsnag(e, Map.of(
"user_id", userId,
"operation", "get_user",
"service", "UserService"
));
throw new ServiceException("Failed to fetch user", e);
}
}
public User createUser(CreateUserRequest request) {
long startTime = System.currentTimeMillis();
try {
validateUserRequest(request);
User user = new User();
user.setEmail(request.getEmail());
user.setName(request.getName());
user.setActive(true);
User savedUser = userRepository.save(user);
long duration = System.currentTimeMillis() - startTime;
logger.info("User created successfully: {} (took {}ms)", savedUser.getId(), duration);
// Record business metric
bugsnag.leaveBreadcrumb("User Created",
Map.of("user_id", savedUser.getId(), "duration_ms", duration),
"process");
return savedUser;
} catch (ValidationException e) {
// Expected validation error
logger.warn("Validation failed for user creation: {}", e.getMessage());
throw e;
} catch (Exception e) {
// Unexpected error - report with detailed context
Map<String, Object> metadata = new HashMap<>();
metadata.put("user_email", request.getEmail());
metadata.put("user_name", request.getName());
metadata.put("operation", "create_user");
metadata.put("duration_ms", System.currentTimeMillis() - startTime);
reportErrorToBugsnag(e, metadata);
throw new ServiceException("Failed to create user", e);
}
}
public void updateUserProfile(String userId, UpdateProfileRequest request) {
try {
User user = getUserById(userId);
// Add breadcrumb for tracking
bugsnag.leaveBreadcrumb("Profile Update Started",
Map.of("user_id", userId, "fields_updated", request.getUpdatedFields()),
"process");
if (request.getEmail() != null) {
user.setEmail(request.getEmail());
}
if (request.getName() != null) {
user.setName(request.getName());
}
userRepository.save(user);
bugsnag.leaveBreadcrumb("Profile Update Completed",
Map.of("user_id", userId),
"process");
} catch (Exception e) {
reportErrorToBugsnag(e, Map.of(
"user_id", userId,
"operation", "update_profile",
"request_data", request.toString()
));
throw e;
}
}
private void reportErrorToBugsnag(Exception error, Map<String, Object> metadata) {
try {
Report report = bugsnag.buildReport(error);
// Add custom metadata
if (metadata != null) {
metadata.forEach((key, value) ->
report.addToTab("Custom", key, value));
}
// Set severity based on error type
if (error instanceof DatabaseException) {
report.setSeverity("error");
} else if (error instanceof ExternalServiceException) {
report.setSeverity("warning");
} else {
report.setSeverity("error");
}
bugsnag.notify(report);
logger.error("Error reported to Bugsnag: {}", error.getMessage(), error);
} catch (Exception bugsnagError) {
logger.error("Failed to report error to Bugsnag: {}", bugsnagError.getMessage());
// Don't throw - we don't want Bugsnag failures to break the application
}
}
private void validateUserRequest(CreateUserRequest request) {
if (request.getEmail() == null || request.getEmail().trim().isEmpty()) {
throw new ValidationException("Email is required");
}
if (!isValidEmail(request.getEmail())) {
throw new ValidationException("Invalid email format");
}
}
private boolean isValidEmail(String email) {
return email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
}
}
2. Custom Exceptions
package com.example.exception;
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}
public class ValidationException extends RuntimeException {
public ValidationException(String message) {
super(message);
}
}
public class ServiceException extends RuntimeException {
public ServiceException(String message) {
super(message);
}
public ServiceException(String message, Throwable cause) {
super(message, cause);
}
}
public class DatabaseException extends RuntimeException {
public DatabaseException(String message, Throwable cause) {
super(message, cause);
}
}
public class ExternalServiceException extends RuntimeException {
public ExternalServiceException(String message, Throwable cause) {
super(message, cause);
}
}
3. REST Controller with Error Handling
package com.example.controller;
import com.bugsnag.Bugsnag;
import com.example.model.User;
import com.example.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/users")
public class UserController {
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
@Autowired
private UserService userService;
@Autowired
private Bugsnag bugsnag;
@GetMapping("/{userId}")
public ResponseEntity<?> getUser(@PathVariable String userId, HttpServletRequest request) {
try {
// Add breadcrumb for request tracking
bugsnag.leaveBreadcrumb("API Request",
Map.of(
"method", "GET",
"path", "/api/users/" + userId,
"user_id", userId,
"client_ip", getClientIp(request)
),
"request");
User user = userService.getUserById(userId);
return ResponseEntity.ok(user);
} catch (UserNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(createErrorResponse("USER_NOT_FOUND", e.getMessage()));
} catch (Exception e) {
logger.error("Failed to fetch user: {}", userId, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(createErrorResponse("INTERNAL_ERROR", "Failed to fetch user"));
}
}
@PostMapping
public ResponseEntity<?> createUser(@RequestBody CreateUserRequest request,
HttpServletRequest httpRequest) {
long startTime = System.currentTimeMillis();
try {
// Add request context to Bugsnag
bugsnag.leaveBreadcrumb("Create User API",
Map.of(
"method", "POST",
"path", "/api/users",
"user_email", request.getEmail(),
"client_ip", getClientIp(httpRequest)
),
"request");
User user = userService.createUser(request);
long duration = System.currentTimeMillis() - startTime;
logger.info("User creation API completed in {}ms", duration);
return ResponseEntity.status(HttpStatus.CREATED).body(user);
} catch (ValidationException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(createErrorResponse("VALIDATION_ERROR", e.getMessage()));
} catch (Exception e) {
logger.error("Failed to create user", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(createErrorResponse("INTERNAL_ERROR", "Failed to create user"));
}
}
@PutMapping("/{userId}/profile")
public ResponseEntity<?> updateProfile(@PathVariable String userId,
@RequestBody UpdateProfileRequest request,
HttpServletRequest httpRequest) {
try {
bugsnag.leaveBreadcrumb("Update Profile API",
Map.of(
"method", "PUT",
"path", "/api/users/" + userId + "/profile",
"user_id", userId,
"client_ip", getClientIp(httpRequest)
),
"request");
userService.updateUserProfile(userId, request);
return ResponseEntity.ok().build();
} catch (UserNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(createErrorResponse("USER_NOT_FOUND", e.getMessage()));
} catch (Exception e) {
logger.error("Failed to update user profile: {}", userId, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(createErrorResponse("INTERNAL_ERROR", "Failed to update profile"));
}
}
private Map<String, Object> createErrorResponse(String code, String message) {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("error", code);
errorResponse.put("message", message);
errorResponse.put("timestamp", System.currentTimeMillis());
return errorResponse;
}
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();
}
}
4. Global Exception Handler
package com.example.config;
import com.bugsnag.Bugsnag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@Autowired
private Bugsnag bugsnag;
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleAllExceptions(Exception ex, WebRequest request) {
// Report unexpected exceptions to Bugsnag
if (!isExpectedException(ex)) {
reportUnexpectedException(ex, request);
}
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("error", "INTERNAL_SERVER_ERROR");
errorResponse.put("message", "An unexpected error occurred");
errorResponse.put("timestamp", System.currentTimeMillis());
errorResponse.put("path", request.getDescription(false));
logger.error("Unhandled exception occurred", ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
}
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<Map<String, Object>> handleUserNotFound(UserNotFoundException ex, WebRequest request) {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("error", "USER_NOT_FOUND");
errorResponse.put("message", ex.getMessage());
errorResponse.put("timestamp", System.currentTimeMillis());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse);
}
@ExceptionHandler(ValidationException.class)
public ResponseEntity<Map<String, Object>> handleValidationException(ValidationException ex, WebRequest request) {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("error", "VALIDATION_ERROR");
errorResponse.put("message", ex.getMessage());
errorResponse.put("timestamp", System.currentTimeMillis());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
}
private boolean isExpectedException(Exception ex) {
return ex instanceof UserNotFoundException ||
ex instanceof ValidationException;
}
private void reportUnexpectedException(Exception ex, WebRequest request) {
try {
Map<String, Object> metadata = new HashMap<>();
metadata.put("path", request.getDescription(false));
metadata.put("parameters", request.getParameterMap());
metadata.put("user_agent", request.getHeader("User-Agent"));
bugsnag.notify(ex, (report) -> {
report.setSeverity("error");
metadata.forEach((key, value) ->
report.addToTab("Request", key, value));
});
} catch (Exception bugsnagEx) {
logger.error("Failed to report exception to Bugsnag", bugsnagEx);
}
}
}
Advanced Features
1. Custom Breadcrumb Service
package com.example.service;
import com.bugsnag.Bugsnag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Map;
@Service
public class BreadcrumbService {
@Autowired
private Bugsnag bugsnag;
public void leaveBreadcrumb(String message, String type) {
bugsnag.leaveBreadcrumb(message, type);
}
public void leaveBreadcrumb(String message, Map<String, Object> metadata, String type) {
bugsnag.leaveBreadcrumb(message, metadata, type);
}
public void recordUserAction(String userId, String action, Map<String, Object> details) {
Map<String, Object> metadata = new HashMap<>();
metadata.put("user_id", userId);
metadata.put("action", action);
if (details != null) {
metadata.putAll(details);
}
leaveBreadcrumb("User Action: " + action, metadata, "user");
}
public void recordSystemEvent(String event, Map<String, Object> details) {
Map<String, Object> metadata = new HashMap<>();
metadata.put("event", event);
if (details != null) {
metadata.putAll(details);
}
leaveBreadcrumb("System Event: " + event, metadata, "system");
}
public void recordExternalCall(String service, String operation, long duration, boolean success) {
Map<String, Object> metadata = new HashMap<>();
metadata.put("service", service);
metadata.put("operation", operation);
metadata.put("duration_ms", duration);
metadata.put("success", success);
leaveBreadcrumb("External Call: " + service + "." + operation, metadata, "network");
}
}
2. Performance Monitoring Integration
package com.example.aspect;
import com.bugsnag.Bugsnag;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Aspect
@Component
public class PerformanceMonitoringAspect {
private static final Logger logger = LoggerFactory.getLogger(PerformanceMonitoringAspect.class);
@Autowired
private Bugsnag bugsnag;
@Around("@annotation(MonitorPerformance)")
public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().toShortString();
long startTime = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
long duration = System.currentTimeMillis() - startTime;
// Record performance breadcrumb
if (duration > 1000) { // Log slow methods
Map<String, Object> metadata = new HashMap<>();
metadata.put("method", methodName);
metadata.put("duration_ms", duration);
metadata.put("threshold_ms", 1000);
bugsnag.leaveBreadcrumb("Slow Method Execution", metadata, "performance");
logger.warn("Slow method detected: {} took {}ms", methodName, duration);
}
return result;
} catch (Exception e) {
long duration = System.currentTimeMillis() - startTime;
// Record error with performance context
Map<String, Object> metadata = new HashMap<>();
metadata.put("method", methodName);
metadata.put("duration_ms", duration);
metadata.put("error", e.getClass().getSimpleName());
bugsnag.leaveBreadcrumb("Method Execution Failed", metadata, "error");
throw e;
}
}
}
3. Database Monitoring
package com.example.repository;
import com.bugsnag.Bugsnag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Map;
@Repository
public class UserRepository {
private static final Logger logger = LoggerFactory.getLogger(UserRepository.class);
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private Bugsnag bugsnag;
public Optional<User> findById(String userId) {
long startTime = System.currentTimeMillis();
try {
String sql = "SELECT * FROM users WHERE id = ? AND deleted = false";
List<User> users = jdbcTemplate.query(sql, new Object[]{userId}, new UserRowMapper());
long duration = System.currentTimeMillis() - startTime;
// Record slow queries
if (duration > 500) {
Map<String, Object> metadata = new HashMap<>();
metadata.put("query", "findById");
metadata.put("user_id", userId);
metadata.put("duration_ms", duration);
metadata.put("result_count", users.size());
bugsnag.leaveBreadcrumb("Slow Database Query", metadata, "database");
}
return users.isEmpty() ? Optional.empty() : Optional.of(users.get(0));
} catch (DataAccessException e) {
long duration = System.currentTimeMillis() - startTime;
// Report database errors to Bugsnag
Map<String, Object> metadata = new HashMap<>();
metadata.put("query", "findById");
metadata.put("user_id", userId);
metadata.put("duration_ms", duration);
metadata.put("error_type", "DataAccessException");
bugsnag.notify(e, report -> {
report.setSeverity("error");
metadata.forEach((key, value) -> report.addToTab("Database", key, value));
});
throw new DatabaseException("Database error while fetching user", e);
}
}
public List<User> findByEmail(String email) {
// Similar implementation with monitoring
return jdbcTemplate.query(
"SELECT * FROM users WHERE email = ? AND deleted = false",
new Object[]{email},
new UserRowMapper()
);
}
public User save(User user) {
long startTime = System.currentTimeMillis();
try {
if (user.getId() == null) {
// Insert new user
String sql = "INSERT INTO users (id, email, name, active, created_at) VALUES (?, ?, ?, ?, ?)";
jdbcTemplate.update(sql, user.getId(), user.getEmail(), user.getName(), user.isActive(), user.getCreatedAt());
} else {
// Update existing user
String sql = "UPDATE users SET email = ?, name = ?, active = ?, updated_at = ? WHERE id = ?";
jdbcTemplate.update(sql, user.getEmail(), user.getName(), user.isActive(), user.getUpdatedAt(), user.getId());
}
long duration = System.currentTimeMillis() - startTime;
if (duration > 1000) {
Map<String, Object> metadata = new HashMap<>();
metadata.put("operation", user.getId() == null ? "insert" : "update");
metadata.put("user_id", user.getId());
metadata.put("duration_ms", duration);
bugsnag.leaveBreadcrumb("Slow Database Operation", metadata, "database");
}
return user;
} catch (DataAccessException e) {
long duration = System.currentTimeMillis() - startTime;
Map<String, Object> metadata = new HashMap<>();
metadata.put("operation", "save");
metadata.put("user_id", user.getId());
metadata.put("user_email", user.getEmail());
metadata.put("duration_ms", duration);
bugsnag.notify(e, report -> {
report.setSeverity("error");
metadata.forEach((key, value) -> report.addToTab("Database", key, value));
});
throw new DatabaseException("Database error while saving user", e);
}
}
}
4. Scheduled Health Monitoring
package com.example.scheduler;
import com.bugsnag.Bugsnag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Component
public class HealthMonitorScheduler {
private static final Logger logger = LoggerFactory.getLogger(HealthMonitorScheduler.class);
@Autowired
private Bugsnag bugsnag;
@Autowired
private UserRepository userRepository;
@Scheduled(fixedRate = 300000) // Run every 5 minutes
public void checkDatabaseHealth() {
long startTime = System.currentTimeMillis();
try {
// Simple health check query
userRepository.countActiveUsers();
long duration = System.currentTimeMillis() - startTime;
if (duration > 1000) {
Map<String, Object> metadata = new HashMap<>();
metadata.put("check", "database_health");
metadata.put("duration_ms", duration);
metadata.put("status", "slow");
bugsnag.leaveBreadcrumb("Slow Health Check", metadata, "monitoring");
}
logger.debug("Database health check completed in {}ms", duration);
} catch (Exception e) {
Map<String, Object> metadata = new HashMap<>();
metadata.put("check", "database_health");
metadata.put("duration_ms", System.currentTimeMillis() - startTime);
metadata.put("status", "failed");
bugsnag.notify(e, report -> {
report.setSeverity("warning");
report.setContext("Health Check");
metadata.forEach((key, value) -> report.addToTab("Health", key, value));
});
logger.error("Database health check failed", e);
}
}
@Scheduled(cron = "0 0 * * * *") // Run every hour
public void reportApplicationMetrics() {
try {
Runtime runtime = Runtime.getRuntime();
Map<String, Object> metadata = new HashMap<>();
metadata.put("free_memory", runtime.freeMemory());
metadata.put("total_memory", runtime.totalMemory());
metadata.put("max_memory", runtime.maxMemory());
metadata.put("available_processors", runtime.availableProcessors());
bugsnag.leaveBreadcrumb("Application Metrics", metadata, "system");
logger.info("Application metrics recorded: freeMemory={}, totalMemory={}",
runtime.freeMemory(), runtime.totalMemory());
} catch (Exception e) {
logger.error("Failed to record application metrics", e);
}
}
}
Testing
1. Test Configuration
package com.example.config;
import com.bugsnag.Bugsnag;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import static org.mockito.Mockito.mock;
@TestConfiguration
public class TestBugsnagConfig {
@Bean
@Primary
public Bugsnag bugsnag() {
return mock(Bugsnag.class);
}
}
2. Service Test with Mocked Bugsnag
package com.example.service;
import com.bugsnag.Bugsnag;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private Bugsnag bugsnag;
@Mock
private UserRepository userRepository;
private UserService userService;
@BeforeEach
void setUp() {
userService = new UserService();
// Use reflection to set mocks or create a package-private setter
}
@Test
void shouldReportUnexpectedErrorToBugsnag() {
// Given
CreateUserRequest request = new CreateUserRequest();
request.setEmail("[email protected]");
request.setName("Test User");
when(userRepository.save(any(User.class))).thenThrow(new RuntimeException("Database error"));
// When & Then
assertThrows(ServiceException.class, () -> userService.createUser(request));
// Verify Bugsnag was called
verify(bugsnag, times(1)).notify(any(Report.class));
}
@Test
void shouldNotReportExpectedErrorsToBugsnag() {
// Given
CreateUserRequest request = new CreateUserRequest();
request.setEmail("invalid-email");
request.setName("Test User");
// When & Then
assertThrows(ValidationException.class, () -> userService.createUser(request));
// Verify Bugsnag was NOT called for expected errors
verify(bugsnag, never()).notify(any(Report.class));
}
}
Best Practices
- Error Filtering:
- Don't report expected business errors
- Filter sensitive data from reports
- Use appropriate severity levels
- Breadcrumb Strategy:
- Leave breadcrumbs for important user actions
- Track external service calls
- Monitor performance bottlenecks
- Context Management:
- Add relevant context to error reports
- Include user information when available
- Track request IDs for correlation
- Performance:
- Use async reporting when possible
- Don't block application flow for error reporting
- Monitor Bugsnag's impact on application performance
// Example of async error reporting
public void reportErrorAsync(Exception error, Map<String, Object> metadata) {
CompletableFuture.runAsync(() -> {
try {
bugsnag.notify(error, report -> {
metadata.forEach((key, value) ->
report.addToTab("Async", key, value));
});
} catch (Exception e) {
logger.error("Failed to report error async", e);
}
});
}
Conclusion
Bugsnag Java SDK provides:
- Comprehensive error monitoring and reporting
- Rich context for debugging production issues
- Breadcrumb tracking for understanding error paths
- Custom metadata for business-specific debugging
- Integration with Spring Boot and other frameworks
By implementing the patterns shown above, you can achieve comprehensive error monitoring, track user journeys, identify performance issues, and quickly diagnose and fix problems in production environments. The combination of automatic exception capture and manual instrumentation gives you complete visibility into your application's health and user experience.