|
import logging |
|
import os |
|
import sys |
|
from logging.handlers import TimedRotatingFileHandler |
|
from pathlib import Path |
|
|
|
import structlog |
|
from dotenv import load_dotenv |
|
from structlog.processors import CallsiteParameter |
|
from structlog.stdlib import BoundLogger |
|
from structlog.typing import EventDict, Processor |
|
|
|
|
|
load_dotenv() |
|
|
|
|
|
class Logger: |
|
""" |
|
Configure and setup logging with Structlog. |
|
|
|
Args: |
|
json_logs (bool, optional): Whether to log in JSON format. Defaults to False. |
|
log_level (str, optional): Minimum log level to display. Defaults to "INFO". |
|
""" |
|
|
|
def __init__(self, json_logs: bool = False, log_level: str = "INFO"): |
|
self.json_logs = json_logs |
|
self.log_level = log_level.upper() |
|
self.environment = os.getenv("ENVIRONMENT", "PROD").upper() |
|
|
|
|
|
if self.environment in ["PROD", "HUGGINGFACE"]: |
|
self.log_file_path = None |
|
else: |
|
self.log_file_path = os.getenv("LOG_FILE_PATH", self._get_default_log_file_path()) |
|
|
|
def _get_default_log_file_path(self) -> str: |
|
"""Get the default log file path.""" |
|
if self.environment == "DEV": |
|
|
|
log_dir = Path("logs") |
|
log_dir.mkdir(parents=True, exist_ok=True) |
|
return str(log_dir / "app.log") |
|
return None |
|
|
|
def _rename_event_key(self, _, __, event_dict: EventDict) -> EventDict: |
|
""" |
|
Renames the 'event' key to 'message' in log entries. |
|
""" |
|
event_dict["message"] = event_dict.pop("event", "") |
|
return event_dict |
|
|
|
def _drop_color_message_key(self, _, __, event_dict: EventDict) -> EventDict: |
|
""" |
|
Removes the 'color_message' key from log entries. |
|
""" |
|
event_dict.pop("color_message", None) |
|
return event_dict |
|
|
|
def _get_processors(self) -> list[Processor]: |
|
""" |
|
Returns a list of structlog processors based on the specified configuration. |
|
""" |
|
processors: list[Processor] = [ |
|
structlog.contextvars.merge_contextvars, |
|
structlog.stdlib.add_logger_name, |
|
structlog.stdlib.add_log_level, |
|
structlog.stdlib.PositionalArgumentsFormatter(), |
|
structlog.stdlib.ExtraAdder(), |
|
self._drop_color_message_key, |
|
structlog.processors.TimeStamper(fmt="iso"), |
|
structlog.processors.StackInfoRenderer(), |
|
structlog.processors.CallsiteParameterAdder( |
|
[ |
|
CallsiteParameter.FILENAME, |
|
CallsiteParameter.FUNC_NAME, |
|
CallsiteParameter.LINENO, |
|
], |
|
), |
|
] |
|
|
|
if self.json_logs: |
|
processors.append(self._rename_event_key) |
|
processors.append(structlog.processors.format_exc_info) |
|
|
|
return processors |
|
|
|
def _clear_uvicorn_loggers(self): |
|
""" |
|
Clears the log handlers for uvicorn loggers. |
|
""" |
|
for _log in ["uvicorn", "uvicorn.error", "uvicorn.access"]: |
|
logging.getLogger(_log).handlers.clear() |
|
logging.getLogger(_log).propagate = True |
|
|
|
def _configure_structlog(self, processors: list[Processor]): |
|
""" |
|
Configures structlog with the specified processors. |
|
""" |
|
structlog.configure( |
|
processors=processors |
|
+ [ |
|
structlog.stdlib.ProcessorFormatter.wrap_for_formatter, |
|
], |
|
logger_factory=structlog.stdlib.LoggerFactory(), |
|
cache_logger_on_first_use=True, |
|
) |
|
|
|
def _configure_logging(self, processors: list[Processor]) -> logging.Logger: |
|
"""Configures logging with the specified processors.""" |
|
formatter = structlog.stdlib.ProcessorFormatter( |
|
foreign_pre_chain=processors, |
|
processors=[ |
|
structlog.stdlib.ProcessorFormatter.remove_processors_meta, |
|
structlog.processors.JSONRenderer() |
|
if self.json_logs |
|
else structlog.dev.ConsoleRenderer(colors=True), |
|
], |
|
) |
|
|
|
root_logger = logging.getLogger() |
|
root_logger.handlers.clear() |
|
|
|
|
|
stream_handler = logging.StreamHandler() |
|
stream_handler.setFormatter(formatter) |
|
root_logger.addHandler(stream_handler) |
|
|
|
|
|
if self.environment == "DEV" and self.log_file_path: |
|
try: |
|
file_handler = TimedRotatingFileHandler( |
|
filename=self.log_file_path, |
|
when="midnight", |
|
interval=1, |
|
backupCount=7, |
|
encoding="utf-8", |
|
) |
|
file_handler.setFormatter(formatter) |
|
root_logger.addHandler(file_handler) |
|
except PermissionError: |
|
|
|
pass |
|
|
|
root_logger.setLevel(self.log_level) |
|
return root_logger |
|
|
|
def _configure(self): |
|
""" |
|
Configures logging and structlog, and sets up exception handling. |
|
""" |
|
shared_processors: list[Processor] = self._get_processors() |
|
self._configure_structlog(shared_processors) |
|
root_logger = self._configure_logging(shared_processors) |
|
self._clear_uvicorn_loggers() |
|
|
|
def handle_exception(exc_type, exc_value, exc_traceback): |
|
""" |
|
Logs uncaught exceptions. |
|
""" |
|
if issubclass(exc_type, KeyboardInterrupt): |
|
sys.__excepthook__(exc_type, exc_value, exc_traceback) |
|
return |
|
|
|
root_logger.error( |
|
"Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback) |
|
) |
|
|
|
sys.excepthook = handle_exception |
|
|
|
def setup_logging(self) -> BoundLogger: |
|
""" |
|
Sets up logging configuration for the application. |
|
|
|
Returns: |
|
BoundLogger: The configured logger instance. |
|
""" |
|
self._configure() |
|
return structlog.get_logger() |
|
|