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.
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
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 .
Metadata
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)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| obstack-0.1.1.tar.gz | 65.9 kB | With provenance | Details for obstack-0.1.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| obstack-0.1.1-py3-none-any.whl | Python 3 | none | any | Details for obstack-0.1.1-py3-none-any.whl |
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 logRelease 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 logSecurity for obstack 0.1.1
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.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for obstack-0.1.1.tar.gz
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 logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.