"""Runtime helpers that bundle lifecycle, observer, and span orchestration."""
import re
from typing import TYPE_CHECKING, Any, cast
from sqlspec.observability._common import compute_sql_hash, get_trace_context, resolve_db_system
from sqlspec.observability._config import LoggingConfig, ObservabilityConfig, StatementObserver
from sqlspec.observability._dispatcher import LifecycleDispatcher, LifecycleHook
from sqlspec.observability._observer import create_event, create_statement_observer
from sqlspec.observability._spans import SpanManager
from sqlspec.utils.correlation import CorrelationContext
from sqlspec.utils.type_guards import has_span_attribute
if TYPE_CHECKING:
from collections.abc import Iterable
from sqlspec.storage import StorageTelemetry
__all__ = ("ObservabilityRuntime", "compute_sql_hash")
_LITERAL_PATTERN = re.compile(r"'(?:''|[^'])*'")
class ObservabilityRuntime:
"""Aggregates dispatchers, observers, spans, and custom metrics."""
__slots__ = (
"_is_idle_cached",
"_metrics",
"_redaction",
"_statement_observers",
"bind_key",
"config",
"config_name",
"lifecycle",
"span_manager",
)
# Allow test injection with fake span managers (mypyc strict typing workaround)
span_manager: "Any"
def __init__(
self, config: ObservabilityConfig | None = None, *, bind_key: str | None = None, config_name: str | None = None
) -> None:
config = config.copy() if config else ObservabilityConfig()
if config.logging is None:
config.logging = LoggingConfig()
self.config = config
self.bind_key = bind_key
self.config_name = config_name or "SQLSpecConfig"
lifecycle_config = cast("dict[str, Iterable[LifecycleHook]] | None", config.lifecycle)
self.lifecycle = LifecycleDispatcher(lifecycle_config)
self.span_manager = SpanManager(config.telemetry)
observers: list[StatementObserver] = []
if config.statement_observers:
observers.extend(config.statement_observers)
if config.print_sql:
observers.append(create_statement_observer(config.logging))
self._statement_observers = tuple(observers)
self._redaction = config.redaction.copy() if config.redaction else None
self._metrics: dict[str, float] = {}
# Pre-compute the non-span idle state (lifecycle and observers are immutable)
# span_manager can be replaced for testing so we check it separately
self._is_idle_cached = not self.lifecycle.is_enabled and not self._statement_observers