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
- Use appropriate sampling - Don't send every error in high-volume applications
- Implement rate limiting - Prevent overwhelming Raygun during error storms
- Include meaningful context - User info, request details, custom data
- Filter sensitive information - Never send passwords, tokens, or PII
- Use async processing - Don't block your application on error reporting
- Monitor error volumes - Track both sent and dropped errors
- Set up alerts - In Raygun dashboard for critical errors
- 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.