Easy-to-Miss Details When Integrating Sentry with Spring Boot
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
1
2
3
4
5
6
7
8
9
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
1
2
3
4
5
6
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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.

민갤
Back-End Developer
백엔드 개발자입니다.

Building a Design System with Developers: Page Variables
Let's create a developer-friendly design system.
Design

Experience with Blaze-Persistence
spring boot 에 Blaze-Persistence 사용기
Spring Boot