[Python] logging: 로거, 핸들러, 포매터
정의
logging은 Python 표준 로깅 프레임워크. logger - handler - formatter - filter 4구성 요소로 유연한 로그 라우팅을 구현. 단순 사용은 한 줄로, 복잡한 구성은 dict/yaml로.
빠른 시작
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger.debug("debug")
logger.info("info")
logger.warning("warning")
logger.error("error", extra={"user_id": 1})
logger.critical("critical")
basicConfig는 한 번만 동작 (이미 핸들러 있으면 무시). 라이브러리에선 호출 금지: 사용자의 logging 설정을 침범한다.
레벨
| 레벨 | 숫자 | 용도 |
|---|---|---|
| DEBUG | 10 | 디버그 정보 |
| INFO | 20 | 일반 정보 |
| WARNING | 30 | 경고 (기본 임계값) |
| ERROR | 40 | 에러 |
| CRITICAL | 50 | 치명적 |
logger와 handler 각자 레벨을 가진다. 둘 다 통과해야 출력.
logger 계층
logger 이름은 .으로 계층 표현. 부모-자식 관계.
log = logging.getLogger("myapp")
log_api = logging.getLogger("myapp.api")
log_db = logging.getLogger("myapp.db")
핸들러는 일반적으로 루트 logger 또는 앱 최상위 logger에 부착. 자식 logger의 메시지가 propagate되어 부모 핸들러로 흐름.
log_api.info("...") # myapp.api → myapp → root 로 전파
logging.getLogger(__name__) 패턴이 표준: 모듈 경로 그대로 logger 이름이 됨.
핸들러 설정
import logging
import sys
logger = logging.getLogger("myapp")
logger.setLevel(logging.DEBUG)
# 콘솔 핸들러
console = logging.StreamHandler(sys.stdout)
console.setLevel(logging.INFO)
# 파일 핸들러
file = logging.FileHandler("app.log")
file.setLevel(logging.DEBUG)
# 포매터
fmt = logging.Formatter(
"%(asctime)s [%(levelname)s] %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
console.setFormatter(fmt)
file.setFormatter(fmt)
logger.addHandler(console)
logger.addHandler(file)
logger.info("hello")
INFO 이상은 콘솔, DEBUG 이상은 파일에 기록.
포매터 변수
| 변수 | 의미 |
|---|---|
%(asctime)s | 타임스탬프 |
%(name)s | logger 이름 |
%(levelname)s | 레벨 이름 |
%(message)s | 메시지 |
%(funcName)s | 호출 함수 |
%(filename)s | 파일 |
%(lineno)d | 줄번호 |
%(pathname)s | 절대 경로 |
%(process)d | PID |
%(thread)d | 스레드 ID |
%(threadName)s | 스레드 이름 |
dictConfig: 선언적 설정
import logging.config
config = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
},
"json": {
"()": "pythonjsonlogger.json.JsonFormatter",
"format": "%(asctime)s %(levelname)s %(name)s %(message)s",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "default",
"level": "INFO",
},
"file": {
"class": "logging.handlers.RotatingFileHandler",
"filename": "app.log",
"formatter": "json",
"maxBytes": 10485760,
"backupCount": 5,
"level": "DEBUG",
},
},
"loggers": {
"myapp": {
"handlers": ["console", "file"],
"level": "DEBUG",
"propagate": False,
},
},
}
logging.config.dictConfig(config)
YAML/JSON 파일에 두고 dictConfig(yaml.safe_load(open("logging.yaml")))처럼 로드 가능. Django, Flask 같은 프레임워크도 이 방식.
자주 쓰이는 핸들러
| 핸들러 | 용도 |
|---|---|
StreamHandler | stdout/stderr |
FileHandler | 단일 파일 |
RotatingFileHandler | 크기 기준 로테이션 |
TimedRotatingFileHandler | 시간 기준 (매일 등) |
SysLogHandler | syslog |
SMTPHandler | 이메일 알림 (스팸 위험) |
HTTPHandler | HTTP POST |
QueueHandler + QueueListener | 멀티스레드/프로세스 안전 |
NullHandler | 라이브러리 기본 (소비자가 설정 안 했을 때 경고 방지) |
라이브러리 로깅 가이드
라이브러리 코드는:
logger = logging.getLogger(__name__)모듈마다- 핸들러 부착 금지 (사용자 책임)
logging.NullHandler추가 (사용자 미설정 시 경고 방지)
# library_init.py
import logging
logging.getLogger(__name__).addHandler(logging.NullHandler())
예외 로깅
try:
risky()
except Exception:
logger.exception("something failed")
# 자동으로 traceback 포함, level=ERROR
logger.exception(msg) = logger.error(msg, exc_info=True).
구조화 로깅
표준 logger는 문자열 위주. 구조화(JSON) 로그는:
pip install python-json-logger
또는 structlog, loguru 같은 라이브러리 사용. ELK/Datadog/CloudWatch 같은 로그 시스템과 잘 통합.
logger.info("user_login", extra={"user_id": 42, "ip": "1.2.3.4"})
extra로 임의 필드 추가. 포매터가 JSON이면 그대로 직렬화.
성능
- 로그 호출은 디스에이블 레벨도 함수 호출 비용 있음. hot loop에선:
if logger.isEnabledFor(logging.DEBUG):
logger.debug("expensive: %s", expensive_calc())
- f-string 대신
%s포매팅 권장 (debug 비활성화 시 포매팅 안 함)
logger.debug("user %s logged in", user.id) # OK
logger.debug(f"user {user.id} logged in") # 항상 포매팅됨
- 매우 hot한 경로:
QueueHandler로 비동기 처리
print vs logging
| logging | ||
|---|---|---|
| 레벨 | X | O |
| 라우팅 | stdout 고정 | 다중 destination |
| 포맷 | 수동 | 자동 |
| 비활성화 | 코드 수정 | 설정 변경 |
| 시간/소스 정보 | 수동 | 자동 |
스크립트 일회성은 print, 프로덕션은 항상 logging.
함정
1. logging.info()와 root logger
import logging
logging.info("hello") # root logger 사용 + 기본 핸들러 없음 = 출력 안 됨
basicConfig() 호출 또는 명시적 logger 사용 필수.
2. 모듈 import 시 핸들러 부착
# myapp/foo.py
import logging
logger = logging.getLogger(__name__)
logger.addHandler(logging.StreamHandler()) # 다시 import되면 중복 추가
핸들러 부착은 앱 시작 한 번만. 모듈 import time 부착 금지.
3. 사용자 입력을 로그 메시지로
logger.info(user_input) # 로그 인젝션 가능
신뢰할 수 없는 입력은 escape하거나 분리된 필드(extra)로.
대안
- loguru: 더 친근한 API, JSON 출력, 자동 컨텍스트. 단일 라이브러리 사용.
- structlog: 구조화 로그 우선. 컨텍스트 바인딩 강력.
- rich.logging: 컬러풀한 콘솔 로그.
표준 logging이 라이브러리 인터페이스라 다른 패키지와 호환성이 가장 좋음.
💬 댓글