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.

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

obstack-0.1.0.tar.gz (79.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

obstack-0.1.0-py3-none-any.whl (14.0 kB view details)

Uploaded Python 3

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

Hashes for obstack-0.1.0.tar.gz
Algorithm Hash digest
SHA256 df4b69966491babf0a98bb2dbdaede3804692e81b93ff29b1a7bc05da9bf98ed
MD5 3ae7666ae7b3f1a59fbe4964907d6759
BLAKE2b-256 d29ec7ae41a8913c05786264214828cefdf2399680b28d83bd860d4205b310bb

See more details on using hashes here.

Provenance

The following attestation bundles were made for obstack-0.1.0.tar.gz:

Publisher: cd.yml on atick-faisal/observability-stack

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for obstack-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 af55fa4eac1a6382df75008255f226ce0af75e01232dcbfe1f5d9862f6e7fee7
MD5 16506d108b8025f6167460266a815d35
BLAKE2b-256 4e99c904738e40464923211548b55a7ed4152a53c85bef3395229cfaf7933c25

See more details on using hashes here.

Provenance

The following attestation bundles were made for obstack-0.1.0-py3-none-any.whl:

Publisher: cd.yml on atick-faisal/observability-stack

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page