88 lines
3.2 KiB
Python
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]
|