From 549d987908817d191473f24baa11cd00f32be554 Mon Sep 17 00:00:00 2001 From: Mirko Dietrich Date: Thu, 6 Nov 2025 13:54:46 +0100 Subject: [PATCH] feat(backend): configurable web server access logging - Add `--access-log-mode` option to `run` command to control logging level. - Introduce `FilteredAccessLogger` to filter requests according to mode. --- questionpy_sdk/commands/run.py | 13 ++++++++++++- questionpy_sdk/webserver/server.py | 21 +++++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/questionpy_sdk/commands/run.py b/questionpy_sdk/commands/run.py index 37353cf5..f00596f8 100644 --- a/questionpy_sdk/commands/run.py +++ b/questionpy_sdk/commands/run.py @@ -13,7 +13,7 @@ from questionpy_sdk.watcher import Watcher from questionpy_sdk.webserver import WebServer from questionpy_sdk.webserver.errors import EnvironmentVariablesMissingError -from questionpy_sdk.webserver.server import WebServerArgs +from questionpy_sdk.webserver.server import AccessLogMode, WebServerArgs from questionpy_server.worker.impl.subprocess import SubprocessWorker from questionpy_server.worker.impl.thread import ThreadWorker from questionpy_server.worker.runtime.package_location import DirPackageLocation @@ -58,6 +58,15 @@ async def async_run(webserver_args: WebServerArgs) -> None: show_default=True, help="The worker implementation to use. Thread workers offer no isolation but may improve debugging experience.", ) +@click.option( + "--access-log", + "-l", + "access_log_mode", + type=click.Choice(("none", "api", "all"), case_sensitive=False), + default="api", + show_default=True, + help="Access log mode to use, none disables logging, api logs only API requests, all logs everything.", +) def run( package: str, state_storage_path: Path, @@ -66,6 +75,7 @@ def run( *, watch: bool, worker: Literal["subprocess", "thread"], + access_log_mode: AccessLogMode, ) -> None: """Run a package. @@ -85,6 +95,7 @@ def run( host=host, port=port, worker_class=ThreadWorker if worker == "thread" else SubprocessWorker, + access_log_mode=access_log_mode, ) if watch: diff --git a/questionpy_sdk/webserver/server.py b/questionpy_sdk/webserver/server.py index 7fc26657..4380ccf7 100644 --- a/questionpy_sdk/webserver/server.py +++ b/questionpy_sdk/webserver/server.py @@ -6,7 +6,7 @@ import os from pathlib import Path from types import TracebackType -from typing import ClassVar, NotRequired, Self, TypedDict, Unpack +from typing import ClassVar, Literal, NotRequired, Self, TypedDict, Unpack from aiohttp import web @@ -29,6 +29,8 @@ log = logging.getLogger("questionpy-sdk:web-server") +type AccessLogMode = Literal["all", "api", "none"] + class WebServerArgs(TypedDict): package_location: PackageLocation @@ -36,6 +38,19 @@ class WebServerArgs(TypedDict): host: NotRequired[str] port: NotRequired[int] worker_class: NotRequired[type[Worker]] + access_log_mode: NotRequired[AccessLogMode] + + +class FilteredAccessLogger(web.AccessLogger): + mode: AccessLogMode = "api" + + def log(self, request: web.BaseRequest, response: web.StreamResponse, time: float) -> None: + if self.mode == "none": + return + if self.mode == "api" and not request.path.startswith(f"{API_PATH_PREFIX}/"): + return + + super().log(request, response, time) class WebServer: @@ -47,6 +62,7 @@ def __init__(self, **kwargs: Unpack[WebServerArgs]) -> None: self._host = kwargs.get("host", "localhost") self._port = kwargs.get("port", 8080) self._worker_class = kwargs.get("worker_class", self.DEFAULT_WORKER_CLASS) + self._access_log_mode = kwargs.get("access_log_mode", "api") self._app: web.Application self._api_app: web.Application @@ -83,7 +99,8 @@ async def __aenter__(self) -> Self: # Create web app self._app = self._create_webapp() - self._runner = web.AppRunner(self.app) + FilteredAccessLogger.mode = self._access_log_mode + self._runner = web.AppRunner(self.app, access_log=log, access_log_class=FilteredAccessLogger) await self._runner.setup() await web.TCPSite(self._runner, self._host, self._port).start() self._print_status()