프로젝트/여행지 오픈 API

[LogBack]API 요청 로그 수집하기

블랑v 2023. 11. 22. 10:32

 

개요

 

API 요청에 따라 springboot에서 로그를 수집하고,

발생하는 로그를 ES의 인덱스에 추가하려고 한다.

 

전체적인 로직은 다음 포스팅들을 참고하였다.

 

https://prohannah.tistory.com/182

 

Spring Logging (1) : HTTP Request/Response 로그 남기기

서비스를 운영하면서 서버가 받는 HTTP 요청과 서버가 제공하는 응답을 로그로 남긴다면, 추후 무슨 일이 생겼을 때 추적이 용이하다. 이번 포스팅은 Spring Boot을 사용하여 로그 남기기 시리즈 중

prohannah.tistory.com

 

https://devbksheen.tistory.com/entry/ELK-Filebeat%EB%A1%9C-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EB%A1%9C%EA%B7%B8-%EC%88%98%EC%A7%91%ED%95%98%EA%B8%B0with-Spring-Boot 

 

ELK + Filebeat로 실시간 로그 수집하기(with. Spring Boot)

Filebeat는 로그 데이터를 전달하고 중앙화하기 위한 경량의 Producer이다. Filebeat는 지정한 로그 파일 또는 위치를 모니터링하고 로그 이벤트를 수집해 Logstash로 전달해주고, 가공 작업을 거쳐 Elastic

devbksheen.tistory.com

https://awse2050.tistory.com/72

 

Spring Boot Logback 설정으로 로그파일 생성

최근 회사에서 예외처리(에러) 및 엔드포인트에 대한 응답에 대한 데이터를 확인하기 위해서 직접 Slf4j 를 통해서 작성을 했으나 현재 동작시키고 있는 리눅스에서 백그라운드로 실행한다지만

awse2050.tistory.com

 

 

API 로그 수집 준비

AOP의 경우는 해당 포스팅에서 설명하였으니, 생략한다.

https://csg1353.tistory.com/91

 

AOP의 개념과 적용하기

개요 https://csg1353.tistory.com/90 위의 포스팅처럼 elasticsearch의 logstash 로그를 수집하기 위해 AOP의 개념을 사용해 로그를 수집해보려 한다. 이를 위해서는 먼저 AOP에 대한 개념을 학습해야 할 것이다.

csg1353.tistory.com

 

 

AOP 설정

package com.sch.sch_elasticsearch.aop;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
@RequiredArgsConstructor
public class LoggingAspect {
    private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
    private final ObjectMapper objectMapper;

    @Pointcut("execution(* com.sch.sch_elasticsearch.domain..*.*(..)) " +
            "&& @annotation(com.sch.sch_elasticsearch.aop.SaveLogging)" )
    public void pointCut() {}

    @AfterReturning("pointCut()")
    public void logAfterMethodReturn(JoinPoint joinPoint) {
        // SaveLogging 어노테이션이 붙은 메서드의 로그를 DEBUG 레벨로 기록
        // 실제로 아래 내역대로 로깅하면 안 된다. 테스트용으로 시험했다.
        logger.debug("포인트컷");
    }
}

 

다음과 같이 모든 메서드가 실행된 후 PointCut을 실행하게 하였다.

이 과정에서 필요한 부분만 로그를 수집하기 위해, 전용 로그를 생성하였다. (SaveLogging)

 

package com.sch.sch_elasticsearch.aop;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SaveLogging {
    //포인트컷 저장용 어노테이션입니다.
}

 

내가 만든 커스텀 어노테이션이 달린 부분만 인식하여 AOP 과정을 수행한다.두 메서드가 있으면 아래만 반응하는 것이다.

 

 

 

Logback이란?

Logback은 SLF4J(Simple Logging Facade for Java)를 위한 구현체 중 하나로, 자체적인 로깅 라이브러리다.

Logback은 로그를 관리하는 전체적인 기능을 제공하며, 이를 위한 설정 파일을 XML 형식으로 작성한다.

 

간단히 정리하자면,

  • Logback: 로깅을 위한 자체 라이브러리로, 로그 레벨 설정, 출력 형식, 파일 로깅, 콘솔 로깅 등 다양한 기능을 제공
  • XML 설정 파일: Logback의 동작을 설정하는 데 사용되며, 로그 패턴, 로그 레벨, 로그 파일의 저장 위치 등을 정의
  • SLF4J: 로깅에 대한 단순화된 인터페이스를 제공.
    흔히 롬복으로 사용하는 '@Slf4j' 를 어노테이션으로 선언하면, 클래스에 자동으로 SLF4J(Simple Logging Facade for Java)의 Logger 객체가 생성된다. 

 

Logback 설정 예시

 

`src/main/resources` 디렉토리에 Logback의 설정 파일 logback-spring.xml을 생성하고 설정한다.

기본적으로 모든 콘솔 로그를 출력한다. 하지만 저장용 로그는 DEBUG를 사용하고, 이는 해당 경로에 로그 파일 형태로 저장된다.

 

현재 파일 경로는 테스트용으로 하드코딩했으나, 실제로는 변경해야 한다.

 

<configuration>
    <!-- 콘솔 로깅 : 모든 로그를 콘솔에 출력 -->
    <appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>[%d{yyyy-MM-dd HH:mm:ss}][%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 특정 조건 (DEBUG 레벨)의 로그만 파일에 저장, AOP에서 특정 저장용 로그만 DEBUG 설정할 것-->
    <appender name="ApiLogFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>DEBUG</level> <!-- 로그 레벨을 DEBUG로 변경 -->
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <file>C:\Users\SSAFY\Desktop\openTripAPI.log</file> <!-- 파일 경로 -->
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>[%d{yyyy-MM-dd HH:mm:ss}][%thread] %-5level %logger{36} - %msg%n</pattern> <!-- 패턴 추가 -->
        </encoder>
        <!-- 하루에 한번 압축 후 보관, 최대 30일, 1GB까지 보관 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>C:\Users\SSAFY\Desktop\openTripAPI.%d{yyyy-MM-dd}.gz</fileNamePattern> <!-- 파일 경로 -->
            <maxHistory>30</maxHistory>
            <totalSizeCap>1GB</totalSizeCap>
        </rollingPolicy>
    </appender>

    <!-- 기본 로그 설정 -->
    <root level="INFO">
        <appender-ref ref="Console"/>
    </root>

    <!-- 특정 조건 (예: DEBUG 레벨)의 로그만 파일에 저장하도록 설정 -->
    <logger name="com.sch.sch_elasticsearch.aop" level="DEBUG" additivity="false">
        <appender-ref ref="ApiLogFile"/>
    </logger>
</configuration>

 

이제 로거가 콘솔에 찍히고, 내 log 파일에 저장될 것이다.

물론 이 설정을 적용하기 위해, 서버 구동 시 application.yml에 이를 적용하여 알 수 있도록 해야 할 것이다.

 

Application.yml

logging:
  level:
    org.springframework.data.elasticsearch.client.WIRE: TRACE
  config: classpath:logback-spring.xml

 

 

로그 저장 결과

실제로 해당 경로의 로그 파일로 저장된다.

 

 

실제 프로젝트에 맞게 Logback과 AOP 적용

 

logback.xml

<configuration>
    <!-- 콘솔 로깅 : 모든 로그를 콘솔에 출력 -->
    <appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>[%d{yyyy-MM-dd HH:mm:ss}][%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 특정 조건 (DEBUG 레벨)의 로그만 파일에 저장, AOP에서 특정 저장용 로그만 DEBUG 설정할 것-->
    <appender name="ApiLogFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>DEBUG</level> <!-- 로그 레벨을 DEBUG로 변경 -->
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <file>내 로그파일 보관할 경로</file> <!-- 파일 경로 -->
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS},%thread,%logger{36},%msg,%X{ip}%n</pattern> <!-- 저장될 로거 패턴 -->
        </encoder>
        <!-- 하루에 한번 압축 후 보관, 최대 30일, 1GB까지 보관 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>파일 이름 패턴.gz</fileNamePattern> <!-- 파일 경로 -->
            <maxHistory>30</maxHistory>
            <totalSizeCap>1GB</totalSizeCap>
        </rollingPolicy>
    </appender>

    <!-- 기본 로그 설정 -->
    <root level="INFO">
        <appender-ref ref="Console"/>
    </root>

    <!-- 특정 조건 (예: DEBUG 레벨)의 로그만 파일에 저장하도록 설정 -->
    <logger name="com.sch.sch_elasticsearch.aop" level="DEBUG" additivity="false">
        <appender-ref ref="ApiLogFile"/>
    </logger>
</configuration>

 

aop 파일(LoggingAspect)

package com.sch.sch_elasticsearch.aop;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
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.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;


@Aspect
@Component
@RequiredArgsConstructor
public class LoggingAspect {
    private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
    private final ObjectMapper objectMapper;

    //Domain 패턴에 적용 및 내 커스텀 Annotation이 설정된 파일만 포인트컷으로 적용
    @Pointcut("execution(* com.sch.sch_elasticsearch.domain..*.*(..)) " +
            "&& @annotation(com.sch.sch_elasticsearch.aop.SaveLogging)" )
    public void pointCut() {
    }

    //포인트컷 전 데이터 수집
    @Before("pointCut()")
    public void logBeforeMethod() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();
            MDC.put("ip", request.getRemoteAddr()); //MDC에 추가하여 ip를 콘솔 로그에 담아낼 계획이다.
        }
    }

    //지정된 포인트컷 표현식에 맞는 메서드가 성공적으로 반환될 때 실행, xml에서 DEBUG만 저장하기 때문에 DEBUG 형태로 출력
    @AfterReturning("pointCut()")
    public void logAfterMethodReturn(JoinPoint joinPoint) {
        String className = joinPoint.getSignature().getDeclaringTypeName(); //클래스
        String methodName = joinPoint.getSignature().getName(); //메서드
        logger.debug("{},{}", className, methodName); //클래스와 메서드를 로그 파일에 저장
        logger.info("[LoggingAspect] " + joinPoint.getSignature().toShortString()); //콘솔 출력
    }

}

 

 

정상적으로 쌓이는 데이터들. 이를 로그스태시를 통해 수집해야 한다