|
| 1 | +"""Logging configuration for the application.""" |
| 2 | + |
1 | 3 | import logging |
2 | 4 | import os |
3 | 5 | from logging.handlers import RotatingFileHandler |
4 | 6 |
|
| 7 | +import structlog |
| 8 | +from structlog.dev import ConsoleRenderer |
| 9 | +from structlog.processors import JSONRenderer |
| 10 | +from structlog.types import EventDict, Processor |
| 11 | + |
| 12 | +from ..core.config import settings |
| 13 | + |
| 14 | + |
| 15 | +def drop_color_message_key(_, __, event_dict: EventDict) -> EventDict: |
| 16 | + """Uvicorn adds `color_message` which duplicates `event`. |
| 17 | +
|
| 18 | + Remove it to avoid double logging. |
| 19 | + """ |
| 20 | + event_dict.pop("color_message", None) |
| 21 | + return event_dict |
| 22 | + |
| 23 | + |
| 24 | +def file_log_filter_processors(_, __, event_dict: EventDict) -> EventDict: |
| 25 | + """Filter out the request ID, path, method, client host, and status code from the event dict if the |
| 26 | + corresponding setting is False.""" |
| 27 | + |
| 28 | + if not settings.FILE_LOG_INCLUDE_REQUEST_ID: |
| 29 | + event_dict.pop("request_id", None) |
| 30 | + if not settings.FILE_LOG_INCLUDE_PATH: |
| 31 | + event_dict.pop("path", None) |
| 32 | + if not settings.FILE_LOG_INCLUDE_METHOD: |
| 33 | + event_dict.pop("method", None) |
| 34 | + if not settings.FILE_LOG_INCLUDE_CLIENT_HOST: |
| 35 | + event_dict.pop("client_host", None) |
| 36 | + if not settings.FILE_LOG_INCLUDE_STATUS_CODE: |
| 37 | + event_dict.pop("status_code", None) |
| 38 | + return event_dict |
| 39 | + |
| 40 | + |
| 41 | +def console_log_filter_processors(_, __, event_dict: EventDict) -> EventDict: |
| 42 | + """Filter out the request ID, path, method, client host, and status code from the event dict if the |
| 43 | + corresponding setting is False.""" |
| 44 | + |
| 45 | + if not settings.CONSOLE_LOG_INCLUDE_REQUEST_ID: |
| 46 | + event_dict.pop("request_id", None) |
| 47 | + if not settings.CONSOLE_LOG_INCLUDE_PATH: |
| 48 | + event_dict.pop("path", None) |
| 49 | + if not settings.CONSOLE_LOG_INCLUDE_METHOD: |
| 50 | + event_dict.pop("method", None) |
| 51 | + if not settings.CONSOLE_LOG_INCLUDE_CLIENT_HOST: |
| 52 | + event_dict.pop("client_host", None) |
| 53 | + if not settings.CONSOLE_LOG_INCLUDE_STATUS_CODE: |
| 54 | + event_dict.pop("status_code", None) |
| 55 | + return event_dict |
| 56 | + |
| 57 | + |
| 58 | +# Shared processors for all loggers |
| 59 | +timestamper = structlog.processors.TimeStamper(fmt="iso") |
| 60 | +SHARED_PROCESSORS: list[Processor] = [ |
| 61 | + structlog.contextvars.merge_contextvars, |
| 62 | + structlog.stdlib.add_logger_name, |
| 63 | + structlog.stdlib.add_log_level, |
| 64 | + structlog.stdlib.PositionalArgumentsFormatter(), |
| 65 | + structlog.stdlib.ExtraAdder(), |
| 66 | + drop_color_message_key, |
| 67 | + timestamper, |
| 68 | + structlog.processors.StackInfoRenderer(), |
| 69 | +] |
| 70 | + |
| 71 | + |
| 72 | +# Configure structlog globally |
| 73 | +structlog.configure( |
| 74 | + processors=SHARED_PROCESSORS + [structlog.stdlib.ProcessorFormatter.wrap_for_formatter], |
| 75 | + logger_factory=structlog.stdlib.LoggerFactory(), |
| 76 | + cache_logger_on_first_use=True, |
| 77 | +) |
| 78 | + |
| 79 | + |
| 80 | +def build_formatter(*, json_output: bool, pre_chain: list[Processor]) -> structlog.stdlib.ProcessorFormatter: |
| 81 | + """Build a ProcessorFormatter with the specified renderer and processors.""" |
| 82 | + renderer = JSONRenderer() if json_output else ConsoleRenderer() |
| 83 | + |
| 84 | + processors = [structlog.stdlib.ProcessorFormatter.remove_processors_meta, renderer] |
| 85 | + |
| 86 | + if json_output: |
| 87 | + pre_chain = pre_chain + [structlog.processors.format_exc_info] |
| 88 | + |
| 89 | + return structlog.stdlib.ProcessorFormatter(foreign_pre_chain=pre_chain, processors=processors) |
| 90 | + |
| 91 | + |
| 92 | +# Setup log directory |
5 | 93 | LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "logs") |
6 | | -if not os.path.exists(LOG_DIR): |
7 | | - os.makedirs(LOG_DIR) |
| 94 | +os.makedirs(LOG_DIR, exist_ok=True) |
| 95 | + |
8 | 96 |
|
9 | | -LOG_FILE_PATH = os.path.join(LOG_DIR, "app.log") |
| 97 | +# File handler configuration |
| 98 | +file_handler = RotatingFileHandler( |
| 99 | + filename=os.path.join(LOG_DIR, "app.log"), |
| 100 | + maxBytes=settings.FILE_LOG_MAX_BYTES, |
| 101 | + backupCount=settings.FILE_LOG_BACKUP_COUNT, |
| 102 | +) |
| 103 | +file_handler.setLevel(settings.FILE_LOG_LEVEL) |
| 104 | +file_handler.setFormatter( |
| 105 | + build_formatter( |
| 106 | + json_output=settings.FILE_LOG_FORMAT_JSON, pre_chain=SHARED_PROCESSORS + [file_log_filter_processors] |
| 107 | + ) |
| 108 | +) |
10 | 109 |
|
11 | | -LOGGING_LEVEL = logging.INFO |
12 | | -LOGGING_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" |
| 110 | +# Console handler configuration |
| 111 | +console_handler = logging.StreamHandler() |
| 112 | +console_handler.setLevel(settings.CONSOLE_LOG_LEVEL) |
| 113 | +console_handler.setFormatter( |
| 114 | + build_formatter( |
| 115 | + json_output=settings.CONSOLE_LOG_FORMAT_JSON, pre_chain=SHARED_PROCESSORS + [console_log_filter_processors] |
| 116 | + ) |
| 117 | +) |
13 | 118 |
|
14 | | -logging.basicConfig(level=LOGGING_LEVEL, format=LOGGING_FORMAT) |
15 | 119 |
|
16 | | -file_handler = RotatingFileHandler(LOG_FILE_PATH, maxBytes=10485760, backupCount=5) |
17 | | -file_handler.setLevel(LOGGING_LEVEL) |
18 | | -file_handler.setFormatter(logging.Formatter(LOGGING_FORMAT)) |
| 120 | +# Root logger configuration |
| 121 | +root_logger = logging.getLogger() |
| 122 | +root_logger.setLevel(logging.INFO) |
| 123 | +root_logger.handlers.clear() # avoid duplicate logs |
| 124 | +root_logger.addHandler(file_handler) |
| 125 | +root_logger.addHandler(console_handler) |
19 | 126 |
|
20 | | -logging.getLogger("").addHandler(file_handler) |
| 127 | +# Uvicorn logger integration |
| 128 | +for logger_name in ("uvicorn", "uvicorn.error", "uvicorn.access"): |
| 129 | + logger = logging.getLogger(logger_name) |
| 130 | + logger.handlers.clear() |
| 131 | + logger.propagate = True |
| 132 | + logger.setLevel(logging.INFO) |
0 commit comments