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.
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] @ git+https://github.com/atick-faisal/observability-stack@v0.1.0#subdirectory=sdk/obstack"
| 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
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 mypy src
uv run ruff check .
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file obstack-0.1.0.tar.gz.
File metadata
- Download URL: obstack-0.1.0.tar.gz
- Upload date:
- Size: 79.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
df4b69966491babf0a98bb2dbdaede3804692e81b93ff29b1a7bc05da9bf98ed
|
|
| MD5 |
3ae7666ae7b3f1a59fbe4964907d6759
|
|
| BLAKE2b-256 |
d29ec7ae41a8913c05786264214828cefdf2399680b28d83bd860d4205b310bb
|
Provenance
The following attestation bundles were made for obstack-0.1.0.tar.gz:
Publisher:
cd.yml on atick-faisal/observability-stack
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
obstack-0.1.0.tar.gz -
Subject digest:
df4b69966491babf0a98bb2dbdaede3804692e81b93ff29b1a7bc05da9bf98ed - Sigstore transparency entry: 2370392861
- Sigstore integration time:
-
Permalink:
atick-faisal/observability-stack@c656ed7c348d9551d7427369db7c36263beadd62 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/atick-faisal
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
cd.yml@c656ed7c348d9551d7427369db7c36263beadd62 -
Trigger Event:
push
-
Statement type:
File details
Details for the file obstack-0.1.0-py3-none-any.whl.
File metadata
- Download URL: obstack-0.1.0-py3-none-any.whl
- Upload date:
- Size: 14.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
af55fa4eac1a6382df75008255f226ce0af75e01232dcbfe1f5d9862f6e7fee7
|
|
| MD5 |
16506d108b8025f6167460266a815d35
|
|
| BLAKE2b-256 |
4e99c904738e40464923211548b55a7ed4152a53c85bef3395229cfaf7933c25
|
Provenance
The following attestation bundles were made for obstack-0.1.0-py3-none-any.whl:
Publisher:
cd.yml on atick-faisal/observability-stack
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
obstack-0.1.0-py3-none-any.whl -
Subject digest:
af55fa4eac1a6382df75008255f226ce0af75e01232dcbfe1f5d9862f6e7fee7 - Sigstore transparency entry: 2370392949
- Sigstore integration time:
-
Permalink:
atick-faisal/observability-stack@c656ed7c348d9551d7427369db7c36263beadd62 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/atick-faisal
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
cd.yml@c656ed7c348d9551d7427369db7c36263beadd62 -
Trigger Event:
push
-
Statement type: