도입 배경
예상치 못한 에러에 빠르게 대응하기 위해서 로깅 시스템 구축은 중요하다고 생각합니다. 그리고 분산 환경의 경우 서버 인스턴스들이 여러 개가 될 수 있기 때문에 각 인스턴스에 직접 접속하지 않고도 해당 인스턴스들의 로그를 중앙집중식으로 관리할 수 있도록 분산 환경 로깅 시스템을 구축하는 것이 필요합니다.
저는 Spring boot의 SLF4J (Simple Logging Facade for Java)의 구현체인 Logback을 이용하여 로깅 설정을 커스텀하였습니다. 그리고 Filebeat + ELK 스택을 이용하여 마이크로 서비스의 로그를 로깅 서버로 전송하여 로깅 서버 한곳에서 모든 서비스의 로그를 관리할 수 있는 로깅 시스템을 구축해보았습니다.
Spring boot 로깅 설정
Spring boot 로깅에 대한 간단한 설명은 다음 공식 문서에서 확인할 수 있습니다.
Spring boot Logging 문서 https://docs.spring.io/spring-boot/how-to/logging.html
Logback을 이용하려면 logging starter 의존성을 추가하여 Logback과 spring-jcl을 클래스 경로에 추가해야 하는데 spring-boot-starter-web 의존성을 추가하면 logging starter 의존성도 자동으로 추가할 수 있다고 합니다.
스프링 부트는 LoggingSystem 추상 계층이 있어서 이 계층이 클래스 경로에 있는 콘텐츠를 바탕으로 로깅을 구성하는데 만약 Logback이 있다면 Logback을 기본적으로 선택한다고 합니다.
application.yml에서 설정
Spring boot에서 로깅 설정을 간단하게 커스텀하고 싶다면 application.yml에 설정을 추가하면 됩니다.
예를 들어 다음과 같이 클래스별로 로깅 레벨을 다르게 설정할 수 있습니다.
# application.yml
# 생략
logging:
level:
org.springframework.web: DEBUG
org.hibernate.SQL: DEBUG
org.hibernate.orm.jdbc.bind: TRACE
logback-spring.xml에서 설정
만약 조금 더 디테일하게 로깅 설정을 커스텀하고 싶다면, 그리고 Logback 로깅 구현체를 사용하고 있다면 src/main/resources 경로에 logback-spring.xml 파일을 두어 spring에 특화된 Logback 설정을 추가할 수 있습니다.
다음 공식 문서를 통해 설정 방법을 확인할 수 있습니다.
Logback 공식 문서 https://logback.qos.ch/manual/appenders.html
저는 다음과 같이 로깅 설정을 해주었습니다.
property태그를 이용하여 설정 파일에서 변수처럼 사용할 LOG_PATTERN과 LOG_PATH (로그 파일 생성 경로) 등록
<!-- src/main/resources/logback-spring.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="LOG_PATTERN"
value="[%d{yyyy-MM-dd HH:mm:ss}][%thread] %-5level %logger - %msg%n" />
<property name="LOG_PATH" value="logs" />
<!-- 생략 -->
</configuration>
appender태그를 이용하여 rolling policy 설정
appender 태그를 이용하여 일반적인 콘솔 로깅, API 로깅, JDBC 로깅, GENERAL 로깅 시 각각 다른 로깅 전략을 사용할 수 있게 설정해주었습니다. 각각의 상황마다 다르게 설정한 부분은 실시간 로깅 파일의 경로를 설정하는 file과 오래된 로그를 어떻게 압축하고 삭제할지 정하는 rollingPolicy 부분입니다.
<!-- src/main/resources/logback-spring.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 생략 -->
<!-- 콘솔 로그 -->
<appender name="CONSOLE"
class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- API 로그 파일 -->
<appender name="API_LOG"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/api.log</file>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
<rollingPolicy
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/%d{yyyy-MM,
aux}/api-%d{yyyy-MM-dd}.log.gz</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
<!-- 생략 -->
</configuration>
Rolling Policy의 경우 로그 파일의 용량 기준 또는 시간 기준으로 관리할 수 있습니다. 저는 하루 단위로 로그 파일이 압축되게 하였고 압축 파일들은 월별 폴더에서 관리되게 하였습니다. 로그 파일의 최대 저장 기간은 30일로, 30일이 지난 로그들은 자동 삭제됩니다.
이렇게 로깅을 설정하면 다음과 같은 식으로 로그가 관리됩니다.

springProfile과 `logger’를 이용하여 프로파일별 로깅 설정 (로깅 대상 클래스, 로깅 레벨 설정)
스프링 프로파일별로 다른 로깅 설정을 사용할 수 있습니다. 배포 서버에서의 로깅의 경우 서버 성능을 위해 콘솔에는 로깅하지 않는 것이 좋다고 하여 저는 배포 서버인 dev 프로파일의 로깅의 경우 콘솔에 출력하는 로깅은 사용하지 않았습니다.
logger는 “ROOT”로 설정하면 모든 클래스를 대상으로 로깅이 실행됩니다. 클래스별로 로깅 레벨을 다르게 설정하고 싶다면 다음과 같이 logger를 추가해주면 됩니다. 그리고 어떤 클래스에 대해서 특정 logger로 로그를 출력했다면 “ROOT” logger에는 출력되지 않게 하고 싶다면 logger에 additivity="false" 속성을 추가해주면 됩니다.
다음은 저의 dev 프로파일의 로깅 설정입니다.
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 생략 -->
<!-- dev 프로파일 로깅 설정 -->
<springProfile name="dev">
<logger name="org.hibernate.SQL" level="TRACE"
additivity="false">
<appender-ref ref="JDBC_LOG" />
</logger>
<logger name="org.hibernate.orm.schema" level="TRACE"
additivity="false">
<appender-ref ref="JDBC_LOG" />
</logger>
<logger name="com.jeein.member.LoggingFilter" level="INFO"
additivity="false">
<appender-ref ref="API_LOG" />
</logger>
<logger name="ROOT" level="INFO">
<appender-ref ref="GENERAL_LOG" />
</logger>
</springProfile>
<!-- 생략 -->
</configuration>
최종적으로 저는 개발용 서버에서 다음과 같이 로그가 출력되고 있습니다.

Docker 환경을 고려한 로깅 경로 설정
저는 배포 서버에서 Docker를 이용하여 스프링 부트 애플리케이션을 운영하고 있습니다. Docker 안에서 실행되는 스프링 부트 애플리케이션이 생성하는 로그를 Docker 외부에서도 접근할 수 있게 하기 위하여 Docker volume 설정을 해주었습니다.
Spring boot 프로젝트에서 로그 파일 경로를 logs로 설정하면 Docker 컨테이너 내부에서 /app/logs 위치에 로그가 저장됩니다. 이 경로의 파일이 Docker 외부인 서버와 연결될 수 있도록 docker-compose.yml에서 volumes를 "./logs:/app/logs"로 설정해주었습니다.
services:
member:
image: "${DOCKER_USERNAME}/socketing:member-service"
network_mode: "host" # required to register public ip to Spring Eureka
container_name: "member-service-app"
volumes:
- "./logs:/app/logs"
분산 환경의 로깅 시스템
이제 마이크로 서비스 서버에 저장된 로그를 중앙집중식으로 관리하기 위해 로깅 서버로 보내는 방법을 알아보겠습니다.
Logback 공식 문서를 보면 Logback의 appender 설정을 통해서도 외부에 로그를 전송할 수 있는 방법이 있는 것으로 보였습니다. 그렇지만 저는 경량 로그 수집기인 Filebeat를 이용하여 Logback은 로그의 책임만 담당하고 로그를 외부로 전송하는 책임은 Filebeat에 두고자 하였습니다.
그리고 로깅 서버에서 로그를 인덱싱할 때는 json 형식의 로그가 필요하기 때문에 로그를 json으로 변환하는 과정이 어디엔가 필요합니다. 이를 위해 Logback에서 로그를 json으로 출력하게 설정하는 방법이 있고 또는 로깅 서버에서 Logstash가 일반 로그를 json으로 변환하게 하는 방법이 있습니다.
두 방법 모두 장단점이 있지만 저는 로깅 서버에서 로그를 유연하게 파싱할 수 있게 하기 위해 Logstash 방법을 선택해보았습니다.
Filebeat 설정
마이크로 서비스가 위치한 서버에 docker-compose.yml을 이용하여 Filebeat 도커 컨테이너를 실행시킵니다. 도커를 이용한 이유는 한 번 docker-compose 파일을 작성해두면 다른 마이크로 서비스에서 다시 활용하기 좋기 때문입니다.
다음은 Filebeat 컨테이너 생성을 위한 docker-compose.yml 설정입니다.
Filebeat 도커 컨테이너에서 도커 외부에 있는 Spring boot가 생성한 로그에 접근할 수 있도록 volume을 설정해줍니다. 그리고 Filebeat에서 사용할 설정 정보를 Docker 내부의 Filebeat가 읽을 수 있게 volume을 설정해줍니다.
STACK_VERSION은 .env 파일에서 관리하였습니다.
# docker-compose.yml
services:
filebeat:
image: docker.elastic.co/beats/filebeat:${STACK_VERSION}
container_name: filebeat-container
volumes:
- ./logs:/var/log/app
- ./filebeat.yml:/usr/share/filebeat/filebeat.yml:ro # read only
다음은 filebeat 설정을 위한 filebeat.yml 파일입니다.
# filebeat.yml
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/log/app/api.log
fields:
server_name: "member-service"
environment: "dev"
log_type: api
fields_under_root: true
- type: log
enabled: true
paths:
- /var/log/app/general.log
fields:
server_name: "member-service"
environment: "dev"
log_type: general
fields_under_root: true
- type: log
enabled: true
paths:
- /var/log/app/jdbc.log
fields:
server_name: "member-service"
environment: "dev"
log_type: jdbc
fields_under_root: true
output.logstash:
hosts: ["logstash:5044"]
ELK 스택 구축
ELK 스택 구축 다음 레퍼런스를 참고했다.
몇 가지 시행착오들이 있었다.