Access-log extras: remote-user on /_api, visitor info on /_ws

This commit is contained in:
2026-09-03 19:38:15 +00:00
parent 38af57218a
commit d6d07db2c5
2 changed files with 32 additions and 1 deletions
+25
View File
@@ -37,6 +37,7 @@ from pathlib import Path
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from fastapi.responses import Response from fastapi.responses import Response
from fastapi_vue import Frontend from fastapi_vue import Frontend
from starlette.types import ASGIApp, Receive, Scope, Send
from pagerite import api, files, pages, tracking from pagerite import api, files, pages, tracking
from pagerite.__main__ import DEVMODE from pagerite.__main__ import DEVMODE
@@ -53,6 +54,28 @@ frontend = Frontend(
) )
class _AccessLogExtraMiddleware:
"""Fill the ``log_extra`` slot of fastapi_vue's access log.
Everything under ``/_api`` is gated by the SSO forward-auth, which names
the authenticated user in the ``remote-user`` header; put that user on
the access-log line, for plain requests and WebSocket open/close alike.
The scope dict is shared with the outer AccessLogMiddleware, which reads
the slot back at response/accept/close time.
"""
def __init__(self, app: ASGIApp) -> None:
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] in ("http", "websocket") and scope["path"].startswith("/_api"):
headers = dict(scope["headers"])
user = headers.get(b"remote-user", b"").decode("latin-1")
if user:
scope.setdefault("state", {})["log_extra"] = user
await self.app(scope, receive, send)
@asynccontextmanager @asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncGenerator: async def lifespan(_app: FastAPI) -> AsyncGenerator:
"""Open the database (migrations run inside kanta.open), load assets, load GeoIP.""" """Open the database (migrations run inside kanta.open), load assets, load GeoIP."""
@@ -83,6 +106,8 @@ app = FastAPI(
openapi_url=None, openapi_url=None,
) )
app.add_middleware(_AccessLogExtraMiddleware)
@app.middleware("http") @app.middleware("http")
async def _headers(request: Request, call_next) -> Response: async def _headers(request: Request, call_next) -> Response:
+7 -1
View File
@@ -435,10 +435,16 @@ async def activity_ws(ws: WebSocket) -> None:
DB-IP geoip lookups happen in background tasks so message handling is DB-IP geoip lookups happen in background tasks so message handling is
never delayed by slow DNS or the first MMDB decompress. never delayed by slow DNS or the first MMDB decompress.
""" """
await ws.accept()
ip = _client_ip(ws) ip = _client_ip(ws)
ua = ws.headers.get("user-agent", "") ua = ws.headers.get("user-agent", "")
accept_language = ws.headers.get("accept-language", "") accept_language = ws.headers.get("accept-language", "")
# Identify the visitor on the access-log open/close lines (the IP is
# already printed there): compact UA plus the browser's language tag.
lang, _country = analytics._parse_accept_language(accept_language)
ws.scope.setdefault("state", {})["log_extra"] = " ".join(
part for part in (analytics._compact_user_agent(ua), lang) if part
)
await ws.accept()
try: try:
while True: while True:
text = await ws.receive_text() text = await ws.receive_text()