리눅스 명령어

grep으로 로그에서 원하는 줄만 뽑기, 실전 옵션 정리

grep으로 로그에서 원하는 줄만 뽑기, 실전 옵션 정리

사이트가 갑자기 느려졌다거나 특정 페이지에서 500 에러가 난다는 연락을 받으면 제일 먼저 하는 일이 로그를 보는 겁니다. 그런데 막상 열어보면 하루치만 해도 수만 줄입니다. 눈으로 훑어서 문제 줄을 찾는 건 사실상 불가능합니다. 이때 쓰는 게 grep입니다.

grep은 한 줄로 정의하면 파일에서 조건에 맞는 줄만 골라서 출력하는 도구입니다. 어렵게 생각할 게 없습니다. “이 단어가 들어간 줄만 보여줘”가 전부고, 나머지는 그 위에 붙는 옵션입니다. 옵션도 실제로 쓰는 건 예닐곱 개 정도라서, 한 번 익혀두면 서버 운영하는 내내 씁니다.

한눈에 · 로그 파일 위치 확인 → head/wc로 구조와 크기 파악 → grep 기본 옵션으로 걸러내기 → 파이프·정규식으로 조합 → 안 나올 때 원인 진단 → awk로 넘어가기

로그 파일을 열었는데 화면이 끝없이 흘러갈 때

웹서버 로그는 생각보다 빨리 커집니다. 방문자 한 명이 페이지 하나를 열어도 이미지·CSS·JS까지 요청 수가 수십 개고, 그게 전부 access_log에 한 줄씩 쌓입니다. 봇 크롤링까지 더해지면 하루 수십 MB는 흔합니다.

이런 파일을 cat으로 열면 터미널에 전부 쏟아집니다. 스크롤은 끝까지 올라가지도 않고, SSH 세션이 잠깐 멈춘 것처럼 보이기도 합니다. vi로 여는 것도 좋은 방법은 아닙니다. vi는 파일을 통째로 읽어들이려 하기 때문에, 수백 MB짜리 로그를 열면 메모리를 그만큼 잡아먹고 한참 멈춥니다.

grep은 파일을 한 줄씩 읽으면서 조건에 맞는 것만 출력하고 나머지는 버립니다. 그래서 파일이 아무리 커도 메모리를 크게 쓰지 않습니다. 로그 분석에서 grep을 먼저 배우라는 이유가 이겁니다.

시작 전에 알아둘 것: 어떤 로그가 어디에 있나

grep이 뭔지 잡았으니 이제 걸러낼 대상 파일부터 찾아야 합니다. 문제는 로그 경로가 배포판마다, 설치 방식마다 다르다는 점입니다. 구글에서 본 경로를 그대로 쳤는데 파일이 없다면 대개 이 이유입니다.

/var/log에서 볼 만한 파일들

일단 /var/log 아래를 훑어보는 것부터 시작합니다.

ls -lh /var/log
ls -lh /var/log/nginx
ls -lh /var/log/httpd

-lh는 파일 크기를 사람이 읽기 좋은 단위(K, M, G)로 보여줍니다. 어느 파일이 비정상적으로 커졌는지 여기서 바로 눈에 들어옵니다.

파일내용
/var/log/syslog 또는 /var/log/messages시스템 전반 로그 (데비안 계열 / RHEL 계열)
/var/log/auth.log 또는 /var/log/secure로그인·인증 기록
/var/log/nginx/access.log, error.log엔진엑스 접속·에러
/var/log/httpd/access_log, error_log아파치 접속·에러 (RHEL 계열 기본)

아파치와 엔진엑스는 설정에서 경로를 바꿀 수 있어서, 위 경로에 없으면 설정 파일을 직접 확인하는 게 빠릅니다.

grep -r 'error_log' /etc/nginx/
grep -r 'CustomLog\|ErrorLog' /etc/httpd/conf/

여기서 grep -r은 디렉터리 전체를 재귀적으로 뒤지는 옵션입니다. 편하긴 한데 / 같은 최상위에 걸면 시스템 전체를 훑느라 부하가 심하게 걸립니다. 범위를 좁혀서 쓰세요.

head와 wc -l로 파일 크기 감 잡기

파일을 찾았으면 곧바로 grep부터 치지 말고, 로그 한 줄이 어떻게 생겼는지 먼저 봅니다. 그래야 무슨 단어로 검색할지 정할 수 있습니다.

head -n 3 /var/log/nginx/access.log
wc -l /var/log/nginx/access.log

head -n 3은 앞 세 줄만, wc -l은 전체 줄 수를 알려줍니다. 이때 이런 게 뜰 수 있습니다.

grep: /var/log/secure: Permission denied

로그 파일 대부분은 root 소유고 일반 사용자가 읽을 수 없게 되어 있습니다. sudo를 붙이면 됩니다. 파일 권한 표기가 헷갈린다면 chmod 755와 644의 실제 차이, 왜 폴더는 755여야 하나를 먼저 보고 오면 이해가 빠릅니다.

sudo grep 'Failed password' /var/log/secure

가장 많이 쓰는 grep 옵션 6개

로그 위치를 찾았으니 이제 실제로 걸러낼 차례입니다. 기본형은 이렇게 생겼습니다.

grep '찾을문자열' 파일명

따옴표는 습관적으로 붙이세요. 공백이나 특수문자가 들어가면 따옴표 없이는 셸이 먼저 해석해버려서 엉뚱하게 동작합니다.

-i, -n: 일단 찾고 위치 확인

에러 로그에서 error라는 단어를 찾는다고 해봅시다. 그런데 실제 로그에는 Error, ERROR, error가 섞여 있습니다. 대소문자를 무시하려면 -i입니다.

grep -i 'error' /var/log/nginx/error.log

찾긴 찾았는데 그게 파일 몇 번째 줄인지 알아야 할 때가 있습니다. 나중에 vi로 열어 그 줄로 바로 이동하려면요. -n을 붙이면 줄 번호가 앞에 붙습니다.

grep -n -i 'error' /var/log/nginx/error.log

출력이 1523:2024/05/01 10:22:31 [error] ... 형태로 나옵니다. 앞의 숫자가 줄 번호입니다.

-v: 필요 없는 줄 걷어내기

로그를 보다 보면 찾는 것보다 빼는 게 더 급할 때가 많습니다. 헬스체크 요청, 모니터링 봇, 정적 파일 요청이 로그의 절반 이상을 차지하는 경우가 흔합니다. -v는 조건에 맞는 줄을 제외합니다.

grep -v 'healthcheck' access.log

개수만 알고 싶을 땐 -c입니다. 줄 내용은 안 나오고 몇 줄인지 숫자만 나옵니다.

grep -c 'POST' access.log

-w는 단어 단위로 정확히 맞는 것만 찾습니다. grep 'log'는 login, catalog도 걸리지만 grep -w 'log'는 독립된 log만 잡습니다.

-A -B -C: 에러 앞뒤 상황 보기

에러 로그에서 제일 유용한 옵션입니다. PHP Fatal error 같은 건 그 줄만 봐서는 원인을 모릅니다. 바로 앞뒤 줄에 스택 트레이스나 요청 정보가 들어 있습니다.

grep -n -C 3 'Fatal error' error_log

-C 3은 매칭된 줄의 앞뒤 3줄씩 같이 보여줍니다. 앞쪽만 보고 싶으면 -B 2(Before), 뒤쪽만 보고 싶으면 -A 2(After)입니다.

grep 기본 옵션 맛보기
grep 기본 옵션 맛보기

옵션이 여러 개라 헷갈리니 표로 정리합니다.

옵션하는 일언제 쓰나
-i대소문자 무시Error/ERROR가 섞여 있을 때
-v조건에 맞는 줄 제외헬스체크·봇 요청 걷어낼 때
-n줄 번호 출력나중에 에디터로 그 줄 찾아갈 때
-c매칭 줄 개수만 출력“몇 건이나 났나” 셀 때
-w단어 단위 일치짧은 단어가 다른 단어에 묻힐 때
-A -B -C뒤 / 앞 / 양쪽 문맥 함께 출력에러 전후 상황 볼 때
-E확장 정규식 사용 (OR 조건 등)`404\500` 같이 여러 조건
-F패턴을 문자 그대로 취급[, ., * 가 들어간 문자열 찾을 때
-r디렉터리 재귀 검색어느 파일에 있는지 모를 때

실전 조합: 에러만, 특정 IP만, 특정 시간대만

옵션을 하나씩 봤으니 이제 실제 상황별로 묶어 씁니다. 실무에서 grep을 한 번만 쓰는 경우는 거의 없습니다. 파이프(|)로 앞 명령의 결과를 다음 grep에 넘겨 계속 좁혀 나갑니다.

에러 코드만 골라내기

접속 로그에서 500 에러만 뽑는다고 해봅시다. 그런데 헬스체크 URL이 계속 500을 뱉고 있어서 그건 빼고 싶습니다.

grep -v 'healthcheck' access.log | grep ' 500 '

' 500 '처럼 앞뒤에 공백을 넣은 이유가 있습니다. 공백 없이 500만 찾으면 응답 크기 50012나 URL의 /product/500 같은 것까지 다 걸립니다. 로그에서 숫자를 찾을 땐 앞뒤 공백을 같이 넣는 습관을 들이세요.

404와 500을 한 번에 보고 싶다면 OR 조건이 필요합니다. -E를 붙이면 |가 “또는”으로 동작합니다.

grep -E '404|500' access.log

-E 없이 쓰려면 \|처럼 역슬래시를 붙여야 해서 번거롭습니다. 그냥 -E를 쓰는 게 편합니다.

IP·경로별로 추리기

특정 IP가 이상하게 요청을 많이 보낸다는 의심이 들 때는 그 IP만 뽑아봅니다.

grep '203.0.113.45' access.log | grep -v '.css\|.js\|.png'

정적 파일 요청을 빼면 실제로 어떤 페이지를 훑고 갔는지가 드러납니다.

시간대를 자르는 것도 자주 씁니다. 아파치·엔진엑스 기본 로그는 [01/May/2024:14:23:11 +0900] 형식이라 문자열로 잘라낼 수 있습니다.

grep '01/May/2024:14:' access.log
grep -E '01/May/2024:1[4-5]:' access.log

두 번째는 14시대와 15시대를 한꺼번에 보는 겁니다. [4-5]는 4 또는 5 한 글자를 뜻합니다. 정규식은 이 정도만 알아도 로그에선 대부분 됩니다.

tail -f와 묶어 실시간 보기

지금 이 순간 들어오는 요청을 보고 싶을 때가 있습니다. 재현 테스트를 하면서 로그를 실시간으로 지켜보는 상황이죠. tail -f는 파일 끝에 새로 붙는 줄을 계속 출력합니다. 여기에 grep을 물리면 원하는 것만 흘러갑니다.

tail -f access.log | grep --line-buffered '192.168.'

--line-buffered가 핵심입니다. 이게 없으면 grep이 출력을 일정량 모아뒀다가 한꺼번에 내보내기 때문에, 화면에 뜨는 게 몇 초씩 늦거나 아예 안 보이는 것처럼 느껴집니다. 실시간 감시할 땐 꼭 붙이세요.

원격 서버에 붙어서 이런 작업을 하다 보면 세션이 끊겨서 짜증나는 경우가 있는데, 그건 SSH 접속이 자꾸 끊길 때, timeout 설정으로 잡는 법 쪽을 참고하세요.

로그를 좁혀가는 순서
로그를 좁혀가는 순서

여기서 자주 막힙니다

명령은 맞게 친 것 같은데 결과가 안 나오는 경우, 원인은 대체로 정해져 있습니다.

아무것도 안 나올 때는 순서대로 확인합니다. 첫째, 대소문자. -i를 붙여서 다시 쳐봅니다. 둘째, 검색어에 공백이 들어갔는지. 셋째, 파일 경로가 맞는지. 넷째, 그 파일에 진짜 그 단어가 있는지.

grep은 결과가 없으면 종료 코드 1을 반환합니다. 명령이 실패한 건지 결과가 없는 건지 구분하려면 바로 다음에 이걸 쳐봅니다.

grep 'nonexist' access.log
echo $?

1이 나오면 정상 동작했는데 매칭이 없는 것이고, 2가 나오면 파일을 못 읽었거나 문법이 틀린 겁니다. 0이면 찾은 게 있다는 뜻입니다.

파일 자체를 못 찾을 때는 이렇게 뜹니다.

grep: 파일명: No such file or directory

경로 오타이거나, 상대 경로로 쳤는데 현재 디렉터리가 다른 경우입니다. pwd로 지금 위치를 확인하고 절대 경로로 다시 쳐보세요.

특수문자가 정규식으로 해석될 때

., *, [, ? 같은 문자는 grep이 정규식 기호로 해석합니다. IP 주소 192.168.0.1을 찾을 때 .은 “아무 문자 하나”라서 192a168b0c1도 걸립니다. 실무에선 큰 문제가 안 되는 경우가 많지만, 정확히 그 문자열만 찾아야 한다면 -F를 씁니다.

grep -F '192.168.0.1' access.log
grep -F 'array[0]' error_log

-F는 패턴을 정규식이 아니라 그냥 글자 그대로 취급합니다.

Broken pipe와 압축 로그

grep 'GET' access.log | head 같은 걸 돌리면 가끔 이런 게 뜹니다.

grep: write error: Broken pipe

head가 열 줄만 받고 종료했는데 grep은 아직 출력할 게 남아서 생기는 현상입니다. 결과 자체는 정상이니 무시해도 됩니다. 신경 쓰이면 head -n 10 대신 grep에 -m 10(최대 10건만 매칭)을 주면 됩니다.

마지막으로, logrotate가 돌면서 지난 로그는 .gz로 압축됩니다. 압축 파일에는 grep이 안 통합니다. 이럴 땐 zgrep을 쓰세요.

zgrep 'error' error_log-20240101.gz

zgrep은 grep과 옵션이 거의 같습니다. -i, -n, -C도 그대로 됩니다.

다음 단계: grep으로 부족해질 때

여기까지 익히면 로그에서 “찾는” 일은 대부분 해결됩니다. 그런데 곧 “세는” 일이 필요해집니다. 어떤 IP가 제일 많이 들어왔는지, 어떤 URL이 404를 제일 많이 냈는지 같은 질문이요.

grep은 줄을 고를 뿐 열 단위로 쪼개지는 못합니다. 그건 awk의 영역입니다. 접속 로그는 첫 번째 칸이 IP라서, 이렇게 하면 IP별 접속 횟수 순위가 나옵니다.

awk '{print $1}' access.log | sort | uniq -c | sort -rn | head

awk '{print $1}'으로 첫 칸만 뽑고, sort로 정렬한 뒤 uniq -c로 중복을 세고, sort -rn으로 숫자 역순 정렬합니다. 이 조합 하나가 로그 통계의 90%를 담당합니다. 문자열을 바꿔치기해야 한다면 sed를 보면 됩니다.

로그 파일이 계속 커져서 디스크를 먹는다면 logrotate 설정(/etc/logrotate.d/)을 함께 점검하세요. 그리고 자주 쓰는 명령은 매번 치지 말고 ~/.bashrc에 alias로 넣어두면 편합니다.

alias errlog="sudo grep -n -i -C 2 'error' /var/log/nginx/error.log | tail -n 50"

다음에 할 일 체크리스트

  • [ ] ls -lh /var/log로 내 서버의 실제 로그 경로를 확인해 메모해 둔다
  • [ ] head -n 3으로 access 로그 한 줄의 칸 구조를 눈으로 익힌다
  • [ ] grep -c 로 오늘 500 에러가 몇 건인지 세어본다
  • [ ] grep -n -C 3으로 에러 한 건의 앞뒤 문맥을 실제로 열어본다
  • [ ] tail -f | grep --line-buffered를 띄워놓고 브라우저에서 페이지를 열어본다
  • [ ] .gz 로그가 쌓여 있는지 보고 zgrep으로 지난주 로그를 검색해 본다
  • [ ] awk ... | sort | uniq -c | sort -rn | head로 상위 접속 IP를 뽑아본다
  • [ ] 자주 쓰게 된 명령 두세 개를 alias로 등록한다

댓글 1개

댓글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다