{"slug": "i-paid-for-the-embedding-my-logs-never-saw-the-request", "title": "I Paid for the Embedding. My Logs Never Saw the Request.", "summary": "A developer building the evorag retrieval service fixed a blind spot in their FastAPI logging middleware that caused failed requests to go unrecorded, including ones that had already incurred OpenAI embedding costs. The original middleware logged only after call_next returned, so any route that raised skipped the log entirely; the fix wraps the call in try/finally, initializes status_code to 500, and logs only variables guaranteed to exist on every path. The developer also notes that returning inside a finally block silently discards exceptions — a pattern Python 3.14 now warns on and ruff flags — and added a Decimal-based cost calculator for text-embedding-3-small at $0.02 per million tokens.", "body_md": "*A follow-up to [Counting OpenAI Tokens per Request in FastAPI](https://yusufoztoprak.hashnode.dev/counting-openai-tokens-per-request-in-fastapi-without-touching-my-use-cases).*\n\nI sent a document to evorag while the database was down.\n\nOpenAI was called. The embedding was created. I paid for it.\n\nThen the database write failed. The user got a 500.\n\nMy logs showed `embedding.created`. They did not show `request.completed`.\n\nNo status. No latency. No token count. The failed request was invisible.\n\nThat's the request I most needed to see.\n\nIn the last post, I built a logging middleware. This was its core:\n\n```\nmetrics = start_metrics()\nstarted = time.perf_counter()\nresponse = await call_next(request)\nlatency_ms = round((time.perf_counter() - started) * 1000, 2)\n\nlogger.info(\"request.completed\", ...)\n```\n\nI tested the happy path. I never tested a route that raises.\n\nIf the route raises, `call_next` raises. Every line below it is skipped. The log never runs.\n\nSo my request log had a blind spot. Successful requests were logged. Failed ones disappeared. Including the ones that had already cost money.\n\nThis time I wrote the test before the fix. A route that always raises. An assertion that `request.completed` is still logged.\n\nIt failed. Zero log records.\n\nThe tool is `try`/` finally`. `finally` runs however the `try` block ends: normally, with `return`, or with an exception.\n\nMy first attempt:\n\n```\nstatus_code = 500\nstarted = time.perf_counter()\ntry:\n    response = await call_next(request)\n    status_code = response.status_code\n    response.headers[\"x-request-id\"] = request_id\n    return response\nfinally:\n    latency_ms = round((time.perf_counter() - started) * 1000, 2)\n    response.headers[\"x-request-id\"] = request_id   # bug 1\n    logger.info(\"request.completed\", ...)\n    return response                                  # bug 2\n```\n\nThe test still failed. Both bugs were lines I had left over from the old version.\n\n**Bug 1.** When the route fails, `response` never exists. That line raises `UnboundLocalError`. The log still never runs. Same bug, one line lower.\n\n**Bug 2** is the one worth remembering.\n\nPeople usually say `finally` doesn't swallow exceptions. That's true. After `finally` finishes, the exception keeps going.\n\nUnless you `return` inside `finally`:\n\n``` python\ndef demo():\n    try:\n        raise ValueError(\"something broke\")\n    finally:\n        return \"everything is fine\"\n\ndemo()  # returns \"everything is fine\". No error.\n```\n\nThe exception is silently discarded. In a middleware, that hides the real error from the framework. It's dangerous enough that Python 3.14 emits a warning for it, and ruff flags it too.\n\n```\nstatus_code = 500\nstarted = time.perf_counter()\ntry:\n    response = await call_next(request)\n    status_code = response.status_code\n    response.headers[\"x-request-id\"] = request_id\n    return response\nfinally:\n    latency_ms = round((time.perf_counter() - started) * 1000, 2)\n    logger.info(\n        \"request.completed\",\n        extra={\"fields\": {\n            \"method\": request.method,\n            \"path\": request.url.path,\n            \"status\": status_code,\n            \"latency_ms\": latency_ms,\n            \"embedding_tokens\": metrics.embedding_tokens,\n            \"embedding_cost_usd\": metrics.embedding_cost_usd,\n        }},\n    )\n```\n\n`status_code` starts at 500. On success, it's overwritten. On failure, the log says 500.\n\n`finally` only uses variables that exist on every path: `started`, `status_code`, `metrics`. Never `response`.\n\nThe test went green.\n\nThe last post counted tokens. But I wanted the log to answer a simpler question: what did this request cost?\n\n`text-embedding-3-small` is $0.02 per million tokens, per OpenAI's pricing page. I wrote a small calculator, test first, using `Decimal`. Floats are the wrong type for money.\n\nNow the failed request from the opening shows up properly: status 500, the tokens it used, and what it cost. Before the fix, that money was spent and never recorded anywhere.\n\n`await` is a log line that skips failures.`finally` runs every time. A `return` inside it hides errors. I wrote that bug while fixing another one.\n*Code: [github.com/YusufOztoprak/evorag](https://github.com/YusufOztoprak/evorag)*\n\n*I drafted this post with help from Claude; the code, bugs, and measurements are from my own work on evorag.*", "url": "https://wpnews.pro/news/i-paid-for-the-embedding-my-logs-never-saw-the-request", "canonical_source": "https://dev.to/yusufoztoprak/i-paid-for-the-embedding-my-logs-never-saw-the-request-20l9", "published_at": "2026-11-05 12:40:14+00:00", "updated_at": "2026-11-05 12:49:42.754093+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "ai-infrastructure"], "entities": ["evorag", "OpenAI", "FastAPI", "text-embedding-3-small", "Python", "ruff"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/i-paid-for-the-embedding-my-logs-never-saw-the-request", "markdown": "https://wpnews.pro/news/i-paid-for-the-embedding-my-logs-never-saw-the-request.md", "text": "https://wpnews.pro/news/i-paid-for-the-embedding-my-logs-never-saw-the-request.txt", "jsonld": "https://wpnews.pro/news/i-paid-for-the-embedding-my-logs-never-saw-the-request.jsonld"}}