Log device-facing /frame/* requests in the server log
Build and push server image / test (push) Successful in 39s
Build and push server image / build-and-push (push) Failing after 1m59s
Build and push server image / deploy (push) Has been skipped

The admin log viewer only ever showed exceptions from device.py, not
successful requests -- no way to see a request that was slow-but-200,
or a device probing with a stale/wrong token. Adds a middleware that
logs method, path, device id (never the token), status, and wall time
for every /frame/* request.
This commit is contained in:
2026-07-28 03:24:13 +00:00
parent 83994aab7b
commit d1f1968317
2 changed files with 40 additions and 0 deletions
+25
View File
@@ -16,6 +16,7 @@ pre-database config.json deployment on first boot."""
from __future__ import annotations from __future__ import annotations
import logging import logging
import time
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
@@ -48,6 +49,30 @@ migration.run_migrations()
app = FastAPI(title="ESPresso Frame Server") app = FastAPI(title="ESPresso Frame Server")
templates = Jinja2Templates(directory="app/templates") templates = Jinja2Templates(directory="app/templates")
@app.middleware("http")
async def log_device_requests(request: Request, call_next):
"""Access log for the firmware-facing /frame/* protocol -- the admin
log viewer otherwise only ever shows exceptions (device.py logs
those, not successful requests), so a slow-but-200 request or a
device hammering a stale/wrong token leaves no trace at all. Logs
the device id (query param, not the token -- never log credentials)
and wall time, which is exactly what's needed to spot a request that
blew past the firmware's fixed HTTP timeout without technically
failing server-side."""
if not request.url.path.startswith("/frame/"):
return await call_next(request)
start = time.monotonic()
device_id = request.query_params.get("id", "") or "-"
response = await call_next(request)
elapsed_ms = (time.monotonic() - start) * 1000
logger.info(
"%s %s id=%s -> %d (%.0fms)",
request.method, request.url.path, device_id, response.status_code, elapsed_ms,
)
return response
app.mount("/static", StaticFiles(directory="app/static"), name="static") app.mount("/static", StaticFiles(directory="app/static"), name="static")
app.include_router(device.router) app.include_router(device.router)
+15
View File
@@ -8,6 +8,7 @@ from __future__ import annotations
import logging import logging
from app.logging_setup import LOG_PATH from app.logging_setup import LOG_PATH
from app.models import Frame
from .conftest import login, make_user from .conftest import login, make_user
@@ -52,6 +53,20 @@ def test_admin_can_download_log_file(client, db_session):
assert b"marker-line-for-download" in resp.content assert b"marker-line-for-download" in resp.content
def test_device_requests_are_logged(client, db_session):
_setup_admin_and_user(client, db_session)
frame = db_session.get(Frame, 1)
frame.device_id = "aabbccddeeff"
db_session.commit()
resp = client.get(f"/frame/config?id={frame.device_id}&token={frame.device_token}")
assert resp.status_code == 200
login(client, "alice", "hunter22")
log_resp = client.get("/admin/logs")
# Jinja HTML-escapes the rendered <pre>, so "->" becomes "-&gt;".
assert f"GET /frame/config id={frame.device_id} -&gt; 200" in log_resp.text
def test_download_404s_before_any_log_written(client, db_session, monkeypatch): def test_download_404s_before_any_log_written(client, db_session, monkeypatch):
_setup_admin_and_user(client, db_session) _setup_admin_and_user(client, db_session)
login(client, "alice", "hunter22") login(client, "alice", "hunter22")