|
| 1 | +# SQLSpec Observability Runtime |
| 2 | + |
| 3 | +This guide explains how the consolidated observability stack works after the Lifecycle Dispatcher + Statement Observer integration. Use it as the single source of truth when wiring new adapters, features, or docs. |
| 4 | + |
| 5 | +## Goals |
| 6 | + |
| 7 | +1. **Unified Hooks** – every pool, connection, session, and query event is emitted through one dispatcher with zero work when no listeners exist. |
| 8 | +2. **Structured Statement Events** – observers receive normalized payloads (`StatementEvent`) for printing, logging, or exporting to tracing systems. |
| 9 | +3. **Optional OpenTelemetry Spans** – span creation is lazy and never imports `opentelemetry` unless spans are enabled. |
| 10 | +4. **Diagnostics** – storage bridge + serializer metrics + lifecycle counters roll up under `SQLSpec.telemetry_snapshot()` (Phase 5). |
| 11 | +5. **Loader & Migration Telemetry** – SQL file loader, caching, and migration runners emit metrics/spans without additional plumbing (Phase 7). |
| 12 | + |
| 13 | +## Configuration Sources |
| 14 | + |
| 15 | +There are three ways to enable observability today: |
| 16 | + |
| 17 | +1. **Registry-Level** – pass `observability_config=ObservabilityConfig(...)` to `SQLSpec()`. |
| 18 | +2. **Adapter Override** – each config constructor accepts `observability_config=` for adapter-specific knobs. |
| 19 | +3. **`driver_features` Compatibility** – existing keys such as `"on_connection_create"`, `"on_pool_destroy"`, and `"on_session_start"` are automatically promoted into lifecycle observers, so user-facing APIs do **not** change. |
| 20 | + |
| 21 | +```python |
| 22 | +from sqlspec import SQLSpec |
| 23 | +from sqlspec.adapters.duckdb import DuckDBConfig |
| 24 | + |
| 25 | +def ensure_extensions(connection): |
| 26 | + connection.execute("INSTALL http_client; LOAD http_client;") |
| 27 | + |
| 28 | +config = DuckDBConfig( |
| 29 | + pool_config={"database": ":memory:"}, |
| 30 | + driver_features={ |
| 31 | + "extensions": [{"name": "http_client"}], |
| 32 | + "on_connection_create": ensure_extensions, # promoted to observability runtime |
| 33 | + }, |
| 34 | +) |
| 35 | + |
| 36 | +sql = SQLSpec(observability_config=ObservabilityConfig(print_sql=True)) |
| 37 | +sql.add_config(config) |
| 38 | +``` |
| 39 | + |
| 40 | +> **Implementation note:** During config initialization we inspect `driver_features` for known hook keys and wrap them into `ObservabilityConfig` callbacks. Hooks that accepted a raw resource (e.g., connection) continue to do so without additional adapter plumbing. |
| 41 | +
|
| 42 | +## Lifecycle Events |
| 43 | + |
| 44 | +The dispatcher exposes the following events (all opt-in and guard-checked): |
| 45 | + |
| 46 | +| Event | Context contents | |
| 47 | +| --- | --- | |
| 48 | +| `on_pool_create` / `on_pool_destroy` | `pool`, `config`, `bind_key`, `correlation_id` | |
| 49 | +| `on_connection_create` / `on_connection_destroy` | `connection`, plus base context | |
| 50 | +| `on_session_start` / `on_session_end` | `session` / driver instance | |
| 51 | +| `on_query_start` / `on_query_complete` | SQL text, parameters, metadata | |
| 52 | +| `on_error` | `exception` plus last query context | |
| 53 | + |
| 54 | +`SQLSpec.provide_connection()` and `SQLSpec.provide_session()` now emit these events automatically, regardless of whether the caller uses registry helpers or adapter helpers directly. |
| 55 | + |
| 56 | +## Statement Observers & Print SQL |
| 57 | + |
| 58 | +Statement observers receive `StatementEvent` objects. Typical uses: |
| 59 | + |
| 60 | +* enable `print_sql=True` to attach the built-in logger. |
| 61 | +* add custom redaction rules via `RedactionConfig` (mask parameters, mask literals, allow-list names). |
| 62 | +* forward events to bespoke loggers or telemetry exporters. |
| 63 | + |
| 64 | +```python |
| 65 | +def log_statement(event: StatementEvent) -> None: |
| 66 | + logger.info("%s (%s) -> %ss", event.operation, event.driver, event.duration_s) |
| 67 | + |
| 68 | +ObservabilityConfig( |
| 69 | + print_sql=False, |
| 70 | + statement_observers=(log_statement,), |
| 71 | + redaction=RedactionConfig(mask_parameters=True, parameter_allow_list=("tenant_id",)), |
| 72 | +) |
| 73 | +``` |
| 74 | + |
| 75 | +### Optional Exporters (OpenTelemetry & Prometheus) |
| 76 | + |
| 77 | +Two helper modules wire optional dependencies into the runtime without forcing unconditional imports: |
| 78 | + |
| 79 | +* `sqlspec.extensions.otel.enable_tracing()` ensures `opentelemetry-api` is installed, then returns an `ObservabilityConfig` whose `TelemetryConfig` enables spans and (optionally) injects a tracer provider factory. |
| 80 | +* `sqlspec.extensions.prometheus.enable_metrics()` ensures `prometheus-client` is installed and appends a `PrometheusStatementObserver` that emits counters and histograms for every `StatementEvent`. |
| 81 | + |
| 82 | +Both helpers rely on the conditional stubs defined in `sqlspec/typing.py`, so they remain safe to import even when the extras are absent. |
| 83 | + |
| 84 | +```python |
| 85 | +from sqlspec.extensions import otel, prometheus |
| 86 | + |
| 87 | +config = otel.enable_tracing(resource_attributes={"service.name": "orders-api"}) |
| 88 | +config = prometheus.enable_metrics(base_config=config, label_names=("driver", "operation", "adapter")) |
| 89 | +sql = SQLSpec(observability_config=config) |
| 90 | +``` |
| 91 | + |
| 92 | +You can also opt in per adapter by passing `extension_config["otel"]` or `extension_config["prometheus"]` when constructing a config; the helpers above are invoked automatically during initialization. |
| 93 | + |
| 94 | +## Loader & Migration Telemetry |
| 95 | + |
| 96 | +`SQLSpec` instantiates a dedicated `ObservabilityRuntime` for the SQL file loader and shares it with every migration command/runner. Instrumentation highlights: |
| 97 | + |
| 98 | +- Loader metrics such as `SQLFileLoader.loader.load.invocations`, `.cache.hit`, `.files.loaded`, `.statements.loaded`, and `.directories.scanned` fire automatically when queries are loaded or cache state is inspected. |
| 99 | +- Migration runners publish cache stats (`{Config}.migrations.listing.cache_hit`, `.cache_miss`, `.metadata.cache_hit`), command metrics (`{Config}.migrations.command.upgrade.invocations`, `.downgrade.errors`), and per-migration execution metrics (`{Config}.migrations.upgrade.duration_ms`, `.downgrade.applied`). |
| 100 | +- Command and migration spans (`sqlspec.migration.command.upgrade`, `sqlspec.migration.upgrade`) include version numbers, bind keys, and correlation IDs; they end with duration attributes even when exceptions occur. |
| 101 | + |
| 102 | +All metrics surface through `SQLSpec.telemetry_snapshot()` under the adapter key, so exporters observe a flat counter space regardless of which subsystem produced the events. |
| 103 | + |
| 104 | +## Span Manager & Diagnostics |
| 105 | + |
| 106 | +* **Span Manager:** Query spans ship today, lifecycle events emit `sqlspec.lifecycle.*` spans, storage bridge helpers wrap reads/writes with `sqlspec.storage.*` spans, and migration runners create `sqlspec.migration.*` spans for both commands and individual revisions. Mocked span tests live in `tests/unit/test_observability.py`. |
| 107 | +* **Diagnostics:** `TelemetryDiagnostics` aggregates lifecycle counters, loader/migration metrics, storage bridge telemetry, and serializer cache stats. Storage telemetry carries backend IDs, bind key, and correlation IDs so snapshots/spans inherit the same context, and `SQLSpec.telemetry_snapshot()` exposes that data via flat counters plus a `storage_bridge.recent_jobs` list detailing the last 25 operations. |
| 108 | + |
| 109 | +Example snapshot payload: |
| 110 | + |
| 111 | +``` |
| 112 | +{ |
| 113 | + "storage_bridge.bytes_written": 2048, |
| 114 | + "storage_bridge.recent_jobs": [ |
| 115 | + { |
| 116 | + "destination": "alias://warehouse/users.parquet", |
| 117 | + "backend": "s3", |
| 118 | + "bytes_processed": 2048, |
| 119 | + "rows_processed": 16, |
| 120 | + "config": "AsyncpgConfig", |
| 121 | + "bind_key": "analytics", |
| 122 | + "correlation_id": "8f64c0f6", |
| 123 | + "format": "parquet" |
| 124 | + } |
| 125 | + ], |
| 126 | + "serializer.hits": 12, |
| 127 | + "serializer.misses": 2, |
| 128 | + "AsyncpgConfig.lifecycle.query_start": 4 |
| 129 | +} |
| 130 | +``` |
| 131 | + |
| 132 | +## Next Steps (2025 Q4) |
| 133 | + |
| 134 | +1. **Exporter Validation:** Exercise the OpenTelemetry/Prometheus helpers against the new loader + migration metrics and document recommended dashboards. |
| 135 | +2. **Adapter Audit:** Confirm every adapter’s migration tracker benefits from the instrumentation (especially Oracle/BigQuery fixtures) and extend coverage where needed. |
| 136 | +3. **Performance Budgets:** Add guard-path benchmarks/tests to ensure disabled observability remains near-zero overhead now that migration/loader events emit metrics by default. |
0 commit comments