Loading...
Loading...
02-reusable-code-python/structlog_utils/structlog_setup.py
"""
structlog 설정 - 구조화된 로깅 + ContextVars + JSON 포매터
@source: GitHub-커뮤니티
@extracted: 2026-02-16
@version: 1.0.0
의존성:
- structlog (필수, pip install structlog)
사용법:
from structlog_utils.structlog_setup import setup_logging, get_logger
setup_logging(level="INFO", json_format=True)
logger = get_logger("my_module")
logger.info("서버 시작", port=8000)
"""
import logging
import sys
from contextvars import ContextVar
from typing import Any
try:
import structlog
HAS_STRUCTLOG = True
except ImportError:
HAS_STRUCTLOG = False
# 요청별 컨텍스트 (request_id, user_id 등)
_request_context: ContextVar[dict[str, Any]] = ContextVar(
"request_context", default={}
)
# ============================================
# 컨텍스트 관리
# ============================================
def bind_context(**kwargs: Any) -> None:
"""현재 컨텍스트에 값 바인딩
Args:
**kwargs: 바인딩할 키-값 쌍
Example:
bind_context(request_id="abc-123", user_id="user-1")
logger.info("요청 처리") # 자동으로 request_id, user_id 포함
"""
ctx = _request_context.get().copy()
ctx.update(kwargs)
_request_context.set(ctx)
def clear_context() -> None:
"""현재 컨텍스트 초기화"""
_request_context.set({})
def get_context() -> dict[str, Any]:
"""현재 컨텍스트 조회"""
return _request_context.get().copy()
# ============================================
# 로깅 설정
# ============================================
def setup_logging(
level: str = "INFO",
json_format: bool = False,
) -> None:
"""로깅 초기 설정
Args:
level: 로그 레벨 (DEBUG, INFO, WARNING, ERROR)
json_format: True면 JSON 포맷, False면 컬러 콘솔 포맷
Example:
# 개발 환경 (컬러 콘솔)
setup_logging(level="DEBUG", json_format=False)
# 프로덕션 (JSON)
setup_logging(level="INFO", json_format=True)
"""
if HAS_STRUCTLOG:
_setup_structlog(level, json_format)
else:
_setup_stdlib(level)
def _setup_structlog(level: str, json_format: bool) -> None:
"""structlog 기반 설정"""
# 공통 프로세서
shared_processors: list[Any] = [
structlog.contextvars.merge_contextvars,
structlog.stdlib.add_log_level,
structlog.stdlib.add_logger_name,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.StackInfoRenderer(),
structlog.processors.UnicodeDecoder(),
]
if json_format:
renderer = structlog.processors.JSONRenderer(ensure_ascii=False)
else:
renderer = structlog.dev.ConsoleRenderer(colors=True)
structlog.configure(
processors=[
*shared_processors,
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
],
logger_factory=structlog.stdlib.LoggerFactory(),
wrapper_class=structlog.stdlib.BoundLogger,
cache_logger_on_first_use=True,
)
# 표준 라이브러리 로거 설정
formatter = structlog.stdlib.ProcessorFormatter(
processors=[
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
renderer,
],
)
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(formatter)
root_logger = logging.getLogger()
root_logger.handlers.clear()
root_logger.addHandler(handler)
root_logger.setLevel(getattr(logging, level.upper()))
# 서드파티 로거 레벨 조정
for name in ("uvicorn", "httpx", "sqlalchemy"):
logging.getLogger(name).setLevel(logging.WARNING)
def _setup_stdlib(level: str) -> None:
"""표준 라이브러리 기반 설정 (structlog 미설치 시)"""
logging.basicConfig(
level=getattr(logging, level.upper()),
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
stream=sys.stdout,
)
# ============================================
# 로거 팩토리
# ============================================
def get_logger(name: str) -> Any:
"""모듈별 로거 생성
Args:
name: 로거 이름 (보통 __name__)
Returns:
structlog 바운드 로거 또는 표준 로거
Example:
logger = get_logger(__name__)
logger.info("처리 완료", count=42, duration_ms=150)
"""
if HAS_STRUCTLOG:
return structlog.get_logger(name)
return logging.getLogger(name)
# ============================================
# FastAPI 미들웨어 헬퍼
# ============================================
def create_logging_middleware():
"""FastAPI 요청 로깅 미들웨어 생성
Example:
from fastapi import FastAPI
app = FastAPI()
app.middleware("http")(create_logging_middleware())
"""
import time
import uuid
async def logging_middleware(request, call_next):
request_id = str(uuid.uuid4())[:8]
bind_context(
request_id=request_id,
method=request.method,
path=request.url.path,
)
logger = get_logger("http")
start_time = time.time()
try:
response = await call_next(request)
duration_ms = (time.time() - start_time) * 1000
logger.info(
"요청 처리",
status_code=response.status_code,
duration_ms=round(duration_ms, 1),
)
response.headers["X-Request-ID"] = request_id
return response
except Exception as e:
logger.error("요청 처리 실패", error=str(e))
raise
finally:
clear_context()
return logging_middleware