Observability¶
SQLSpec ships an opt-in observability pipeline (sqlspec.observability) with zero hot-path overhead when idle and fast-path preservation when active. Instrument queries with custom StatementObserver callbacks, deterministic sampling, OpenTelemetry spans, Prometheus metrics, structured cloud log formatters, correlation IDs, wire-level sqlcommenter tags, and DuckDB statement profiling.
Custom Statement Observers¶
Register one or more StatementObserver functions on ObservabilityConfig to inspect StatementEvent records (sql, operation, driver, duration_s, rows_affected, correlation_id, profile):
"""Attach a custom StatementEvent observer callback to ObservabilityConfig."""
from sqlspec import SQLSpec
from sqlspec.adapters.sqlite import SqliteConfig
from sqlspec.observability import ObservabilityConfig, StatementEvent
__all__ = ("main",)
def main() -> list[StatementEvent]:
"""Capture per-statement telemetry events across DDL, DML, and SELECT operations."""
captured_events: list[StatementEvent] = []
def observe_statement(event: StatementEvent) -> None:
captured_events.append(event)
observability = ObservabilityConfig(statement_observers=(observe_statement,), print_sql=False)
spec = SQLSpec(observability_config=observability)
config = spec.add_config(SqliteConfig(connection_config={"database": ":memory:"}))
with spec.provide_session(config) as session:
session.execute("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)")
session.execute("INSERT INTO items (name) VALUES ('Widget')")
session.select("SELECT id, name FROM items")
assert len(captured_events) >= 3
assert all(isinstance(event, StatementEvent) for event in captured_events)
return captured_events
if __name__ == "__main__":
events = main()
for item in events:
print(f"{item.operation} - {item.driver} - {item.duration_s:.4f}s")
StatementEvent Fields¶
| Field | Type | Description |
|---|---|---|
sql |
str |
Executed SQL text (with literals redacted when RedactionConfig.mask_literals is enabled). |
parameters |
Any |
Bound parameters (masked automatically when RedactionConfig.mask_parameters is enabled). |
driver |
str |
Adapter driver class name (such as "SqliteDriver" or "AsyncpgDriver"). |
adapter |
str |
Adapter identifier (such as "sqlite", "asyncpg", or "oracledb"). |
bind_key |
str \| None |
Optional bind_key assigned on the owning database configuration. |
operation |
str |
Normalized SQL operation label ("SELECT", "INSERT", "UPDATE", "DELETE", "DDL"). |
execution_mode |
str |
Driver execution path (such as "single", "many", "script", or "stream"). |
is_many / is_script |
bool |
Flags indicating batched execute_many or multi-statement execute_script execution. |
rows_affected |
int \| None |
Driver-reported affected or fetched row count when available. |
duration_s |
float |
Wall-clock driver execution latency in seconds. |
correlation_id |
str \| None |
Active CorrelationContext identifier bound for the current request or task. |
storage_backend |
str \| None |
Storage backend name when emitted during select_to_storage or load_from_storage jobs. |
started_at |
float |
Epoch timestamp when statement execution began. |
profile |
dict[str, Any] \| None |
Parsed execution plan payload when DuckDB enable_statement_profiling is enabled. |
script_ordinal / script_statement_count |
int \| None |
Statement index and total count when profiling multi-statement scripts. |
Sampling and Parameter Redaction¶
High-throughput services can sample routine queries deterministically while always capturing failed statements (force_sample_on_error=True) and masking sensitive bind parameters via RedactionConfig:
"""Configure deterministic trace and statement sampling with error-forced capture."""
from sqlspec.observability import ObservabilityConfig, SamplingConfig
__all__ = ("main",)
def main() -> ObservabilityConfig:
"""Create an ObservabilityConfig with 10% deterministic sampling and forced error capture."""
sampling = SamplingConfig(sample_rate=0.1, force_sample_on_error=True, deterministic=True)
observability = ObservabilityConfig(sampling=sampling, print_sql=False)
assert observability.sampling is sampling
return observability
if __name__ == "__main__":
print(main())
from sqlspec.observability import ObservabilityConfig, RedactionConfig
observability = ObservabilityConfig(
redaction=RedactionConfig(mask_parameters=True, mask_literals=False, parameter_allow_list=("tenant_id",))
)
OpenTelemetry, Prometheus, and Cloud Log Formatters¶
Enable OpenTelemetry spans (uv add "sqlspec[opentelemetry]") and Prometheus counters/histograms (uv add "sqlspec[prometheus]") declaratively through extension_config on any adapter config, or format structured logs for Google Cloud Logging and AWS CloudWatch:
from sqlspec.adapters.sqlite import SqliteConfig
config = SqliteConfig(
connection_config={"database": ":memory:"},
extension_config={
"otel": {"enable_spans": True},
"prometheus": {"enabled": True, "namespace": "app", "subsystem": "db"},
},
)
"""Configure structured GCP Cloud Logging and AWS CloudWatch log formatters."""
from sqlspec.observability import AWSLogFormatter, GCPLogFormatter, ObservabilityConfig
__all__ = ("main",)
def main() -> tuple[ObservabilityConfig, ObservabilityConfig]:
"""Create ObservabilityConfig instances for GCP and AWS structured log output."""
gcp_logs = ObservabilityConfig(cloud_formatter=GCPLogFormatter())
aws_logs = ObservabilityConfig(cloud_formatter=AWSLogFormatter())
assert gcp_logs.cloud_formatter is not None
assert aws_logs.cloud_formatter is not None
return gcp_logs, aws_logs
if __name__ == "__main__":
print(main())
Request Correlation and Wire-Level sqlcommenter¶
Bind request IDs with CorrelationContext (or enable enable_correlation_middleware in Litestar) so logs, spans, and database slow-query logs share the same trace context:
"""Bind request correlation IDs with CorrelationContext and framework middleware."""
from sqlspec import SQLSpec
from sqlspec.adapters.sqlite import SqliteConfig
from sqlspec.utils.correlation import CorrelationContext
__all__ = ("main",)
def main() -> str:
"""Configure correlation middleware and scope a request correlation identifier."""
spec = SQLSpec()
spec.add_config(
SqliteConfig(
connection_config={"database": ":memory:"},
extension_config={"litestar": {"enable_correlation_middleware": True}},
)
)
with CorrelationContext.context("req-123") as correlation_id:
active_id = CorrelationContext.get()
assert active_id == "req-123"
assert CorrelationContext.get() is None
return correlation_id
if __name__ == "__main__":
print(main())
Setting StatementConfig(enable_sqlcommenter=True) appends W3C traceparent and CorrelationContext tags at wire dispatch time (append_wire_sqlcomment), keeping compiled SQL cache keys stable across requests so statement caching stays active. On Oracle (python-oracledb), driver_features={"enable_session_tracing": True} piggybacks module, action, client_identifier, and econtext_id onto the next TTC packet with zero extra round-trips.
DuckDB Statement Profiling¶
On DuckDBConfig and DuckDB-backed AdbcConfig, setting driver_features={"enable_statement_profiling": True} captures per-statement JSON execution profiles onto StatementEvent.profile (along with script_ordinal and script_statement_count for multi-statement scripts).
Recap¶
- Attach
ObservabilityConfigtoSQLSpec(...)or individual adapter configs for uniform telemetry across sync and async drivers. - Combine
SamplingConfig(force_sample_on_error=True)withRedactionConfig(mask_parameters=True)in production. - Enable
enable_sqlcommenter=Trueto correlate slow database queries back to application traces without sacrificing statement cache hit rates.