본문 바로가기

Spring/boot

Spring boot log 파일을 AWS S3에 자동으로 업로드 시키기

반응형
spring boot 2.7.3 버전에서 작성된 코드입니다.
logback class, core 1.2.11 버전의 라이브러리입니다. 또한 아래 옵션으로 로그를 보존한다는 가정하에 글을 작성합니다.
    - Appender : RollingFileAppender
    - RollingPolicy : SizeAndTimeBasedRollingPolicy
AWS S3 라이브러리 : com.amazonaws:aws-java-sdk-s3:1.12.290

 

아래는 Import 코드 입니다.

import ch.qos.logback.core.Context;
import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.FileAppender;
import ch.qos.logback.core.rolling.RolloverFailure;
import ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy;
import ch.qos.logback.core.rolling.TimeBasedFileNamingAndTriggeringPolicy;
import ch.qos.logback.core.rolling.helper.ArchiveRemover;
import ch.qos.logback.core.rolling.helper.CompressionMode;
import ch.qos.logback.core.rolling.helper.Compressor;
import ch.qos.logback.core.rolling.helper.FileFilterUtil;
import ch.qos.logback.core.rolling.helper.RenameUtil;
import ch.qos.logback.core.status.Status;
import ch.qos.logback.core.status.StatusManager;
import ch.qos.logback.core.util.FileSize;
import lombok.Getter;
import lombok.Setter;

import java.io.File;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

 


서버에서 logback을 이용하여 로그를 보존하던 중 이전 로그를 확인해야하는 상황에서 아래 설정해놓은 옵션으로인해 해당 로그가 삭제되어 볼 수 없었던 문제가 발생하였다. 이를 계기로 또 이러한 상황이 발생하지 않도록 미연에 방지하기 위해 이번 글을 작성하기로 마음먹게되었다.

<fileNamePattern>{{로그파일의 명명 규칙}}.{{압축 확장자}}</fileNamePattern>
<maxFileSize>{{기록중인 로그파일의 최대 용량}}</maxFileSize>
<maxHistory>{{보관중인 로그파일의 최대 갯수}}</maxHistory>
<totalSizeCap>{{보관중인 로그파일들의 용량의 최대 합}}</totalSizeCap>

 

 

  • yaml 또는  properties에 아래 내용중 필요한 내용을 입력
// 로그 파일이 저장될, 기입될 경로
<springProperty scope="context" name="LOG_DIR" source="{{yaml 또는  properties에 기입한 경로 입력 ex : log.dir}}"/>
// AWS S3 접근 권한이 존재하는 accessKey, secretKey, region
<springProperty scope="context" name="ACCESS_KEY" source="{{yaml 또는  properties에 기입한 경로 입력 ex : aws.s3.access_key}}"/>
<springProperty scope="context" name="SECRET_KEY" source="{{yaml 또는  properties에 기입한 경로 입력 ex : aws.s3.secret_key}}"/>
<springProperty scope="context" name="BUCKET_NAME" source="{{yaml 또는  properties에 기입한 경로 입력 ex : aws.s3.sucket_name}}"/>
<springProperty scope="context" name="REGION" source="{{yaml 또는  properties에 기입한 경로 입력 ex : aws.s3.region}}"/>
// 실서버와, 테스트 서버가 각 구동중인 상황일때 특정 서버의 로그파일만 저장되길 원하는 경우
<springProperty scope="context" name="SYS_TYPE" source="{{yaml 또는  properties에 기입한 경로 입력 ex : system.type}}"/>

 

  • S3에 업로드 하는 로그의 확인해야할 필요성은 낮음으로 판단하여 아래 옵션을 logback 설정 xml에 추가한다.
<logger name ="org.apache.http.wire" level="ERROR"/>

 

  • 위 작성하였듯이 ~ 를 사용하고 로그가 특정 압축 확장자로 압축될때(후 "rollOver"로 칭함) 압축된 파일을 S3에 업로드 하기 위해 기존 사용중인 라이브러리의 파일을 상속받는 파일을 생성하며
    S3에 접근하기 위한 필드변수를 선언한다.
    또한 라이브러리에서 사용하는 파일 압축과 명명규칙을 위해 추가로 필드변수를 선언한다.
public class TimeBasedRollingPolicyToS3<E> extends SizeAndTimeBasedRollingPolicy<E> {
    @Getter
    @Setter
    private String awsAccessKey;
    @Getter
    @Setter
    private String awsSecretKey;
    @Getter
    @Setter
    private String awsBucketName;
    @Getter
    @Setter
    private String awsRegion;
    
    // 아래 변수는 선택
    @Getter
    @Setter
    private String sysType;
    
    // 현재 rollOver 되고있는 날짜를 전역으로 세팅하기 위해 선언
    // 필자의 타임존은 UTC
    private Date ROLL_OVER_DATE;
    
    // 파일 압축 class
    private Compressor compressor;
    // S3 Client 유틸 클래스
    private LogBackS3Client logBackS3Client;
    // 파일 보전시 위 선언한 명명규칙에 의해 보존할 파일명을 결정짓는 유틸 class
    private final RenameUtil renameUtil = new RenameUtil();
    private final ExecutorService executor = Executors.newFixedThreadPool(1);

	// 파일을 압축하는 Thread
    Future<?> compressionFuture;
    // TEMP파일을 지우는 Thread
    Future<?> cleanUpFuture;
}

 

  • 여기서 필요한건 로그파일이 "압축"된 후 해당 파일을 S3에 올려야 하므로 파일을 압축하는 rollOver 메서드와 start 메서드를 Override한다.
@Override
public void start() {
    super.start();
    
    // S3에 업로드하기위해 AmazonS3Client를 선언하는 코드
    logBackS3Client = new LogBackS3Client(getAwsAccessKey(), getAwsSecretKey(), getAwsBucketName(), getAwsRegion());
}

@Override
public void rollover() throws RolloverFailure {
    // sysType은 선택사항이므로 이후 작성할 메서드
    // this.rollOverWithS3();
    // 만 입력하여 주어도 무관
    if ("{{본인이 지정할 프로필}}".equals(sysType)) {
        ROLL_OVER_DATE = new Date(getTimeBasedFileNamingAndTriggeringPolicy().getCurrentTime());
        this.rollOverWithS3();
    } else {
        super.rollover();
    }
}

 

  • 파일 압축 후 S3에 업로드 하기 위한 메서드 선언
private void rollOverWithS3() {
    if (getTimeBasedFileNamingAndTriggeringPolicy().getElapsedPeriodsFileName() != null) {
        final String elapsedPeriodsFileName = String.format("%s%s", getTimeBasedFileNamingAndTriggeringPolicy().getElapsedPeriodsFileName(), getFileNameSuffix());

        String elapsedPeriodStem = FileFilterUtil.afterLastSlash(elapsedPeriodsFileName);

        this.compressor = new Compressor(compressionMode);
        if (getParentsRawFileProperty() == null) {
            compressionFuture = compressor.asyncCompress(elapsedPeriodsFileName, elapsedPeriodsFileName, elapsedPeriodStem);
        } else {
            compressionFuture = renameRawAndAsyncCompress(elapsedPeriodsFileName, elapsedPeriodStem);
        }

        ArchiveRemover archiveRemover = getTimeBasedFileNamingAndTriggeringPolicy().getArchiveRemover();
        if (archiveRemover != null) {
            cleanUpFuture = archiveRemover.cleanAsynchronously(ROLL_OVER_DATE);
        }

        executor.execute(new UploadQueue(elapsedPeriodsFileName));
    } else {
        logBackS3Client.putLogFile(getTimeBasedFileNamingAndTriggeringPolicy().getElapsedPeriodsFileName(), ROLL_OVER_DATE);
    }
}

 

  • 압축 확장자를 확인하기 위한 메서드
private String getFileNameSuffix() {
    return switch (compressionMode) {
        case GZ -> ".gz";
        case ZIP -> ".zip";
        default -> "";
    };
}

 

  • Async 압축을 위한 코드
Future<?> renameRawAndAsyncCompress(String nameOfCompressedFile, String innerEntryName) throws RolloverFailure {
    String parentsRawFile = getParentsRawFileProperty();
    String tmpTarget = nameOfCompressedFile + System.nanoTime() + ".tmp";
    renameUtil.rename(parentsRawFile, tmpTarget);
    return this.asyncCompress(tmpTarget, nameOfCompressedFile, innerEntryName);
}

public Future<?> asyncCompress(String nameOfFile2Compress, String nameOfCompressedFile, String innerEntryName) throws RolloverFailure {
    CompressionRunnable runnable = new CompressionRunnable(nameOfFile2Compress, nameOfCompressedFile, innerEntryName);
    return executor.submit(runnable);
}

class CompressionRunnable implements Runnable {
    final String nameOfFile2Compress;
    final String nameOfCompressedFile;
    final String innerEntryName;

    public CompressionRunnable(String nameOfFile2Compress, String nameOfCompressedFile, String innerEntryName) {
        this.nameOfFile2Compress = nameOfFile2Compress;
        this.nameOfCompressedFile = nameOfCompressedFile;
        this.innerEntryName = innerEntryName;
    }

    public void run() {
        compressor.compress(nameOfFile2Compress, nameOfCompressedFile, innerEntryName);
    }
}

 

  • 압축 및 TEMP파일 삭제 후 S3에 업로드하는 코드
class UploadQueue implements Runnable {
    private final String elapsedPeriodsFileName;

    public UploadQueue(String elapsedPeriodsFileName) {
        this.elapsedPeriodsFileName = elapsedPeriodsFileName;
    }

    @Override
    public void run() {
        try {
            compressionFuture.get(CoreConstants.SECONDS_TO_WAIT_FOR_COMPRESSION_JOBS, TimeUnit.SECONDS);
            cleanUpFuture.get(CoreConstants.SECONDS_TO_WAIT_FOR_COMPRESSION_JOBS, TimeUnit.SECONDS);
            logBackS3Client.putLogFile(elapsedPeriodsFileName, ROLL_OVER_DATE);
        } catch (Exception e) {
            //noinspection CallToPrintStackTrace
            e.printStackTrace();
        }
    }
}

 

  • S3 유틸 클래스의 putLogFile 메서드
private AmazonS3 amazonS3;

public void putLogFile(String fileName, Date rollOverDate) {
	// 단순하게 날짜별 파일 묶음을 위한 필자의 선택으로 원하는 방식으로 수정
    String nowDate = new SimpleDateFormat("yyyy-MM-dd").format(rollOverDate);

    if (amazonS3 == null) {
        amazonS3 = AmazonS3ClientBuilder.standard().withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(awsAccessKey, awsSecretKey))).withRegion(awsRegion).build();
    }

    final File file = new File(fileName);

    if (file.exists()) {
        executor.execute(() -> {
            try {
            	// Metadata 부분으로 이부분은 선택사항
                ObjectMetadata metaData = new ObjectMetadata();
                metaData.setContentDisposition("filename=" + URLEncoder.encode(Objects.requireNonNull(file.getName()), StandardCharsets.UTF_8));
                metaData.setContentLength(file.length());
                amazonS3.putObject(new PutObjectRequest(awsBucketName, String.join("/", "{{원하는 S3의 경로 또는 디렉토리 명}}", nowDate, file.getName()), file).withMetadata(metaData));
            } catch (Exception e) {
                //noinspection CallToPrintStackTrace
                e.printStackTrace();
            }
        });
    }
}

 

  • 선택사항이긴하나 비용절감을 위해 S3 수명주기(lifeCycle) 설정
    로그파일을 저장할 버킷의 "관리"(= Management) 탭에 수명 주기 규칙(= Lifecycle rules) 영역이 존재하는데 해당 부분에서 설정
  • 수명 주기 규칙 생성(= Create lifecycle rule)
    • 규칙명 입력
    • 하나 이상의 필터를 사용하여 이 규칙의 범위 제한(= Limit the scope of this rule using one or more filters)
    • 접두사(= Prefix)에 로그를 저장하는 디렉토리명 입력
      주의점으로 경로가 아닌 디렉토리명으로 만약 "s3://test_bucket/logs/" 라는 경로로 저장한다면 "logs/" 만 입력
    • 버전관리를 할 필요성이 없으므로
      객체의 현재 버전 만료(= Expire current versions of objects) 선택
    • 위 옵션을 선택 시 바로 아래 입력창이 열리는데 만료기한이므로 원하는 날짜 입력
      필자는 365일 입력하여 1년치의 로그를 보존하도록 설정

 

저장되는 디렉토리 규칙
순번으로 저장되도록 해놓은 상태

반응형

'Spring > boot' 카테고리의 다른 글

Junit VM Options  (0) 2022.08.23
Jasypt를 이용한 암호화  (0) 2022.08.16