Files
pyTorrent/pytorrent/logging_config.py
2026-05-19 13:43:37 +00:00

88 lines
3.2 KiB
Python

from __future__ import annotations
import logging
import time
from logging.handlers import TimedRotatingFileHandler
from pathlib import Path
from typing import Any
from flask import Flask, g, request
from .config import LOG_DIR, LOG_RETENTION_HOURS
_CONFIGURED = False
def _make_handler(path: Path, level: int) -> TimedRotatingFileHandler:
"""Create an hourly rotating log handler with retention configured in hours."""
path.parent.mkdir(parents=True, exist_ok=True)
handler = TimedRotatingFileHandler(
path,
when="H",
interval=1,
backupCount=max(1, int(LOG_RETENTION_HOURS)),
encoding="utf-8",
utc=False,
)
handler.setLevel(level)
handler.suffix = "%Y%m%d%H"
handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s [%(name)s] %(message)s"))
return handler
def configure_logging(app: Flask | None = None) -> None:
"""Route pyTorrent app, error and access logs to the configured data log directory."""
global _CONFIGURED
LOG_DIR.mkdir(parents=True, exist_ok=True)
if not _CONFIGURED:
app_handler = _make_handler(LOG_DIR / "app.log", logging.INFO)
error_handler = _make_handler(LOG_DIR / "error.log", logging.WARNING)
root = logging.getLogger()
root.setLevel(logging.INFO)
root.addHandler(app_handler)
root.addHandler(error_handler)
for name in ("pytorrent", "werkzeug", "gunicorn.error"):
logger = logging.getLogger(name)
logger.setLevel(logging.INFO)
logger.propagate = True
_CONFIGURED = True
if app is not None:
app.logger.setLevel(logging.INFO)
if not getattr(app, "_pytorrent_access_logging", False):
access_logger = logging.getLogger("pytorrent.access")
access_logger.setLevel(logging.INFO)
access_logger.propagate = False
access_logger.addHandler(_make_handler(LOG_DIR / "access.log", logging.INFO))
@app.before_request
def _mark_access_start() -> None:
g._access_started_at = time.perf_counter()
@app.after_request
def _write_access_log(response):
duration_ms = int((time.perf_counter() - getattr(g, "_access_started_at", time.perf_counter())) * 1000)
# Note: Application access logging is rotated hourly, unlike raw gunicorn stdout logs.
access_logger.info(
'%s "%s %s" %s %s %sms "%s"',
request.headers.get("X-Forwarded-For", request.remote_addr or "-"),
request.method,
request.full_path.rstrip("?"),
response.status_code,
response.calculate_content_length() or 0,
duration_ms,
request.headers.get("User-Agent", "-"),
)
return response
@app.teardown_request
def _log_unhandled_error(error: BaseException | None) -> None:
if error is not None:
app.logger.error("Unhandled request error", exc_info=(type(error), error, error.__traceback__))
app._pytorrent_access_logging = True # type: ignore[attr-defined]