Observability (tracing + slow queries)¶
Logs tell you what happened in a service; distributed tracing tells you
where the time went on a request that crosses several services, and the
SlowQueryLogger points at which query is dragging your p99. This
recipe covers both.
Where this fits
The RequestIDMiddleware correlates logs per request;
OpenTelemetry correlates spans across services. They complement each
other — use them together.
Distributed tracing with OpenTelemetry¶
setup_tracing installs an OpenTelemetry provider and auto-instruments the
common layers of a Tempest service: FastAPI (incoming requests), SQLAlchemy
(queries), and httpx (outbound calls). Requires the [otel] extra:
Call it once at startup, after the app exists and (when you want to trace queries) after the database has connected:
from fastapi import FastAPI
from tempest_fastapi_sdk import AsyncDatabaseManager, setup_tracing
app: FastAPI = FastAPI()
db: AsyncDatabaseManager = AsyncDatabaseManager("postgresql+asyncpg://...")
@app.on_event("startup")
async def _startup() -> None:
"""Connect the database and turn tracing on."""
await db.connect()
setup_tracing(
app,
service_name="orders-api",
otlp_endpoint="http://otel-collector:4317",
sqlalchemy_engine=db.engine,
)
That's it: every request becomes a parent span, every query and every httpx
call becomes a child span, and the whole trace shows up in Jaeger / Tempo /
Honeycomb under the name orders-api.
No collector (local debugging)¶
Pass otlp_endpoint=None to install a console exporter — spans print to
stdout, no collector required:
from fastapi import FastAPI
from tempest_fastapi_sdk import setup_tracing
app: FastAPI = FastAPI()
setup_tracing(app, service_name="orders-api", otlp_endpoint=None)
Sampling¶
In high-traffic production, tracing 100% of requests is expensive. Pass
sample_ratio to sample a fraction (a head-based decision propagated to
child spans):
from fastapi import FastAPI
from tempest_fastapi_sdk import setup_tracing
app: FastAPI = FastAPI()
setup_tracing(
app,
service_name="orders-api",
otlp_endpoint="http://otel-collector:4317",
sample_ratio=0.1, # ~10% of requests
resource_attributes={"deployment.environment": "prod"},
)
Arguments, not env vars
The endpoint, the sampling and the attributes come from the function
arguments — the call site is the single source of truth. No
configuring half in code and half in OTEL_* env vars.
Best-effort instrumentation
SQLAlchemy and httpx are only instrumented when the
opentelemetry-instrumentation-sqlalchemy / ...-httpx packages are
installed (the [otel] extra ships both). If they are missing,
instrumentation is silently skipped instead of breaking boot.
Slow query logger¶
SlowQueryLogger registers a listener on the SQLAlchemy engine events and
emits a log line whenever a statement exceeds a configurable threshold. It
is the cheapest way to find the N+1 or the missing index. No extra
needed — it uses only SQLAlchemy.
import logging
from tempest_fastapi_sdk import AsyncDatabaseManager, SlowQueryLogger
db: AsyncDatabaseManager = AsyncDatabaseManager("postgresql+asyncpg://...")
async def wire_slow_query_log() -> None:
"""Turn slow-query logging on at startup."""
await db.connect()
slow: SlowQueryLogger = SlowQueryLogger(
db.engine,
threshold_ms=200.0, # logs queries >= 200ms
level=logging.WARNING,
)
slow.attach()
Each slow query becomes a line like:
Parameters and EXPLAIN (dev only)¶
By default, bind parameters are not logged (they often carry
PII/secrets). In development, turn on log_parameters=True and/or
explain=True to see the execution plan:
import logging
from tempest_fastapi_sdk import SlowQueryLogger
slow: SlowQueryLogger = SlowQueryLogger(
db.engine,
threshold_ms=50.0,
log_parameters=True, # include the binds — dev only
explain=True, # run EXPLAIN and append the plan — costs 1 round-trip
)
slow.attach()
EXPLAIN costs a round-trip
With explain=True every slow query fires an extra EXPLAIN. Keep it
off in production, turn it on only while hunting a bad plan.
To turn it off (e.g. on shutdown or in a test), call slow.detach().
Recap¶
setup_tracing(app, service_name=..., otlp_endpoint=...)turns on distributed tracing with FastAPI/SQLAlchemy/httpx auto-instrumentation —[otel]extra.otlp_endpoint=Noneexports spans to the console (local debugging);sample_ratiocontrols sampling.SlowQueryLogger(engine, threshold_ms=...).attach()logs slow queries with no extra at all; parameters andEXPLAINsit behind opt-in flags.