본문으로 건너뛰기

로그 분석 기초

웹 서버 로그는 트래픽 동향 파악, 장애 원인 분석, 보안 위협 탐지를 위한 핵심 데이터입니다. 이 챕터에서는 Nginx와 Apache의 로그 포맷을 이해하고, 리눅스 기본 명령어(awk, grep, sed, cut, sort, uniq)로 로그를 실전 수준으로 분석하는 방법을 다룹니다. JSON 로그 포맷 설정부터 실시간 모니터링까지 운영 현장에서 바로 활용할 수 있는 기법을 단계적으로 설명합니다.


Nginx 로그 포맷 이해

main 포맷 (기본값)

Nginx의 기본 로그 포맷은 nginx.conf에 정의된 main 포맷입니다.

# /etc/nginx/nginx.conf
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main;
}

실제 로그 한 줄 예시:

192.168.1.100 - john [28/Mar/2026:10:15:32 +0900] "GET /api/users HTTP/1.1" 200 1523 "https://example.com" "Mozilla/5.0 (Windows NT 10.0)" "-"

각 필드 설명:

필드값 예시설명
$remote_addr192.168.1.100클라이언트 IP
$remote_userjohnHTTP 인증 사용자 (없으면 -)
$time_local28/Mar/2026:10:15:32 +0900요청 시각 (로컬 타임존)
$requestGET /api/users HTTP/1.1HTTP 메서드, URI, 프로토콜
$status200HTTP 응답 상태 코드
$body_bytes_sent1523응답 바이트 수 (헤더 제외)
$http_refererhttps://example.comReferer 헤더
$http_user_agentMozilla/5.0 ...클라이언트 User-Agent
$http_x_forwarded_for-프록시를 통한 원본 IP

응답 시간 포함 포맷 설정

기본 포맷에는 응답 시간이 없습니다. 성능 분석을 위해 $request_time$upstream_response_time을 추가합니다.

# /etc/nginx/nginx.conf
http {
log_format timed '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" '
'rt=$request_time uct=$upstream_connect_time '
'uht=$upstream_header_time urt=$upstream_response_time';

access_log /var/log/nginx/access.log timed;
}
추가 필드설명
$request_time요청 수신부터 응답 완료까지 전체 시간(초, 소수점 3자리)
$upstream_connect_timeupstream 서버와 연결 수립 시간
$upstream_header_timeupstream에서 응답 헤더를 받기까지 시간
$upstream_response_timeupstream 전체 응답 시간

Apache 로그 포맷 이해

combined 포맷

Apache의 기본 포맷인 combined는 Nginx의 main과 거의 동일합니다.

# /etc/apache2/apache2.conf 또는 httpd.conf
LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined
CustomLog /var/log/apache2/access.log combined

실제 로그 한 줄 예시:

203.0.113.5 - - [28/Mar/2026:09:45:12 +0900] "POST /login HTTP/1.1" 302 0 "-" "curl/7.81.0"
필드 토큰설명
%h클라이언트 호스트 (IP)
%lRFC 1413 식별자 (보통 -)
%u인증된 사용자명
%t요청 시각
%r요청 라인 전체
%>s최종 응답 상태 코드
%O응답 바이트 수 (헤더 포함)
%{Referer}iReferer 헤더
%{User-Agent}iUser-Agent 헤더

awk/grep/sed 로그 파싱 실전

상태 코드 분포 확인

# HTTP 상태 코드별 요청 수 집계
awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn

출력 예시:

  15823 200
2341 304
876 404
123 500
45 302

TOP 10 접속 IP

# 가장 많이 요청한 IP 상위 10개
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10
# Apache combined 로그도 동일 (첫 번째 필드가 IP)
awk '{print $1}' /var/log/apache2/access.log | sort | uniq -c | sort -rn | head -10

TOP 10 요청 URL

# 가장 많이 요청된 URL 상위 10개
awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10
# 쿼리 스트링 제거 후 URL만 집계
awk '{print $7}' /var/log/nginx/access.log | cut -d'?' -f1 | sort | uniq -c | sort -rn | head -10

특정 시간대 요청 필터링

# 특정 날짜 요청만 추출
grep "28/Mar/2026" /var/log/nginx/access.log | awk '{print $9}' | sort | uniq -c

# 특정 시간대 (10시~11시) 요청 추출
awk '/28\/Mar\/2026:10:/' /var/log/nginx/access.log | wc -l

에러율 계산 및 4xx/5xx 집계

4xx 에러 집계

# 4xx 에러 전체 카운트
awk '$9 ~ /^4/' /var/log/nginx/access.log | wc -l

# 4xx 상태 코드별 세부 집계
awk '$9 ~ /^4/ {print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn

5xx 에러 및 에러율 계산

# 5xx 에러 전체 카운트
awk '$9 ~ /^5/' /var/log/nginx/access.log | wc -l

# 전체 요청 대비 에러율 계산 스크립트
#!/bin/bash
LOG="/var/log/nginx/access.log"
TOTAL=$(wc -l < "$LOG")
ERR5XX=$(awk '$9 ~ /^5/' "$LOG" | wc -l)
ERR4XX=$(awk '$9 ~ /^4/' "$LOG" | wc -l)

echo "Total requests : $TOTAL"
echo "4xx errors : $ERR4XX ($(echo "scale=2; $ERR4XX * 100 / $TOTAL" | bc)%)"
echo "5xx errors : $ERR5XX ($(echo "scale=2; $ERR5XX * 100 / $TOTAL" | bc)%)"

5xx 에러 발생 URL 상위 10개

awk '$9 ~ /^5/ {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10

응답 시간 분석

$request_time이 포함된 timed 포맷을 사용한다고 가정합니다. 로그 형식에서 rt= 접두어 뒤의 값이 응답 시간입니다.

평균/최대 응답 시간

# rt= 패턴에서 응답 시간 추출 후 평균 계산
awk '{
for(i=1; i<=NF; i++) {
if($i ~ /^rt=/) {
split($i, a, "=")
sum += a[2]
count++
}
}
}
END {
if(count > 0) printf "평균 응답 시간: %.3f초\n최대: %.3f초\n", sum/count, max
}' /var/log/nginx/access.log
# 더 간단한 방법: grep + awk
grep -o 'rt=[0-9.]*' /var/log/nginx/access.log | cut -d= -f2 | \
awk '{sum+=$1; if($1>max) max=$1; count++} END {printf "avg=%.3f max=%.3f count=%d\n", sum/count, max, count}'

응답 시간 1초 초과 요청 추출

# 1초 이상 걸린 느린 요청 추출
awk '{
for(i=1; i<=NF; i++) {
if($i ~ /^rt=/) {
split($i, a, "=")
if(a[2]+0 >= 1.0) print $0
}
}
}' /var/log/nginx/access.log | head -20

비정상 패턴 탐지 (스캐너, 브루트포스)

단일 IP의 404 폭발 탐지 (스캐너)

# 404를 대량으로 발생시키는 IP (스캐너 의심)
awk '$9 == "404" {print $1}' /var/log/nginx/access.log | \
sort | uniq -c | sort -rn | awk '$1 > 50 {print "SCANNER SUSPECT:", $2, "404 count:", $1}'

로그인 엔드포인트 브루트포스 탐지

# /login 또는 /api/auth에 POST 요청 과다 발생 IP
awk '$6 == "\"POST" && ($7 ~ /\/login/ || $7 ~ /\/api\/auth/) {print $1}' /var/log/nginx/access.log | \
sort | uniq -c | sort -rn | awk '$1 > 20 {print "BRUTE-FORCE SUSPECT:", $2, "attempts:", $1}'

비정상 User-Agent 탐지

# 알려진 스캐너/봇 User-Agent 탐지
grep -E '"(sqlmap|nikto|nmap|masscan|zgrab|python-requests|curl)' /var/log/nginx/access.log | \
awk '{print $1, $6, $7}' | sort | uniq -c | sort -rn | head -20

짧은 시간 내 대량 요청 탐지 (분당 요청)

# 분당 요청 수 상위 IP 추출 (같은 분에 50회 이상)
awk '{
# 시간 필드에서 분 단위 추출: [28/Mar/2026:10:15:
match($4, /\[([0-9\/A-Za-z:]+:[0-9]+:[0-9]+)/, arr)
key = $1 " " arr[1]
count[key]++
}
END {
for(k in count) if(count[k] >= 50) print count[k], k
}' /var/log/nginx/access.log | sort -rn | head -20

cut/sort/uniq/awk 원라이너 예제 모음

# 1. 요청 메서드 분포 (GET/POST/PUT/DELETE)
awk '{print $6}' /var/log/nginx/access.log | tr -d '"' | sort | uniq -c | sort -rn

# 2. Referer 도메인 TOP 10
awk '{print $11}' /var/log/nginx/access.log | grep -v '"-"' | \
cut -d/ -f3 | sort | uniq -c | sort -rn | head -10

# 3. 시간대별 요청 수 (시간 단위 히스토그램)
awk '{print $4}' /var/log/nginx/access.log | \
cut -d: -f2 | sort | uniq -c

# 4. 대역폭 TOP 10 URL (바이트 합계)
awk '{bytes[$7]+=$10} END {for(u in bytes) print bytes[u], u}' /var/log/nginx/access.log | \
sort -rn | head -10

# 5. 특정 날짜 특정 IP의 전체 요청 경로 추출
grep "28/Mar/2026" /var/log/nginx/access.log | grep "192.168.1.100" | awk '{print $7}'

# 6. HTTP 버전 분포
awk '{print $8}' /var/log/nginx/access.log | tr -d '"' | sort | uniq -c

# 7. 응답 크기 0 (빈 응답) 요청 탐지
awk '$10 == "0" || $10 == "-" {print $1, $7, $9}' /var/log/nginx/access.log | head -20

# 8. 분당 평균 요청 수 계산
TOTAL=$(wc -l < /var/log/nginx/access.log)
MINUTES=$(awk '{print $4}' /var/log/nginx/access.log | cut -d: -f1-3 | sort -u | wc -l)
echo "분당 평균 요청 수: $(echo "scale=1; $TOTAL / $MINUTES" | bc)"

JSON 로그 포맷 설정 및 jq 분석

Nginx JSON 로그 포맷 설정

# /etc/nginx/nginx.conf
http {
log_format json_combined escape=json
'{'
'"time":"$time_iso8601",'
'"remote_addr":"$remote_addr",'
'"method":"$request_method",'
'"uri":"$uri",'
'"args":"$args",'
'"status":$status,'
'"bytes_sent":$body_bytes_sent,'
'"request_time":$request_time,'
'"upstream_response_time":"$upstream_response_time",'
'"referer":"$http_referer",'
'"user_agent":"$http_user_agent",'
'"x_forwarded_for":"$http_x_forwarded_for"'
'}';

access_log /var/log/nginx/access.json json_combined;
}

설정 적용:

nginx -t && systemctl reload nginx

jq를 활용한 JSON 로그 분석

# jq 설치
apt-get install -y jq # Debian/Ubuntu
yum install -y jq # CentOS/RHEL

# 상태 코드 500인 요청 필터링
jq 'select(.status == 500) | {time, uri, remote_addr}' /var/log/nginx/access.json

# 응답 시간 상위 10개 요청
jq -s 'sort_by(.request_time) | reverse | .[0:10] | .[] | {uri, request_time, status}' \
/var/log/nginx/access.json

# 상태 코드별 집계
jq -s 'group_by(.status) | map({status: .[0].status, count: length}) | sort_by(.count) | reverse[]' \
/var/log/nginx/access.json

# 특정 IP의 모든 요청 추출
jq 'select(.remote_addr == "192.168.1.100") | {time, method, uri, status}' \
/var/log/nginx/access.json

# 평균 응답 시간 계산
jq -s '[.[].request_time] | add / length' /var/log/nginx/access.json

# 1초 이상 걸린 요청 URI 목록
jq 'select(.request_time >= 1.0) | .uri' /var/log/nginx/access.json | sort | uniq -c | sort -rn

실시간 로그 모니터링

tail -f 기본 사용

# 실시간 로그 스트림
tail -f /var/log/nginx/access.log

# 에러 로그 실시간 확인
tail -f /var/log/nginx/error.log

# access + error 동시 모니터링
tail -f /var/log/nginx/access.log /var/log/nginx/error.log

grep 필터링과 조합

# 5xx 에러만 실시간 확인
tail -f /var/log/nginx/access.log | grep ' 5[0-9][0-9] '

# 특정 IP 실시간 추적
tail -f /var/log/nginx/access.log | grep "203.0.113.5"

# 느린 요청 실시간 탐지 (응답 시간 2초 이상)
tail -f /var/log/nginx/access.log | awk '{
for(i=1;i<=NF;i++) if($i~/^rt=/) {split($i,a,"="); if(a[2]+0>=2.0) print $0}
}'

watch 명령어로 주기적 통계 갱신

# 10초마다 에러 카운트 갱신
watch -n 10 "awk '\$9~/^[45]/{print \$9}' /var/log/nginx/access.log | sort | uniq -c"

# 10초마다 TOP 5 IP 갱신
watch -n 10 "awk '{print \$1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -5"

로그 로테이션 후에도 실시간 추적

# --follow=name 옵션: 파일 이름으로 추적 (로테이션 후 새 파일 자동 추적)
tail --follow=name /var/log/nginx/access.log

# 오래된 로그까지 병합해서 실시간 보기
tail -f /var/log/nginx/access.log.1 /var/log/nginx/access.log

multitail로 멀티 창 모니터링 (선택 설치)

# 설치
apt-get install -y multitail

# Nginx access + error를 분할 창으로 모니터링
multitail /var/log/nginx/access.log /var/log/nginx/error.log

# 색상 필터 적용
multitail -cS nginx /var/log/nginx/access.log

고수 팁: 로그 분석 자동화 스크립트

운영 환경에서는 아래처럼 일일 리포트를 자동 생성하는 스크립트를 cron에 등록해 두면 유용합니다.

#!/bin/bash
# /usr/local/bin/daily-log-report.sh
# cron: 0 6 * * * /usr/local/bin/daily-log-report.sh >> /var/log/nginx/daily-report.log 2>&1

LOG="/var/log/nginx/access.log.1" # 전날 로테이션된 로그
DATE=$(date -d "yesterday" +%d/%b/%Y)
REPORT="/tmp/nginx-report-$(date +%Y%m%d).txt"

{
echo "===== Nginx Daily Report: $DATE ====="
echo ""
echo "[총 요청 수]"
grep "$DATE" "$LOG" | wc -l

echo ""
echo "[상태 코드 분포]"
grep "$DATE" "$LOG" | awk '{print $9}' | sort | uniq -c | sort -rn

echo ""
echo "[TOP 10 IP]"
grep "$DATE" "$LOG" | awk '{print $1}' | sort | uniq -c | sort -rn | head -10

echo ""
echo "[TOP 10 URL]"
grep "$DATE" "$LOG" | awk '{print $7}' | cut -d? -f1 | sort | uniq -c | sort -rn | head -10

echo ""
echo "[5xx 에러 URL]"
grep "$DATE" "$LOG" | awk '$9~/^5/{print $7}' | sort | uniq -c | sort -rn | head -10
} > "$REPORT"

cat "$REPORT"

이 스크립트를 cron에 등록하면 매일 아침 전날 트래픽 리포트를 자동으로 생성합니다. JSON 포맷으로 로그를 수집하는 환경이라면 jq를 활용해 더 정교한 분석이 가능합니다.