Files
remnawave-bedolaga-telegram…/app/logging_config.py

172 lines
6.1 KiB
Python

"""Centralized structlog configuration.
Configures structlog with ProcessorFormatter so that both structlog.get_logger(__name__)
and logging.getLogger() calls produce identically formatted output.
Usage::
from app.logging_config import setup_logging
file_formatter, console_formatter, telegram_notifier = setup_logging()
# Apply formatters to handlers...
# Later: telegram_notifier.set_bot(bot)
"""
from __future__ import annotations
import logging
from typing import Any
import structlog
from app.config import settings
def _create_timezone_timestamper() -> structlog.types.Processor:
"""Create a timestamper processor that uses the configured timezone."""
from zoneinfo import ZoneInfo
try:
tz = ZoneInfo(settings.TIMEZONE)
except Exception:
tz = ZoneInfo('UTC')
from datetime import datetime
def timestamper(logger: Any, method_name: str, event_dict: dict[str, Any]) -> dict[str, Any]:
dt = datetime.now(tz=tz)
event_dict['timestamp'] = dt.strftime('%Y-%m-%d %H:%M:%S')
return event_dict
return timestamper
def _clean_logger_name(logger: Any, method_name: str, event_dict: dict[str, Any]) -> dict[str, Any]:
"""Strip __main__ logger name — it's redundant noise in startup logs."""
if event_dict.get('logger') == '__main__':
del event_dict['logger']
return event_dict
def _prefix_logger_name(logger: Any, method_name: str, event_dict: dict[str, Any]) -> dict[str, Any]:
"""Move logger name before event text: [module.name] event text."""
logger_name = event_dict.pop('logger', None)
if logger_name:
event_dict['event'] = f'[{logger_name}] {event_dict.get("event", "")}'
return event_dict
def setup_logging() -> tuple[logging.Formatter, logging.Formatter, Any]:
"""Configure structlog and return formatters + notifier.
Returns:
(file_formatter, console_formatter, telegram_notifier)
- file_formatter: ProcessorFormatter without ANSI colors (for file handlers)
- console_formatter: ProcessorFormatter with auto-detected colors (for console)
- telegram_notifier: TelegramNotifierProcessor (call .set_bot(bot) later)
"""
from app.logging_handler import TelegramNotifierProcessor
telegram_notifier = TelegramNotifierProcessor()
timestamper = _create_timezone_timestamper()
# Shared processors applied to both structlog and stdlib log entries.
# Order matters: each processor enriches event_dict for the next one.
shared_processors: list[structlog.types.Processor] = [
structlog.contextvars.merge_contextvars,
structlog.stdlib.add_log_level,
structlog.stdlib.add_logger_name,
_clean_logger_name,
structlog.stdlib.ExtraAdder(),
structlog.stdlib.PositionalArgumentsFormatter(),
timestamper,
structlog.processors.StackInfoRenderer(),
# TelegramNotifierProcessor MUST run while exc_info is still a raw
# tuple so it can extract the traceback for Telegram notifications.
# ConsoleRenderer handles exc_info formatting downstream (with Rich
# tracebacks on console, plain text in files).
telegram_notifier,
]
# Configure structlog for structlog-originated logs.
# wrap_for_formatter packages event_dict into the stdlib LogRecord
# so ProcessorFormatter can extract and render it.
structlog.configure(
processors=shared_processors
+ [
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
],
wrapper_class=structlog.make_filtering_bound_logger(
getattr(logging, settings.LOG_LEVEL, logging.INFO),
),
logger_factory=structlog.stdlib.LoggerFactory(),
# NOTE: cache is safe because LOG_LEVEL is set once at startup.
# If dynamic level changes are ever added, switch to False.
cache_logger_on_first_use=True,
)
# File formatter: no ANSI colors, plain tracebacks (safe for log files)
file_formatter = structlog.stdlib.ProcessorFormatter(
foreign_pre_chain=shared_processors,
processors=[
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
_prefix_logger_name,
structlog.dev.ConsoleRenderer(
colors=False,
pad_event_to=0,
pad_level=False,
exception_formatter=structlog.dev.plain_traceback,
),
],
)
# Console formatter: colors controlled by LOG_COLORS env var (default: true).
# Rich tracebacks with conservative limits to avoid 5000-line dumps.
use_colors = settings.LOG_COLORS
console_renderer_kwargs: dict[str, Any] = {
'colors': use_colors,
'pad_event_to': 0,
'pad_level': False,
}
if use_colors:
console_renderer_kwargs['exception_formatter'] = structlog.dev.RichTracebackFormatter(
show_locals=False,
max_frames=20,
extra_lines=1,
width=120,
suppress=['aiogram', 'aiohttp'],
)
else:
console_renderer_kwargs['exception_formatter'] = structlog.dev.plain_traceback
console_formatter = structlog.stdlib.ProcessorFormatter(
foreign_pre_chain=shared_processors,
processors=[
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
_prefix_logger_name,
structlog.dev.ConsoleRenderer(**console_renderer_kwargs),
],
)
_configure_noisy_loggers()
return file_formatter, console_formatter, telegram_notifier
def _configure_noisy_loggers() -> None:
"""Suppress noisy third-party loggers."""
for name, level in {
'aiohttp.access': logging.ERROR,
'aiohttp.client': logging.WARNING,
'aiohttp.internal': logging.WARNING,
'app.external.remnawave_api': logging.WARNING,
'aiogram': logging.WARNING,
'uvicorn.access': logging.ERROR,
'uvicorn.error': logging.WARNING,
'uvicorn.protocols.websockets.websockets_impl': logging.WARNING,
'websockets.server': logging.WARNING,
'websockets': logging.WARNING,
}.items():
logging.getLogger(name).setLevel(level)