이미지 로딩 중...
AI Generated
2025. 11. 18. · 2 Views
로그 통계 분석 및 리포트 생성 완벽 가이드
서버 로그 파일을 체계적으로 분석하고 의미 있는 통계 리포트를 자동으로 생성하는 방법을 배워봅니다. awk, grep, sed 등의 리눅스 도구를 활용하여 시간대별 에러 분석, HTTP 상태 코드 집계, 응답 시간 통계를 산출하고, 최종적으로 HTML 형식의 보기 좋은 리포트를 만드는 실전 기법을 다룹니다.
목차
1. awk로 로그 통계 계산
시작하며
여러분이 서버를 운영하다가 갑자기 사용자들이 "사이트가 느려요!"라고 불평하는 상황을 겪어본 적 있나요? 수백만 줄의 로그 파일을 눈으로 하나하나 확인하려니 막막하기만 합니다.
이런 문제는 실제 개발 현장에서 매일같이 발생합니다. 로그 파일은 쌓이는 속도가 빠르고, 그 안에 숨겨진 패턴을 찾아내기란 쉽지 않습니다.
문제의 원인을 빨리 찾지 못하면 서비스 중단으로 이어질 수도 있죠. 바로 이럴 때 필요한 것이 awk를 활용한 로그 통계 계산입니다.
awk는 텍스트 데이터를 빠르게 처리하고 집계할 수 있는 강력한 도구로, 복잡한 프로그램 없이도 수백만 줄의 로그를 몇 초 만에 분석할 수 있습니다.
개요
간단히 말해서, awk는 텍스트 파일을 한 줄씩 읽으면서 원하는 데이터를 추출하고 계산하는 도구입니다. 왜 awk가 필요한지 생각해볼까요?
Python이나 다른 프로그래밍 언어로도 로그를 분석할 수 있지만, awk는 별도의 설치 없이 모든 리눅스 시스템에 기본으로 깔려 있고, 한 줄짜리 명령어로도 복잡한 통계를 뽑아낼 수 있습니다. 예를 들어, "오늘 가장 많이 접속한 IP 주소 찾기" 같은 작업을 단 한 줄로 해결할 수 있죠.
기존에는 로그 파일을 텍스트 에디터로 열어서 Ctrl+F로 검색했다면, 이제는 awk 명령어 하나로 자동으로 집계하고 정렬된 결과를 얻을 수 있습니다. awk의 핵심 특징은 세 가지입니다.
첫째, 공백이나 특정 구분자로 나뉜 '필드' 단위로 데이터를 쉽게 다룰 수 있습니다. 둘째, 조건문과 반복문을 지원해서 복잡한 로직도 구현 가능합니다.
셋째, 연관 배열(해시맵)을 지원해서 데이터를 집계하고 카운팅하기에 최적입니다. 이러한 특징들이 로그 분석에서 왜 중요한지는 코드를 보면 바로 이해될 것입니다.
코드 예제
# access.log에서 각 IP별 접속 횟수를 집계
awk '{ip[$1]++} END {for (i in ip) print ip[i], i}' access.log | sort -rn | head -10
# 특정 시간대(10시~11시)의 요청만 필터링하여 카운트
awk '$4 ~ /10:/ {count++} END {print "10시대 요청:", count}' access.log
# 404 에러 발생 횟수 집계
awk '$9 == 404 {errors++} END {print "404 에러:", errors, "건"}' access.log
# 평균 응답 시간 계산 (응답 시간이 10번째 필드라고 가정)
awk '{sum+=$10; count++} END {print "평균 응답시간:", sum/count, "ms"}' access.log
# 상태 코드별 통계 (200, 404, 500 등)
awk '{status[$9]++} END {for (s in status) print s, status[s]}' access.log | sort
설명
이것이 하는 일은 로그 파일의 각 줄을 읽어서 특정 패턴에 맞는 데이터를 추출하고, 그것을 집계하여 의미 있는 통계를 만들어내는 것입니다. 첫 번째로, awk '{ip[$1]++}' 부분은 각 줄의 첫 번째 필드($1, 보통 IP 주소)를 읽어서 연관 배열 ip에 저장하고 카운트를 1씩 증가시킵니다.
왜 이렇게 하냐면, awk는 자동으로 각 줄을 공백으로 나누어 $1, $2, $3... 같은 변수에 저장하기 때문입니다.
마치 엑셀의 열처럼 생각하면 쉽습니다. 그 다음으로, END {for (i in ip) print ip[i], i} 부분이 실행되면서 모든 줄을 다 읽은 후에 집계 결과를 출력합니다.
END 블록은 파일을 다 읽은 뒤에 한 번만 실행되는 특별한 영역입니다. for 루프로 연관 배열을 순회하면서 "접속 횟수 IP주소" 형태로 출력하죠.
마지막으로, sort -rn | head -10 부분이 숫자 기준으로 내림차순 정렬하여 상위 10개만 보여줍니다. 최종적으로 "가장 많이 접속한 IP 주소 TOP 10" 목록을 얻게 되는 것입니다.
여러분이 이 코드를 사용하면 수백만 줄의 로그 파일도 몇 초 안에 분석할 수 있고, 어떤 IP가 가장 많이 접속했는지, 평균 응답 시간은 얼마인지, 어떤 에러가 가장 많이 발생했는지를 즉시 파악할 수 있습니다. 이를 통해 서버 과부하의 원인을 찾거나, DDoS 공격을 탐지하거나, 성능 병목 지점을 발견하는 등의 실무 문제를 해결할 수 있죠.
실전 팁
💡 awk는 기본적으로 공백으로 필드를 나누지만, -F 옵션으로 구분자를 변경할 수 있습니다. CSV 파일이라면 awk -F',' '{print $1}'처럼 사용하세요.
💡 흔한 실수는 배열 초기화를 잊는 것입니다. awk의 변수는 자동으로 0이나 빈 문자열로 초기화되므로 count++처럼 바로 사용해도 됩니다.
💡 큰 로그 파일은 메모리를 많이 사용할 수 있으니, 필요한 데이터만 추출하도록 조건문을 앞쪽에 배치하세요. awk '$9==404 {errors++}'가 awk '{if($9==404) errors++}'보다 빠릅니다.
💡 디버깅할 때는 중간 결과를 출력해보세요. {print "DEBUG:", $1, $9; ip[$1]++}처럼 작성하면 어떤 데이터가 처리되는지 볼 수 있습니다.
💡 여러 조건을 조합할 때는 &&(AND)와 ||(OR)를 사용하세요. awk '$9==404 && $7~/api/ {count++}'는 "404이면서 URL에 api가 포함된 경우"만 카운트합니다.
2. 시간대별 에러 집계
시작하며
여러분의 서비스에서 갑자기 에러가 폭증했다는 알림을 받았을 때, 가장 먼저 궁금한 것은 "언제부터 에러가 발생했지?"입니다. 막연히 "오늘 에러가 많았다"는 것보다 "오후 3시부터 3시 30분 사이에 집중적으로 발생했다"는 정보가 훨씬 유용하죠.
이런 문제는 실제 장애 대응에서 가장 중요한 첫 단계입니다. 시간대별 패턴을 파악하지 못하면 원인 분석이 어렵고, 같은 문제가 반복될 수 있습니다.
예를 들어, 매일 오후 3시에 배치 작업이 돌면서 DB 커넥션이 고갈되는 문제라면, 시간대별 분석 없이는 절대 발견할 수 없습니다. 바로 이럴 때 필요한 것이 시간대별 에러 집계입니다.
로그의 타임스탬프를 파싱하여 시간대별로 그룹화하고, 각 시간대에 발생한 에러 건수를 집계하면 문제의 발생 시점과 패턴을 한눈에 파악할 수 있습니다.
개요
간단히 말해서, 시간대별 에러 집계는 로그 파일에서 타임스탬프를 추출하고 시간 단위로 그룹화하여 에러 발생 추이를 파악하는 기법입니다. 왜 이 기법이 필요한지 생각해볼까요?
단순히 "오늘 에러가 몇 건 발생했다"는 정보는 큰 의미가 없습니다. 하루 종일 고르게 10건씩 발생한 것과, 특정 1시간에 100건이 몰린 것은 완전히 다른 문제니까요.
예를 들어, 출퇴근 시간대에 트래픽이 몰리면서 타임아웃이 발생하는 경우, 시간대별 분석 없이는 원인을 찾기 어렵습니다. 기존에는 로그를 수동으로 검색하면서 "3시쯤에 에러가 많았던 것 같은데..."라고 추측했다면, 이제는 정확한 숫자로 "15:00~15:59 사이에 327건의 500 에러 발생"이라고 보고할 수 있습니다.
이 기법의 핵심 특징은 두 가지입니다. 첫째, 정규표현식을 활용하여 다양한 로그 포맷에서 타임스탬프를 유연하게 추출합니다.
둘째, awk의 연관 배열을 활용하여 시간대를 키로 사용하고 에러 건수를 값으로 저장합니다. 이를 통해 메모리 효율적으로 대용량 로그도 빠르게 처리할 수 있습니다.
코드 예제
# Apache/Nginx 로그에서 시간대별 에러(4xx, 5xx) 집계
# 로그 형식: 192.168.1.1 - - [18/Nov/2025:15:23:45 +0900] "GET /api" 500 1234
awk '$9 >= 400 {
# 타임스탬프에서 시간 부분만 추출 (예: 15시)
match($4, /[0-9]{2}:[0-9]{2}:[0-9]{2}/)
time = substr($4, RSTART, 2) # 시간만 추출
errors[time]++
# 상태 코드별로도 집계
status_count[time":"$9]++
}
END {
print "=== 시간대별 에러 발생 건수 ==="
for (h in errors) {
printf "%s시: %d건\n", h, errors[h]
}
}' access.log | sort
# 분 단위로 더 세밀하게 집계 (15:00, 15:01, 15:02...)
awk '$9 >= 400 {
match($4, /[0-9]{2}:[0-9]{2}/)
hour_min = substr($4, RSTART, 5)
errors[hour_min]++
}
END {
for (hm in errors) print hm, errors[hm]
}' access.log | sort
설명
이것이 하는 일은 로그의 각 줄에서 타임스탬프를 찾아내고, 시간 부분만 추출하여 그 시간대에 발생한 에러를 카운팅하는 것입니다. 첫 번째로, $9 >= 400 조건문이 HTTP 상태 코드가 400 이상인 줄만 처리하도록 필터링합니다.
왜냐하면 4xx는 클라이언트 에러(404 Not Found 등), 5xx는 서버 에러(500 Internal Server Error 등)를 의미하기 때문이죠. 200이나 304 같은 정상 응답은 제외됩니다.
그 다음으로, match($4, /[0-9]{2}:[0-9]{2}:[0-9]{2}/) 부분이 실행되면서 네 번째 필드($4, 보통 타임스탬프 필드)에서 "15:23:45" 같은 시간 패턴을 찾습니다. match 함수는 패턴이 시작되는 위치를 RSTART 변수에 저장하고, substr($4, RSTART, 2)로 시간 부분(15)만 잘라냅니다.
마치 가위로 종이를 자르듯이 문자열에서 원하는 부분만 추출하는 것이죠. 마지막으로, errors[time]++가 해당 시간대의 카운터를 1 증가시키고, END 블록에서 모든 시간대별 집계 결과를 출력합니다.
최종적으로 "14시: 23건, 15시: 327건, 16시: 45건" 같은 형태로 시간대별 에러 발생 추이를 확인할 수 있습니다. 여러분이 이 코드를 사용하면 장애 발생 시점을 정확히 특정할 수 있고, 특정 시간대에만 발생하는 문제(예: 배치 작업 시간, 트래픽 피크 시간)를 발견할 수 있습니다.
또한 시계열 그래프로 시각화하면 더욱 직관적으로 패턴을 파악할 수 있죠. 예를 들어, 매일 같은 시간에 에러가 발생한다면 주기적인 외부 요인(크론잡, 외부 API 호출 등)을 의심할 수 있습니다.
실전 팁
💡 로그 포맷이 다르다면 match 함수의 정규표현식을 수정하세요. ISO 8601 형식(2025-11-18T15:23:45)이라면 /T[0-9]{2}/로 시간을 추출할 수 있습니다.
💡 흔한 실수는 타임존을 고려하지 않는 것입니다. 서버 로그가 UTC라면 +9시간을 더해서 KST로 변환해야 정확한 분석이 가능합니다.
💡 성능을 위해 큰 로그 파일은 먼저 날짜로 필터링하세요. grep '18/Nov/2025' access.log | awk ...처럼 필요한 날짜만 추출하면 훨씬 빠릅니다.
💡 디버깅 시 타임스탬프 추출이 제대로 되는지 확인하려면 {print "원본:", $4, "추출:", time} 같은 디버그 출력을 추가하세요.
💡 분 단위 집계는 데이터가 많아지니, 특정 시간대만 상세 분석하고 싶다면 awk '$4 ~/15:/ && $9>=400 {..}'처럼 시간 필터를 먼저 적용하세요.
3. HTTP 상태 코드 분석
시작하며
여러분이 웹 서비스를 운영하다 보면 "왜 이렇게 404 에러가 많지?", "500 에러는 어디서 발생한 거지?" 같은 궁금증이 생깁니다. 막연히 "에러가 많다"는 것보다 "404가 80%, 500이 15%, 502가 5%"라는 구체적인 비율을 알면 우선순위를 정할 수 있죠.
이런 문제는 실제 서비스 품질 관리에서 매우 중요합니다. 404가 많다면 깨진 링크나 잘못된 URL이 퍼져있다는 뜻이고, 500이 많다면 서버 코드에 버그가 있다는 신호입니다.
502/503이 많다면 업스트림 서버(DB, 백엔드 API)의 문제를 의심해야 하고요. 바로 이럴 때 필요한 것이 HTTP 상태 코드 분석입니다.
각 상태 코드의 발생 빈도와 비율을 집계하고, 어떤 URL에서 어떤 에러가 발생했는지 파악하면 문제의 근본 원인을 찾을 수 있습니다.
개요
간단히 말해서, HTTP 상태 코드 분석은 로그에서 상태 코드(200, 404, 500 등)를 추출하여 각 코드별 발생 건수와 비율을 계산하고, 어떤 경로에서 에러가 발생했는지 파악하는 기법입니다. 왜 이 분석이 필요한지 실무 관점에서 생각해볼까요?
사용자는 "사이트가 안 돼요"라고만 말하지, 정확히 어떤 에러인지 알려주지 않습니다. 로그를 분석해서 404가 많다면 SEO 문제이거나 잘못된 링크가 SNS에 퍼진 것이고, 500이 많다면 긴급하게 코드를 수정해야 하는 상황입니다.
예를 들어, "/api/users" 엔드포인트에서 500 에러가 100건 발생했다면 해당 API의 버그를 최우선으로 수정해야겠죠. 기존에는 "에러가 좀 있는 것 같은데..."라고 막연하게 말했다면, 이제는 "404 에러 1,250건 중 80%가 /old-page.html에서 발생, 리다이렉트 설정 필요"라고 구체적으로 보고할 수 있습니다.
이 분석의 핵심 특징은 세 가지입니다. 첫째, 상태 코드의 범주(2xx=성공, 3xx=리다이렉트, 4xx=클라이언트 에러, 5xx=서버 에러)별로 그룹화하여 전체적인 건강도를 파악합니다.
둘째, 특정 상태 코드가 발생한 URL을 함께 추적하여 문제의 정확한 위치를 찾습니다. 셋째, 비율을 계산하여 상대적인 심각도를 평가합니다.
이러한 특징들이 효과적인 장애 대응과 서비스 개선에 필수적입니다.
코드 예제
# 상태 코드별 발생 건수와 비율 계산
awk '{
status[$9]++
total++
}
END {
print "=== HTTP 상태 코드 분석 ==="
print "총 요청 수:", total
print "\n[상태 코드별 통계]"
for (code in status) {
percentage = (status[code] / total) * 100
printf "%s: %d건 (%.2f%%)\n", code, status[code], percentage
}
}' access.log | sort
# 4xx, 5xx 에러가 발생한 URL TOP 10
awk '$9 >= 400 {
# URL은 보통 7번째 필드
error_url[$7":"$9]++
}
END {
for (eu in error_url) {
print error_url[eu], eu
}
}' access.log | sort -rn | head -10
# 시간대별 + 상태 코드별 크로스 집계
awk '{
match($4, /[0-9]{2}:/)
hour = substr($4, RSTART, 2)
time_status[hour":"$9]++
}
END {
for (ts in time_status) {
split(ts, arr, ":")
printf "%s시 - %s: %d건\n", arr[1], arr[2], time_status[ts]
}
}' access.log | sort
설명
이것이 하는 일은 로그의 각 줄에서 상태 코드를 읽어서 카운팅하고, 전체 요청 대비 각 코드의 비율을 계산하여 서비스의 건강 상태를 수치화하는 것입니다. 첫 번째로, status[$9]++와 total++ 부분이 각 줄을 읽을 때마다 해당 상태 코드의 카운터를 증가시키고 전체 요청 수도 함께 카운팅합니다.
$9는 일반적인 웹 서버 로그 포맷에서 상태 코드가 위치한 필드입니다. 왜 total도 세냐면, 나중에 비율을 계산하기 위해서죠.
그 다음으로, END 블록에서 percentage = (status[code] / total) * 100 계산이 실행되면서 각 상태 코드의 발생 비율을 백분율로 변환합니다. 예를 들어, 총 10,000건 중 200 코드가 8,500건이면 85%가 되는 것이죠.
printf 함수로 소수점 둘째 자리까지 깔끔하게 포맷팅합니다. 마지막으로, 두 번째 예제의 error_url[$7":"$9]++ 부분이 URL과 상태 코드를 조합한 키로 집계합니다.
최종적으로 "가장 많은 404를 발생시킨 URL은 /old-api/users (523건)"처럼 구체적인 문제 지점을 찾아낼 수 있습니다. 여러분이 이 코드를 사용하면 서비스의 전반적인 건강도를 한눈에 파악할 수 있습니다.
200 코드가 95% 이상이면 건강한 상태, 4xx가 10% 이상이면 클라이언트 통합 문제 의심, 5xx가 1% 이상이면 긴급 대응이 필요한 상황입니다. 또한 특정 URL에서 집중적으로 에러가 발생한다면 해당 엔드포인트의 코드나 인프라를 점검할 수 있죠.
실무에서는 이 데이터를 시계열로 모니터링하여 "어제는 5xx가 0.1%였는데 오늘은 2%로 증가"처럼 추세를 파악하기도 합니다.
실전 팁
💡 상태 코드 의미를 정확히 알아야 합니다. 2xx=성공, 3xx=리다이렉트(정상), 4xx=클라이언트 에러, 5xx=서버 에러. 301/302는 정상이지만 너무 많으면 성능 저하 원인이 됩니다.
💡 흔한 실수는 상태 코드 필드 위치를 잘못 지정하는 것입니다. 로그 포맷이 다르면 $9가 아닐 수 있으니 먼저 head -1 access.log로 확인하세요.
💡 성능 분석 시 2xx 성공 응답도 중요합니다. $9 == 200 && $10 > 1000처럼 응답 시간이 1초 이상인 느린 성공 요청도 찾아보세요.
💡 특정 상태 코드만 상세 분석하려면 별도로 필터링하세요. awk '$9==404 {print $7}' | sort | uniq -c | sort -rn으로 404 발생 URL만 집계할 수 있습니다.
💡 비정상적으로 많은 특정 코드는 공격 시도일 수 있습니다. 특정 IP에서 403 Forbidden이 수천 건 발생하면 무차별 대입 공격을 의심하세요.
4. 응답 시간 통계
시작하며
여러분의 API가 "느리다"는 불만을 받았을 때, 구체적으로 얼마나 느린지 숫자로 말할 수 있나요? "평균 응답 시간 500ms"라는 정보는 괜찮아 보이지만, 실제로는 90%가 100ms인데 10%가 5초라면 심각한 문제입니다.
이런 문제는 실제 성능 최적화에서 가장 중요한 포인트입니다. 평균만 보면 숨겨지는 극단적인 값들(outlier)이 사용자 경험을 망칩니다.
예를 들어, 결제 API가 평균 300ms인데 1%는 10초가 걸린다면, 100명 중 1명은 결제가 안 되는 것처럼 느끼고 이탈할 수 있죠. 바로 이럴 때 필요한 것이 응답 시간 통계입니다.
평균뿐만 아니라 중앙값(median), 95 percentile, 99 percentile 같은 다양한 지표를 계산하면 실제 사용자가 경험하는 성능을 정확히 파악할 수 있습니다.
개요
간단히 말해서, 응답 시간 통계는 로그에서 각 요청의 응답 시간을 추출하여 평균, 최소, 최대, 백분위수 등 다양한 통계 지표를 계산하는 기법입니다. 왜 이 통계가 필요한지 실무 시나리오를 생각해볼까요?
SLA(Service Level Agreement)에서 "99%의 요청이 1초 이내에 응답"이라고 약속했다면, 평균만으로는 이를 검증할 수 없습니다. 99 percentile이 900ms라면 약속을 지킨 것이고, 2초라면 위반한 것이죠.
예를 들어, 전자상거래 사이트에서 상품 목록 API의 95 percentile이 2초를 넘으면 사용자 5%는 느린 경험을 하게 되고, 이는 곧 매출 손실로 이어집니다. 기존에는 "API가 느린 것 같아요"라고 막연하게 말했다면, 이제는 "평균 300ms, 중앙값 200ms, 95p 800ms, 99p 2.1s - 상위 1%에서 병목 발생"이라고 데이터 기반으로 보고할 수 있습니다.
이 통계의 핵심 특징은 네 가지입니다. 첫째, 평균(mean)은 전체적인 성능을 보여주지만 극단값에 영향을 많이 받습니다.
둘째, 중앙값(median)은 절반의 요청이 경험하는 실제 성능을 보여줍니다. 셋째, 95/99 percentile은 대부분의 사용자가 경험하는 최악의 케이스를 보여줍니다.
넷째, 최소/최대값은 캐시 효과나 타임아웃 같은 극단적 상황을 파악하게 해줍니다. 이러한 다양한 지표를 함께 보면 성능 문제의 본질을 정확히 이해할 수 있습니다.
코드 예제
# 응답 시간 통계 계산 (응답 시간이 마지막 필드라고 가정)
awk '{
response_time = $NF # NF는 마지막 필드
times[NR] = response_time # 배열에 저장 (percentile 계산용)
sum += response_time
if (NR == 1 || response_time < min) min = response_time
if (NR == 1 || response_time > max) max = response_time
count++
}
END {
# 평균 계산
avg = sum / count
# 배열 정렬 (버블 소트)
for (i = 1; i <= count; i++) {
for (j = i + 1; j <= count; j++) {
if (times[i] > times[j]) {
temp = times[i]
times[i] = times[j]
times[j] = temp
}
}
}
# Percentile 계산
median_idx = int(count * 0.5)
p95_idx = int(count * 0.95)
p99_idx = int(count * 0.99)
print "=== 응답 시간 통계 (ms) ==="
printf "총 요청 수: %d\n", count
printf "평균: %.2f\n", avg
printf "최소: %.2f\n", min
printf "최대: %.2f\n", max
printf "중앙값 (50p): %.2f\n", times[median_idx]
printf "95 percentile: %.2f\n", times[p95_idx]
printf "99 percentile: %.2f\n", times[p99_idx]
}' access.log
# URL별 평균 응답 시간
awk '{url[$7] += $NF; count[$7]++}
END {
for (u in url) {
printf "%s: %.2fms\n", u, url[u]/count[u]
}
}' access.log | sort -t: -k2 -rn | head -10
설명
이것이 하는 일은 모든 응답 시간을 배열에 저장하고 정렬한 뒤, 다양한 백분위수를 계산하여 성능 분포를 파악하는 것입니다. 첫 번째로, times[NR] = response_time 부분이 각 줄의 응답 시간을 배열에 저장합니다.
NR은 현재 행 번호로, 1, 2, 3... 순서대로 증가합니다.
$NF는 마지막 필드를 의미하는데, 많은 로그 포맷에서 응답 시간이 마지막에 위치하기 때문입니다. 동시에 sum에 누적하여 나중에 평균을 계산할 준비를 하고, min/max 값도 업데이트합니다.
그 다음으로, END 블록에서 버블 정렬 알고리즘을 사용하여 배열을 오름차순으로 정렬합니다. 왜 정렬이 필요하냐면, percentile을 계산하려면 데이터가 정렬되어 있어야 하기 때문입니다.
예를 들어, 100개 데이터의 95 percentile은 정렬 후 95번째 값이죠. 버블 소트는 간단하지만 대용량 데이터에는 느리므로, 수만 건 이상이라면 외부 sort 명령과 조합하는 것이 좋습니다.
마지막으로, int(count * 0.95) 같은 계산으로 백분위수에 해당하는 인덱스를 찾고, times[p95_idx]로 실제 값을 가져옵니다. 최종적으로 "95%의 요청은 800ms 이내에 완료되고, 나머지 5%는 800ms~5초 사이"처럼 성능 분포를 정확히 파악할 수 있습니다.
여러분이 이 코드를 사용하면 성능 최적화의 우선순위를 정할 수 있습니다. 만약 평균은 낮은데 99p가 높다면, 일부 극단적인 케이스만 최적화하면 됩니다.
반대로 평균 자체가 높다면 전반적인 아키텍처 개선이 필요하죠. URL별 응답 시간을 보면 어떤 API가 가장 느린지 알 수 있어서, 캐싱을 추가하거나 쿼리를 최적화할 엔드포인트를 선택할 수 있습니다.
실무에서는 이 통계를 시간대별로도 집계하여 "새벽에는 빠른데 낮에는 느리다" 같은 트래픽 패턴도 파악합니다.
실전 팁
💡 로그에 응답 시간이 없다면 웹 서버 설정을 수정하세요. Nginx는 $request_time, Apache는 %D로 응답 시간을 로그에 추가할 수 있습니다.
💡 큰 데이터셋은 awk 버블 소트가 느리니, sort -n을 활용하세요. awk '{print $NF}' | sort -n | awk '{a[NR]=$1} END {print a[int(NR*0.95)]}'처럼 파이프로 연결할 수 있습니다.
💡 성능 기준은 서비스마다 다릅니다. API는 보통 95p < 500ms, 웹페이지는 95p < 2s가 좋은 기준이지만, 여러분의 SLA에 맞게 조정하세요.
💡 단위를 확인하세요! 로그가 초 단위인지 밀리초 단위인지 확인하고, 필요하면 response_time * 1000처럼 변환하세요.
💡 타임아웃 설정된 값(예: 30초)에 응답이 몰려있다면 실제로는 더 오래 걸리지만 타임아웃에 걸린 것입니다. 타임아웃 값을 늘려서 실제 응답 시간을 측정하거나, 코드를 최적화하세요.
5. HTML 리포트 자동 생성
시작하며
여러분이 아무리 훌륭한 로그 분석을 해도, 결과를 터미널의 텍스트로만 보면 이해하기 어렵고 공유하기도 불편합니다. "15시에 에러 327건"보다는 시간대별 막대 그래프로 보는 게 훨씬 직관적이죠.
이런 문제는 실제 업무에서 매우 중요합니다. 분석 결과를 팀원들과 공유하거나, 경영진에게 보고할 때, 보기 좋은 HTML 리포트가 있으면 의사소통이 훨씬 원활합니다.
텍스트를 복사해서 이메일로 보내는 것보다, 링크 하나로 브라우저에서 볼 수 있는 리포트가 훨씬 전문적이고 설득력 있습니다. 바로 이럴 때 필요한 것이 HTML 리포트 자동 생성입니다.
쉘 스크립트로 로그를 분석하고, 그 결과를 HTML 파일로 변환하면 누구나 쉽게 읽을 수 있는 시각적인 리포트를 만들 수 있습니다.
개요
간단히 말해서, HTML 리포트 자동 생성은 로그 분석 결과를 HTML 형식으로 변환하여 웹 브라우저에서 보기 좋게 표시하고, 필요하면 차트나 테이블로 시각화하는 기법입니다. 왜 이 기법이 필요한지 생각해볼까요?
데이터는 시각화되었을 때 가장 큰 가치를 발휘합니다. 숫자 나열보다는 표로 정리하고, 색상으로 중요도를 구분하면 한눈에 파악됩니다.
예를 들어, 시간대별 에러 발생 건수를 빨간색 막대 그래프로 보여주면, "15시에 급증했네!"라는 인사이트를 즉시 얻을 수 있죠. 기존에는 분석 결과를 수동으로 엑셀에 붙여넣고 차트를 만들었다면, 이제는 스크립트 하나로 자동으로 HTML 리포트가 생성되어 매일 아침 메일로 받아볼 수 있습니다.
이 기법의 핵심 특징은 세 가지입니다. 첫째, HTML/CSS를 활용하여 테이블, 색상, 레이아웃을 자유롭게 구성합니다.
둘째, Chart.js나 Google Charts 같은 라이브러리를 CDN으로 불러와서 그래프를 추가할 수 있습니다. 셋째, 쉘 스크립트에서 here-document 문법으로 HTML을 생성하면 템플릿 엔진 없이도 동적인 리포트를 만들 수 있습니다.
이를 통해 복잡한 개발 환경 없이도 전문적인 리포트를 만들 수 있습니다.
코드 예제
#!/bin/bash
# generate_report.sh - 로그 분석 HTML 리포트 생성
LOG_FILE="access.log"
REPORT_FILE="report_$(date +%Y%m%d).html"
# 통계 계산
TOTAL=$(wc -l < "$LOG_FILE")
ERRORS=$(awk '$9 >= 400' "$LOG_FILE" | wc -l)
ERROR_RATE=$(awk "BEGIN {printf \"%.2f\", ($ERRORS/$TOTAL)*100}")
# 상태 코드별 통계를 JSON 형식으로
STATUS_JSON=$(awk '{status[$9]++} END {
printf "["
first=1
for (s in status) {
if (!first) printf ","
printf "{\"code\":\"%s\",\"count\":%d}", s, status[s]
first=0
}
printf "]"
}' "$LOG_FILE")
# HTML 생성 (here-document 사용)
cat > "$REPORT_FILE" << EOF
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>로그 분석 리포트 - $(date +%Y-%m-%d)</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; }
h1 { color: #333; border-bottom: 3px solid #4CAF50; padding-bottom: 10px; }
.summary { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin: 20px 0; }
.card { background: #f9f9f9; padding: 20px; border-radius: 8px; border-left: 4px solid #4CAF50; }
.card h3 { margin: 0 0 10px 0; color: #666; font-size: 14px; }
.card .value { font-size: 32px; font-weight: bold; color: #333; }
.error { border-left-color: #f44336; }
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
th { background: #4CAF50; color: white; }
tr:hover { background: #f5f5f5; }
</style>
</head>
<body>
<div class="container">
<h1>📊 로그 분석 리포트</h1>
<p>생성 시간: $(date +"%Y-%m-%d %H:%M:%S")</p>
<div class="summary">
<div class="card">
<h3>총 요청 수</h3>
<div class="value">$(printf "%'d" $TOTAL)</div>
</div>
<div class="card error">
<h3>에러 발생 건수</h3>
<div class="value">$(printf "%'d" $ERRORS)</div>
</div>
<div class="card error">
<h3>에러 비율</h3>
<div class="value">${ERROR_RATE}%</div>
</div>
</div>
<h2>상태 코드별 통계</h2>
<table>
<tr><th>상태 코드</th><th>발생 건수</th></tr>
$(awk '{status[$9]++} END {for (s in status) printf " <tr><td>%s</td><td>%d</td></tr>\n", s, status[s]}' "$LOG_FILE" | sort)
</table>
<p style="color: #666; font-size: 12px; margin-top: 40px;">
자동 생성된 리포트입니다. 문의: devops@example.com
</p>
</div>
</body>
</html>
EOF
echo "리포트 생성 완료: $REPORT_FILE"
# 브라우저로 자동 열기 (선택사항)
# xdg-open "$REPORT_FILE" # Linux
# open "$REPORT_FILE" # macOS
설명
이것이 하는 일은 쉘 스크립트로 로그를 분석하고, 그 결과를 HTML 템플릿에 삽입하여 완전한 웹 페이지를 생성하는 것입니다. 첫 번째로, TOTAL=$(wc -l < "$LOG_FILE") 같은 명령어들이 실행되면서 필요한 통계를 변수에 저장합니다.
wc -l은 파일의 총 줄 수를 세고, awk 명령어는 에러 건수를 세죠. 이렇게 먼저 데이터를 준비해두는 이유는 HTML 생성 시 변수를 바로 삽입하기 위해서입니다.
마치 요리하기 전에 재료를 모두 준비하는 것과 같습니다. 그 다음으로, cat > "$REPORT_FILE" << EOF 구문이 시작되면서 here-document 방식으로 HTML을 생성합니다.
EOF(End Of File)까지의 모든 내용이 파일에 기록되는데, 이 과정에서 $(date)나 $TOTAL 같은 변수들이 실제 값으로 치환됩니다. 예를 들어, <div class="value">$TOTAL</div>는 <div class="value">15234</div>처럼 변환되죠.
마지막으로, 테이블 부분의 $(awk ... | sort) 같은 명령어 치환이 실행되면서 동적으로 HTML 테이블 행들이 생성됩니다.
최종적으로 완전한 HTML 파일이 만들어지고, 브라우저로 열면 깔끔하게 정리된 리포트를 볼 수 있습니다. 여러분이 이 코드를 사용하면 매일 자동으로 로그 리포트를 생성하여 팀과 공유할 수 있습니다.
크론잡으로 매일 아침 6시에 실행하도록 설정하면, 출근하자마자 어제의 서비스 상태를 한눈에 파악할 수 있죠. CSS를 수정하면 회사 CI에 맞는 디자인으로 변경할 수 있고, Chart.js를 추가하면 시간대별 그래프도 그릴 수 있습니다.
실무에서는 이 HTML을 이메일로 자동 발송하거나, S3에 업로드하여 팀원들이 언제든 접속할 수 있게 만들기도 합니다.
실전 팁
💡 Chart.js를 추가하려면 <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>를 head에 넣고, JavaScript로 데이터를 차트로 렌더링하세요.
💡 here-document 안에서 $ 기호를 문자 그대로 사용하려면 \$로 이스케이프하세요. jQuery 같은 JavaScript 코드에서 주의해야 합니다.
💡 대용량 데이터는 테이블이 너무 길어지니, TOP 10이나 TOP 20으로 제한하세요. | head -20 같은 필터를 추가하면 됩니다.
💡 이메일로 발송하려면 mail -s "일일 로그 리포트" -a "$REPORT_FILE" team@example.com < /dev/null 같은 명령어를 사용하세요.
💡 보안을 위해 민감한 정보(IP 주소, API 키 등)는 마스킹하거나 제외하세요. sed 's/[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}/XXX.XXX.XXX.XXX/g'로 IP를 마스킹할 수 있습니다.
6. 일일 요약 리포트 스크립트
시작하며
여러분이 매일 아침 출근하면 가장 먼저 하는 일이 뭔가요? 많은 운영 담당자들은 어젯밤 서버에 문제가 없었는지, 이상한 패턴은 없었는지 확인합니다.
하지만 여러 로그 파일을 일일이 열어보는 것은 시간 낭비죠. 이런 문제는 실제 DevOps 업무에서 매일 반복됩니다.
로그 확인, 통계 계산, 리포트 작성을 수동으로 하면 30분 이상 걸리고, 실수로 중요한 이슈를 놓칠 수도 있습니다. 특히 주말이나 휴일에는 아무도 확인하지 않아서 문제가 방치되기도 하고요.
바로 이럴 때 필요한 것이 일일 요약 리포트 스크립트입니다. 모든 분석 과정을 자동화하고 크론잡으로 매일 실행하면, 여러분이 자고 있는 동안 로그를 분석하고 리포트를 생성하여 이메일로 보내줍니다.
문제가 발견되면 알림을 보내서 즉시 대응할 수도 있죠.
개요
간단히 말해서, 일일 요약 리포트 스크립트는 지금까지 배운 모든 분석 기법을 하나로 통합하여 자동으로 실행되는 완전한 리포트 시스템입니다. 왜 이 자동화가 필요한지 실무 관점에서 생각해볼까요?
사람이 매일 같은 작업을 반복하면 실수가 생기고 피로도가 쌓입니다. 하지만 스크립트는 절대 피곤해하지 않고, 정확하게 같은 작업을 매일 수행합니다.
예를 들어, 에러율이 5%를 넘으면 자동으로 경고 이메일을 보내고, 특정 IP에서 비정상적인 트래픽이 감지되면 Slack으로 알림을 보낼 수 있습니다. 기존에는 담당자가 출근해서 수동으로 확인했다면, 이제는 출근하자마자 받은편지함에 이미 분석된 리포트가 도착해 있고, 이상이 없으면 커피 한 잔 마시며 가볍게 확인만 하면 됩니다.
이 스크립트의 핵심 특징은 네 가지입니다. 첫째, 모든 분석 로직을 함수로 모듈화하여 유지보수가 쉽습니다.
둘째, 임계값(threshold)을 설정하여 이상 징후를 자동으로 탐지합니다. 셋째, 여러 출력 형식(HTML, JSON, 텍스트)을 지원하여 다양한 용도로 활용할 수 있습니다.
넷째, 크론잡과 통합하여 완전히 무인으로 작동합니다. 이러한 특징들이 안정적이고 지속 가능한 운영 자동화를 가능하게 합니다.
코드 예제
#!/bin/bash
# daily_report.sh - 일일 로그 분석 종합 리포트
set -euo pipefail # 에러 발생 시 즉시 종료
# 설정
LOG_FILE="/var/log/nginx/access.log"
REPORT_DIR="/var/reports"
DATE=$(date +%Y%m%d)
REPORT_FILE="$REPORT_DIR/daily_report_$DATE.html"
ALERT_EMAIL="devops@example.com"
ERROR_THRESHOLD=5 # 에러율 5% 이상이면 알림
# 로그 파일 존재 확인
if [[ ! -f "$LOG_FILE" ]]; then
echo "ERROR: 로그 파일을 찾을 수 없습니다: $LOG_FILE"
exit 1
fi
# 리포트 디렉토리 생성
mkdir -p "$REPORT_DIR"
# 통계 계산 함수들
calculate_stats() {
awk -v threshold="$ERROR_THRESHOLD" '
{
total++
status[$9]++
# 시간대별 집계
match($4, /[0-9]{2}:/)
hour = substr($4, RSTART, 2)
hourly[hour]++
# 에러 집계
if ($9 >= 400) {
errors++
error_urls[$7]++
}
# 응답 시간 (마지막 필드)
response_time += $NF
}
END {
error_rate = (errors / total) * 100
avg_response = response_time / total
# JSON 형식으로 출력
printf "{\"total\":%d,\"errors\":%d,\"error_rate\":%.2f,\"avg_response\":%.2f,",
total, errors, error_rate, avg_response
# 알림 필요 여부
alert = (error_rate > threshold) ? "true" : "false"
printf "\"alert\":%s,", alert
# 상태 코드별
printf "\"status\":{"
first=1
for (s in status) {
if (!first) printf ","
printf "\"%s\":%d", s, status[s]
first=0
}
printf "},"
# 시간대별
printf "\"hourly\":{"
first=1
for (h in hourly) {
if (!first) printf ","
printf "\"%s\":%d", h, hourly[h]
first=0
}
printf "},"
# TOP 5 에러 URL
printf "\"top_errors\":["
count=0
# 정렬을 위한 임시 저장
for (url in error_urls) {
sorted[error_urls[url]":"url] = 1
}
n = asorti(sorted, sorted_keys, "@ind_str_desc")
for (i=1; i<=n && i<=5; i++) {
split(sorted_keys[i], parts, ":")
if (i > 1) printf ","
printf "{\"url\":\"%s\",\"count\":%s}", parts[2], parts[1]
}
printf "]}"
}
' "$LOG_FILE"
}
# HTML 리포트 생성
generate_html_report() {
local stats="$1"
# JSON 파싱 (jq 사용)
local total=$(echo "$stats" | jq -r '.total')
local errors=$(echo "$stats" | jq -r '.errors')
local error_rate=$(echo "$stats" | jq -r '.error_rate')
local avg_response=$(echo "$stats" | jq -r '.avg_response')
local alert=$(echo "$stats" | jq -r '.alert')
# 알림 배너 색상
local alert_color="#4CAF50"
local alert_text="정상"
if [[ "$alert" == "true" ]]; then
alert_color="#f44336"
alert_text="주의 필요"
fi
cat > "$REPORT_FILE" << EOF
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>일일 리포트 - $(date +%Y-%m-%d)</title>
<style>
body { font-family: 'Segoe UI', sans-serif; margin: 0; padding: 20px; background: #f0f2f5; }
.container { max-width: 1200px; margin: 0 auto; }
.alert-banner { background: $alert_color; color: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; }
.alert-banner h2 { margin: 0; }
.metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-bottom: 20px; }
.metric-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.metric-card h3 { margin: 0 0 10px 0; color: #666; font-size: 14px; text-transform: uppercase; }
.metric-card .value { font-size: 36px; font-weight: bold; color: #333; }
.chart-container { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
</style>
</head>
<body>
<div class="container">
<div class="alert-banner">
<h2>📊 일일 로그 분석 리포트</h2>
<p>$(date +"%Y년 %m월 %d일") | 상태: $alert_text</p>
</div>
<div class="metrics">
<div class="metric-card">
<h3>총 요청 수</h3>
<div class="value">$(printf "%'d" $total)</div>
</div>
<div class="metric-card">
<h3>에러 발생</h3>
<div class="value" style="color: #f44336;">$(printf "%'d" $errors)</div>
</div>
<div class="metric-card">
<h3>에러율</h3>
<div class="value" style="color: #f44336;">${error_rate}%</div>
</div>
<div class="metric-card">
<h3>평균 응답시간</h3>
<div class="value" style="color: #2196F3;">${avg_response}ms</div>
</div>
</div>
<div class="chart-container">
<h3>상세 분석 결과</h3>
<p>전체 통계 데이터는 첨부된 JSON 파일을 참고하세요.</p>
</div>
</div>
</body>
</html>
EOF
echo "$REPORT_FILE"
}
# 메인 실행
echo "=== 일일 로그 분석 시작 ($(date)) ==="
# 통계 계산
STATS=$(calculate_stats)
# JSON 파일 저장
echo "$STATS" | jq '.' > "$REPORT_DIR/stats_$DATE.json"
# HTML 리포트 생성
generate_html_report "$STATS"
# 알림 필요 시 이메일 발송
ALERT=$(echo "$STATS" | jq -r '.alert')
if [[ "$ALERT" == "true" ]]; then
echo "⚠️ 경고: 에러율이 임계값을 초과했습니다!"
# mail -s "🚨 로그 분석 알림" -a "$REPORT_FILE" "$ALERT_EMAIL" <<< "에러율 임계값 초과. 리포트를 확인하세요."
fi
echo "✅ 리포트 생성 완료: $REPORT_FILE"
# 7일 이상 된 리포트 자동 삭제
find "$REPORT_DIR" -name "daily_report_*.html" -mtime +7 -delete
설명
이것이 하는 일은 하나의 스크립트로 전체 로그 분석 파이프라인을 구축하여, 사람의 개입 없이 매일 자동으로 서버 상태를 모니터링하는 것입니다. 첫 번째로, set -euo pipefail 같은 안전장치가 설정되어 에러 발생 시 스크립트가 즉시 중단되고, 파이프라인 어디서든 에러가 나면 감지됩니다.
왜 이게 중요하냐면, 자동화 스크립트는 무인으로 돌기 때문에 조용히 실패하면 안 되기 때문입니다. 로그 파일이 없거나 권한이 없으면 바로 알아야 조치할 수 있죠.
그 다음으로, calculate_stats 함수가 한 번의 awk 실행으로 모든 통계를 계산하고 JSON 형식으로 출력합니다. 여러 번 awk를 실행하면 큰 로그 파일을 여러 번 읽어야 해서 느리지만, 한 번에 모든 집계를 수행하면 훨씬 효율적입니다.
JSON으로 출력하는 이유는 jq 같은 도구로 파싱하기 쉽고, API로도 활용할 수 있기 때문입니다. 마지막으로, generate_html_report 함수가 JSON 데이터를 jq로 파싱하여 HTML에 삽입하고, $alert 값에 따라 동적으로 배너 색상을 변경합니다.
최종적으로 에러율이 임계값을 넘으면 이메일을 발송하고, 오래된 리포트는 자동으로 삭제하여 디스크 공간을 관리합니다. 여러분이 이 스크립트를 사용하면 완전히 자동화된 로그 모니터링 시스템을 구축할 수 있습니다.
크론탭에 0 6 * * * /path/to/daily_report.sh로 등록하면 매일 아침 6시에 실행되어, 출근하면 이미 분석된 리포트가 준비되어 있습니다. 스크립트를 확장하여 Slack 웹훅으로 알림을 보내거나, InfluxDB에 통계를 저장하여 Grafana로 시각화하거나, S3에 리포트를 업로드하여 팀 전체가 접근할 수 있게 만들 수도 있습니다.
실무에서는 이런 자동화가 운영 부담을 크게 줄여주고, 문제를 조기에 발견하게 해줍니다.
실전 팁
💡 크론잡 설정 시 PATH 환경변수를 명시하세요. PATH=/usr/local/bin:/usr/bin:/bin처럼 크론탭 상단에 추가하면 jq, mail 같은 명령어를 찾지 못하는 문제를 방지합니다.
💡 에러 발생 시 로그를 남기세요. exec 2>> /var/log/daily_report_error.log를 스크립트 상단에 추가하면 stderr가 파일로 저장되어 디버깅이 쉽습니다.
💡 임계값은 서비스 특성에 맞게 조정하세요. API 서버는 에러율 1% 이상이 심각할 수 있고, 웹사이트는 5%까지 허용할 수도 있습니다.
💡 스크립트 실행 시간을 기록하세요. time ./daily_report.sh로 측정하여 로그 파일이 커지면서 성능이 저하되는지 모니터링하세요.
💡 테스트용 크론을 먼저 만드세요. */5 * * * *로 5분마다 실행하면서 제대로 작동하는지 확인한 후, 실제 일일 스케줄로 변경하세요.