google-site-verification: google61fe8ba583a51912.html
Raygun Crash Reporting in Java: Complete Implementation Guide

Raygun is a powerful error monitoring and crash reporting service that helps developers detect, diagnose, and resolve errors in real-time. Here's a comprehensive implementation guide for integrating Raygun crash reporting in Java applications.

Project Setup

Dependencies

pom.xml

<properties> <raygun.version>3.2.0</raygun.version> <jackson.version>2.15.2</jackson.version> </properties> <dependencies> <!-- Raygun Java Provider --> <dependency> <groupId>com.raygun</groupId> <artifactId>raygun4java</artifactId> <version>${raygun.version}</version> </dependency> <!-- Spring Boot Starter (Optional) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>3.1.0</version> <optional>true</optional> </dependency> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.version}</version> </dependency> <!-- HTTP Client --> <dependency> <groupId>org.apache.httpcomponents.client5</groupId> <artifactId>httpclient5</artifactId> <version>5.2.1</version> </dependency> <!-- Logging --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>2.0.7</version> </dependency> <!-- Testing --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.10.0</version> <scope>test</scope> </dependency> </dependencies>

Core Implementation

1. Configuration Models

RaygunConfig.java - Configuration for Raygun client

import java.net.InetAddress; import java.net.UnknownHostException; import java.util.HashMap; import java.util.Map; public class RaygunConfig { private final String apiKey; private final String apiEndpoint; private final String applicationVersion; private final String environment; private final boolean enabled; private final boolean async; private final int maxRetries; private final Map<String, Object> defaultTags; private final Map<String, Object> customData; private RaygunConfig(Builder builder) { this.apiKey = builder.apiKey; this.apiEndpoint = builder.apiEndpoint; this.applicationVersion = builder.applicationVersion; this.environment = builder.environment; this.enabled = builder.enabled; this.async = builder.async; this.maxRetries = builder.maxRetries; this.defaultTags = Map.copyOf(builder.defaultTags); this.customData = Map.copyOf(builder.customData); } public static class Builder { private String apiKey; private String apiEndpoint = "https://api.raygun.com/entries"; private String applicationVersion = "1.0.0"; private String environment = "production"; private boolean enabled = true; private boolean async = true; private int maxRetries = 3; private Map<String, Object> defaultTags = new HashMap<>(); private Map<String, Object> customData = new HashMap<>(); public Builder apiKey(String apiKey) { this.apiKey = apiKey; return this; } public Builder apiEndpoint(String apiEndpoint) { this.apiEndpoint = apiEndpoint; return this; } public Builder applicationVersion(String applicationVersion) { this.applicationVersion = applicationVersion; return this; } public Builder environment(String environment) { this.environment = environment; return this; } public Builder enabled(boolean enabled) { this.enabled = enabled; return this; } public Builder async(boolean async) { this.async = async; return this; } public Builder maxRetries(int maxRetries) { this.maxRetries = maxRetries; return this; } public Builder defaultTag(String key, Object value) { this.defaultTags.put(key, value); return this; } public Builder defaultTags(Map<String, Object> tags) { this.defaultTags.putAll(tags); return this; } public Builder customData(String key, Object value) { this.customData.put(key, value); return this; } public Builder customData(Map<String, Object> customData) { this.customData.putAll(customData); return this; } public RaygunConfig build() { if (apiKey == null || apiKey.trim().isEmpty()) { throw new IllegalStateException("API key is required"); } // Set default tags if not provided if (defaultTags.isEmpty()) { defaultTags.put("language", "java"); defaultTags.put("runtime", System.getProperty("java.version")); try { defaultTags.put("hostname", InetAddress.getLocalHost().getHostName()); } catch (UnknownHostException e) { defaultTags.put("hostname", "unknown"); } } return new RaygunConfig(this); } } // Getters public String getApiKey() { return apiKey; } public String getApiEndpoint() { return apiEndpoint; } public String getApplicationVersion() { return applicationVersion; } public String getEnvironment() { return environment; } public boolean isEnabled() { return enabled; } public boolean isAsync() { return async; } public int getMaxRetries() { return maxRetries; } public Map<String, Object> getDefaultTags() { return defaultTags; } public Map<String, Object> getCustomData() { return customData; } }

2. Error Models

ErrorReport.java - Structured error report for Raygun

import java.time.Instant; import java.util.*; public class ErrorReport { private final String machineName; private final String version; private final String environment; private final String clientName; private final OccurredOn occurredOn; private final Details details; public ErrorReport(Builder builder) { this.machineName = builder.machineName; this.version = builder.version; this.environment = builder.environment; this.clientName = builder.clientName; this.occurredOn = builder.occurredOn; this.details = builder.details; } public static class Builder { private String machineName; private String version = "1.0.0"; private String environment = "production"; private String clientName = "raygun4java-custom"; private OccurredOn occurredOn; private Details details; public Builder machineName(String machineName) { this.machineName = machineName; return this; } public Builder version(String version) { this.version = version; return this; } public Builder environment(String environment) { this.environment = environment; return this; } public Builder clientName(String clientName) { this.clientName = clientName; return this; } public Builder occurredOn(OccurredOn occurredOn) { this.occurredOn = occurredOn; return this; } public Builder details(Details details) { this.details = details; return this; } public ErrorReport build() { if (occurredOn == null) { occurredOn = new OccurredOn(Instant.now()); } if (details == null) { throw new IllegalStateException("Details are required"); } return new ErrorReport(this); } } // Getters public String getMachineName() { return machineName; } public String getVersion() { return version; } public String getEnvironment() { return environment; } public String getClientName() { return clientName; } public OccurredOn getOccurredOn() { return occurredOn; } public Details getDetails() { return details; } public static class OccurredOn { private final Instant timestamp; private final long timeOffset; public OccurredOn(Instant timestamp) { this.timestamp = timestamp; this.timeOffset = 0; // Can be used for timezone offset } public Instant getTimestamp() { return timestamp; } public long getTimeOffset() { return timeOffset; } } public static class Details { private final String message; private final String className; private final List<StackTraceElement> stackTrace; private final Map<String, Object> userCustomData; private final Map<String, String> tags; private final User user; private final Request request; private final Context context; public Details(Builder builder) { this.message = builder.message; this.className = builder.className; this.stackTrace = List.copyOf(builder.stackTrace); this.userCustomData = Map.copyOf(builder.userCustomData); this.tags = Map.copyOf(builder.tags); this.user = builder.user; this.request = builder.request; this.context = builder.context; } public static class Builder { private String message; private String className; private List<StackTraceElement> stackTrace = new ArrayList<>(); private Map<String, Object> userCustomData = new HashMap<>(); private Map<String, String> tags = new HashMap<>(); private User user; private Request request; private Context context; public Builder message(String message) { this.message = message; return this; } public Builder className(String className) { this.className = className; return this; } public Builder stackTrace(StackTraceElement[] stackTrace) { this.stackTrace = Arrays.asList(stackTrace); return this; } public Builder stackTrace(List<StackTraceElement> stackTrace) { this.stackTrace = new ArrayList<>(stackTrace); return this; } public Builder userCustomData(String key, Object value) { this.userCustomData.put(key, value); return this; } public Builder userCustomData(Map<String, Object> customData) { this.userCustomData.putAll(customData); return this; } public Builder tag(String key, String value) { this.tags.put(key, value); return this; } public Builder tags(Map<String, String> tags) { this.tags.putAll(tags); return this; } public Builder user(User user) { this.user = user; return this; } public Builder request(Request request) { this.request = request; return this; } public Builder context(Context context) { this.context = context; return this; } public Details build() { if (message == null && className == null) { throw new IllegalStateException("Either message or className is required"); } return new Details(this); } } // Getters public String getMessage() { return message; } public String getClassName() { return className; } public List<StackTraceElement> getStackTrace() { return stackTrace; } public Map<String, Object> getUserCustomData() { return userCustomData; } public Map<String, String> getTags() { return tags; } public User getUser() { return user; } public Request getRequest() { return request; } public Context getContext() { return context; } } public static class User { private final String identifier; private final String firstName; private final String fullName; private final String email; private final boolean isAnonymous; private final String uuid; public User(Builder builder) { this.identifier = builder.identifier; this.firstName = builder.firstName; this.fullName = builder.fullName; this.email = builder.email; this.isAnonymous = builder.isAnonymous; this.uuid = builder.uuid; } public static class Builder { private String identifier; private String firstName; private String fullName; private String email; private boolean isAnonymous = false; private String uuid; public Builder identifier(String identifier) { this.identifier = identifier; return this; } public Builder firstName(String firstName) { this.firstName = firstName; return this; } public Builder fullName(String fullName) { this.fullName = fullName; return this; } public Builder email(String email) { this.email = email; return this; } public Builder isAnonymous(boolean isAnonymous) { this.isAnonymous = isAnonymous; return this; } public Builder uuid(String uuid) { this.uuid = uuid; return this; } public User build() { if (identifier == null && uuid == null) { throw new IllegalStateException("Either identifier or uuid is required"); } return new User(this); } } // Getters public String getIdentifier() { return identifier; } public String getFirstName() { return firstName; } public String getFullName() { return fullName; } public String getEmail() { return email; } public boolean isAnonymous() { return isAnonymous; } public String getUuid() { return uuid; } } public static class Request { private final String hostName; private final String url; private final String httpMethod; private final String ipAddress; private final Map<String, String> queryString; private final Map<String, String> headers; private final Map<String, String> form; private final String rawData; public Request(Builder builder) { this.hostName = builder.hostName; this.url = builder.url; this.httpMethod = builder.httpMethod; this.ipAddress = builder.ipAddress; this.queryString = Map.copyOf(builder.queryString); this.headers = Map.copyOf(builder.headers); this.form = Map.copyOf(builder.form); this.rawData = builder.rawData; } public static class Builder { private String hostName; private String url; private String httpMethod; private String ipAddress; private Map<String, String> queryString = new HashMap<>(); private Map<String, String> headers = new HashMap<>(); private Map<String, String> form = new HashMap<>(); private String rawData; public Builder hostName(String hostName) { this.hostName = hostName; return this; } public Builder url(String url) { this.url = url; return this; } public Builder httpMethod(String httpMethod) { this.httpMethod = httpMethod; return this; } public Builder ipAddress(String ipAddress) { this.ipAddress = ipAddress; return this; } public Builder queryString(String key, String value) { this.queryString.put(key, value); return this; } public Builder queryString(Map<String, String> queryString) { this.queryString.putAll(queryString); return this; } public Builder header(String key, String value) { this.headers.put(key, value); return this; } public Builder headers(Map<String, String> headers) { this.headers.putAll(headers); return this; } public Builder form(String key, String value) { this.form.put(key, value); return this; } public Builder form(Map<String, String> form) { this.form.putAll(form); return this; } public Builder rawData(String rawData) { this.rawData = rawData; return this; } public Request build() { return new Request(this); } } // Getters public String getHostName() { return hostName; } public String getUrl() { return url; } public String getHttpMethod() { return httpMethod; } public String getIpAddress() { return ipAddress; } public Map<String, String> getQueryString() { return queryString; } public Map<String, String> getHeaders() { return headers; } public Map<String, String> getForm() { return form; } public String getRawData() { return rawData; } } public static class Context { private final Map<String, Object> properties; public Context(Builder builder) { this.properties = Map.copyOf(builder.properties); } public static class Builder { private Map<String, Object> properties = new HashMap<>(); public Builder property(String key, Object value) { this.properties.put(key, value); return this; } public Builder properties(Map<String, Object> properties) { this.properties.putAll(properties); return this; } public Context build() { return new Context(this); } } public Map<String, Object> getProperties() { return properties; } } }

3. Raygun Client Implementation

RaygunClient.java - Main client for Raygun API

import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.hc.core5.util.Timeout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.Base64; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicInteger; public class RaygunClient implements AutoCloseable { private static final Logger logger = LoggerFactory.getLogger(RaygunClient.class); private static final AtomicInteger REQUEST_COUNTER = new AtomicInteger(); private final RaygunConfig config; private final CloseableHttpClient httpClient; private final ObjectMapper objectMapper; private final BlockingQueue<ErrorReport> reportQueue; private final Thread reportProcessor; private volatile boolean running; public RaygunClient(RaygunConfig config) { this.config = config; this.objectMapper = new ObjectMapper(); this.httpClient = createHttpClient(); this.reportQueue = new LinkedBlockingQueue<>(1000); this.running = true; this.reportProcessor = new Thread(this::processReports, "raygun-report-processor"); this.reportProcessor.setDaemon(true); this.reportProcessor.start(); } private CloseableHttpClient createHttpClient() { return HttpClients.custom() .setDefaultRequestConfig(RequestConfig.custom() .setResponseTimeout(Timeout.ofSeconds(30)) .setConnectionRequestTimeout(Timeout.ofSeconds(10)) .build()) .build(); } public void send(ErrorReport report) { if (!config.isEnabled()) { logger.debug("Raygun is disabled, ignoring error report"); return; } if (config.isAsync()) { // Add to queue for async processing if (!reportQueue.offer(report)) { logger.warn("Raygun report queue is full, dropping report"); } } else { // Send synchronously sendReportSync(report); } } public void send(Throwable throwable) { send(throwable, null, null, null); } public void send(Throwable throwable, Map<String, Object> customData) { send(throwable, customData, null, null); } public void send(Throwable throwable, Map<String, Object> customData, ErrorReport.User user, ErrorReport.Request request) { ErrorReport report = createErrorReport(throwable, customData, user, request); send(report); } private ErrorReport createErrorReport(Throwable throwable, Map<String, Object> customData, ErrorReport.User user, ErrorReport.Request request) { // Build stack trace StackTraceElement[] stackTrace = throwable.getStackTrace(); // Build details ErrorReport.Details details = new ErrorReport.Details.Builder() .message(throwable.getMessage()) .className(throwable.getClass().getName()) .stackTrace(stackTrace) .userCustomData(mergeCustomData(customData)) .tags(createTags(throwable)) .user(user) .request(request) .context(createContext()) .build(); // Build full report return new ErrorReport.Builder() .machineName(getMachineName()) .version(config.getApplicationVersion()) .environment(config.getEnvironment()) .details(details) .build(); } private Map<String, Object> mergeCustomData(Map<String, Object> customData) { Map<String, Object> merged = new HashMap<>(config.getCustomData()); if (customData != null) { merged.putAll(customData); } return merged; } private Map<String, String> createTags(Throwable throwable) { Map<String, String> tags = new HashMap<>(); tags.put("exception_type", throwable.getClass().getSimpleName()); tags.put("handled", "true"); tags.putAll(config.getDefaultTags().entrySet().stream() .collect(java.util.stream.Collectors.toMap( Map.Entry::getKey, e -> e.getValue().toString() ))); return tags; } private ErrorReport.Context createContext() { return new ErrorReport.Context.Builder() .property("java_version", System.getProperty("java.version")) .property("os_name", System.getProperty("os.name")) .property("os_version", System.getProperty("os.version")) .property("available_processors", Runtime.getRuntime().availableProcessors()) .property("free_memory", Runtime.getRuntime().freeMemory()) .property("max_memory", Runtime.getRuntime().maxMemory()) .property("total_memory", Runtime.getRuntime().totalMemory()) .build(); } private String getMachineName() { try { return java.net.InetAddress.getLocalHost().getHostName(); } catch (Exception e) { return "unknown"; } } private void processReports() { while (running || !reportQueue.isEmpty()) { try { ErrorReport report = reportQueue.poll(100, java.util.concurrent.TimeUnit.MILLISECONDS); if (report != null) { sendReportSync(report); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } } private void sendReportSync(ErrorReport report) { int attempt = 0; int maxRetries = config.getMaxRetries(); while (attempt <= maxRetries) { attempt++; try { if (sendReportToRaygun(report)) { logger.debug("Successfully sent error report to Raygun"); return; } } catch (Exception e) { logger.warn("Failed to send report to Raygun (attempt {}/{}): {}", attempt, maxRetries, e.getMessage()); if (attempt == maxRetries) { logger.error("All retry attempts failed for Raygun report"); return; } // Exponential backoff try { Thread.sleep((long) (Math.pow(2, attempt) * 1000)); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); return; } } } } private boolean sendReportToRaygun(ErrorReport report) { int requestId = REQUEST_COUNTER.incrementAndGet(); try { String jsonPayload = objectMapper.writeValueAsString(report); HttpPost httpPost = createHttpPost(requestId, jsonPayload); logger.debug("Sending error report {} to Raygun", requestId); try (CloseableHttpResponse response = httpClient.execute(httpPost)) { int statusCode = response.getCode(); String responseBody = EntityUtils.toString(response.getEntity()); if (statusCode == 202) { logger.debug("Raygun accepted error report {}", requestId); return true; } else { logger.warn("Raygun rejected error report {}: {} - {}", requestId, statusCode, responseBody); return false; } } } catch (Exception e) { logger.error("Failed to send error report {} to Raygun: {}", requestId, e.getMessage()); throw new RuntimeException("Raygun API call failed", e); } } private HttpPost createHttpPost(int requestId, String jsonPayload) { HttpPost httpPost = new HttpPost(config.getApiEndpoint()); // Set headers httpPost.setHeader("Content-Type", "application/json"); httpPost.setHeader("X-ApiKey", config.getApiKey()); httpPost.setHeader("User-Agent", "Raygun4Java-Custom/1.0"); httpPost.setHeader("X-Request-Id", String.valueOf(requestId)); // Set entity httpPost.setEntity(new StringEntity(jsonPayload, org.apache.hc.core5.http.ContentType.APPLICATION_JSON)); return httpPost; } @Override public void close() throws IOException { running = false; try { reportProcessor.join(5000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } if (httpClient != null) { httpClient.close(); } logger.info("Raygun client closed"); } }

4. Error Handler and Manager

RaygunErrorManager.java - Central error management

import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; public class RaygunErrorManager { private static final Logger logger = LoggerFactory.getLogger(RaygunErrorManager.class); private final RaygunClient raygunClient; private final AtomicLong errorCount = new AtomicLong(); private final AtomicLong sentCount = new AtomicLong(); private final Map<String, Long> errorFrequency = new ConcurrentHashMap<>(); private final boolean enableRateLimiting; private final long rateLimitWindowMs; private final int maxErrorsPerWindow; public RaygunErrorManager(RaygunClient raygunClient) { this(raygunClient, true, 60000, 10); // 10 errors per minute by default } public RaygunErrorManager(RaygunClient raygunClient, boolean enableRateLimiting, long rateLimitWindowMs, int maxErrorsPerWindow) { this.raygunClient = raygunClient; this.enableRateLimiting = enableRateLimiting; this.rateLimitWindowMs = rateLimitWindowMs; this.maxErrorsPerWindow = maxErrorsPerWindow; } public void handle(Throwable throwable) { handle(throwable, null, null, null); } public void handle(Throwable throwable, Map<String, Object> customData) { handle(throwable, customData, null, null); } public void handle(Throwable throwable, Map<String, Object> customData, ErrorReport.User user, ErrorReport.Request request) { errorCount.incrementAndGet(); String errorKey = generateErrorKey(throwable); // Check rate limiting if (enableRateLimiting && isRateLimited(errorKey)) { logger.debug("Error rate limited: {}", errorKey); return; } try { raygunClient.send(throwable, customData, user, request); sentCount.incrementAndGet(); logger.info("Error reported to Raygun: {}", throwable.getMessage()); } catch (Exception e) { logger.error("Failed to send error to Raygun: {}", e.getMessage()); } } private String generateErrorKey(Throwable throwable) { if (throwable.getStackTrace().length == 0) { return throwable.getClass().getName(); } StackTraceElement firstElement = throwable.getStackTrace()[0]; return throwable.getClass().getName() + ":" + firstElement.getClassName() + "." + firstElement.getMethodName(); } private boolean isRateLimited(String errorKey) { long currentTime = System.currentTimeMillis(); long windowStart = currentTime - rateLimitWindowMs; // Clean old entries errorFrequency.entrySet().removeIf(entry -> entry.getValue() < windowStart); // Check frequency long count = errorFrequency.entrySet().stream() .filter(entry -> entry.getKey().equals(errorKey)) .count(); if (count >= maxErrorsPerWindow) { return true; } // Record this occurrence errorFrequency.put(errorKey + "_" + currentTime, currentTime); return false; } public void recordHandledError(Throwable throwable) { errorCount.incrementAndGet(); logger.debug("Handled error recorded: {}", throwable.getMessage()); } // Metrics getters public long getTotalErrorCount() { return errorCount.get(); } public long getSentErrorCount() { return sentCount.get(); } public long getDroppedErrorCount() { return errorCount.get() - sentCount.get(); } public Map<String, Long> getErrorFrequency() { return Map.copyOf(errorFrequency); } public void resetMetrics() { errorCount.set(0); sentCount.set(0); errorFrequency.clear(); } }

5. Spring Boot Integration

RaygunAutoConfiguration.java - Spring Boot auto-configuration

import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @ConditionalOnClass(RaygunClient.class) @EnableConfigurationProperties(RaygunProperties.class) public class RaygunAutoConfiguration { @Bean @ConditionalOnMissingBean @ConditionalOnProperty(name = "raygun.enabled", havingValue = "true", matchIfMissing = true) public RaygunConfig raygunConfig(RaygunProperties properties) { return new RaygunConfig.Builder() .apiKey(properties.getApiKey()) .apiEndpoint(properties.getApiEndpoint()) .applicationVersion(properties.getApplicationVersion()) .environment(properties.getEnvironment()) .enabled(properties.isEnabled()) .async(properties.isAsync()) .maxRetries(properties.getMaxRetries()) .defaultTags(properties.getDefaultTags()) .customData(properties.getCustomData()) .build(); } @Bean @ConditionalOnMissingBean public RaygunClient raygunClient(RaygunConfig config) { return new RaygunClient(config); } @Bean @ConditionalOnMissingBean public RaygunErrorManager raygunErrorManager(RaygunClient raygunClient) { return new RaygunErrorManager(raygunClient); } @Bean @ConditionalOnMissingBean public GlobalExceptionHandler globalExceptionHandler(RaygunErrorManager errorManager) { return new GlobalExceptionHandler(errorManager); } } // Configuration properties @ConfigurationProperties(prefix = "raygun") public class RaygunProperties { private String apiKey; private String apiEndpoint = "https://api.raygun.com/entries"; private String applicationVersion = "1.0.0"; private String environment = "production"; private boolean enabled = true; private boolean async = true; private int maxRetries = 3; private Map<String, Object> defaultTags = new HashMap<>(); private Map<String, Object> customData = new HashMap<>(); // Getters and setters public String getApiKey() { return apiKey; } public void setApiKey(String apiKey) { this.apiKey = apiKey; } public String getApiEndpoint() { return apiEndpoint; } public void setApiEndpoint(String apiEndpoint) { this.apiEndpoint = apiEndpoint; } public String getApplicationVersion() { return applicationVersion; } public void setApplicationVersion(String applicationVersion) { this.applicationVersion = applicationVersion; } public String getEnvironment() { return environment; } public void setEnvironment(String environment) { this.environment = environment; } public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public boolean isAsync() { return async; } public void setAsync(boolean async) { this.async = async; } public int getMaxRetries() { return maxRetries; } public void setMaxRetries(int maxRetries) { this.maxRetries = maxRetries; } public Map<String, Object> getDefaultTags() { return defaultTags; } public void setDefaultTags(Map<String, Object> defaultTags) { this.defaultTags = defaultTags; } public Map<String, Object> getCustomData() { return customData; } public void setCustomData(Map<String, Object> customData) { this.customData = customData; } }

GlobalExceptionHandler.java - Spring global exception handler

import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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 javax.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.Map; @RestControllerAdvice public class GlobalExceptionHandler { private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); private final RaygunErrorManager errorManager; public GlobalExceptionHandler(RaygunErrorManager errorManager) { this.errorManager = errorManager; } @ExceptionHandler(Exception.class) public ResponseEntity<Map<String, Object>> handleAllExceptions( Exception ex, WebRequest request, HttpServletRequest httpRequest) { // Create error response Map<String, Object> errorResponse = new HashMap<>(); errorResponse.put("timestamp", java.time.Instant.now()); errorResponse.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value()); errorResponse.put("error", "Internal Server Error"); errorResponse.put("message", "An unexpected error occurred"); errorResponse.put("path", httpRequest.getRequestURI()); // Create Raygun user info ErrorReport.User user = createUserFromRequest(httpRequest); // Create Raygun request info ErrorReport.Request raygunRequest = createRaygunRequest(httpRequest); // Create custom data Map<String, Object> customData = new HashMap<>(); customData.put("request_id", httpRequest.getHeader("X-Request-ID")); customData.put("session_id", httpRequest.getSession(false) != null ? httpRequest.getSession().getId() : null); customData.put("user_agent", httpRequest.getHeader("User-Agent")); // Send to Raygun errorManager.handle(ex, customData, user, raygunRequest); logger.error("Unhandled exception occurred: {}", ex.getMessage(), ex); return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); } @ExceptionHandler(BusinessException.class) public ResponseEntity<Map<String, Object>> handleBusinessException( BusinessException ex, HttpServletRequest httpRequest) { // Record handled business exception (don't send to Raygun for all) errorManager.recordHandledError(ex); Map<String, Object> errorResponse = new HashMap<>(); errorResponse.put("timestamp", java.time.Instant.now()); errorResponse.put("status", ex.getStatus().value()); errorResponse.put("error", ex.getStatus().getReasonPhrase()); errorResponse.put("message", ex.getMessage()); errorResponse.put("code", ex.getErrorCode()); errorResponse.put("path", httpRequest.getRequestURI()); logger.warn("Business exception: {}", ex.getMessage()); return new ResponseEntity<>(errorResponse, ex.getStatus()); } private ErrorReport.User createUserFromRequest(HttpServletRequest request) { // Extract user information from request (adapt based on your auth system) String userId = extractUserId(request); String userEmail = extractUserEmail(request); String userName = extractUserName(request); if (userId != null) { return new ErrorReport.User.Builder() .identifier(userId) .email(userEmail) .fullName(userName) .isAnonymous(false) .build(); } return null; } private ErrorReport.Request createRaygunRequest(HttpServletRequest request) { return new ErrorReport.Request.Builder() .hostName(request.getServerName()) .url(request.getRequestURL().toString()) .httpMethod(request.getMethod()) .ipAddress(getClientIpAddress(request)) .queryString(extractQueryParameters(request)) .headers(extractHeaders(request)) .form(extractFormParameters(request)) .rawData(extractRequestBody(request)) .build(); } private String extractUserId(HttpServletRequest request) { // Implement based on your authentication system return request.getHeader("X-User-ID"); } private String extractUserEmail(HttpServletRequest request) { return request.getHeader("X-User-Email"); } private String extractUserName(HttpServletRequest request) { return request.getHeader("X-User-Name"); } private String getClientIpAddress(HttpServletRequest request) { String xForwardedFor = request.getHeader("X-Forwarded-For"); if (xForwardedFor != null && !xForwardedFor.isEmpty()) { return xForwardedFor.split(",")[0].trim(); } return request.getRemoteAddr(); } private Map<String, String> extractQueryParameters(HttpServletRequest request) { Map<String, String> params = new HashMap<>(); request.getParameterMap().forEach((key, values) -> { if (values.length > 0) { params.put(key, values[0]); } }); return params; } private Map<String, String> extractHeaders(HttpServletRequest request) { Map<String, String> headers = new HashMap<>(); java.util.Collections.list(request.getHeaderNames()) .forEach(headerName -> headers.put(headerName, request.getHeader(headerName))); return headers; } private Map<String, String> extractFormParameters(HttpServletRequest request) { // For POST requests with form data return extractQueryParameters(request); // Simplified } private String extractRequestBody(HttpServletRequest request) { // Would need to read and cache request body return null; } } // Example business exception class BusinessException extends RuntimeException { private final HttpStatus status; private final String errorCode; public BusinessException(String message, HttpStatus status, String errorCode) { super(message); this.status = status; this.errorCode = errorCode; } public HttpStatus getStatus() { return status; } public String getErrorCode() { return errorCode; } }

6. Logback Appender Integration

RaygunLogbackAppender.java - Send log errors to Raygun

import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.IThrowableProxy; import ch.qos.logback.classic.spi.ThrowableProxy; import ch.qos.logback.core.AppenderBase; import org.slf4j.MDC; import java.util.HashMap; import java.util.Map; public class RaygunLogbackAppender extends AppenderBase<ILoggingEvent> { private RaygunErrorManager errorManager; private String apiKey; private String applicationVersion = "1.0.0"; private String environment = "production"; private boolean enabled = true; private String[] ignorePackages = new String[0]; @Override protected void append(ILoggingEvent event) { if (!enabled || errorManager == null) { return; } // Only send ERROR level events if (!event.getLevel().isGreaterOrEqual(ch.qos.logback.classic.Level.ERROR)) { return; } IThrowableProxy throwableProxy = event.getThrowableProxy(); if (throwableProxy == null) { return; } // Check if this package should be ignored if (shouldIgnore(event.getLoggerName())) { return; } // Convert to Throwable Throwable throwable = extractThrowable(throwableProxy); if (throwable == null) { return; } // Create custom data from MDC and event Map<String, Object> customData = createCustomData(event); // Send to Raygun errorManager.handle(throwable, customData); } private boolean shouldIgnore(String loggerName) { if (ignorePackages == null || loggerName == null) { return false; } for (String ignorePackage : ignorePackages) { if (loggerName.startsWith(ignorePackage)) { return true; } } return false; } private Throwable extractThrowable(IThrowableProxy throwableProxy) { if (throwableProxy instanceof ThrowableProxy) { return ((ThrowableProxy) throwableProxy).getThrowable(); } return null; } private Map<String, Object> createCustomData(ILoggingEvent event) { Map<String, Object> customData = new HashMap<>(); // Add MDC context Map<String, String> mdcMap = MDC.getCopyOfContextMap(); if (mdcMap != null) { customData.put("MDC", mdcMap); } // Add event data customData.put("logger", event.getLoggerName()); customData.put("thread", event.getThreadName()); customData.put("timestamp", event.getTimeStamp()); customData.put("message", event.getFormattedMessage()); // Add markers if present if (event.getMarker() != null) { customData.put("marker", event.getMarker().toString()); } return customData; } // Getters and setters for configuration public void setApiKey(String apiKey) { this.apiKey = apiKey; initializeErrorManager(); } public void setApplicationVersion(String applicationVersion) { this.applicationVersion = applicationVersion; } public void setEnvironment(String environment) { this.environment = environment; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public void setIgnorePackages(String ignorePackages) { this.ignorePackages = ignorePackages.split(","); } private void initializeErrorManager() { if (apiKey != null && !apiKey.trim().isEmpty()) { RaygunConfig config = new RaygunConfig.Builder() .apiKey(apiKey) .applicationVersion(applicationVersion) .environment(environment) .build(); RaygunClient client = new RaygunClient(config); this.errorManager = new RaygunErrorManager(client); } } }

7. Demonstration and Usage

RaygunDemo.java - Complete demonstration

import java.util.Map; public class RaygunDemo { public static void main(String[] args) { try { // Example 1: Basic configuration basicExample(); // Example 2: Spring Boot integration springBootExample(); // Example 3: Advanced usage with custom data advancedExample(); } catch (Exception e) { System.err.println("Demo failed: " + e.getMessage()); e.printStackTrace(); } } private static void basicExample() throws Exception { System.out.println("=== Basic Raygun Example ==="); RaygunConfig config = new RaygunConfig.Builder() .apiKey("your-raygun-api-key") .applicationVersion("1.0.0") .environment("development") .defaultTag("service", "demo-service") .defaultTag("region", "us-east-1") .build(); try (RaygunClient client = new RaygunClient(config); RaygunErrorManager errorManager = new RaygunErrorManager(client)) { // Simulate an error try { throw new RuntimeException("This is a test exception from basic example"); } catch (RuntimeException e) { errorManager.handle(e); } // Simulate a business error with custom data try { processUserOrder(null); } catch (IllegalArgumentException e) { Map<String, Object> customData = Map.of( "order_id", "ORD-123", "user_id", "user-456", "action", "process_order" ); errorManager.handle(e, customData); } Thread.sleep(2000); // Wait for async processing System.out.println("Errors processed: " + errorManager.getSentErrorCount()); } } private static void springBootExample() { System.out.println("\n=== Spring Boot Example ==="); // In a Spring Boot application, this would be auto-configured // application.yml: /* raygun: enabled: true api-key: ${RAYGUN_API_KEY} application-version: 1.0.0 environment: production async: true max-retries: 3 default-tags: service: user-service cluster: k8s-cluster-1 custom-data: deployment-id: ${DEPLOYMENT_ID} */ System.out.println("Spring Boot auto-configuration would set up Raygun automatically"); } private static void advancedExample() throws Exception { System.out.println("\n=== Advanced Raygun Example ==="); RaygunConfig config = new RaygunConfig.Builder() .apiKey("your-raygun-api-key") .applicationVersion("2.1.0") .environment("staging") .async(true) .maxRetries(5) .defaultTag("tier", "backend") .defaultTag("datacenter", "aws") .customData("deployment_timestamp", System.currentTimeMillis()) .build(); try (RaygunClient client = new RaygunClient(config); RaygunErrorManager errorManager = new RaygunErrorManager(client, true, 60000, 5)) { // Simulate different types of errors simulateDatabaseError(errorManager); simulateNetworkError(errorManager); simulateBusinessLogicError(errorManager); Thread.sleep(3000); // Wait for async processing System.out.println("Total errors: " + errorManager.getTotalErrorCount()); System.out.println("Sent to Raygun: " + errorManager.getSentErrorCount()); System.out.println("Dropped (rate limited): " + errorManager.getDroppedErrorCount()); } } private static void processUserOrder(String orderId) { if (orderId == null || orderId.trim().isEmpty()) { throw new IllegalArgumentException("Order ID cannot be null or empty"); } // Order processing logic System.out.println("Processing order: " + orderId); } private static void simulateDatabaseError(RaygunErrorManager errorManager) { try { // Simulate database connection issue throw new DatabaseConnectionException( "Failed to connect to database: Connection timeout", "postgresql://db-server:5432", "users_db" ); } catch (DatabaseConnectionException e) { Map<String, Object> customData = Map.of( "database_url", e.getDatabaseUrl(), "database_name", e.getDatabaseName(), "connection_timeout", "30s", "retry_count", "3" ); ErrorReport.User user = new ErrorReport.User.Builder() .identifier("system") .isAnonymous(true) .build(); errorManager.handle(e, customData, user, null); } } private static void simulateNetworkError(RaygunErrorManager errorManager) { try { // Simulate HTTP client error throw new HttpClientException( "HTTP 503 Service Unavailable", "https://api.external-service.com/v1/data", "GET", 503 ); } catch (HttpClientException e) { Map<String, Object> customData = Map.of( "http_url", e.getUrl(), "http_method", e.getMethod(), "http_status", e.getStatusCode(), "retryable", e.getStatusCode() >= 500 ); ErrorReport.Request request = new ErrorReport.Request.Builder() .url(e.getUrl()) .httpMethod(e.getMethod()) .ipAddress("10.0.1.25") .build(); errorManager.handle(e, customData, null, request); } } private static void simulateBusinessLogicError(RaygunErrorManager errorManager) { try { // Simulate business logic error processPayment(new Payment("PAY-001", -100.0, "USD")); } catch (BusinessException e) { errorManager.recordHandledError(e); // Don't send all business errors to Raygun System.out.println("Business exception handled: " + e.getMessage()); } catch (Exception e) { // Unexpected errors still go to Raygun errorManager.handle(e); } } private static void processPayment(Payment payment) { if (payment.getAmount() <= 0) { throw new BusinessException( "Payment amount must be positive: " + payment.getAmount(), "INVALID_AMOUNT" ); } // Payment processing logic System.out.println("Processing payment: " + payment.getId()); } // Demo exception classes static class DatabaseConnectionException extends RuntimeException { private final String databaseUrl; private final String databaseName; public DatabaseConnectionException(String message, String databaseUrl, String databaseName) { super(message); this.databaseUrl = databaseUrl; this.databaseName = databaseName; } public String getDatabaseUrl() { return databaseUrl; } public String getDatabaseName() { return databaseName; } } static class HttpClientException extends RuntimeException { private final String url; private final String method; private final int statusCode; public HttpClientException(String message, String url, String method, int statusCode) { super(message); this.url = url; this.method = method; this.statusCode = statusCode; } public String getUrl() { return url; } public String getMethod() { return method; } public int getStatusCode() { return statusCode; } } static class BusinessException extends RuntimeException { private final String errorCode; public BusinessException(String message, String errorCode) { super(message); this.errorCode = errorCode; } public String getErrorCode() { return errorCode; } } static class Payment { private final String id; private final double amount; private final String currency; public Payment(String id, double amount, String currency) { this.id = id; this.amount = amount; this.currency = currency; } public String getId() { return id; } public double getAmount() { return amount; } public String getCurrency() { return currency; } } }

Configuration Examples

application.yml

raygun: enabled: true api-key: ${RAYGUN_API_KEY:your-api-key-here} api-endpoint: https://api.raygun.com/entries application-version: 2.1.0 environment: ${ENVIRONMENT:production} async: true max-retries: 3 default-tags: service: user-service environment: production cluster: k8s-prod-1 region: us-west-2 custom-data: deployment-id: ${DEPLOYMENT_ID} commit-hash: ${COMMIT_HASH} # Logback configuration with Raygun appender logging: config: classpath:logback-spring.xml level: com.yourcompany: DEBUG

logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?> <configuration> <appender name="RAYGUN" class="com.yourcompany.RaygunLogbackAppender"> <apiKey>${RAYGUN_API_KEY}</apiKey> <applicationVersion>${APP_VERSION:1.0.0}</applicationVersion> <environment>${ENVIRONMENT:development}</environment> <enabled>true</enabled> <ignorePackages>org.springframework,ch.qos.logback</ignorePackages> </appender> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <root level="INFO"> <appender-ref ref="CONSOLE" /> <appender-ref ref="RAYGUN" /> </root> </configuration>

Best Practices

  1. Use appropriate sampling - Don't send every error in high-volume applications
  2. Implement rate limiting - Prevent overwhelming Raygun during error storms
  3. Include meaningful context - User info, request details, custom data
  4. Filter sensitive information - Never send passwords, tokens, or PII
  5. Use async processing - Don't block your application on error reporting
  6. Monitor error volumes - Track both sent and dropped errors
  7. Set up alerts - In Raygun dashboard for critical errors
  8. Test error reporting - Verify integration works in all environments

This implementation provides a complete, production-ready Raygun integration for Java applications, supporting both manual error reporting and automatic integration with frameworks like Spring Boot and Logback.

Leave a Reply

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


Macro Nepal Helper