|
7 | 7 | import structlog |
8 | 8 | from whenever._whenever import Instant |
9 | 9 |
|
10 | | - |
11 | | -def _configure_logger() -> structlog.BoundLogger: |
12 | | - """ |
13 | | - Configures and returns a structlog logger with a rotating file handler. |
14 | | -
|
15 | | - The logger is configured using environment variables for path, file size, |
16 | | - and backup count. It formats logs as JSON. |
17 | | - """ |
18 | | - log_dir = Path(os.environ.get("ROTOGER_LOG_PATH", ".")) |
| 10 | +# --------------------------------------------------------------------------- |
| 11 | +# Constants / defaults |
| 12 | +# --------------------------------------------------------------------------- |
| 13 | +_DEFAULT_LOG_PATH = "." |
| 14 | +_DEFAULT_MAX_BYTES = 10 * 1024 * 1024 # 10 MiB |
| 15 | +_DEFAULT_BACKUP_COUNT = 5 |
| 16 | + |
| 17 | +# Generic registry: add any stdlib logger name + its desired level here. |
| 18 | +_STDLIB_LOGGERS: dict[str, int] = { |
| 19 | + "root": logging.INFO, |
| 20 | + "uvicorn": logging.INFO, |
| 21 | + "sqlalchemy": logging.WARNING, |
| 22 | +} |
| 23 | + |
| 24 | +# Shared processor chain used by both structlog and the stdlib formatter. |
| 25 | +_SHARED_PROCESSORS: list[structlog.types.Processor] = [ |
| 26 | + structlog.contextvars.merge_contextvars, |
| 27 | + structlog.stdlib.add_log_level, |
| 28 | + structlog.stdlib.add_logger_name, |
| 29 | + structlog.stdlib.PositionalArgumentsFormatter(), |
| 30 | + structlog.processors.TimeStamper(fmt="iso", utc=True), |
| 31 | + structlog.processors.format_exc_info, |
| 32 | +] |
| 33 | + |
| 34 | + |
| 35 | +def _build_handler() -> RotatingFileHandler: |
| 36 | + log_dir = Path(os.getenv("ROTOGER_LOG_PATH", _DEFAULT_LOG_PATH)) |
19 | 37 | log_dir.mkdir(parents=True, exist_ok=True) |
20 | | - log_date = Instant.now().py_datetime().strftime("%Y%m%d") |
21 | | - log_path = log_dir / f"{log_date}_{os.getpid()}.log" |
22 | | - |
23 | | - # Use int() to ensure env var values are correctly typed |
24 | | - max_bytes = int(os.environ.get("ROTOGER_LOG_MAX_BYTES", 10 * 1024 * 1024)) |
25 | | - backup_count = int(os.environ.get("ROTOGER_LOG_BACKUP_COUNT", 5)) |
| 38 | + log_path = log_dir / f"{Instant.now().py_datetime().strftime('%Y%m%d')}_{os.getpid()}.log" |
26 | 39 |
|
27 | 40 | handler = RotatingFileHandler( |
28 | 41 | filename=log_path, |
29 | | - maxBytes=max_bytes, |
30 | | - backupCount=backup_count, |
| 42 | + maxBytes=int(os.getenv("ROTOGER_LOG_MAX_BYTES", _DEFAULT_MAX_BYTES)), |
| 43 | + backupCount=int(os.getenv("ROTOGER_LOG_BACKUP_COUNT", _DEFAULT_BACKUP_COUNT)), |
31 | 44 | encoding="utf-8", |
32 | | - |
33 | 45 | ) |
| 46 | + handler.setFormatter( |
| 47 | + structlog.stdlib.ProcessorFormatter( |
| 48 | + foreign_pre_chain=_SHARED_PROCESSORS, |
| 49 | + processor=structlog.processors.JSONRenderer( |
| 50 | + serializer=lambda *a, **kw: orjson.dumps(*a, **kw).decode() |
| 51 | + ), |
| 52 | + ) |
| 53 | + ) |
| 54 | + return handler |
| 55 | + |
34 | 56 |
|
35 | | - # Use structlog's standard library integration |
| 57 | +def _configure_logger() -> structlog.BoundLogger: |
| 58 | + """Configure structlog + stdlib loggers and return a bound logger.""" |
36 | 59 | structlog.configure( |
37 | 60 | processors=[ |
38 | | - structlog.contextvars.merge_contextvars, |
39 | | - structlog.stdlib.add_log_level, |
40 | | - structlog.stdlib.PositionalArgumentsFormatter(), |
41 | | - structlog.processors.TimeStamper(fmt="iso", utc=True), |
42 | | - structlog.processors.format_exc_info, |
| 61 | + *_SHARED_PROCESSORS, |
43 | 62 | structlog.stdlib.ProcessorFormatter.wrap_for_formatter, |
44 | | - # structlog.stdlib.add_logger_name, |
45 | 63 | ], |
46 | 64 | logger_factory=structlog.stdlib.LoggerFactory(), |
47 | 65 | wrapper_class=structlog.stdlib.BoundLogger, |
48 | 66 | cache_logger_on_first_use=True, |
49 | 67 | ) |
50 | 68 |
|
51 | | - # Configure the underlying standard logger |
52 | | - formatter = structlog.stdlib.ProcessorFormatter( |
53 | | - # These run after the processors defined in structlog.configure |
54 | | - foreign_pre_chain=[ |
55 | | - structlog.contextvars.merge_contextvars, |
56 | | - structlog.stdlib.add_log_level, |
57 | | - structlog.stdlib.PositionalArgumentsFormatter(), |
58 | | - structlog.processors.TimeStamper(fmt="iso", utc=True), |
59 | | - structlog.processors.format_exc_info, |
60 | | - structlog.stdlib.add_logger_name, |
61 | | - ], |
62 | | - processor=structlog.processors.JSONRenderer( |
63 | | - serializer=lambda *args, **kwargs: orjson.dumps(*args, **kwargs).decode() |
64 | | - ), |
65 | | - ) |
66 | | - handler.setFormatter(formatter) |
67 | | - root_logger = logging.getLogger("root") # Get the root logger |
68 | | - root_logger.addHandler(handler) |
69 | | - root_logger.propagate = False # Prevent logs from being propagated to the root logger |
70 | | - root_logger.setLevel(logging.INFO) |
71 | | - |
72 | | - uvicorn_logger = logging.getLogger("uvicorn") # Get the root logger |
73 | | - uvicorn_logger.addHandler(handler) |
74 | | - uvicorn_logger.propagate = False # Prevent logs from being propagated to the root logger |
75 | | - uvicorn_logger.setLevel(logging.INFO) |
76 | | - |
77 | | - sa_logger = logging.getLogger("sqlalchemy") # Get the root logger |
78 | | - sa_logger.addHandler(handler) |
79 | | - sa_logger.propagate = False # Prevent logs from being propagated to the root logger |
80 | | - sa_logger.setLevel(logging.WARNING) |
81 | | - |
82 | | - # Set SQLAlchemy engine logger level specifically if needed |
83 | | - # logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) |
84 | | - return structlog.get_logger() |
| 69 | + handler = _build_handler() |
| 70 | + |
| 71 | + for name, level in _STDLIB_LOGGERS.items(): |
| 72 | + logger = logging.getLogger(name) |
| 73 | + logger.addHandler(handler) |
| 74 | + logger.propagate = False |
| 75 | + logger.setLevel(level) |
85 | 76 |
|
| 77 | + return structlog.get_logger() |
86 | 78 |
|
87 | 79 |
|
88 | | -# Module-level singleton instance |
| 80 | +# Module-level singleton |
89 | 81 | _logger_instance = _configure_logger() |
90 | 82 |
|
91 | 83 |
|
92 | 84 | def get_logger() -> structlog.BoundLogger: |
93 | | - """ |
94 | | - Returns the configured singleton logger instance. |
95 | | - """ |
| 85 | + """Return the configured singleton logger instance.""" |
96 | 86 | return _logger_instance |
0 commit comments