Skip to main content

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_resolver is injected rather than imported, so this package depends on no web framework and neva-fastapi depends on no OpenTelemetry. neva.fastapi.route_template is the FastAPI implementation; any (scope) -> str | None will do.
  • The correlation ID is stamped on the span as correlation_id when CorrelationMiddleware has 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, or HTTP for a method the client invented) and carry no http.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 explicit tracer_provider all still work. Two upstream defaults are overridden and can be set back: default_span_details and exclude_spans, the latter because upstream emits a child span per ASGI receive and send event.

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)

Source distribution for neva-asgi 0.3.0
File Size Uploaded
neva_asgi-0.3.0.tar.gz 132.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for neva-asgi 0.3.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.0

2 release files

0.1.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page