Coverage for src / mysingle / core / logging.py: 0%
147 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-02 00:58 +0900
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-02 00:58 +0900
1"""
2통합 로깅 시스템 (Structured + Traditional Logging)
4이 모듈은 기존 logging_config.py와 structured_logging.py를 통합하여
5다음 기능을 제공합니다:
71. 구조화된 로깅 (structlog 기반)
8 - Correlation ID, User ID, Request ID 컨텍스트 변수
9 - JSON 출력 지원
10 - 서비스명 자동 태깅
11 - 편의 함수들 (log_user_action, log_service_call, log_database_operation)
132. 전통적인 로깅 (logging 기반)
14 - 컬러 출력 (colorlog)
15 - 파일 로깅 (app.log, error.log)
16 - 외부 라이브러리 로그 레벨 조정
183. 통합 설정
19 - 환경별 설정 (development, production)
20 - 자동 서비스명 감지
21 - FastAPI 미들웨어 통합 지원
22"""
24import logging
25import sys
26from contextvars import ContextVar
27from pathlib import Path
28from typing import Optional
30import structlog
32try:
33 import colorlog
35 HAS_COLORLOG = True
36except ImportError:
37 HAS_COLORLOG = False
40# Correlation ID, User ID, Request ID 컨텍스트 변수
41correlation_id_var: ContextVar[str] = ContextVar("correlation_id", default="")
42user_id_var: ContextVar[str] = ContextVar("user_id", default="")
43request_id_var: ContextVar[str] = ContextVar("request_id", default="")
46# =============================================================================
47# 구조화된 로깅 시스템 (structlog)
48# =============================================================================
51class CorrelationIdProcessor:
52 """Correlation ID를 로그에 추가하는 프로세서"""
54 def __call__(self, logger, method_name, event_dict):
55 correlation_id = correlation_id_var.get()
56 if correlation_id:
57 # 로그 메시지에 correlation ID 프리픽스 추가
58 event_dict["event"] = (
59 f"[{correlation_id[:8]}] {event_dict.get('event', '')}"
60 )
61 return event_dict
64class ServiceNameProcessor:
65 """서비스명을 로그에 추가하는 프로세서"""
67 def __init__(self, service_name: str):
68 self.service_name = service_name
70 def __call__(self, logger, method_name, event_dict):
71 event_dict["service"] = self.service_name
72 return event_dict
75class UserContextProcessor:
76 """User ID와 Request ID를 로그에 추가하는 프로세서"""
78 def __call__(self, logger, method_name, event_dict):
79 user_id = user_id_var.get()
80 request_id = request_id_var.get()
82 if user_id:
83 event_dict["user_id"] = user_id
85 if request_id:
86 event_dict["request_id"] = request_id
88 return event_dict
91def configure_structured_logging(
92 service_name: str,
93 log_level: str = "INFO",
94 enable_json: bool = False,
95 enable_correlation_id: bool = True,
96 enable_user_context: bool = True,
97):
98 """
99 구조화된 로깅 설정
101 Args:
102 service_name: 서비스명
103 log_level: 로그 레벨 (DEBUG, INFO, WARNING, ERROR, CRITICAL)
104 enable_json: JSON 형식 출력 활성화
105 enable_correlation_id: Correlation ID 추가 활성화
106 enable_user_context: User/Request ID 컨텍스트 추가 활성화
107 """
108 processors = [
109 structlog.processors.TimeStamper(fmt="ISO"),
110 structlog.processors.add_log_level,
111 ServiceNameProcessor(service_name),
112 ]
114 if enable_correlation_id:
115 processors.append(CorrelationIdProcessor())
117 if enable_user_context:
118 processors.append(UserContextProcessor())
120 if enable_json:
121 processors.append(structlog.processors.JSONRenderer())
122 else:
123 processors.append(structlog.dev.ConsoleRenderer())
125 structlog.configure(
126 processors=processors,
127 wrapper_class=structlog.make_filtering_bound_logger(
128 getattr(logging, log_level.upper())
129 ),
130 context_class=dict,
131 logger_factory=structlog.WriteLoggerFactory(),
132 cache_logger_on_first_use=True,
133 )
135 logger = get_structured_logger(__name__)
136 logger.info(
137 "Structured logging configured",
138 extra={
139 "json_logging": enable_json,
140 "correlation_id_enabled": enable_correlation_id,
141 },
142 )
145def get_structured_logger(name: str):
146 """구조화된 로거 인스턴스 획득"""
147 return structlog.get_logger(name)
150# =============================================================================
151# 컨텍스트 변수 관리
152# =============================================================================
155def set_correlation_id(correlation_id: str):
156 """Correlation ID 설정"""
157 correlation_id_var.set(correlation_id)
160def set_user_id(user_id: str):
161 """User ID 설정"""
162 user_id_var.set(user_id)
165def set_request_id(request_id: str):
166 """Request ID 설정"""
167 request_id_var.set(request_id)
170def get_correlation_id() -> str:
171 """현재 Correlation ID 획득"""
172 return correlation_id_var.get()
175def get_user_id() -> str:
176 """현재 User ID 획득"""
177 return user_id_var.get()
180def get_request_id() -> str:
181 """현재 Request ID 획득"""
182 return request_id_var.get()
185def clear_logging_context():
186 """로깅 컨텍스트 초기화"""
187 correlation_id_var.set("")
188 user_id_var.set("")
189 request_id_var.set("")
192# =============================================================================
193# 편의 함수들 (구조화된 로깅)
194# =============================================================================
197def log_user_action(
198 action: str,
199 resource_type: str,
200 resource_id: Optional[str] = None,
201 details: Optional[dict] = None,
202 success: bool = True,
203 error: Optional[str] = None,
204):
205 """사용자 액션 로깅"""
206 logger = get_structured_logger(__name__)
208 log_data = {
209 "action": action,
210 "resource_type": resource_type,
211 "success": success,
212 }
214 if resource_id:
215 log_data["resource_id"] = resource_id
217 if details:
218 log_data["details"] = details
220 if error:
221 log_data["error"] = error
222 logger.error("User action failed", extra=log_data)
223 else:
224 logger.info("User action completed", extra=log_data)
227def log_service_call(
228 service_name: str,
229 method: str,
230 endpoint: str,
231 duration: float,
232 status_code: Optional[int] = None,
233 error: Optional[str] = None,
234):
235 """서비스 호출 로깅"""
236 logger = get_structured_logger(__name__)
238 log_data = {
239 "target_service": service_name,
240 "http_method": method,
241 "endpoint": endpoint,
242 "duration_ms": round(duration * 1000, 2),
243 }
245 if status_code:
246 log_data["status_code"] = status_code
248 if error:
249 log_data["error"] = error
250 logger.error("Service call failed", extra=log_data)
251 else:
252 logger.info("Service call completed", extra=log_data)
255def log_database_operation(
256 operation: str,
257 collection: str,
258 duration: float,
259 document_count: Optional[int] = None,
260 error: Optional[str] = None,
261):
262 """데이터베이스 작업 로깅"""
263 logger = get_structured_logger(__name__)
265 log_data = {
266 "operation": operation,
267 "collection": collection,
268 "duration_ms": round(duration * 1000, 2),
269 }
271 if document_count is not None:
272 log_data["document_count"] = document_count
274 if error:
275 log_data["error"] = error
276 logger.error("Database operation failed", extra=log_data)
277 else:
278 logger.info("Database operation completed", extra=log_data)
281# =============================================================================
282# 전통적인 로깅 시스템 (기존 logging_config.py 통합)
283# =============================================================================
286def setup_traditional_logging():
287 """전통적인 파일/콘솔 로깅 설정"""
289 # 로그 디렉토리 생성
290 log_dir = Path("logs")
291 log_dir.mkdir(exist_ok=True)
293 # 루트 로거 설정
294 root_logger = logging.getLogger()
295 root_logger.setLevel(logging.INFO)
297 # 기존 핸들러 제거
298 for handler in root_logger.handlers[:]:
299 root_logger.removeHandler(handler)
301 # 컬러 로그 포맷 (colorlog 사용)
302 if HAS_COLORLOG:
303 color_format = (
304 "%(log_color)s%(asctime)s%(reset)s | "
305 "%(log_color)s%(levelname)-8s%(reset)s | "
306 "%(cyan)s%(name)-30s%(reset)s | "
307 "%(message_log_color)s%(message)s%(reset)s"
308 )
309 date_format = "%H:%M:%S"
311 console_formatter = colorlog.ColoredFormatter(
312 color_format,
313 datefmt=date_format,
314 log_colors={
315 "DEBUG": "blue",
316 "INFO": "green",
317 "WARNING": "yellow",
318 "ERROR": "red",
319 "CRITICAL": "red,bg_white",
320 },
321 secondary_log_colors={
322 "message": {
323 "DEBUG": "white",
324 "INFO": "white",
325 "WARNING": "yellow",
326 "ERROR": "red",
327 "CRITICAL": "red",
328 }
329 },
330 )
331 else:
332 # colorlog 없을 때 기본 포맷
333 log_format = "%(asctime)s | %(levelname)-8s | %(name)-30s | %(message)s"
334 date_format = "%H:%M:%S"
335 console_formatter = logging.Formatter(log_format, datefmt=date_format)
337 # 콘솔 핸들러
338 console_handler = logging.StreamHandler(sys.stdout)
339 console_handler.setLevel(logging.INFO)
340 console_handler.setFormatter(console_formatter)
341 root_logger.addHandler(console_handler)
343 # 파일 핸들러 (일반 로그) - 컬러 없이
344 file_format = "%(asctime)s | %(levelname)-8s | %(name)-30s | %(message)s"
345 file_date_format = "%Y-%m-%d %H:%M:%S"
347 file_handler = logging.FileHandler(log_dir / "app.log", encoding="utf-8")
348 file_handler.setLevel(logging.INFO)
349 file_formatter = logging.Formatter(file_format, datefmt=file_date_format)
350 file_handler.setFormatter(file_formatter)
351 root_logger.addHandler(file_handler)
353 # 에러 로그 파일 핸들러
354 error_handler = logging.FileHandler(log_dir / "error.log", encoding="utf-8")
355 error_handler.setLevel(logging.ERROR)
356 error_formatter = logging.Formatter(file_format, datefmt=file_date_format)
357 error_handler.setFormatter(error_formatter)
358 root_logger.addHandler(error_handler)
360 # 외부 라이브러리 로그 레벨 조정
361 _configure_external_loggers()
364def _configure_external_loggers():
365 """외부 라이브러리 로거 설정"""
366 external_loggers = {
367 "uvicorn.access": logging.WARNING,
368 "uvicorn.error": logging.INFO,
369 "httpx": logging.WARNING,
370 "watchfiles": logging.WARNING,
371 "watchfiles.main": logging.WARNING,
372 "pymongo": logging.WARNING,
373 "pymongo.serverSelection": logging.WARNING,
374 "pymongo.connection": logging.WARNING,
375 "pymongo.command": logging.WARNING,
376 "pymongo.topology": logging.WARNING,
377 # Email 및 User 관리 로거 (서비스별)
378 "app.utils.email": logging.INFO,
379 "app.services.user_manager": logging.INFO,
380 }
382 for logger_name, level in external_loggers.items():
383 logging.getLogger(logger_name).setLevel(level)
386# =============================================================================
387# 통합 로깅 설정 (권장)
388# =============================================================================
391def setup_logging(
392 service_name: str = "unknown-service",
393 log_level: str = "INFO",
394 environment: str = "development",
395 enable_structured: bool = True,
396 enable_traditional: bool = True,
397 enable_json: bool = False,
398):
399 """
400 통합 로깅 시스템 설정 (구조화된 + 전통적인 로깅)
402 Args:
403 service_name: 서비스명
404 log_level: 로그 레벨
405 environment: 환경 (development, production)
406 enable_structured: 구조화된 로깅 활성화
407 enable_traditional: 전통적인 로깅 활성화
408 enable_json: JSON 출력 활성화 (production 권장)
409 """
410 # 환경별 기본 설정
411 if environment == "production":
412 enable_json = True
413 log_level = "INFO"
414 elif environment == "development":
415 enable_json = False
416 log_level = "DEBUG"
418 # 전통적인 로깅 설정 (파일, 콘솔)
419 if enable_traditional:
420 setup_traditional_logging()
422 # 구조화된 로깅 설정
423 if enable_structured:
424 configure_structured_logging(
425 service_name=service_name,
426 log_level=log_level,
427 enable_json=enable_json,
428 )
430 # 설정 완료 로그
431 logger = get_structured_logger(__name__)
432 logger.info(f"✅ Integrated logging configured for {service_name}")
433 logger.info(f"Environment: {environment}, Level: {log_level}, JSON: {enable_json}")
436# =============================================================================
437# 레거시 호환성 함수
438# =============================================================================
441def setup_logging_legacy():
442 """기존 setup_logging 함수 (호환성 유지)"""
443 setup_traditional_logging()
446# 기본 설정 함수 (서비스명 없이 호출하는 경우)
447def configure_logging_for_service(service_name: str):
448 """서비스별 로깅 설정 (간단한 인터페이스)"""
449 setup_logging(service_name=service_name)