python 환경 기준으로 설명.
Standard stream
POSIX 호환 운영체제(Linux, Unix, macOS 등)는 새로운 프로세스를 생성할 때 운영체제 차원에서 기본적으로 3개의 데이터 통로(File Descriptor, FD)를 자동으로 열어준다.
- Standard Input (stdin, FD 0): 프로그램으로 들어오는 기본 입력 채널 (주로 키보드)
- Standard Output (stdout, FD 1): 정상적인 실행 결과를 내보내는 기본 채널 (주로 터미널 화면)
- Standard Error (stderr, FD 2): 에러 및 진단 메시지를 내보내는 채널 (주로 터미널 화면)
즉, ‘Standard’란 개발자가 별도로 목적지(파일 경로, 네트워크 주소 등)를 명시하지 않아도 OS가 ‘기본적으로 연결해 주는 디폴트 통로’를 의미한다.
반면 standard 가 아닌 출력/에러는, 프로세스가 OS가 쥐여준 기본 터미널(stdout/stderr)을 사용하지 않고, 목적지를 직접 명시하여 데이터를 내보내는 모든 행위가 해당한다. 이를 ‘non-stdout’ 같은 고유 명사로 부르지는 않으며, 파일 입출력(File I/O)이나 소켓 스트림(Socket Stream) 등으로 지칭한다.
- 디스크 파일 출력 (File I/O): 데이터를 화면에 출력하지 않고
open('/var/log/app_error.log', 'w')를 호출해 물리적 디스크에 에러를 직접 기록하는 방식. - 네트워크 스트림 (Network Socket): 데이터를 원격 로그 수집 서버(예: Logstash, Datadog)의 특정 IP와 포트로 직접 전송하는 방식.
- 파이프 (Pipe) 및 IPC: 한 프로세스의 결과를 화면에 띄우는 대신, 다른 프로세스의 입력으로 직접 밀어넣는 방식.
Python의 print() 함수는 오직 stdout 이라는 단일 목적지로만 데이터를 보낸다.
이때, Docker는 기본적으로 컨테이너 내부의 프로세스가 생성하는 stdout과 stderr를 수집하여 데몬 로그(예: json-file 로깅 드라이버)로 기록한다. 컨테이너의 1번(stdout)과 2번(stderr) 파이프를 감시하다가 데이터를 수집해 로그로 보여주므로, 별도의 추가 설정을 한 것이 아니라면 Python의 print() 함수 출력물도 원칙적으로는 docker logs 명령어를 통해 확인할 수 있다.
반면, 커스텀 모듈에서 logging 모듈을 사용해 FileHandler를 부착하면, 에러나 결과 데이터를 stdout/stderr(표준 스트림)로 보내지 않고 지정된 파일에 직접 쓴다. 이 경우 Docker의 표준 로그 수집기(docker logs)에는 해당 내용이 찍히지 않게 된다.
docker 로그에 print가 누락됨
print()의 결과가 docker 로그에 즉시 나타나지 않거나 누락되는 현상이 빈번하게 발생한다. 이는 Python의 표준 출력 버퍼링(Standard Output Buffering) 정책 때문.
python은 대화형(interactive) 터미널에 직접 연결되어 있지 않을 경우, 성능 최적화를 위해 표준 출력을 메모리 버퍼에 임시 저장하는 방식을 기본으로 사용한다. 이로 인해 컨테이너 구동 중에 출력된 내용이 지연되어 보이거나, 시스템 오류로 컨테이너가 예기치 않게 종료될 경우 버퍼에 쌓여있던 로그가 증발해버리는 문제가 발생한다.
- Docker 컨테이너 내부와 같은 non-interactive 환경도 이에 해당함.
- 해당 모드에서 실제 출력 스트림으로 데이터를 내보내는 (Flush) 조건은 아래와 같다.
- 버퍼가 꽉 차거나
- 스크립트 프로세스가 정상 종료
버퍼링을 우회하고 print() 출력을 즉시 Docker 로그 스트림으로 전달하려면 다음 방법들이 있음.
# Python 프로세스의 stdout 및 stderr 버퍼링을 전역적으로 해제
ENV PYTHONUNBUFFERED=1
# 컨테이너의 시작 명령어(CMD 또는 ENTRYPOINT)에서 Python 인터프리터를 호출할 때 -u 플래그를 추가
CMD ["python", "-u", "main.py"]
ENTRYPOINT ["python", "-u", "main.py"]
# 코드 전체의 동작을 변경하지 않고 특정 print() 함수만 즉시 출력, 인자값으로 제어
print("이 메시지는 즉시 로그에 기록됨", flush=True)내장 logger
파이썬에서 ‘로거 심기’는 보통 표준 logging 설정을 한 번 해두고, 각 파일에서 logger = logging.getLogger(name)로 가져다 쓰는 패턴이 제일 깔끔하다.
basicConfig는 처음 한 번만 먹는다. 이미 핸들러가 붙어 있으면 적용되지 않을 수 있으므로 주의.
라이브러리/모듈 파일에서는 설정하지 말고 getLogger(__name__)만 쓰기.
- basicConfig/handlers 를 설정하지 말라는 말.
- 설정은 엔트리포인트(메인)에서만.
예외 로그는 아래의 둘 중 하나를 추천.
- logger.error(…, exc_info=True)
- logger.exception(…)
파일 회전은 크기/날짜 중 하나를 선택.
- 크기: RotatingFileHandler
- 날짜: TimedRotatingFileHandler
주요 로깅 핸들러 종류
로거(logger) 객체에 어떤 핸들러(Handler)를 부착하느냐에 따라 로그의 최종 목적지가 결정된다.
| 핸들러 | 출력 대상 | 주요 특징 및 용도 |
|---|---|---|
StreamHandler | stdout / stderr | Docker의 docker logs로 수집 가능, Python 기본 설정(stderr) |
FileHandler | 지정된 로컬 파일 | OS 표준 스트림을 거치지 않음(File I/O), Docker 데몬 캡처 불가 |
RotatingFileHandlerTimedRotatingFileHandler | 로컬 파일 (크기/시간 분할) | 조건 충족 시 이전 로그 백업 및 새 파일 생성(Rotation), 서버 디스크 고갈 방지 |
SocketHandlerDatagramHandler | 네트워크 소켓 (TCP/UDP) | 원격 로그 수집 서버(Logstash, Fluentd 등)로 로그 데이터 직접 전송 |
SMTPHandler | 이메일 | ERROR/CRITICAL 수준의 치명적 예외 발생 시 에러 내용 즉시 발송 |
HTTPHandler | 웹 서버 (HTTP 엔드포인트) | GET/POST 요청을 통해 로그 페이로드 전송 |