Coverage for nexios\logging.py: 67%
42 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-21 20:31 +0100
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-21 20:31 +0100
1from __future__ import annotations
2import logging
3import sys
4from logging import (
5 DEBUG,
6 ERROR,
7 Formatter,
8 Handler,
9 INFO,
10 Logger,
11 LogRecord,
12 NOTSET,
13 StreamHandler,
14 getLogger,
15)
16from logging.handlers import QueueHandler, QueueListener, RotatingFileHandler
17from queue import SimpleQueue as Queue
18from typing import TYPE_CHECKING, Optional, Tuple
20if TYPE_CHECKING:
21 from .application import NexiosApp
24class LocalQueueHandler(QueueHandler):
25 """Custom QueueHandler to avoid unnecessary record preparation.
27 Since we use an in-process queue, there's no need to prepare records,
28 reducing logging overhead.
29 """
31 def prepare(self, record: LogRecord) -> LogRecord:
32 return record
35def _setup_logging_queue(*handlers: Handler) -> QueueHandler:
36 """Creates a LocalQueueHandler and starts a QueueListener."""
37 queue: Queue[LogRecord] = Queue()
38 queue_handler = LocalQueueHandler(queue)
40 listener = QueueListener(queue, *handlers, respect_handler_level=True)
41 listener.start()
43 return queue_handler
46def has_level_handler(logger: Logger) -> bool:
47 """Checks if the logger already has an appropriate handler."""
48 level = logger.getEffectiveLevel()
49 current_logger: Optional[Logger] = logger
51 while current_logger:
52 if any(handler.level <= level for handler in current_logger.handlers):
53 return True
54 if not current_logger.propagate:
55 break
56 current_logger = current_logger.parent
58 return False
61def create_logger(
62 logger_name: str = "nexios",
63 log_level: int = DEBUG,
64 log_file: Optional[str] = None,
65 max_bytes: int = 10 * 1024 * 1024, # 10MB per log file
66 backup_count: int = 5,
67) -> Logger:
68 """Creates a high-performance, configurable logger for Nexios.
70 Args:
71 logger_name (str): The name of the logger.
72 log_level (int): The logging level (DEBUG, INFO, ERROR, etc.).
73 log_file (Optional[str]): Path to a log file for file-based logging.
74 max_bytes (int): Max size of each log file before rotating.
75 backup_count (int): Number of backup log files to keep.
77 Returns:
78 Logger: A configured logger instance.
79 """
80 logger = getLogger(logger_name)
81 logger.setLevel(log_level)
83 console_handler = StreamHandler(sys.stderr)
84 console_handler.setFormatter(
85 Formatter("[%(asctime)s] %(levelname)s in %(module)s: %(message)s")
86 )
88 handlers: Tuple[Handler, ...] = (console_handler,)
90 if log_file:
91 file_handler = RotatingFileHandler(
92 log_file, maxBytes=max_bytes, backupCount=backup_count
93 )
94 file_handler.setFormatter(
95 Formatter("[%(asctime)s] %(levelname)s in %(module)s: %(message)s")
96 )
97 handlers += (file_handler,)
99 if not has_level_handler(logger):
100 queue_handler = _setup_logging_queue(*handlers)
101 logger.addHandler(queue_handler)
103 return logger