fix(logging): defensive trace_id seed + falsy error/payload in CotJsonFormatter

log_requests middleware (BaseHTTPMiddleware) showed no-trace
because anyio.create_task_group() in Starlette 0.50.0 does not
always propagate ContextVar from raw ASGI TraceContextMiddleware.

Fix 1: defensive get_trace_id() check at log_requests entry —
        if empty, seed_trace_id() to ensure every request has one.

Fix 2: CotJsonFormatter used 'if error:' and 'if payload:' which
        silently drop empty strings (str(e)='') and empty dicts.
        Changed to 'is not None' checks — preserves all data.

Root cause: 12 EXPLORE-without-error entries from belief_scope's
exception handler where Exception() has empty message.
This commit is contained in:
2026-06-30 17:41:13 +03:00
parent 2f238dee13
commit 60674c8639
2 changed files with 9 additions and 3 deletions

View File

@@ -58,7 +58,7 @@ from .api.routes import (
)
from .api.routes.validation_tasks import router as validation_tasks
from .core.auth.security import get_password_hash
from .core.cot_logger import seed_trace_id
from .core.cot_logger import get_trace_id, seed_trace_id
from .core.database import AuthSessionLocal, init_db
from .core.encryption_key import ensure_encryption_key
from .core.logger import belief_scope, logger
@@ -338,6 +338,12 @@ async def network_error_handler(request: Request, exc: NetworkError):
# @POST Logs request and response details.
@app.middleware("http")
async def log_requests(request: Request, call_next):
# Defensive trace_id seeding: TraceContextMiddleware (raw ASGI) seeds
# trace_id in the root context, but BaseHTTPMiddleware may run in a
# child task that doesn't inherit the ContextVar (Starlette 0.50.0 +
# anyio.create_task_group). If empty, seed now.
if not get_trace_id():
seed_trace_id()
with belief_scope("log_requests"):
# Avoid spamming logs for polling endpoints
is_polling = request.url.path.endswith("/api/tasks") and request.method == "GET"

View File

@@ -103,9 +103,9 @@ class CotJsonFormatter(logging.Formatter):
span_id = _span_id.get()
if span_id:
log_obj["span_id"] = span_id
if payload:
if payload is not None:
log_obj["payload"] = payload
if error:
if error is not None:
log_obj["error"] = error
return json.dumps(log_obj, ensure_ascii=False, default=str)