Skip to main content

obstack

Drop-in observability for FastAPI: structured JSON logs, Prometheus metrics, and OTLP traces that all carry the same four identity labels — app, service, env, host.

[!IMPORTANT] Those four names are the contract. They are spelled identically in every signal so Grafana can jump from a metric to a trace to a log line without renaming anything. See docs/labels.md — if this README and that document ever disagree, that document wins.

Install

uv add "obstack[grpc,sqlalchemy]"
Extra Pulls in When
grpc opentelemetry-exporter-otlp-proto-grpc app → local Alloy on :4317 (the default)
http opentelemetry-exporter-otlp-proto-http app → a collector over HTTP/1.1
sqlalchemy opentelemetry-instrumentation-sqlalchemy you pass engine=
errors sentry-sdk you set OBS_ERROR_DSN

Use

from fastapi import FastAPI
from obstack import setup_observability

app = FastAPI()
obs = setup_observability(app, engine=engine)

That one call configures structlog, registers the metrics and request-logging middleware, serves /metrics, and — when OBS_OTLP_ENDPOINT is set — starts exporting traces.

For a process with no FastAPI app:

from obstack import setup_worker_observability

obs = setup_worker_observability(metrics_port=9100)
...
obs.shutdown()

Configure

Every setting is an environment variable prefixed OBS_. The agent reads the same four identity variables from the same .env, which is what keeps the two halves consistent.

Variable Default Notes
OBS_APP required [a-z0-9-]+. Missing or malformed raises ObservabilityConfigError at startup
OBS_SERVICE api [a-z0-9-]+
OBS_ENV local local | staging | production
OBS_HOST socket.gethostname() lowercased; the agent is authoritative for metrics
OBS_VERSION 0.0.0 surfaces as fastapi_app_info{version=...}
OBS_LOG_LEVEL INFO
OBS_LOG_FORMAT auto auto is console when env=local, JSON otherwise — set json explicitly if a collector tails a container running with env=local
OBS_OTLP_ENDPOINT unset e.g. http://alloy:4317. Unset disables tracing entirely
OBS_OTLP_PROTOCOL grpc grpc | http
OBS_TRACE_SAMPLE_RATIO 1.0 parent-based ratio sampler
OBS_METRICS_ENABLED true
OBS_METRICS_PATH /metrics always excluded from its own metrics
OBS_ERROR_DSN unset GlitchTip / Sentry DSN. Events are tagged environment = OBS_ENV, release = <app>-<service>@<version> and server_name = OBS_HOST, so an error joins the other three signals

setup_observability(app, *, settings=None, engine=None, excluded_paths=()) — pass settings to bypass the environment, engine to trace SQLAlchemy queries (an AsyncEngine is unwrapped for you), and excluded_paths for health checks you do not want in the metrics.

What you get

Logs — newline-delimited JSON on stdout, one "HTTP" line per request, with trace_id and span_id injected whenever a span is active. Standard-library loggers (uvicorn, SQLAlchemy) are routed through the same chain, so every line is JSON. Add your own request-scoped fields with bind_request_context(tenant="acme"). sqlalchemy.engine, uvicorn.access, httpx and watchfiles are pinned to WARNING — each of them logs a line per request that duplicates one the SDK already emits.

Metrics — on an instance-local CollectorRegistry, so importing twice or setting up two apps in one process cannot collide:

fastapi_requests_total{app,service,env,method,route,status_code}
fastapi_requests_duration_seconds{app,service,env,method,route}
fastapi_exceptions_total{app,service,env,method,route,exception_type}
fastapi_requests_in_progress{app,service,env,method}
fastapi_app_info{app,service,env,version}

route is always the matched route pattern (/items/{item_id}), never the raw path — unmatched requests collapse to route="" so a scanner cannot mint a series per probed URL. Exposition is OpenMetrics, which is what makes exemplars survive; a plain-text /metrics endpoint drops them silently.

Traces — a Resource carrying app / service / env / host plus service.name ("{app}-{service}") and deployment.environment.name. If the exporter cannot be built the SDK logs at error and runs on without tracing; it never takes the app down for an infrastructure problem. A missing OBS_APP, by contrast, is a programmer error and fails at startup.

Setup also sets OTEL_SEMCONV_STABILITY_OPT_IN=http if it is unset, so HTTP spans carry http.request.method, http.response.status_code, url.path and server.address rather than the pre-1.0 names OpenTelemetry Python still defaults to. This is a process-wide switch read once, so it applies to every HTTP instrumentation in the process, including ones the SDK does not install (httpx, requests). Export it yourself — http/dup emits both sets — to keep control of the timing during a migration; the SDK will not override an explicit value.

Running multiple workers

[!WARNING] One worker per container. Scale by running more containers, not more workers.

uvicorn --workers N and fastapi run --workers N fork N processes that share one listening socket. Each holds its own CollectorRegistry, so consecutive scrapes land on different workers and a counter appears to move backwards — which Prometheus reads as a counter reset, making rate() and every dashboard built on it quietly wrong. Nothing logs an error; the graphs are just false.

The instance-local registry does not save you here: the problem is one endpoint answering from N independent processes. If you must run multiple workers in one container, set PROMETHEUS_MULTIPROC_DIR and use prometheus_client's multiprocess collector, and note that gauges and exemplars behave differently under it.

Develop

uv sync --all-extras
uv run pytest
uv run pyright
uv run ruff check .

Release files for obstack 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for obstack 0.1.1
File Size Uploaded
obstack-0.1.1.tar.gz 65.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for obstack 0.1.1
File Interpreter ABI Platform
obstack-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 80.6 kB

Release files / obstack-0.1.1.tar.gz

Download URL obstack-0.1.1.tar.gz
Size 65.9 kB
Tags Source
SHA-256 checksum
How to use checksums
013c4156c6fe4b7733ce534131b4e0c8d9a389091d1f7964212dcf1e0c9337a6
BLAKE2b-256 checksum
How to use checksums
761527a37285a173917e6bc7a05a578a5dfe13dc34e550fc2e6ca8e7581ba154
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 15, 2026.

Transparency log

Release files / obstack-0.1.1-py3-none-any.whl

Download URL obstack-0.1.1-py3-none-any.whl
Size 14.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b26f82ee6c04cab2c0d763035b4197b6e9bd5b496c59368ee6c58e64a0ad0175
BLAKE2b-256 checksum
How to use checksums
a08b4ec57473e2b5d4a93206180748ab264eb3ca7497e0c15aa3b6b6102062fc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 15, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

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