null

Логирование Spring Boot с помощью Spring AOP и Logback

Вступление

Логирование - это один из самых важных процессов, позволяющий отслеживать и аналазировать работу системы. Есть множество способов проводить логирование в приложениях, использующих фреймворк Spring Boot. Я же предпочитаю связку таких инструментов как Spring AOP и Logback. В этой статье я планирую рассмотреть уровни логов, некоторые принципы работы Spring AOP и конфигурацию Logback. Также я приведу примеры из реальных проектов, которые могут помочь вам добавить или улучшить логирование в ваших приложениях.

Уровни логирования

Каждое логируемое событие может быть более или менее важным, что привело к появлению термина уровни логирования. ​​​​​​​Они помогают определять и отслеживать события разных степеней важности, разделяя логи по приоритетам и информации
​​​​​​​
​​​​Рассмотрим основные уровни логгировния в порядке возрастания значимости:

  • TRACE:  Является самым детализированным уровнем логирования. Зачастую используется для пошаговой записи процесса, и очень полезен в моменты, когда неоходимо локализовать ошибку. Чаще всего применяется на этапе точеченой отладки работы приложения
  • DEBUG: Является менее детализированным нежели TRACE, но также используется для анализа и выявления проблем на этапе разработки. Чаще всего применяется для записи масштабных переходов состояний, например, обращения к базе данных, запуска или остановки сервисов.
  • INFO:  Является источником общей информации о работе службы или сервиса.  Содержит ключевую информацию о работе приложения, которая может быть полезна для мониторинга. Чаще всего применяется для общих событий в системе, таких как запуск прложения, успешное завершение операций и т.д.
  • WARN: Является предупреждением о возникновении нештатных ситуаций, потенциальных проблем. Например, это могут быть странные форматы запросов или некорректный параметр вызова. Данные предупреждения указывают на потенциальные проблемы, которые не влияют на работу приложения, но могут стать причиной ошибок в будущем.
  • ERROR: ​​​​​​​Является источником сообщений об ошибках. Зачастую используется для обозначения исключений и критических ошибок, которые приводят к сбою работы приложения
  • FATAL: Является источником сообщений о тотальном сбое работоспособности системы, требующего немедленного внимания и часто приводящего к аварийному завершению работы. Например, когда нет доступа к базе данных или сети, системе не хватает памяти.

Spring AOP

Аспектно-ориентированное программирование (АОП) — это парадигма программирования являющаяся дальнейшим развитием процедурного и объектно-ориентированного программирования (ООП), идея которой заключается в выделении сквозной функциональности.

Spring AOP — это механизм, позволяющий внедрять определенное поведение в программу без модификации исходного кода. AOP использует понятие "аспектов", которые можно применить к различным методам или классам, добавляя к ним дополнительные возможности. Примеры использования включают:

  • Логирование.
  • Обработка исключений.
  • Транзакционное управление.

В логировании Spring AOP может быть использован для создания аспектов, которые автоматически добавляют информирования о событиях при вызове методов, обработке их параметров и выводе  результатов.

Основные концепции Spring AOP

  • Аспект: модуль, определяющий точку, в которой будет добавлена дополнительная функциональность (например, логирование). Он объединяет описания Pointcut и Advice.
  • Join Point: точка выполнения или наблюдения программы, присоединения кода, в которой может быть выполнен аспект (например, вызов метода).
  • Advice: конкретный код, который выполняется на Join Point (например, логирование).
  • Pointcut: выражение или срез, определяющее, где именно аспект должен быть применен. Это может быть одна или более точек.

Типы событий в AOP и пример использования

  • Before — срабатывает перед вызовом метода; логирует параметры и начало выполнения.
  • After — выполняется после завершения метода, фиксирует факт его завершения.
  • After returning — срабатывает после успешного выполнения метода; логирует возвращаемое значение.
  • After throwing — активируется при возникновении исключения; фиксирует и логирует ошибки.
  • After finally — выполняется после блока finally, фиксирует завершение независимо от результата.
  • Around — оборачивает метод, позволяет выполнить действия до и после вызова, управлять результатом и при необходимости пропускать выполнение метода.

Logback

Logback — это библиотека для логирования, широко используемая в Spring Boot, которая предоставляет гибкую систему конфигурации и поддерживает различные форматы логов. Logback позволяет легко настраивать различные уровни логирования для разных классов или пакетов и сохранять логи в файл, консоль или базу данных

Альтернативы?

Существует несколько популярных библиотек для логирования, которые могут служить альтернативами Logback в приложениях на Java. Приведем список некоторых из них:

  1. Log4j
    – Очень популярная и широко используемая библиотека логирования с высокой настраиваемостью.

  2. SLF4J (Simple Logging Facade for Java)
    – Это фасад для различных библиотек логирования (включая Logback), который позволяет переключаться между ними без изменения кода.

  3. java.util.logging (JUL)
    – Встроенная библиотека логирования Java, простая в использовании, но с ограниченными возможностями и гибкостью.

  4. TinyLog
    – Легковесная библиотека с простым синтаксисом, хорошо подходит для небольших приложений или проектов с минимальными требованиями к логированию.

  5. Apache Commons Logging (JCL)
    – Логирующий фасад от Apache, но уступающий SLF4J в популярности и гибкости.

Почему же Logback?

Logback зачастую используется в Spring Boot приложениях, поскольку обладает следующими преимуществами:

  1. Производительность
    – Logback работает быстрее, чем большинство других библиотек логирования.

  2. Гибкость и масштабируемость
    – Поддерживает сложные конфигурации и возможность динамического изменения настроек во время выполнения приложения.

  3. Поддержка различных форматов вывода и мест хранения логов
    – Logback может записывать логи как в файлы и консоль, так и в удалённые хранилища, базы данных и другие системы мониторинга.

  4. Интеграция со Spring Boot
    – Logback поддерживается Spring Boot "из коробки" и легко настраивается через конфигурационные файлы, что упрощает его интеграцию и настройку.

  5. Совместимость с SLF4J
    – Logback разработан основателем SLF4J и является его "родной" реализацией, что делает его совместимым с широким спектром логирующих фасадов, а также позволяет легко заменять другие библиотеки логирования.

Так что же мы хотим?

Цель логирования с использованием Spring AOP и Logback — обеспечить прозрачность и контроль за выполнением кода, не перегружая при этом основной функционал приложения. Мы хотим:

  1. Автоматизировать логирование вызовов методов, их параметров и возвращаемых значений. Это позволит отслеживать жизненный цикл методов без явного добавления логирования в каждый метод, а значит, поддерживать код чистым и читабельным.

  2. Упростить отладку и диагностику приложения. Благодаря структурированным логам, которые включают информацию о каждом вызове и результате метода, разработчикам будет легче находить и устранять ошибки.

  3. Гибко настраивать уровни логов для разных компонентов системы. Это позволяет фильтровать важную информацию на различных уровнях: от детализированных данных при отладке до критических ошибок в продакшн-среде.

  4. Собирать и сохранять логи в разных форматах и местах (консоль, файлы, базы данных) для дальнейшего анализа и мониторинга. Такой подход делает возможным эффективный анализ поведения приложения, а также предоставляет исторические данные для анализа ошибок.

Структура конфигурационного файла Logback

Конфигурация Logback может происходить через файл logback.xml или logback-spring.xml, расположенные в src/main/resources. В этих файлах можно определять формат логов, их уровни и место хранения записей.

<configuration>
    <!-- Пример определения уровня логирования -->
    <logger name="org.springframework" level="INFO"/>
    
    <!-- Консольный аппендер -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    
    <!-- Файловый аппендер -->
    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <file>logs/app.log</file>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- Корневой логгер -->
    <root level="DEBUG">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="FILE"/>
    </root>
</configuration>

Описание конфигурации Logback

  • ConsoleAppender: используется для вывода логов в консоль. Удобен для отладки на этапе разработки.
  • FileAppender: сохраняет логи в файл. Подходит для сохранения исторических данных логов.
  • Pattern: задает формат вывода логов. В примере выше указаны время, уровень логов, имя логгера и сообщение.

Применение Spring AOP для логирования

Теперь создадим аспект для логирования вызовов методов и их параметров с использованием Spring AOP. Этот аспект можно использовать для логирования входных данных методов и их результатов.

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* com.example.*.*(..))")
    public void logMethodCall(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        System.out.println("Вызов метода: " + methodName + " с аргументами: " + Arrays.toString(args));
    }

    @AfterReturning(pointcut = "execution(* com.example.*.*(..))", returning = "result")
    public void logMethodReturn(JoinPoint joinPoint, Object result) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Метод " + methodName + " завершился с результатом: " + result);
    }
}

Описание аспекта логирования

  • @Before: аннотация, указывающая, что метод logMethodCall должен вызываться перед каждым вызовом метода в указанном пакете.
  • @AfterReturning: аннотация, указывающая, что метод logMethodReturn должен вызываться после завершения вызова метода и логировать возвращаемое значение.
  • execution( com.example..*(..))**: выражение, определяющее, какие методы будут логироваться. В данном случае, логируются все методы пакета com.example.

Реальные примеры

В завершение статьи хотелось бы привести реальные примеры конфигурации Logback и код, реализующий Spring AOP, из моих проектов

logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <logger name="ch.qos.logback.classic" level="OFF"/>

    <appender name="console"
              class="ch.qos.logback.core.ConsoleAppender">
        <layout class="ch.qos.logback.classic.PatternLayout">
            <Pattern>
                %white(%d{ISO8601}) %highlight(%-5level) [%blue(%t)] RID: %X{RID:-no} UID: %X{UID:-no} %yellow(%C{1}): %msg%n%throwable
            </Pattern>
        </layout>
    </appender>

    <root level="info">
        <appender-ref ref="console" />
    </root>

    <logger name="com.test" level="debug" additivity="false">
        <appender-ref ref="console" />
    </logger>

    <!--    Disabling startup logs from logback-->
    <statusListener class="ch.qos.logback.core.status.NopStatusListener"/>

</configuration>

LoggingAspectConfig

@Slf4j
@Aspect
@Component
public class LoggingAspectConfig {

    /**
     * Pointcut for methods annotated with @Loggable
     */
    @Pointcut("@annotation(com.test.common.annotations.Loggable)")
    public void loggableMethods() {
        // Pointcut for loggable methods
    }

    /**
     * Pointcut that matches all repositories, services, and Web REST endpoints.
     */
    @Pointcut("within(@org.springframework.stereotype.Repository *) || " +
            "within(@org.springframework.stereotype.Service *) || " +
            "within(@org.springframework.web.bind.annotation.RestController *)")
    public void springBeanPointcut() {
        // Pointcut for Spring beans
    }

    /**
     * Pointcut that matches all Spring beans in the application's main packages.
     */
    @Pointcut("within(com.test..*)")
    public void applicationPackagePointcut() {
        // Pointcut for application packages
    }

    /**
     * Advice that logs methods throwing exceptions.
     *
     * @param joinPoint join point for advice
     * @param e         exception
     */
    @AfterThrowing(pointcut = "applicationPackagePointcut() && springBeanPointcut()", throwing = "e")
    public void logAfterThrowing(JoinPoint joinPoint, Throwable e) {
        log.error("Exception in {}.{}() with cause = {}",
                joinPoint.getSignature().getDeclaringTypeName(),
                joinPoint.getSignature().getName(),
                e.getCause() != null ? e.getCause() : "NULL");
    }

    /**
     * Advice that logs when a method is entered and exited.
     *
     * @param joinPoint join point for advice
     * @return result
     * @throws Throwable if there is an error
     */
    @Around("loggableMethods()")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
            log.debug("Enter: {}() with argument[s] = {}",
                    joinPoint.getSignature().getName(),
                    joinPoint.getArgs());
        try {
            Object result = joinPoint.proceed();
                log.debug("Exit: {}() with result = {}",
                        joinPoint.getSignature().getName(),
                        result);
            return result;
        } catch (IllegalArgumentException e) {
            log.error("Illegal argument: {} in {}.{}()",
                    joinPoint.getArgs(),
                    joinPoint.getSignature().getDeclaringTypeName(),
                    joinPoint.getSignature().getName());
            throw e;
        }
    }
}

​​​​​​​

 

RID и UID — это идентификаторы, которые используются для уникальной идентификации объектов, записей или сущностей в различных системах и приложениях. В рамках данной статьи их описание и присвоение не продемонстрировано.

Заключение

Логирование — важная часть процесса разработки, которая помогает отслеживать и диагностировать поведение приложения. В Spring Boot можно эффективно реализовать логирование, используя возможности Spring AOP и Logback. Logback предоставляет гибкие возможности для настройки и форматирования логов, а Spring AOP позволяет минимизировать код логирования, автоматизируя процесс логирования вызовов и результатов методов.

Вперед