Spring boot 에서 CloudWatch 로그 수집 (feat. Profile 별로)
기존에 회사에서 애플리케이션 로그를 수집하는 방식은 애플리케이션 서버에 일주일 로그를 저장을 하고 매 주마다 로그를 저장하는 원격 서버로 파일을 저장하는 명령어인 scp
를 통해 저장하고 로그 정책에 따라 shell
스크립트를 이용해 저장 기간이 지난 로그를 삭제하는 구성이었다.
로그를 원격지에 소산하는 것에는 큰 문제가 없었지만, 생각보다 스크립트를 작성하고 관리하는 측면에서 꽤나 여러가지 문제가 있었다. 예를 들면, 사전에 충분히 테스트가 되지 않아 스크립트 자체에 문제가 있는 경우 혹은 서버 레벨에서 cron
을 통해 주기적으로 스크립트를 실행했는데 cron
이 올바르게 동작하지 않는다든가, 꽤 관리적 측면에서 많이 신경을 써줘야 했다.
그래서 간단한 설정으로 Spring Boot 애플리케이션 자체에서 AWS CloudWatch
로 로그를 전송하여 수집하는 설정을 해보고자 한다.
실습 환경
- AWS
- IAM
- CloudWatch
- CLI
- Java 18 (corretto)
- Spring Boot 2.7.3
- logback
- Gradle 2.7.3
코드 작성
의존성 추가
// https://mvnrepository.com/artifact/ca.pjer/logback-awslogs-appender
implementation group: 'ca.pjer', name: 'logback-awslogs-appender', version: '1.6.0'
로그 생성 클래스
Spring Boot 로그 레벨은 총 5가지로, INFO
, WARN
, ERROR
, DEBUG
, TRACE
로 구분되는데, 여기에선 자세히 다루지 않을 예정이라, 로그 레벨 및 logback
구성에 관련된 내용은 맨 아래 Reference
에 있는 포스팅을 통해 학습하면 된다.
@EnableScheduling
@SpringBootApplication
public class CloudwatchLogExampleApplication {
public static void main(String[] args) {
SpringApplication.run(CloudwatchLogExampleApplication.class, args);
}
}
Scheduling을 사용하기 위해 Spring Boot 애플리케이션을 생성하는 main 메소드에 @EnableScheduling
어노테이션을 설정한다.
@Slf4j
@Component
public class LogSchedule {
@Scheduled(cron = "*/10 * * * * *")
private void printInfoMessage() {
log.info("일반 메세지 출력");
}
@Scheduled(cron = "0 * * * * *")
private void printErrorMessage() {
log.error("에러 메세지 출력");
}
@Scheduled(cron = "*/30 * * * * *")
private void printWarnMessage() {
log.warn("Warning 메세지 출력");
}
}
INFO
로그는 10초마다, WARN
로그는 30초마다, ERROR
로그는 1분마다 찍기 위해 Scheduling 클래스를 생성한다. 해당 코드만 작성하고 애플리케이션을 실행하면, 스케줄링 설정에 따라 아래와 같은 로그가 계속 찍히게 된다.
2022-08-25 11:39:00.013 WARN 53516 --- [ scheduling-1] c.s.cloudwatchlogexample.LogSchedule : Warning 메세지 출력
2022-08-25 11:39:10.002 INFO 53516 --- [ scheduling-1] c.s.cloudwatchlogexample.LogSchedule : 일반 메세지 출력
2022-08-25 11:39:20.005 INFO 53516 --- [ scheduling-1] c.s.cloudwatchlogexample.LogSchedule : 일반 메세지 출력
2022-08-25 11:39:30.005 INFO 53516 --- [ scheduling-1] c.s.cloudwatchlogexample.LogSchedule : 일반 메세지 출력
2022-08-25 11:39:30.007 WARN 53516 --- [ scheduling-1] c.s.cloudwatchlogexample.LogSchedule : Warning 메세지 출력
2022-08-25 11:39:40.002 INFO 53516 --- [ scheduling-1] c.s.cloudwatchlogexample.LogSchedule : 일반 메세지 출력
2022-08-25 11:39:50.001 INFO 53516 --- [ scheduling-1] c.s.cloudwatchlogexample.LogSchedule : 일반 메세지 출력
2022-08-25 11:40:00.002 ERROR 53516 --- [ scheduling-1] c.s.cloudwatchlogexample.LogSchedule : 에러 메세지 출력
logback 설정 파일
<configuration>
<include resource="org/springframework/boot/logging/logback/base.xml"/>
<appender name="dailyRollingFileAppender"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<prudent>true</prudent>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>./logs/application.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<charset>UTF-8</charset>
<pattern>%d{HH:mm:ss.SSS} [%thread] [%5level] %logger{35}[%method:%line] %m%n</pattern>
</encoder>
</appender>
<appender name="dailyCloudwatchAppender"
class="ca.pjer.logback.AwsLogsAppender">
<layout>
<pattern>[%thread] [%date] [%level] [%file:%line] - %msg%n</pattern>
</layout>
<logGroupName>/aws/ec2/instance/cloudwatch-log-example/application-log</logGroupName>
<logStreamUuidPrefix>cloudwatch-log-example-</logStreamUuidPrefix>
<logRegion>ap-northeast-2</logRegion>
<maxBatchLogEvents>50</maxBatchLogEvents>
<maxFlushTimeMillis>30000</maxFlushTimeMillis>
<maxBlockTimeMillis>5000</maxBlockTimeMillis>
<retentionTimeDays>0</retentionTimeDays>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<charset>UTF-8</charset>
<pattern>%d{HH:mm:ss.SSS} [%thread] [%5level] %logger{35}[%method:%line] %m%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="dailyRollingFileAppender"/>
<appender-ref ref="dailyCloudwatchAppender"/>
</root>
</configuration>
해당 설정의 property
에 대해 간략히 설명하자면,
logGroupName
CloudWatch 로그 그룹의 이름logStreamUuidPrefix
CloudWatch 로그 스트림 접두사logRegion
AWS 리전maxBatchLogEvents
배치의 최대 이벤트 개수 설정, 1 ~ 10000 사이 값만 설정 가능maxFlushTimeMillis
마지막 플러스가 발생되고 지정된 시간이 지나면 로그를 전송, 0일 경우에는 동기, 0 이상의 값이면 비동기로 로그 전송maxBlockTimeMillis
로그가 전송되는 동안 코드가 계속 실행되는 것을 차단, 0으로 설정하면 모든 로그를 버리게 됨retentionTimeDays
CloudWatch 로그 그룹의 보존 기간, 0일 경우에 무기한 보존accessKeyId, secretAccessKey
CloudWatch로 로그를 전송할 IAM 계정의 Key
나머지 설정은 dailyRollingFileAppender
는 로컬에 ./logs/application.%d{yyyy-MM-dd}.log
해당 경로에 로그를 지정하는 설정이고, dailyCloudwatchAppender
는 로그를 CloudWatch로 전송하기 위한 설정이다.
따라서, 로컬에도 저장하고 CloudWatch에도 로그를 전송하게 된다. 이 설정은 필요에 따라 로컬에는 저장 안 해도 된다.
AWS 설정
IAM 계정 키 발급
IAM 액세스 키를 사용할 예정이기 때문에, 액세스 키 - 프로그래밍 방식 액세스
를 체크하여 다음으로 넘어간다.
❯ aws configure
AWS Access Key ID [None]: 액세스 키 입력
AWS Secret Access Key [None]: 시크릿 키 입력
Default region name [None]: ap-northeast-2
Default output format [None]: json
생성한 IAM 계정의 액세스 키와 비밀 액세스 키를 aws-cli 를 이용하여 현재 PC에 등록한다.
참고로 지금은 IAM 계정을 발급 받아 사용하지만, 나중에 애플리케이션을 EC2 혹은 ECS와 같은 AWS 컴퓨팅 리소스에 배포하게 된다면 보안 상 액세스 키를 발급 받아 사용하기 보단 IAM 역할
을 등록하여 사용하는 편이 좋다.
AWS CloudWatch 확인
자, 이제 애플리케이션 실행 후 AWS 콘솔로 들어가 CloudWatch에 저장된 로그를 확인해보도록 하겠다. 아까 위에서 코드를 실행했을 때와 동일한 로그가 CloudWatch 로그 그룹 / 로그 스트림에 저장이 되어 있어야 한다.
AWS CloudWatch > 로그 그룹 > /aws/ec2/instance/cloudwatch-log-example/application-log
로 들어가면 아래와 같이 위의 logback
설정 파일의 이름대로 로그 그룹 / 로그 스트림이 생성되어 있다.
로그 스트림을 클릭하여 들어가면 우리가 실행한 애플리케이션의 로그가 저장되어 있다.
참고로, 로그 그룹을 선택하고 작업 > 보존 설정 편집
을 통해서 로그를 일정 기간 저장하고 삭제하는 설정도 쉽게할 수 있다.
Profile 별로 로그 설정
예를 들어, 개발환경에서 로그를 로컬에만 저장하고, 운영환경에서 로그를 CloudWatch로 전송하고자 한다면, 아래와 같이 logback 설정을 변경하면 된다. 단순하게 <SpringProfile name="{PROFILE_NAME}"> </SpringProfile>
만 지정해주면 된다.
<configuration>
<springProfile name="dev">
<include resource="org/springframework/boot/logging/logback/base.xml"/>
<appender name="dailyRollingFileAppender"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<prudent>true</prudent>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>./logs/application.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<charset>UTF-8</charset>
<pattern>%d{HH:mm:ss.SSS} [%thread] [%5level] %logger{35}[%method:%line] %m%n</pattern>
</encoder>
</appender>
</springProfile>
<springProfile name="prod">
<appender name="dailyCloudwatchAppender"
class="ca.pjer.logback.AwsLogsAppender">
<layout>
<pattern>[%thread] [%date] [%level] [%file:%line] - %msg%n</pattern>
</layout>
<logGroupName>/aws/ec2/instance/cloudwatch-log-example/application-log</logGroupName>
<logStreamUuidPrefix>cloudwatch-log-example-</logStreamUuidPrefix>
<logRegion>ap-northeast-2</logRegion>
<maxBatchLogEvents>50</maxBatchLogEvents>
<maxFlushTimeMillis>30000</maxFlushTimeMillis>
<maxBlockTimeMillis>5000</maxBlockTimeMillis>
<retentionTimeDays>0</retentionTimeDays>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<charset>UTF-8</charset>
<pattern>%d{HH:mm:ss.SSS} [%thread] [%5level] %logger{35}[%method:%line] %m%n</pattern>
</encoder>
</appender>
</springProfile>
<root level="INFO">
<appender-ref ref="dailyRollingFileAppender"/>
<appender-ref ref="dailyCloudwatchAppender"/>
</root>
</configuration>
더 디테일한 설정은 logback 설정 파일에 대해서 알아보면 된다.
로그 스트림을 일 단위로 생성할 수는 없을까?
확인해보니, 생성되는 로그 스트림은 애플리케이션 실행 때마다 새로 생성된다. 그래서 기존처럼 일 단위마다 로그 파일을 저장하듯이 일 단위마다 로그 스트림을 생성할 수 없다. (매일 애플리케이션을 다시 시작한다면 모를까..)