neva-asgi
ASGI middleware for the Neva framework.
python-neva is the framework-agnostic core: it must not import an ASGI
framework. This package holds the middleware that used to live in
neva.obs.middleware, so that boundary holds.
It sits below the protocol integrations rather than inside one — neva-fastapi
and neva-faststream (in its ASGI mode) can both consume it without depending
on each other.
Install
uv add neva-asgi
Middleware
CorrelationMiddleware
Assigns a correlation ID to every HTTP and WebSocket request, reusing an
inbound X-Request-ID or X-Correlation-ID when it is a valid UUID and
generating one otherwise. The ID is published on scope["state"]["correlation_id"]
and echoed on the response. It is also bound into structlog's context, so every
log line emitted while handling the request carries correlation_id without the
caller threading it through — structlog's default processor chain merges
contextvars, so this needs no logging configuration.
from neva.asgi import CorrelationMiddleware
app.add_middleware(CorrelationMiddleware, header_name="X-Request-ID")
generator and validator are both injectable, so a service that uses ULIDs or
a non-UUID scheme can swap them.
ProfilerMiddleware
Profiles HTTP requests with pyinstrument
and writes one report per profiled request, named
profile_<duration>s_<METHOD>_<route>_<id> so that a directory listing is a
slowest-first ordering. The trailing ID is the correlation ID when
CorrelationMiddleware runs ahead of it, and a timestamp when it does not.
from neva.asgi import ProfilerMiddleware
app.add_middleware(ProfilerMiddleware, path="./profiles", interval=0.001)
Intended for local and staging use: by default it profiles every request and
writes a file for each. Four knobs narrow that — should_profile (a predicate
over the ASGI scope), sample_rate, max_reports, and min_duration, which
keeps a report only when the request turned out to be slow:
app.add_middleware(
ProfilerMiddleware,
path="./profiles",
min_duration=1.0, # only requests over a second leave a file
max_reports=200, # and never more than 200 of them
)
To profile one request in a running deployment without a restart, give it a
trigger_header and a trigger_secret — the switch then travels with the
request rather than living in one worker process. A manifest.jsonl beside the
reports makes the collection greppable, renderer selects any pyinstrument
renderer (Speedscope included), and a profiled request emits a profile span
carrying the report name so a slow trace can reach its flame graph.
Because pyinstrument's sampler is process-global, only one request is profiled at a time; a request that overlaps a profiled one is served unprofiled rather than queued. The report is rendered and written on a worker thread, after the response has gone out, so it does not delay what the worker serves next.
Profiling never alters the response, and a profile that cannot be taken or written is logged, not raised.
TracingMiddleware
Requires the otel extra:
uv add "neva-asgi[otel]"
Emits an OpenTelemetry HTTP server span per request, carrying the matched
route's template rather than the resolved path — /users/{id}, not
/users/42. Without that, every request is its own operation name and no
endpoint can be aggregated, so a trace backend has nothing to graph.
from neva.asgi import CorrelationMiddleware
from neva.asgi.tracing import TracingMiddleware
from neva.fastapi import route_template
app.add_middleware(TracingMiddleware, route_resolver=route_template)
app.add_middleware(CorrelationMiddleware)
The span is not written here: opentelemetry-instrumentation-asgi already
carries the semantic conventions, the server metrics, header capture, context
propagation and WebSockets. This middleware composes it and adds the one thing
it cannot know — the route — because it runs outside the application, where
nothing has matched yet. opentelemetry-instrumentation-fastapi is not the
answer either: it re-walks app.routes to resolve the route, and since FastAPI
0.137 made include_router lazy that walk no longer reaches the prefixed
template.
route_resolveris injected rather than imported, so this package depends on no web framework andneva-fastapidepends on no OpenTelemetry.neva.fastapi.route_templateis the FastAPI implementation; any(scope) -> str | Nonewill do.- The correlation ID is stamped on the span as
correlation_idwhenCorrelationMiddlewarehas run, which is what pivots a client-reported request ID to the trace that served it. Either middleware order works — the span is stamped once routing is done, by which point both have run. - Unmatched requests keep a bare method as the span name (
GET, orHTTPfor a method the client invented) and carry nohttp.route. Upstream's default appends the request path, which lets a bot sweep mint one operation name per URL it probes. - Unknown keyword arguments are forwarded to the instrumentor, so
excluded_urls, the request hooks, header capture and an explicittracer_providerall still work. Two upstream defaults are overridden and can be set back:default_span_detailsandexclude_spans, the latter because upstream emits a child span per ASGIreceiveandsendevent.
Importing neva.asgi does not import OpenTelemetry — neva.asgi.tracing
is the only module that does, and nothing re-exports it. A test asserts that in
a subprocess, because a single convenience re-export would quietly make the
extra mandatory for every CorrelationMiddleware user.
Develop
uv sync --all-extras
poe lint && poe fmt && poe tc && poe test
Agent guidelines
neva/asgi/guidelines/fragments/ is what this package contributes to
neva-boost. Those files are the source and
the thing to edit; poe guidelines holds them to the frontmatter contract and runs in
CI.
Renders are not committed — requires is evaluated against installed versions, so
one is only correct for the environment that produced it. Compose your own:
poe guidelines-render # .claude/skills/, gitignored
uv run neva-boost install --target agents # AGENTS.md instead
Commits follow Conventional Commits with gitmoji via cz commit; releases are
cut with cz bump.
Metadata
Release files for neva-asgi 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| neva_asgi-0.3.0.tar.gz | 132.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| neva_asgi-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 158.1 kB
Release files / neva_asgi-0.3.0.tar.gz
| Download URL | neva_asgi-0.3.0.tar.gz |
|---|---|
| Size | 132.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7b622e43ca6ecafad779d30dcf367f088e8d05ab6b0e1c482e80a0bf1eb3266f
|
|
BLAKE2b-256 checksum How to use checksums |
e7c78f9b7f4802a07397fc86e7a05f29d84820d2b4a137dfcc6ad8aa6b31769b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.8.22
|
Release files / neva_asgi-0.3.0-py3-none-any.whl
| Download URL | neva_asgi-0.3.0-py3-none-any.whl |
|---|---|
| Size | 25.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b83efcb7b36dcef8a750daaf59b162a68dfbcdc540f1675cceffb3a712950a74
|
|
BLAKE2b-256 checksum How to use checksums |
9739a3fec0a295d0f571fb875379a1e88202a1d3a4e1fa91ab28fdeb63379816
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.8.22
|