Zero-config RED-metrics client for FastAPI/Starlette services, reporting to a mAPI-ng collector.
Open source. Start free on the hosted service (no card), or self-host the complete MIT stack.
Aggregates per-endpoint rate/errors/duration into a DDSketch client-side and
ships batched summaries over the Connect unary protocol, matching the wire
contract used by mAPI-ng's Go clients (maping/client). No MAPING_KEY set
means the middleware is a no-op: safe to add to any app.
This client behaves identically either way: only the env vars you set differ.
- Hosted, forever free, no card. Sign up at
mapi-ng.com, create a key, set
MAPING_KEY. That is the whole setup: the client already defaults to the hosted ingest endpoint, soMAPING_ENDPOINTis not needed. - Self-hosted. Run the complete MIT stack yourself
(
make localfor dev,make upfor prod, see arhuman/maping). SetMAPING_KEYfor your own instance andMAPING_ENDPOINTto point at it.
Nothing here is held back to push you toward the hosted plan: the wire contract, the server, and this client are all MIT. Start hosted and move to self-hosting later, or the reverse, at any time. Convenience, not lock-in.
Early development. Wire contract is pinned from
arhuman/maping's proto/maping/v1/maping.proto via scripts/sync-proto.sh;
see that repo's docs/adr/ for the design rationale behind the wire format
(DDSketch, ADR-0001; Connect protocol, ADR-0002).
Set your credentials (pick one; see "Hosted or self-hosted" above):
# Hosted (forever free, no card)export MAPING_KEY="mk_live_..."# Self-hostedexport MAPING_KEY="mk_live_..."export MAPING_ENDPOINT="https://your-collector.internal"Then the same code either way:
fromcontextlibimportasynccontextmanagerfromfastapiimportFastAPIfrommapingimportRecorderfrommaping.asgiimportMapingMiddlewarerecorder=Recorder() # reads MAPING_KEY, and MAPING_ENDPOINT if self-hosting, from env@asynccontextmanagerasyncdeflifespan(_: FastAPI):
awaitrecorder.start()
yieldawaitrecorder.shutdown() # must run AFTER the server stops accepting requestsfastapi_app=FastAPI(lifespan=lifespan)
# Wrap the WHOLE app -- not app.add_middleware() -- so this sits outside# Starlette's ServerErrorMiddleware and observes the real final status even# after an unhandled exception. See src/maping/asgi.py for why.app=MapingMiddleware(fastapi_app, recorder=recorder)See examples/fastapi_app.py for a fuller quickstart, including labelled
errors, aborted requests, and downstream-call timing.
git clone https://github.com/arhuman/maping-python.git
cd maping-python
pip install -e ".[dev,zstd]"
ruff check src/ tests/ examples/
mypy src/
pytest tests/ -vSame three commands the CI workflow runs on every push and PR (see
.github/workflows/_test.yml, the reusable workflow both ci.yml and
release.yml call).
- Open an issue or PR against arhuman/maping-python.
- Keep changes scoped: one concern per PR, with tests.
ruff check,mypy, andpytestall have to pass before review; CI enforces this on every PR. - The wire contract under
src/maping/proto/v1/is pinned from the corearhuman/mapingrepo viascripts/sync-proto.sh(seePINNED_REFfor the exact pin). Don't hand-edit the generatedmaping_pb2.py/maping_pb2.pyi; change the pin and re-run the sync script instead.
- RED metrics (rate, errors, duration, DDSketch percentiles) match the Go client's wire contract exactly.
- InstanceWindow/USE gauges (CPU, memory, GC, goroutine-equivalents) are
best-effort approximations of the Go runtime stats the field names were
originally defined for; Python's GC/concurrency model has no exact
equivalent to Go's runtime introspection. See
src/maping/_sampler.py.