Cleaner error logging: sanic loggers at INFO, 499/503 for cancelled requests, shortened preview error reasons

- Force sanic.root/error/server back to INFO from before_server_start, after
  dev mode's runtime setLevel(DEBUG) — kills the useless 'Error Page:' noise
- Silence mediapreview.pool WARNINGs (timeouts already in access log extra);
  they previously fell to logging.lastResort with no level prefix
- Handle CancelledError with 499 (client disconnect / RequestCancelled) or
  503 (server shutdown) instead of Sanic's default 500 error page
- 422 preview failures now log 'backend: reason' in the access log, with
  upstream error text shortened (first line, no [Errno] prefix, cut at ': ');
  dev mode prints the full original error to console
This commit is contained in:
2026-08-12 03:45:39 +00:00
parent 3405248554
commit bd96b2c7ba
3 changed files with 61 additions and 3 deletions
+19 -1
View File
@@ -13,7 +13,7 @@ from blake3 import blake3
from mediapreview.office import close_oo_client, log_reachable_info from mediapreview.office import close_oo_client, log_reachable_info
from mediapreview.pool import shutdown_preview_workers, start_preview_workers from mediapreview.pool import shutdown_preview_workers, start_preview_workers
from sanic import Sanic, empty, raw, redirect from sanic import Sanic, empty, raw, redirect
from sanic.exceptions import Forbidden, NotFound from sanic.exceptions import Forbidden, NotFound, RequestCancelled
from sanic.log import logger from sanic.log import logger
from setproctitle import setproctitle from setproctitle import setproctitle
from stream_zip import ZIP_AUTO, stream_zip from stream_zip import ZIP_AUTO, stream_zip
@@ -35,6 +35,7 @@ from cista.sanic_logging import (
configure_access_logging, configure_access_logging,
configure_main_logging, configure_main_logging,
format_access_log, format_access_log,
reset_sanic_log_levels,
) )
from cista.sanic_logging import logger as access_logger from cista.sanic_logging import logger as access_logger
from cista.util.apphelpers import handle_sanic_exception from cista.util.apphelpers import handle_sanic_exception
@@ -124,11 +125,28 @@ app.blueprint(fileserver.bp)
app.exception(Exception)(handle_sanic_exception) app.exception(Exception)(handle_sanic_exception)
@app.exception(asyncio.CancelledError)
async def request_cancelled(req, e):
"""Request cancelled mid-flight (client disconnect or server shutdown).
Sanic wraps this as RequestCancelled (client disconnect only) — a
BaseException, so the generic Exception handler above never sees it — and
its default handler renders a 500 error page. Report 499 for client
disconnects and 503 for server-side cancellation instead; no traceback
(quiet=True), since there is nothing to fix. The connection is usually
already gone.
"""
if not getattr(req.ctx, "log_extra", None):
req.ctx.log_extra = "cancelled"
return empty(499 if isinstance(e, RequestCancelled) else 503)
setproctitle("cista-main") setproctitle("cista-main")
@app.before_server_start @app.before_server_start
async def main_start(app): async def main_start(app):
reset_sanic_log_levels()
config.load_config() config.load_config()
onlyoffice.configure() onlyoffice.configure()
setproctitle(f"cista {config.config.path.name}") setproctitle(f"cista {config.config.path.name}")
+26 -2
View File
@@ -6,6 +6,7 @@ Sanic with auth, etag negotiation and the in-memory response cache.
""" """
import asyncio import asyncio
import re
import urllib.parse import urllib.parse
from pathlib import PurePosixPath from pathlib import PurePosixPath
from urllib.parse import unquote from urllib.parse import unquote
@@ -38,6 +39,21 @@ bp = Blueprint("preview", url_prefix="/preview")
_preview_cache = PreviewCache(capacity=500) _preview_cache = PreviewCache(capacity=500)
def _shorten_error(detail: str) -> str:
"""Shorten an upstream backend error for the single-line access log.
Backend errors arrive verbatim from ffmpeg/pyvips/pymupdf and often carry
an '[Errno N]' prefix, the input file path, and multi-line library noise —
all redundant with the URL already in the log line. Keep the first line,
drop the bracket prefix, and cut at the first ': ' separator.
"""
lines = detail.splitlines()
if not lines:
return ""
first_line = re.sub(r"^\[[^\]]*\]\s*", "", lines[0].strip())
return first_line.split(": ", 1)[0]
@bp.on_request @bp.on_request
async def verify_preview(request): async def verify_preview(request):
"""Verify access to preview routes.""" """Verify access to preview routes."""
@@ -125,8 +141,16 @@ async def preview(req, path):
if captured: if captured:
detail = captured.splitlines()[0] detail = captured.splitlines()[0]
# The worker already logged the failure (with traceback where the # The worker already logged the failure (with traceback where the
# error occurred) — annotate the access log instead of re-logging. # error occurred) — annotate the access log instead of re-logging,
req.ctx.log_extra = e.backend or detail # with a shortened reason. In dev mode, print the full error too.
backend = e.backend or _expected_preview_backend(filepath)
if req.app.debug:
full = detail
if e.stderr and e.stderr.strip() not in detail:
full = f"{detail}\n{e.stderr.strip()}"
logger.warning("[%s] preview failed: %s", backend, full.strip())
short = _shorten_error(detail)
req.ctx.log_extra = f"{backend}: {short}" if short else backend
return empty(422) return empty(422)
except asyncio.CancelledError: except asyncio.CancelledError:
# Server shutdown or client disconnect: the connection is being torn # Server shutdown or client disconnect: the connection is being torn
+16
View File
@@ -294,7 +294,23 @@ def configure_main_logging() -> None:
# Patch the config defaults too, so the level survives Sanic's dictConfig. # Patch the config defaults too, so the level survives Sanic's dictConfig.
LOGGING_CONFIG_DEFAULTS["loggers"]["sanic.websockets"]["level"] = "ERROR" LOGGING_CONFIG_DEFAULTS["loggers"]["sanic.websockets"]["level"] = "ERROR"
logging.getLogger("sanic.websockets").setLevel(logging.ERROR) logging.getLogger("sanic.websockets").setLevel(logging.ERROR)
# Preview worker timeouts are already annotated in the access log extra;
# the pool's WARNING would otherwise fall to logging.lastResort, printing
# a bare message with no level prefix.
logging.getLogger("mediapreview.pool").setLevel(logging.ERROR)
# Also reformat any handlers already attached (covers the initial Sanic() call) # Also reformat any handlers already attached (covers the initial Sanic() call)
for name in ("sanic.root", "sanic.error", "sanic.server", "sanic.websockets"): for name in ("sanic.root", "sanic.error", "sanic.server", "sanic.websockets"):
for handler in logging.getLogger(name).handlers: for handler in logging.getLogger(name).handlers:
handler.setFormatter(_EmojiFormatter()) handler.setFormatter(_EmojiFormatter())
def reset_sanic_log_levels() -> None:
"""Force Sanic's loggers back to INFO in debug/dev mode.
Debug mode enables DEBUG on sanic.root at runtime
(ApplicationState.set_mode calls logger.setLevel(DEBUG)), which unleashes
useless noise like the 'Error Page:' content-negotiation messages. Call
from before_server_start so the override lands after Sanic's own setup.
"""
for name in ("sanic.root", "sanic.error", "sanic.server"):
logging.getLogger(name).setLevel(logging.INFO)