|
| 1 | +# Execution observations |
| 2 | + |
| 3 | +Execution observations let Python integrations observe native SQLMesh work through |
| 4 | +an optional callback. They do not replace SQL generation, execution, retries, |
| 5 | +transactions, or Console progress reporting. |
| 6 | + |
| 7 | +## Register an observer |
| 8 | + |
| 9 | +Override `Console.get_execution_observer()` to return a callback accepting |
| 10 | +`phase`, `facts`, and `error`. Its default return value is `None`. |
| 11 | +Install the console before constructing the SQLMesh `Context`: |
| 12 | + |
| 13 | +```python |
| 14 | +from typing import Any, Literal, Mapping, Optional |
| 15 | + |
| 16 | +from sqlmesh.core.console import TerminalConsole, set_console |
| 17 | +from sqlmesh.core.context import Context |
| 18 | +from sqlmesh.core.execution_observation import Observer |
| 19 | + |
| 20 | + |
| 21 | +def observe( |
| 22 | + phase: Literal["start", "finish"], |
| 23 | + facts: Mapping[str, Any], |
| 24 | + error: Optional[BaseException], |
| 25 | +) -> None: |
| 26 | + if phase == "finish": |
| 27 | + print(f"{facts['kind']}/{facts['action']}: error={error!r}") |
| 28 | + |
| 29 | + |
| 30 | +class ObservedConsole(TerminalConsole): |
| 31 | + def get_execution_observer(self) -> Optional[Observer]: |
| 32 | + return observe |
| 33 | + |
| 34 | + |
| 35 | +set_console(ObservedConsole()) |
| 36 | +context = Context(paths=["path/to/project"]) |
| 37 | +``` |
| 38 | + |
| 39 | +Built-in plan-stage evaluation and scheduler dispatch resolve the getter at the |
| 40 | +outer observation boundary. Nested dispatch reuses the active registration. |
| 41 | +An ordinary `Exception` from the getter disables observation at that boundary; |
| 42 | +native work continues. |
| 43 | + |
| 44 | +For an explicitly bounded registration, use `observer_scope(observer)` from |
| 45 | +`sqlmesh.core.execution_observation`. Nested scopes restore the preceding |
| 46 | +registration on exit. `observer_scope(None)` explicitly disables observation |
| 47 | +inside the scope, including automatic Console registration. |
| 48 | + |
| 49 | +## Callback lifecycle |
| 50 | + |
| 51 | +`start` runs before the observed work, with `error=None`. Once the start callback |
| 52 | +returns, `finish` runs from `finally` after that work, with its original exception |
| 53 | +object or `None`. Both callbacks run synchronously on the invoking thread. |
| 54 | + |
| 55 | +SQLMesh passes the **same read-only mapping object** to start and finish for one |
| 56 | +invocation. Distinct invocations have distinct mappings. Consumers may retain the |
| 57 | +mapping until its matching finish callback to pair the calls by object identity; |
| 58 | +release it afterward. SQLMesh may add facts discovered during execution, so this |
| 59 | +mapping is a live view, not an immutable snapshot of the start facts. |
| 60 | + |
| 61 | +Nested workload calls produce nested observations. Callbacks for concurrent |
| 62 | +workers can interleave, and there is no global callback ordering. Observers must |
| 63 | +support concurrent calls and return promptly. SQLMesh does not allocate execution |
| 64 | +IDs, timestamps, durations, or parent IDs; consumers can maintain that information. |
| 65 | + |
| 66 | +Ordinary observer `Exception` failures are suppressed and do not replace the |
| 67 | +workload result or error. This guarantee does **not** cover observer-raised |
| 68 | +`BaseException` subclasses such as `KeyboardInterrupt` or `SystemExit`: a failure |
| 69 | +in start can prevent work and finish, and a failure in finish can replace a |
| 70 | +workload error. Blocking callbacks and process termination are also outside the |
| 71 | +guarantee. SQLMesh does not recursively log observer failures. |
| 72 | + |
| 73 | +While a callback is being delivered, observations triggered by that callback are |
| 74 | +suppressed, and internal metadata updates are ignored. This guard applies only |
| 75 | +during callback delivery: a normal model operation can still contain observed |
| 76 | +physical operations, audits, and queries. |
| 77 | + |
| 78 | +## Native facts and boundaries |
| 79 | + |
| 80 | +Every mapping contains `kind` and `action`. Other fields depend on the boundary |
| 81 | +and may be absent, particularly when work exits early or fails. |
| 82 | + |
| 83 | +| Kind | Action | Native facts | |
| 84 | +| --- | --- | --- | |
| 85 | +| `stage` | Native plan-stage name | `native_plan_id` | |
| 86 | +| `model` | `evaluate`, `audit_only` | `snapshot`, `interval`, `batch_index`, `execution_time`, `audit_only` | |
| 87 | +| `physical` | `create`, `materialize`, `schema_migration` | `snapshot`; `target` when known; `creating` for materialization; `existed` for schema migration when known | |
| 88 | +| `virtual` | `promote`, `demote` | `snapshot`; `target` when known; `source_table` for promotion when known | |
| 89 | +| `audit` | `audit` | `audit_name`; `audit_result` at finish if produced | |
| 90 | +| `query` | `execute` | `engine` dialect string | |
| 91 | + |
| 92 | +`Snapshot` and `AuditResult` values are borrowed native objects. Treat them and |
| 93 | +other nested values as read-only, and do not retain them beyond finish. The |
| 94 | +mapping's read-only wrapper does not freeze these objects. Normalize any values |
| 95 | +needed for later processing during the callback; facts are not a JSON event |
| 96 | +schema. SQLMesh does not interpret audit results as consumer statuses or inherit |
| 97 | +model facts into child mappings. |
| 98 | + |
| 99 | +Audit observations finish before later audits or WAP publication can fail. An |
| 100 | +audit query failure can produce a finish callback without an `AuditResult`. |
| 101 | +Physical materialization includes the surrounding transaction/session exit. |
| 102 | +An invocation can perform no work, multiple statements, or internal driver |
| 103 | +retries; observations are not an expected-work manifest or a retry inventory. |
| 104 | + |
| 105 | +The query boundary surrounds the base adapter's existing SQL logging and |
| 106 | +`_execute` call. It does not include a surrounding transaction's commit or later |
| 107 | +fetch/result consumption. Adapter paths that bypass this boundary, including |
| 108 | +BigQuery session queries and dataframe loads and ClickHouse dataframe inserts, |
| 109 | +do not necessarily produce query observations. These hooks therefore do not |
| 110 | +provide a complete warehouse activity history or a transaction-success guarantee. |
| 111 | + |
| 112 | +SQL logs retain their existing behavior, including VALUES redaction for |
| 113 | +non-query expressions. The callback does not receive SQL text, adapter objects, |
| 114 | +or warehouse query IDs. Starting the observation before SQL logging lets a |
| 115 | +consumer associate existing logs with the invocation. |
| 116 | + |
| 117 | +## Worker context |
| 118 | + |
| 119 | +When observation is active, the instrumented scheduler and snapshot DAG dispatch |
| 120 | +paths copy the admitting context at execution time. Each node runs in its own |
| 121 | +copy, including serial DAG execution. Consumer-owned `ContextVar` values can |
| 122 | +therefore carry parent identities into workers; successors inherit the admission |
| 123 | +context rather than the preceding worker's changes. Reusing a DAG executor |
| 124 | +captures a fresh context for each run. |
| 125 | + |
| 126 | +Context copies are shallow. Mutable objects stored in ContextVars can still be |
| 127 | +shared; consumers should use immutable values or replace values instead of |
| 128 | +mutating shared state. This behavior applies to the instrumented DAG paths, not |
| 129 | +to arbitrary consumer threads or every SQLMesh concurrency helper. |
| 130 | + |
| 131 | +With no observer registered, these call sites do not opt into context copying |
| 132 | +and retain their existing serial and worker execution behavior. |
0 commit comments