Skip to content

Adopt uvicorn's loggers so the format holds either way - #65

Merged
EdwardPham1615 merged 1 commit into
mainfrom
fix/adopt-uvicorn-loggers
Sep 27, 2026
Merged

EdwardPham1615 merged 1 commit into
mainfrom
fix/adopt-uvicorn-loggers

Conversation

@EdwardPham1615

Copy link
Copy Markdown
Owner

Candidate addition #6.

Started with the uvicorn CLI, uvicorn applies its LOGGING_CONFIG via dictConfig before importing the app: the uvicorn logger gets its own handler and propagate: False. setup_logging replaces the root handler, which never reaches it — so uvicorn's startup and shutdown lines stayed plain text in the middle of an otherwise ECS stream, and a shipper parsing one JSON object per line drops exactly the lines you want during an incident.

Measured in the consuming service: 8 of 19 production lines were not ECS.

run_uvicorn already passes log_config=None and was unaffected. Nothing stopped a consumer from using the CLI, as that service first did.

Where the recorded proposal would have caused a new bug

The note said to clear the handlers of uvicorn, uvicorn.error and uvicorn.access. That third one is wrong:

RequestContextMiddleware already emits the access log (request_completed, with status, duration, route and request ID — request_context.py:217). Adopting uvicorn's as well puts two access lines on every request, which is precisely why setup_logging has been silencing it by level all along. It stays silenced, with a comment saying why it is an exception rather than an oversight, and one of the new tests exists purely to fail if somebody "finishes the job".

uvicorn.error needs no entry of its own — uvicorn gives it no handler, so it propagates to uvicorn and the same fix carries it. Verified against uvicorn.config.LOGGING_CONFIG. Listed anyway so a future change to that config cannot quietly reintroduce the problem.

A diagnostic detour worth recording

The restore-and-recheck step of the mutation check misled me for several rounds, and the cause is nasty:

The mutation was size-neutral — adding , "uvicorn.access" and removing "uvicorn.access", , 18 characters each way — and the restore landed in the same second. Python's bytecode cache validates on (mtime, size), so both matched and it kept running the mutated bytecode from __pycache__ while inspect.getsource showed the corrected file on disk.

The symptom was absurd: httpx got its level set but uvicorn.access did not, from the same loop. Tracing Logger.setLevel was what separated "the code is wrong" from "the code you are reading is not the code running". Both mutations were then re-run with __pycache__ cleared and behave correctly.

Takeaway: a mutation check should clear __pycache__ between steps.

Verification

405 passed (from 403), coverage 92.24%, make extras-check green.

Mutation-checked both directions:

mutation result
remove the adoption loop test_uvicorn_loggers_are_adopted fails
also adopt uvicorn.access test_uvicorn_access_is_not_adopted and the pre-existing test_noisy_libraries_are_quieted fail

The autouse restore_logging fixture was extended to save and restore the handlers and propagate of uvicorn/uvicorn.error, since setup_logging now mutates process-global state that it previously left alone.

Next

Four candidates left: #2 (request.state.user for the access log), #4 (token provider transport), #1 (Repository.get_or_raise), #7 (log.logger, breaking). Then release 0.3.0.

Started with the `uvicorn` CLI, uvicorn applies its LOGGING_CONFIG via
dictConfig before importing the app: the `uvicorn` logger gets its own
handler and propagate: False. setup_logging replaces the *root* handler,
which never reaches it, so uvicorn's startup and shutdown lines stayed
plain text in the middle of an otherwise ECS stream -- and a shipper
parsing one JSON object per line drops exactly the lines you want during
an incident. Measured in a consuming service: 8 of 19 production lines
were not ECS.

run_uvicorn already passes log_config=None and was unaffected; nothing
stopped a consumer from using the CLI, as that service first did.

uvicorn.error needs no entry of its own -- uvicorn gives it no handler, so
it propagates to `uvicorn` and the same fix carries it. Listed anyway so a
future change to that config cannot quietly reintroduce the problem.

uvicorn.access is deliberately NOT adopted, and this is where the recorded
proposal would have caused a new bug: it said to clear the handlers of all
three. RequestContextMiddleware already emits the access log, so uvicorn's
version as well would put two access lines on every request -- which is
why setup_logging has been silencing it by level all along. One of the new
tests exists purely to fail if somebody "finishes the job".

The restore-and-recheck step of the mutation check misled me for several
rounds and is worth recording: the mutation happened to be size-neutral
(adding `, "uvicorn.access"` and removing `"uvicorn.access", ` -- 18
characters each way) and the restore landed in the same second, so
(mtime, size) matched and Python kept running the mutated bytecode from
__pycache__ while inspect.getsource showed the corrected file. Tracing
Logger.setLevel was what separated "the code is wrong" from "the code you
are reading is not the code running". Mutation checks want __pycache__
cleared.
@EdwardPham1615 EdwardPham1615 self-assigned this Sep 27, 2026
@EdwardPham1615 EdwardPham1615 added bug Something isn't working documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file labels Sep 27, 2026
@EdwardPham1615
EdwardPham1615 merged commit efb7a63 into main Sep 27, 2026
3 checks passed
@EdwardPham1615
EdwardPham1615 deleted the fix/adopt-uvicorn-loggers branch September 27, 2026 17:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant