로그 수집 및 모니터링은 애플리케이션의 상태를 모니터링하고, 문제를 신속하게 해결하는 데 필수적인 요소이다.
Docker-compose 환경으로 Promtail과 Spring Boot 애플리케이션을 함께 빌드하여 로그를 수집하고, 별도의 모니터링 서버에 있는 Grafana Loki로 전송하는 방법에 대해 알아보자.
먼저 Promtail은 로그를 수집하고 이를 Loki에 전송하는 역할을 하는 도구이다. 다양한 로그 파일을 읽고, 이를 특정 형식으로 정제하여 Loki에 보낼 수 있다.
Loki는 Grafana에서 로그를 쿼리하고 시각화할 수 있도록 지원하는 로그 관리 시스템이다.
<configuration>
<!-- prod에서만 로그 수집하도록 설정 -->
<springProfile name="prod">
<property name="LOG_DIR" value="/var/log/spring-boot" />
<!-- 콘솔 출력 설정 -->
<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>
<!-- 일반 파일 출력 설정 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_DIR}/general.log</file>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<!-- 지난 로그는 loghistory로 저장 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 로그 파일 이름 설정: 날짜별 -->
<fileNamePattern>${LOG_DIR}/general-%d{yyyy-MM-dd}.loghistory</fileNamePattern>
<maxHistory>7</maxHistory> <!-- 로그 파일을 최대 7일 동안 유지 -->
</rollingPolicy>
</appender>
<!-- 루트 로거 설정: 콘솔 및 일반 파일에 로그 저장 -->
<root level="info">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</springProfile>
<!-- test 환경 설정 -->
<springProfile name="test">
<property name="LOG_DIR" value="logs" />
<!-- 콘솔 출력 설정 -->
<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>
<!-- test 환경에서는 로그를 콘솔에만 출력 -->
<root level="info">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
</configuration>
참고로 테스트 환경을 둔 이유는 통합 테스트 실행 시 logback이 함께 동작하며 /var/log/spring-boot 디렉토리를 찾지 못해 자동으로 테스트가 실패하고 있어, 이를 콘솔에만 출력하도록 명시적으로 설정해주어야 했다.
server:
http_listen_port: ${PROMTAIL_PORT}
positions:
filename: /tmp/positions.yaml
clients:
- url: ${LOKI_URL}
scrape_configs:
- job_name: spring-boot
static_configs:
- targets:
- localhost
labels:
job: springboot
__path__: /var/log/spring-boot/*.log
promtail 서버가 http 요청을 수신할 포트를 정의한다.
positions를 통해 로그 수집을 진행하면서 읽은 위치를 기록하여 재시작 시에도 이전 위치부터 로그 수집을 재개할 수 있게 한다.
로그 데이터를 전송할 Loki URL을 지정한다.
/var/log/spring-boot 디렉토리 내 모든 log 파일을 수집 대상으로 지정한다.
이제 docker-compose에 이 설정을 바탕으로 promtail을 이식하자.
version: '3.8'
services:
spring_server:
image: ${DOCKERHUB_USERNAME}/orange
container_name: orange
env_file:
- ../${ENV_NAME}
ports:
- "${SERVER_PORT}:${SERVER_PORT}"
volumes:
- /var/log/spring-boot:/var/log/spring-boot
networks:
- my_custom_network
promtail:
image: grafana/promtail
container_name: promtail
env_file:
- ../${ENV_NAME}
ports:
- "${PROMTAIL_PORT}:${PROMTAIL_PORT}"
volumes:
- ./promtail-config.yml:/promtail-config.yml
- /var/log/spring-boot:/var/log/spring-boot
command:
- -config.file=/promtail-config.yml
- -config.expand-env=true # 환경 변수 확장 활성화
networks:
- my_custom_network
networks:
my_custom_network:
driver: bridge
먼저 docker compose에서 관리할 2개의 서비스(spring_server, promtail)를 정의한다.
Springboot 서버는 기존에 내가 Docker Image로 관리하고 있었기에 포트 번호와 환경변수를 활용해 빌드한다.
이 때 /var/log/spring-boot 디렉토리에 로그를 저장할 수 있도록 컨테이너와 호스트 간 볼륨을 마운트해주어야 한다.
level=info ts=2024-08-18T07:27:24.764641526Z caller=promtail.go:133 msg="Reloading configuration file" md5sum=a589992849a397d12d685f680c4e418c
level=info ts=2024-08-18T07:27:24.767594712Z caller=server.go:322 http=[::]:???? grpc=[::]:???? msg="server listening on addresses"
level=info ts=2024-08-18T07:27:24.767944002Z caller=main.go:174 msg="Starting Promtail" version="(version=2.9.10, branch=HEAD, revision=7664eda07b)"
level=warn ts=2024-08-18T07:27:24.768096667Z caller=promtail.go:263 msg="enable watchConfig"
level=info ts=2024-08-18T07:27:29.767879827Z caller=filetargetmanager.go:361 msg="Adding target" key="/var/log/spring-boot/*.log:{job=\"springboot\"}"
이 옵션이 없으면 promtail이 켜져 있으나 위처럼 로그를 수집하지 않고 멍때리고 있는 진풍경을 볼 수 있다. (무려 3시간이나 봤다...)
로그를 정상적으로 수집하는 상황이라면 아래 로그를 확인할 수 있을 것이다.
ts=2024-08-18T14:24:51.935022178Z caller=log.go:168 level=info msg="Seeked /var/log/spring-boot/general.log - &{Offset:0 Whence:0}"
level=info ts=2024-08-18T14:24:51.935064479Z caller=tailer.go:145 component=tailer msg="tail routine: started" path=/var/log/spring-boot/general.log
다음으로 promtail이 사용할 포트 번호와 환경 변수를 지정하고, expand-env=true 옵션을 통해 환경 변수 확장을 활성화한다. 참고로 환경변수 파일이 docker-compose 빌드 직전에 반드시 있어야 한다.
참고로 이런 일을 겪을 수 있다.
Unable to parse config: read /promtail-config.yml: is a directory.
Unable to parse config: read /promtail-config.yml: is a directory. Use -config.expand-env=true flag if you want to expand environment variables in your config file
마지막으로 두 서비스 모두 사용자 정의 네트워크인 my_custom_network에 편입시킨다.
나는 Github Actions를 활용한 CI/CD 파이프라인을 구축해 두었기에 변동 사항을 CD에 아래와 같은 형태로 반영해주었다. --env-file 옵션을 통해 환경변수 파일을 지정할 수 있다.
(현재 docker-compose가 infra 디렉토리 안에 존재하고 env 파일이 루트 디렉토리에 있는 상태이기에 상대경로로 접근할 수 있도록 해주었다.)
- name: Deploy with Docker Compose
run: |
cd infra
sudo docker-compose --env-file ../${{ secrets.ENV_NAME }} down
sudo docker-compose --env-file ../${{ secrets.ENV_NAME }} up -d
정상적으로 잘 동작하여 Loki에서 로그를 수집할 수 있다!