kubectl logs 는 로그를 따로 보관하지 않는다. API 서버를 거쳐 해당 파드가 떠 있는 노드의 kubelet 에 요청하고, kubelet 이 컨테이너 런타임이 남긴 노드 로컬 파일을 읽어 그대로 흘려 보낸다. 명령의 출력 자체는 어디에도 남지 않으므로, 보관하려면 받는 쪽에서 리다이렉트해야 한다.
kubectl -n <namespace> logs <pod> > pod.log
따라서 파드가 지워지면 로그도 같이 사라지고, 노드가 빠지면 그 노드에 있던 로그를 더는 볼 수 없다. 오래 남겨야 하는 로그는 반드시 수집기로 밖에 빼 둔다.
CRI 런타임(containerd, CRI-O)을 쓰는 클러스터에서는 kubelet 이 정해진 위치에 로그를 둔다.
/var/log/pods/<namespace>_<pod-name>_<pod-uid>/<container-name>/0.log
/var/log/containers/ 아래에는 이 파일을 가리키는 심볼릭 링크가 평평하게 놓인다. 로그 수집기는 보통 이쪽을 감시한다.
ls -l /var/log/containers/ | head
Docker 를 런타임으로 쓰던 클러스터라면 원본은 다른 자리에 있다.
/var/lib/docker/containers/<container-id>/<container-id>-json.log
컨테이너 ID 는 파드 상태에서 얻는다.
kubectl -n <namespace> get pod <pod> -o jsonpath='{.status.containerStatuses[*].containerID}'
CRI 로그는 한 줄이 네 부분으로 되어 있다.
2026-09-20T01:30:42.031456789+09:00 stdout F 애플리케이션이 출력한 원래 한 줄
앞에서부터 RFC3339Nano 시각, 스트림 이름(stdout · stderr), 태그(F 는 한 줄 완결, P 는 뒤에 이어짐), 그리고 원문이다. 즉 런타임이 줄마다 수신 시각을 이미 기록해 두고 있다. kubectl logs 는 기본적으로 원문만 보여 주기 때문에 시각이 없어 보일 뿐이다.
애플리케이션이 시각을 찍지 않거나 포맷이 깨져 모든 줄이 같은 값으로 보이는 경우, 애플리케이션을 고치기 전에도 런타임이 기록한 시각을 꺼내 볼 수 있다.
kubectl -n <namespace> logs <pod> --timestamps
시간 범위로 잘라 보는 것도 된다.
kubectl -n <namespace> logs <pod> --since=30m
kubectl -n <namespace> logs <pod> --since-time=2026-09-20T01:00:00Z
kubectl -n <namespace> logs <pod> --tail=200
kubectl -n <namespace> logs <pod> --previous # 재시작 직전 컨테이너
--timestamps 가 붙이는 시각은 런타임이 그 줄을 받은 시각이지 애플리케이션이 이벤트를 처리한 시각이 아니다. 버퍼링이 걸린 출력이면 실제 발생 시점보다 늦게 찍힌다. 파이썬처럼 stdout 이 블록 버퍼링되는 런타임은 컨테이너에서 버퍼링을 꺼 두는 편이 낫다.
env:
- name: PYTHONUNBUFFERED
value: "1"
컨테이너와 노드의 시간대가 다르면 같은 사건이 다른 시각으로 보인다. 컨테이너 안은 대개 UTC 다. 애플리케이션 로그는 UTC 로 통일하고 보는 쪽에서 변환하는 편이 헷갈리지 않는다.
로그 전체가 JSON 이 아니면 jq 는 첫 줄에서 바로 죽는다.
parse error: Invalid numeric literal at line 1, column 11
kubectl logs 출력은 JSON 배열이 아니라 한 줄에 JSON 객체 하나씩(JSON Lines) 이고, 그나마도 기동 로그 같은 평문이 섞이는 경우가 많다. 줄 단위로 읽게 하고 파싱 실패를 무시한다.
kubectl -n <namespace> logs <pod> \
| jq -R 'fromjson? | select(.) | [.timestamp, .message] | @tsv' -r
--timestamps 를 같이 쓰면 앞에 붙은 시각 때문에 줄이 JSON 이 아니게 된다. 둘을 같이 쓸 때는 앞부분을 먼저 떼어 낸다.
kubectl -n <namespace> logs <pod> --timestamps \
| sed 's/^[^ ]* //' \
| jq -R 'fromjson? | select(.)'
로그가 무한히 쌓이지는 않는다. CRI 런타임에서는 kubelet 이 회전을 맡고, 기본값은 파일 하나 10Mi, 컨테이너당 5개다.
# kubelet config
containerLogMaxSize: 10Mi
containerLogMaxFiles: 5
즉 컨테이너 하나가 남기는 로그는 기본적으로 50Mi 를 넘지 않고, 그 이상은 오래된 것부터 사라진다. kubectl logs 로 과거 로그가 안 보이는 흔한 이유가 이것이다. 값을 키우면 노드 디스크를 그만큼 더 먹으므로, 길게 보관해야 한다면 값을 올리기보다 수집기로 빼는 쪽이 맞다.
Docker 런타임이면 회전은 로그 드라이버가 한다.
{
"log-driver": "json-file",
"log-opts": { "max-size": "10m", "max-file": "3" }
}