Настройка логирования HTTP-запросов в Spring Boot: полное руководство
Для кого эта статья:
- Разработчики, использующие Spring Boot для создания приложений
- Специалисты по DevOps, заинтересованные в настройке логирования
Студенты и начинающие разработчики, стремящиеся улучшить свои навыки в Java-разработке
Отладка Spring Boot приложения без правильного логирования — всё равно что искать иголку в стоге сена с завязанными глазами. Когда ваше приложение выходит из строя в production, а логи ограничиваются сухим "Internal Server Error", начинается настоящий кошмар для разработчика. Подробное логирование HTTP-запросов и ответов — это ваши глаза и уши в мире Spring Boot приложений, позволяющие быстро выявлять проблемы, анализировать поведение пользователей и обеспечивать стабильную работу системы даже при высокой нагрузке. 🔍
Столкнулись с необходимостью настройки логирования в Spring Boot или хотите углубить свои знания в Java-разработке? Курс Java-разработки от Skypro не только раскрывает тонкости работы с логированием, но и формирует полноценное понимание архитектуры Spring-приложений. Наши студенты уже через 3 месяца самостоятельно реализуют системы логирования производственного уровня, получая высокооплачиваемые предложения о работе! Присоединяйтесь, чтобы от теории перейти к практике.
Механизмы логирования HTTP-трафика в Spring Boot
Spring Boot предоставляет несколько механизмов для логирования HTTP-трафика, каждый из которых имеет свои преимущества и ограничения. Понимание этих механизмов критически важно для выбора оптимального решения под конкретные задачи вашего приложения.
Основные механизмы включают:
- Встроенные возможности логирования (application.properties/yml)
- Фильтры Servlet для перехвата запросов/ответов
- Spring MVC интерцепторы
- AOP (Аспектно-ориентированное программирование)
- Специализированные библиотеки (например, Spring Cloud Sleuth)
Рассмотрим базовую настройку через application.properties, которая позволяет активировать встроенное логирование без написания дополнительного кода:
# Логирование веб-запросов
logging.level.org.springframework.web=DEBUG
# Логирование запросов к БД
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
Эта конфигурация активирует логирование базовой информации о запросах, но не включает полное содержимое тела запроса или ответа. Для более детального логирования требуются дополнительные инструменты.
Сравнение основных механизмов логирования в Spring Boot:
| Механизм | Преимущества | Ограничения | Подходит для |
|---|---|---|---|
| Встроенное логирование | Простота настройки, минимум кода | Ограниченная детализация, нет доступа к телу запроса/ответа | Базовая отладка, разработка |
| Servlet фильтры | Полный контроль над запросом/ответом, низкоуровневый доступ | Требует дополнительной обработки тела запроса | Детальное логирование в production |
| MVC интерцепторы | Интеграция с Spring MVC, доступ к обработанным данным | Работает только с MVC, не перехватывает исключения | Логирование бизнес-логики |
| AOP | Разделение логики логирования от бизнес-логики | Сложность настройки, возможные проблемы с прокси | Сложные сценарии логирования |
Александр Петров, Lead Backend Developer
В одном из наших высоконагруженных проектов мы столкнулись с периодическими таймаутами, которые невозможно было воспроизвести в тестовой среде. Настройка детального логирования HTTP-запросов стала ключом к решению проблемы. Внедрив комбинацию Servlet-фильтра для логирования запросов и @ControllerAdvice для перехвата исключений, мы обнаружили, что определенные комбинации параметров вызывали неоптимальные запросы к базе данных.
Самым сложным было организовать логирование так, чтобы оно не влияло на производительность системы, обрабатывающей более 500 запросов в секунду. Мы реализовали асинхронную запись логов и выборочное логирование только проблемных эндпоинтов. В итоге время отклика сократилось на 40%, а количество инцидентов уменьшилось в 5 раз.

Реализация фильтра для захвата запросов и ответов
Servlet фильтры — наиболее гибкий способ логирования HTTP-трафика, поскольку они перехватывают запрос до его обработки контроллером и ответ после формирования. Это позволяет зафиксировать абсолютно все детали коммуникации. 📊
Основная проблема при реализации фильтра — HTTP запросы и ответы могут быть прочитаны только один раз. Поэтому необходимо создать обертки, которые позволят многократно читать содержимое.
Вот пример реализации фильтра для логирования:
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class RequestResponseLoggingFilter implements Filter {
private static final Logger log = LoggerFactory.getLogger(RequestResponseLoggingFilter.class);
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// Создаем обертки для запроса и ответа
CachedBodyHttpServletRequest cachedRequest = new CachedBodyHttpServletRequest(httpRequest);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(httpResponse);
long startTime = System.currentTimeMillis();
// Логируем запрос
logRequest(cachedRequest);
// Передаем обертки дальше по цепочке фильтров
chain.doFilter(cachedRequest, responseWrapper);
// Логируем ответ
logResponse(responseWrapper, System.currentTimeMillis() – startTime);
// Важно! Копируем данные из обертки обратно в оригинальный ответ
responseWrapper.copyBodyToResponse();
}
private void logRequest(CachedBodyHttpServletRequest request) throws IOException {
String requestBody = new String(request.getCachedBody(), StandardCharsets.UTF_8);
log.info("REQUEST [{}] {} {} – Headers: {}, Body: {}",
request.getRemoteAddr(),
request.getMethod(),
request.getRequestURI(),
getHeadersAsString(request),
requestBody);
}
private void logResponse(ContentCachingResponseWrapper response, long executionTime) {
String responseBody = new String(response.getContentAsByteArray(), StandardCharsets.UTF_8);
log.info("RESPONSE – Status: {}, Time: {}ms, Body: {}",
response.getStatus(),
executionTime,
responseBody);
}
private String getHeadersAsString(HttpServletRequest request) {
StringBuilder headers = new StringBuilder();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
headers.append(headerName).append("=").append(request.getHeader(headerName)).append(", ");
}
return headers.toString();
}
}
Также необходимо реализовать класс CachedBodyHttpServletRequest:
public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
private byte[] cachedBody;
public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
this.cachedBody = StreamUtils.copyToByteArray(request.getInputStream());
}
@Override
public ServletInputStream getInputStream() throws IOException {
return new CachedServletInputStream(cachedBody);
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));
}
public byte[] getCachedBody() {
return cachedBody;
}
// Дополнительный класс для обертки массива байтов в ServletInputStream
private static class CachedServletInputStream extends ServletInputStream {
private final ByteArrayInputStream inputStream;
public CachedServletInputStream(byte[] cachedBody) {
this.inputStream = new ByteArrayInputStream(cachedBody);
}
@Override
public int read() throws IOException {
return inputStream.read();
}
@Override
public boolean isFinished() {
return inputStream.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException("setReadListener is not implemented");
}
}
}
Существуют случаи, когда логирование запросов может быть критичным с точки зрения безопасности или производительности. Вот список типов запросов, которые часто исключают из логирования:
- Запросы, содержащие конфиденциальные данные (пароли, токены авторизации)
- Здоровье проверки (health checks), которые выполняются часто и создают шум в логах
- Запросы к статическим ресурсам (изображения, CSS, JavaScript)
- Запросы с большими бинарными данными (загрузка файлов)
Дополните фильтр логикой для избирательного логирования:
private boolean shouldLogRequest(HttpServletRequest request) {
String path = request.getRequestURI();
// Пропускаем health checks и статические ресурсы
return !path.contains("/actuator/health") &&
!path.endsWith(".css") &&
!path.endsWith(".js") &&
!path.endsWith(".png") &&
!path.endsWith(".jpg") &&
!path.endsWith(".ico");
}
Настройка интерцепторов в Spring MVC для логирования
В отличие от фильтров, которые действуют на уровне Servlet API, интерцепторы Spring MVC интегрируются непосредственно в цикл обработки запросов Spring. Это дает доступ к обработанным данным и позволяет логировать информацию, специфичную для Spring MVC. 🛠️
Интерцепторы особенно полезны, когда требуется доступ к контексту выполнения Spring или когда необходимо логировать только определенные аспекты запроса без полного захвата тела запроса/ответа.
Реализация интерцептора для логирования запросов:
@Component
public class LoggingInterceptor implements HandlerInterceptor {
private static final Logger log = LoggerFactory.getLogger(LoggingInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// Сохраняем время начала обработки запроса
request.setAttribute("startTime", System.currentTimeMillis());
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
log.info("Handling request [{}] to {}.{}",
request.getMethod(),
handlerMethod.getBeanType().getSimpleName(),
handlerMethod.getMethod().getName());
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
// Логирование после обработки контроллером, но до рендеринга представления
if (modelAndView != null) {
log.info("View name: {}", modelAndView.getViewName());
log.info("Model attributes: {}", modelAndView.getModel());
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// Вычисляем время выполнения запроса
long startTime = (Long) request.getAttribute("startTime");
long executionTime = System.currentTimeMillis() – startTime;
log.info("Request completed with status {} in {}ms", response.getStatus(), executionTime);
if (ex != null) {
log.error("Exception during request processing: ", ex);
}
}
}
После создания интерцептора его нужно зарегистрировать в конфигурации Spring MVC:
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
private final LoggingInterceptor loggingInterceptor;
public WebMvcConfig(LoggingInterceptor loggingInterceptor) {
this.loggingInterceptor = loggingInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loggingInterceptor)
.addPathPatterns("/**") // Применяем ко всем путям
.excludePathPatterns("/static/**"); // Исключаем статические ресурсы
}
}
Марина Соколова, DevOps Engineer
Работая в финтех-стартапе, мы столкнулись с необходимостью аудита всех финансовых операций для соответствия регуляторным требованиям. Нам нужно было не просто логировать запросы, а создавать структурированные журналы с возможностью быстрого поиска и анализа.
Мы создали специальный интерцептор, который извлекал бизнес-идентификаторы из запросов (номера счетов, ID транзакций) и добавлял их к MDC (Mapped Diagnostic Context). Это позволило нам связывать все логи, относящиеся к одной бизнес-операции, даже если они распределены между микросервисами.
Самым важным решением стало разделение технических логов и бизнес-аудита. Технические детали запросов шли в Elasticsearch для оперативного мониторинга, а бизнес-события — в специализированную систему аудита с долгосрочным хранением. После внедрения этой системы время расследования инцидентов сократилось с нескольких дней до нескольких часов.
Интерцепторы и фильтры можно комбинировать для получения максимальной информации о запросах. Рассмотрим их сравнение по ключевым характеристикам:
| Характеристика | Servlet Фильтр | MVC Интерцептор |
|---|---|---|
| Уровень выполнения | Servlet API (низкий уровень) | Spring MVC (высокий уровень) |
| Доступ к телу запроса/ответа | Полный доступ (требует кастомной обертки) | Ограниченный доступ |
| Доступ к handler методу | Нет | Да (HandlerMethod) |
| Перехват исключений | Только в рамках фильтра | Да (afterCompletion) |
| Доступ к данным модели | Нет | Да (ModelAndView) |
| URL-маппинг | Базовый (на уровне URL) | Расширенный (интеграция с маппингом Spring) |
Конфигурация логирования исключений через @ControllerAdvice
Централизованная обработка исключений — ключевой аспект логирования в Spring Boot приложениях. С помощью @ControllerAdvice можно перехватывать и логировать все исключения, возникающие в контроллерах, обеспечивая единый подход к обработке ошибок. ⚠️
Создание глобального обработчика исключений:
@ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* Обработка специфических бизнес-исключений
*/
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException ex, WebRequest request) {
log.warn("Business exception: {} for request {}", ex.getMessage(), getRequestDetails(request), ex);
ErrorResponse errorResponse = new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
ex.getMessage(),
generateErrorId()
);
return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}
/**
* Обработка ValidationException
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(
MethodArgumentNotValidException ex, WebRequest request) {
List<String> validationErrors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.toList());
String errorMessage = "Validation failed: " + String.join(", ", validationErrors);
log.warn("Validation failed for request {}: {}",
getRequestDetails(request), validationErrors, ex);
ErrorResponse errorResponse = new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
errorMessage,
generateErrorId()
);
return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}
/**
* Обработка всех остальных исключений
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleAllExceptions(Exception ex, WebRequest request) {
String errorId = generateErrorId();
log.error("Unexpected error occurred (ID: {}): {} for request {}",
errorId, ex.getMessage(), getRequestDetails(request), ex);
ErrorResponse errorResponse = new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"An unexpected error occurred. Reference: " + errorId,
errorId
);
return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
/**
* Создание уникального ID для отслеживания ошибки
*/
private String generateErrorId() {
return UUID.randomUUID().toString();
}
/**
* Извлечение деталей запроса для логирования
*/
private String getRequestDetails(WebRequest request) {
if (request instanceof ServletWebRequest) {
ServletWebRequest servletRequest = (ServletWebRequest) request;
HttpServletRequest httpRequest = servletRequest.getNativeRequest(HttpServletRequest.class);
if (httpRequest != null) {
return httpRequest.getMethod() + " " + httpRequest.getRequestURI();
}
}
return request.getDescription(false);
}
/**
* Класс для структурированного ответа об ошибке
*/
@Getter
@AllArgsConstructor
public static class ErrorResponse {
private int status;
private String message;
private String errorId;
}
}
Ключевые принципы эффективного логирования исключений:
- Используйте разные уровни логирования (ERROR, WARN, INFO) в зависимости от типа исключения
- Создавайте уникальные ID для каждой ошибки, чтобы пользователи могли предоставить их в службу поддержки
- Включайте контекст запроса (метод, URL, параметры) для упрощения отладки
- Структурируйте ответы об ошибках для унификации пользовательского опыта
- Скрывайте конфиденциальные данные и внутренние детали реализации в production-окружении
Для улучшения логирования исключений можно настроить MDC (Mapped Diagnostic Context) в фильтре, чтобы добавить контекст ко всем логам запроса:
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class MdcLoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// Генерируем уникальный ID запроса
String requestId = UUID.randomUUID().toString();
try {
// Добавляем данные в MDC
MDC.put("requestId", requestId);
MDC.put("method", httpRequest.getMethod());
MDC.put("path", httpRequest.getRequestURI());
MDC.put("remoteAddr", httpRequest.getRemoteAddr());
// Извлекаем ID пользователя из JWT токена, если есть
Optional.ofNullable(httpRequest.getHeader("Authorization"))
.map(this::extractUserIdFromToken)
.ifPresent(userId -> MDC.put("userId", userId));
// Продолжаем выполнение цепочки фильтров
chain.doFilter(request, response);
} finally {
// Очищаем MDC после завершения запроса
MDC.clear();
}
}
private String extractUserIdFromToken(String authHeader) {
// Логика извлечения ID пользователя из JWT токена
// В реальном приложении тут должна быть полноценная валидация токена
if (authHeader.startsWith("Bearer ")) {
try {
String token = authHeader.substring(7);
// Упрощенный пример, в реальности используйте проверенную библиотеку для JWT
String payload = new String(Base64.getDecoder().decode(token.split("\\.")[1]));
return new JSONObject(payload).getString("sub");
} catch (Exception e) {
// Игнорируем ошибки парсинга
return "unknown";
}
}
return "unknown";
}
}
Для форматирования логов с учетом MDC контекста, настройте logback.xml:
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} – [rid:%X{requestId}] [user:%X{userId}] [%X{method} %X{path}] %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="CONSOLE" />
</root>
</configuration>
Оптимизация логирования для production-окружения
Эффективное логирование в production требует баланса между информативностью логов и производительностью системы. Неоптимизированное логирование может привести к значительному падению производительности и даже отказу приложения. 🏭
Ключевые стратегии оптимизации логирования:
- Асинхронное логирование для минимизации воздействия на основной поток выполнения
- Выборочное логирование для снижения объема данных
- Ротация и архивация логов для управления дисковым пространством
- Центральное хранение логов для распределенных систем
- Структурированные логи (JSON) для упрощения анализа
Настройка асинхронного логирования в logback.xml:
<configuration>
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>256</queueSize>
<appender-ref ref="FILE" />
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/application.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/application.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
<encoder class="net.logstash.logback.encoder.LogstashEncoder" />
</appender>
<root level="info">
<appender-ref ref="ASYNC" />
</root>
<!-- Повышенный уровень логирования для часто вызываемых классов -->
<logger name="org.springframework.web.filter.CommonsRequestLoggingFilter" level="WARN" />
</configuration>
Для профилирования логирования в разных окру
жениях используйте Spring Profiles:
# application-dev.properties
logging.level.root=INFO
logging.level.com.myapp=DEBUG
logging.level.org.springframework.web=DEBUG
# application-prod.properties
logging.level.root=WARN
logging.level.com.myapp=INFO
logging.level.org.springframework.web=WARN
В production особенно важно обеспечить отказоустойчивость системы логирования. Рассмотрим сравнение стратегий логирования для production:
| Стратегия | Преимущества | Недостатки | Рекомендуемое использование |
|---|---|---|---|
| Локальные файлы + ротация | Простота настройки, минимальная задержка | Сложность централизованного анализа, риск потери при сбое сервера | Небольшие приложения, один сервер |
| ELK Stack (Elasticsearch, Logstash, Kibana) | Мощный поиск и визуализация, централизованное хранение | Высокие требования к ресурсам, сложность настройки | Средние и крупные распределенные приложения |
| Cloud Logging Services (CloudWatch, Stackdriver) | Масштабируемость, интеграция с облачной инфраструктурой | Зависимость от провайдера, потенциально высокая стоимость | Приложения, размещенные в облаке |
| APM системы (AppDynamics, New Relic) | Комплексный мониторинг с трассировкой, аналитика производительности | Высокая стоимость, избыточность для простых задач | Критически важные бизнес-приложения |
Лучшие практики для оптимизации производительности логирования:
- Используйте условную логику с проверкой уровня логирования:
if (log.isDebugEnabled()) { log.debug("Complex " + expensiveOperation() + " log"); } - Применяйте параметризованное логирование вместо конкатенации строк:
log.info("Processing user {}", userId) - Ограничивайте размер логируемых данных (особенно тел запросов)
- Используйте выборочное семплирование для высоконагруженных систем
- Настраивайте фильтры для чувствительных данных (PII, кредитные карты)
Для централизованного сбора логов в production можно использовать агенты, такие как Filebeat, Fluentd или Logstash, которые отправляют логи в центральное хранилище без существенной нагрузки на приложение.
Настройка правильного логирования HTTP-запросов и ответов — инвестиция, которая окупается при первом же серьезном инциденте в production. Комбинируя различные подходы — фильтры для полного доступа к HTTP-трафику, интерцепторы для интеграции со Spring MVC, @ControllerAdvice для единообразной обработки исключений и асинхронные аппендеры для производительности — вы создаете надежную систему наблюдаемости, способную выдержать любую нагрузку. Помните: лучшие системы логирования те, о существовании которых разработчики вспоминают только когда нужно найти причину проблемы, а не когда они создают проблемы сами.