스프링부트 에러로그 디스코드 웹훅으로 알람 전송하는 방법
도입 배경
기존 로컬에서 API 개발, 연동을 할 때, 에러가 발생하게 된다면 로그를 보고 에러를 잡았었습니다.
배포 환경에서는 VM에 들어가서 에러를 봐야하기 때문에 개발자가 직접 VM에 들어가서 로그를 보는 것이 번거로웠습니다.
사실 실무 프로젝트는 아니고 개발자들도 운영환경의 VM에 접근할 수는 있지만, 역할기반제어 차원에서도 prod 환경 VM에 개발자가 접근하는게 좋을 거 같지 않다는 생각도 있었습니다.
그리고 가장 결정적으로 서비스에 장애가 발생하였을 때, 빠른 대응을 하는 것이 꼭 필요하기 때문에 도입하게 되었습니다.
구현 과정
우선 디스코드에 에러가 발생할 때 마다 알림을 보내기 위해서는 다음 내용들이 필요했습니다.
- Spring 로그에서 ERROR 가 생길 때마다 탐지하고 ERROR 부분 추출하는 로직
- Discord 웹훅 URL
- AWS IAM ACCESS_KEY, SECRET_KEY, 리전
- S3 버킷이름, prefix, CloudFront URL
AWS 설정정보들이 필요한 이유는 Discord는 한 대화에 텍스트 2000자 제한이 걸려있는데요, 스프링로그는 4~5줄만 되면 2000자가 훌쩍 넘어가기 때문에 텍스트로 보낼 수 없어 파일로 만들어서 보내야했습니다.
그래서 VM에 파일을 만들어서 보내야했는데요, 이렇게 되면 로그가 쌓이면 쌓일수록 VM내의 저장공간이 부족해질 문제가 있었습니다.
그래서 S3로 로그파일을 저장하고 VM의 에러로그 파일을 삭제하였습니다.
또한 S3 public-read를 막고 cloudfront URL로 로그를 다운로드 받을 수 있게 구현하였습니다.
크리덴셜 준비
필요한 크리덴셜은 디스코드 웹훅 URL, AWS 설정 정보들이였습니다.
디스코드 웹훅 URL은 제가 서버의 관리자가 아니라서 팀장님이 만들어둔 웹훅 URL을 받아서 사용하였습니다.
웹훅은 공식홈페이지에서 잘 설명해주고 있습니다.
https://support.discord.com/hc/ko/articles/228383668-웹훅을-소개합니다
AWS 설정 정보는 ACCESS_KEY, SECRET_KEY 같은 경우
AWS 콘솔 들어가서 IAM - 사용자 - 보안자격증명 에서 생성할 수 있습니다. SECRET_KEY 같은 경우 한번 보이고 사라지니 잘 저장해둬야합니다. 이 때, S3 put 권한이 있는 사용자여야 S3에 업로드할 권한이 생깁니다.
그리고 S3 버킷이름, prefix, cloudfront URL은 콘솔에서 잘 확인한 후 넣어주시면됩니다.
그리고 S3 버킷 정책 설정을 다음과 같이 해주고 (버킷 - 3번째 탭(권한))
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontOACReadOnly",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": [
"arn:aws:s3:::{버킷이름}/*",
],
"Condition": {
"StringEquals": {
"AWS:SourceArn": "{CLOUDFRONT 배포주소}"
}
}
}
]
}
CloudFront 배포 - 원본의 원본 생성을 눌러 다음 그림과 같이 설정합니다.
이러면 필요한 크리덴셜은 준비 되었습니다.
그래서 GPT한테 몇번 물어봐서 다음과 같은 코드를 받았습니다.
#!/bin/bash
# === 사용자 설정 ===
LOG_DIR="{로그가쌓이는_디렉토리}"
WEBHOOK_URL="{디스코드_웹훅URL}"
S3_BUCKET="{S3버킷이름}"
S3_PREFIX="{S3로그저장될위치}"
CLOUDFRONT_URL="{CLOUDFRONT_경로}"
AWS_ACCESS_KEY_ID="{AWS_IAM액세스키}"
AWS_SECRET_ACCESS_KEY="{AWS_IAM시크릿키}"
AWS_REGION="{S3버킷_AWS리전}"
# === 날짜 및 파일 경로 설정 ===
today=$(date +"%Y-%m-%d")
LOG_FILE="${LOG_DIR}/newsum.log"
ERROR_OUTPUT_DIR="{에러파일_저장될위치}"
mkdir -p "$ERROR_OUTPUT_DIR"
error_file="/tmp/error-${today}.log"
# === AWS 자격 증명 환경변수 설정 ===
export AWS_ACCESS_KEY_ID
export AWS_SECRET_ACCESS_KEY
export AWS_DEFAULT_REGION=$AWS_REGION
echo "[INFO] 에러 블록 감시 시작..."
# 에러 블록 추출을 위한 버퍼
buffer=""
collecting=0
tail -F "$LOG_FILE" | while read -r line; do
# 날짜로 시작하는 줄인 경우
if [[ "$line" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2} ]]; then
# 이전 에러 블록 처리
if [[ $collecting -eq 1 ]]; then
timestamp=$(date "+%Y-%m-%d_%H-%M-%S")
file_path="${ERROR_OUTPUT_DIR}/error-${timestamp}.log"
echo -e "$buffer" > "$file_path"
s3_key="${S3_PREFIX}/error-${timestamp}.log"
aws s3 cp "$file_path" "s3://${S3_BUCKET}/${s3_key}" && rm -f "$file_path"
public_url="${CLOUDFRONT_URL}/${s3_key}"
# Discord로 전송
curl -H "Content-Type: application/json" -X POST \\
-d "{\\"content\\": \\"🚨 *에러 감지됨 - ${timestamp}*\\n📎 [에러 로그 다운로드](${public_url})\\"}" \\
"$WEBHOOK_URL"
echo "[ALERT] 에러 블록 전송 완료 at ${timestamp}"
buffer=""
collecting=0
fi
# 현재 줄이 " ERROR " 포함한 경우에만 블록 시작
if [[ "$line" =~ [[:space:]]ERROR[[:space:]] ]]; then
collecting=1
buffer="$line"
fi
else
# 계속 수집
if [[ $collecting -eq 1 ]]; then
buffer="${buffer}\\n${line}"
fi
fi
done
큰 흐름은 다음과 같습니다.
- tail -F 를 통해 실시간 로그를 감시
- ERROR가 포함된 로그 블록을 감지
- 해당 블록을 .log 파일로 저장
- S3에 업로드하고 CloudFront의 링크를 생성
- Discord에 해당 .log 파일을 전송
코드 상세 설명은 다음과 같습니다.
코드 상세 설명
echo "[INFO] 에러 블록 감시 시작..."
- 감시 시작 알림을 콘솔에 출력
에러 블록 수집 초기화
buffer=""
collecting=0
- buffer: 에러 블록을 저장하는 문자열 변수
- collecting: 에러 수집 여부를 나타내는 플래그 (0: 비활성, 1: 수집 중)
실시간 로그 감시 시작
tail -F "$LOG_FILE" | while read -r line; do
- tail -F는 파일이 갱신될 때마다 새 줄을 감지
- 새로운 로그 라인을 읽을 때마다 루프 반복
날짜로 시작하는 줄인지 확인
if [[ "$line" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2} ]]; then
- 로그의 각 줄이 YYYY-MM-DD 형식으로 시작하면 새로운 로그 라인의 시작으로 판단
if [[ $collecting -eq 1 ]]; then
- 이전에 ERROR로 시작해서 블록을 수집 중이었다면:
timestamp=$(date "+%Y-%m-%d_%H-%M-%S")
file_path="${ERROR_OUTPUT_DIR}/error-${timestamp}.log"
echo -e "$buffer" > "$file_path"
- 에러 블록을 error-날짜_시간.log로 저장
S3 업로드 및 디스코드 전송
s3_key="${S3_PREFIX}/error-${timestamp}.log"
aws s3 cp "$file_path" "s3://${S3_BUCKET}/${s3_key}"
public_url="${CLOUDFRONT_URL}/${s3_key}
- S3에 업로드하고 CloudFront 링크 생성
curl -H "Content-Type: application/json" -X POST \\
-d "{\\"content\\": \\"🚨 *에러 감지됨 - ${timestamp}*\\n📎 [에러 로그 다운로드](${public_url})\\"}" \\
"$WEBHOOK_URL"
- Discord Webhook을 통해 링크 전송
buffer=""
collecting=0
- 버퍼 및 수집 상태 초기화
새로운 에러 블록 시작 조건
if [[ "$line" =~ [[:space:]]ERROR[[:space:]] ]]; then
collecting=1
buffer="$line"
fi
- 줄에 " ERROR "가 포함된 경우 → 새로운 에러 수집 시작
에러 블록 계속 수집
else
if [[ $collecting -eq 1 ]]; then
buffer="${buffer}\\n${line}"
fi
- 날짜로 시작하지 않는 줄이지만 수집 중이라면 에러 블록에 이어 붙임
이렇게 구성하니 discord에 알림이 잘 갔습니다.
2025-06-02 19:09:47.010 [https-jsse-nio-443-exec-2046] ERROR c.a.n.c.e.GlobalExceptionHandler - Unexpected exception occurred: No static resource .env.staging.local.
org.springframework.web.servlet.resource.NoResourceFoundException: No static resource .env.staging.local.
로그를 확인해보니 .env 파일을 털어갈라고 스캐닝 요청오는 게 있는 것 같습니다.
env 파일 관리를 철저하게 해야겠다는 생각을 했습니다.
이걸 보고 생각이 든것이 제한시간을 걸어두지 않는다면 로그가 공격받는 것처럼 공격적으로 알람으로 뜨는 경우가 있고, 공격의 여지가 있을 것 같다고 판단되어 뭔가 보완할 방법이 필요했습니다.
그래서 에러 로그 COOLDOWN 시간을 3분으로 두어서 3분동안 발생한 에러로그를 한번에 전송하는 방식으로 수정했습니다.
!/bin/bash
# === 사용자 설정 ===
LOG_DIR="{로그가쌓이는_디렉토리}"
WEBHOOK_URL="{디스코드_웹훅URL}"
S3_BUCKET="{S3버킷이름}"
S3_PREFIX="{S3로그저장될위치}"
CLOUDFRONT_URL="{CLOUDFRONT_경로}"
AWS_ACCESS_KEY_ID="{AWS_IAM액세스키}"
AWS_SECRET_ACCESS_KEY="{AWS_IAM시크릿키}"
AWS_REGION="{S3버킷_AWS리전}"
# === 환경 설정 ===
today=$(date +"%Y-%m-%d")
LOG_FILE="${LOG_DIR}/newsum.log" # [필수] 로그 파일 경로
ERROR_OUTPUT_DIR="{에러파일_저장될위치}"
BUFFER_FILE="{tmp파일_저장될위치}"
COOLDOWN_SECONDS=180 # 3분
mkdir -p "$ERROR_OUTPUT_DIR"
# === AWS 환경변수 ===
export AWS_ACCESS_KEY_ID
export AWS_SECRET_ACCESS_KEY
export AWS_DEFAULT_REGION=$AWS_REGION
# === 에러 수집 시작 ===
echo "[INFO] 에러 감지 시작..."
# 에러 수집 프로세스 (백그라운드)
tail -n 0 -F "$LOG_FILE" | while read -r line; do
# 날짜+ERROR로 시작하는 줄이면 새 에러 시작ㅑ
if [[ "$line" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}.*ERROR ]]; then
echo "[INFO] 에러 발생!"
{
echo -e "\\n==== 에러 발생! ===="
echo -e "$line"
} >> "$BUFFER_FILE"
elif [[ -s "$BUFFER_FILE" ]]; then
echo "$line" >> "$BUFFER_FILE"
fi
done &
# === 3분마다 전송 프로세스 ===
while true; do
sleep "$COOLDOWN_SECONDS"
if [[ -s "$BUFFER_FILE" ]]; then
timestamp=$(date "+%Y-%m-%d_%H-%M-%S")
file_path="${ERROR_OUTPUT_DIR}/batch-error-${timestamp}.log"
# 파일 이동 및 버퍼 초기화
mv "$BUFFER_FILE" "$file_path"