This code is based on Spring Boot 3.2.5.
Following my previous post on simply applying Sentry to Spring Boot, I am documenting the custom configurations I applied afterward.
When integrating Sentry, I decided that there was no need to collect simple errors occurring during API calls (such as validation errors, login failures, etc.). Therefore, I configured it to report only 500 errors. Additionally, I performed extra work to ensure that errors occurring in schedulers and asynchronous tasks are reported without being missed.
1. Sentry Setup
Documentation: Sentry Docs
Add the dependencies required to use Sentry in Spring Boot.
build.gradle
Plain Text
plugins {
id "io.sentry.jvm.gradle" version "3.12.0"
}
dependencies {
implementation 'io.sentry:sentry-spring-boot-starter-jakarta:6.30.0'
implementation 'org.jetbrains:annotations:26.0.2' // For Nullable checks in BeforeSendCallback
}
src/main/resources/application.yml
Plain Text
sentry:
dsn: {sentry_url}
send-default-pii: true
traces-sample-rate: 1.0
logging:
enabled: falsedsn: Sets the location where events will be sent.
sentry.logging.enabled: Whether to automatically record @Slf4j logs.
- Adding the sentry-spring-boot-starter dependency automatically runs SentryLogbackInitializer at application startup, registering SentryAppender to the ROOT logger.
- It is enabled (true) by default, automatically recording log.error, etc., to Sentry.
- Here, it is set to false to control error reporting directly in the GlobalExceptionHandler.
send-default-pii: Records personally identifiable information, such as the requester's IP.
traces-sample-rate: Controls the volume of transactions sent to Sentry upon error (0.0 ~ 1.0).
- Example: Setting it to 0.2 means approximately 20% of transactions are recorded and sent.
2. Global Exception Handler (ControllerAdvice)
This handles errors occurring during API calls by selectively filtering them.
GlobalExceptionHandler.java
Java
import io.sentry.Sentry;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.ErrorResponse;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* Errors occurring in Spring Security are not sent to Sentry.
*/
@ExceptionHandler({AccessDeniedException.class, AuthenticationException.class})
public ResponseEntity<ErrorResponseDto> handleSecurityException(Exception e) {
log.warn("Security Exception: {}", e.getMessage());
HttpStatus status = (e instanceof AccessDeniedException) ? HttpStatus.FORBIDDEN : HttpStatus.UNAUTHORIZED;
return ResponseEntity.status(status)
.body(new ErrorResponseDto("SECURITY_ERROR", e.getMessage()));
}
/**
* Consolidated handling for all exceptions.
* - Dynamically determines whether it is a 4xx or 5xx error to decide on Sentry transmission.
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponseDto> handleUnexpectedException(Exception e) {
// Check for Spring standard web exceptions (Implementations of ErrorResponse)
// (e.g., NoResourceFoundException, HttpRequestMethodNotSupportedException, ResponseStatusException, etc.)
if (e instanceof ErrorResponse errorResponse) {
HttpStatus status = HttpStatus.valueOf(errorResponse.getStatusCode().value());
// If it is a 4xx Client Error: Leave only a WARN log and do NOT send to Sentry.
if (status.is4xxClientError()) {
if (errorResponse instanceof MethodArgumentNotValidException m) {
if (m.getBindingResult().getFieldError() != null && StringUtils.hasText(m.getBindingResult().getFieldError().getDefaultMessage())) {
return ResponseEntity.status(status)
.body(new ErrorResponseDto("BAD_REQUEST", m.getBindingResult().getFieldError().getDefaultMessage());
}
}
log.warn("Client Error ({}): {}", status, e.getMessage());
return ResponseEntity.status(status)
.body(new ErrorResponseDto("BAD_REQUEST", e.getMessage()));
}
}
// Real 500 Server Error (All other exceptions)
log.error("Unexpected Error", e);
// Send to Sentry
Sentry.captureException(e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponseDto("SERVER_ERROR", "Internal Server Error occurred."));
}
public record ErrorResponseDto(String code, String message) {}
}
3. BeforeSendCallback
Intercepts events right before they are sent to Sentry to modify or filter them.
SentryBeforeSendCallback.java
Java
import io.sentry.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.stereotype.Component;
import java.util.List; // 추가됨
@Component
public class SentryBeforeSendCallback implements SentryOptions.BeforeSendCallback {
@Override
public @Nullable SentryEvent execute(@NotNull SentryEvent event, @NotNull Hint hint) {
// 1. Do not send specific exceptions at all.
if (event.getThrowable() instanceof IllegalArgumentException) {
return null;
}
// 2. Mask sensitive information (e.g., credit card numbers).
// Alternatively, you can enable 'Data Scrubber' in the Sentry server settings.
// 3. Custom Fingerprinting (Grouping similar errors).
// Useful when you want to group DB connection errors as the same issue regardless of the specific query.
if (event.getThrowable() instanceof java.sql.SQLException) {
event.setFingerprints(List.of("database-connection-error"));
}
return event;
}
}
4. Scheduler Error Handling
Errors occurring in schedulers (@Scheduled) are not caught by the global exception handler, so a separate configuration is required.
ScheduledConfig.java
Java
import io.sentry.Sentry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
@Configuration
@EnableScheduling
public class ScheduledConfig implements SchedulingConfigurer {
@Bean
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(5);
scheduler.setAwaitTerminationSeconds(60);
scheduler.setWaitForTasksToCompleteOnShutdown(true);
scheduler.setThreadNamePrefix("scheduler-thread-task-");
// Register Sentry capture in the error handler
scheduler.setErrorHandler(Sentry::captureException);
scheduler.initialize();
return scheduler;
}
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setTaskScheduler(taskScheduler());
}
}
5. Message Channel Error Handling
This covers error handling for asynchronous tasks using message channels, such as Spring Integration.
MyJobEndpoint.java
Java
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.integration.annotation.MessageEndpoint;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.support.ErrorMessage;
@Slf4j
@MessageEndpoint
@RequiredArgsConstructor
public class MyJobEndpoint {
private final MessageChannel errorChannel;
@Bean
@ServiceActivator(inputChannel = "executorChannel", async = "true")
public MessageHandler messageHandler() {
return new MessageHandler() {
@Override
public void handleMessage(Message<?> message) throws MessagingException {
try {
// Processing logic
} catch (Exception e) {
var errMsg = new ErrorMessage(e);
errorChannel.send(errMsg);
}
}
};
}
}
AsyncJobConfig.java
Plain Text
import org.springframework.integration.annotation.ServiceActivator;
@Configuration
@EnableIntegration
public class AsyncJobConfig {
@ServiceActivator(inputChannel = "errorChannel")
public void handleError(Message<?> message) {
Object payload = message.getPayload();
if (payload instanceof MessagingException ex) {
Sentry.captureException(ex);
} else if (payload instanceof Throwable throwable) {
Sentry.captureException(throwable);
}
}
}Conclusion
With these settings, you can ignore 4xx client errors while ensuring that 5xx server errors and asynchronous errors are monitored without fail.
Applying these configurations allows you to build a robust monitoring environment that reduces unnecessary Alert Fatigue while not missing critical server outages and failures in asynchronous tasks.



