|
| 1 | +# Execution observations |
| 2 | + |
| 3 | +Execution observations expose native SQLMesh work to Python integrations through |
| 4 | +an optional callback. They do not replace SQL generation, execution, retries, |
| 5 | +transactions, or `Console` progress reporting. |
| 6 | + |
| 7 | +## Console observer registration |
| 8 | + |
| 9 | +`Console.get_execution_observer()` returns `None` by default. A `Console` |
| 10 | +subclass can return an `Observer` callback from `sqlmesh.core.execution_observation`. |
| 11 | +The callback receives a |
| 12 | +phase, a read-only mapping of facts, and the original exception or `None`. |
| 13 | +For general Python integration details, see the [Python API reference](./python.md). |
| 14 | + |
| 15 | +The following console prints the kind, action, and finish error for each observed |
| 16 | +operation. The example uses `examples/sushi` relative to the SQLMesh repository root: |
| 17 | + |
| 18 | +```python |
| 19 | +from typing import Any, Literal, Mapping, Optional |
| 20 | + |
| 21 | +from sqlmesh.core.console import TerminalConsole, set_console |
| 22 | +from sqlmesh.core.context import Context |
| 23 | +from sqlmesh.core.execution_observation import Observer |
| 24 | + |
| 25 | + |
| 26 | +def observe( |
| 27 | + phase: Literal["start", "finish"], |
| 28 | + facts: Mapping[str, Any], |
| 29 | + error: Optional[BaseException], |
| 30 | +) -> None: |
| 31 | + if phase == "finish": |
| 32 | + print(f"{facts['kind']}/{facts['action']}: error={error!r}") |
| 33 | + |
| 34 | + |
| 35 | +class ObservedConsole(TerminalConsole): |
| 36 | + def get_execution_observer(self) -> Optional[Observer]: |
| 37 | + return observe |
| 38 | + |
| 39 | + |
| 40 | +set_console(ObservedConsole()) |
| 41 | +context = Context(paths=["examples/sushi"]) |
| 42 | +``` |
| 43 | + |
| 44 | +A `Context` uses the `Console` that was current when the context was created. |
| 45 | +Unless an explicit `observer_scope()` is active, SQLMesh resolves |
| 46 | +`get_execution_observer()` once for each outer plan evaluation or scheduler run. |
| 47 | +Nested calls reuse that observer. If the getter raises an ordinary `Exception`, |
| 48 | +SQLMesh disables observation for that plan evaluation or scheduler run and |
| 49 | +continues native work. |
| 50 | + |
| 51 | +## Observer scopes |
| 52 | + |
| 53 | +`observer_scope(observer)` from `sqlmesh.core.execution_observation` sets the |
| 54 | +observer for the duration of the scope. A nested scope restores the previously |
| 55 | +active observer when it exits. `observer_scope(None)` disables observation inside |
| 56 | +that scope, including automatic Console registration. |
| 57 | + |
| 58 | +## Callback lifecycle |
| 59 | + |
| 60 | +SQLMesh calls `start` before observed work with `error=None`. When `start` |
| 61 | +returns or raises an ordinary `Exception`, SQLMesh continues the work and calls |
| 62 | +`finish` from a `finally` block afterward. The `finish` callback receives the |
| 63 | +original workload exception object or `None`. Both callbacks run synchronously |
| 64 | +on the thread that performs the observed work. Callbacks for work dispatched to |
| 65 | +a worker run on that worker's thread. |
| 66 | + |
| 67 | +Every invocation that reaches `finish` passes the same read-only mapping object |
| 68 | +to `start` and `finish`. Distinct invocations use distinct mapping objects. |
| 69 | +Consumers can pair the phases by mapping identity. SQLMesh may add facts while |
| 70 | +the work runs, so values in the mapping at `finish` can differ from its values |
| 71 | +at `start`. The mapping's supported lifetime ends when the matching `finish` |
| 72 | +callback returns. |
| 73 | + |
| 74 | +Ordinary observer `Exception` failures are suppressed and do not replace the |
| 75 | +workload result or error. SQLMesh does not suppress observer-raised |
| 76 | +`BaseException` subclasses that are not `Exception` subclasses, such as |
| 77 | +`KeyboardInterrupt` and `SystemExit`. If `start` raises a `BaseException` that |
| 78 | +is not an `Exception`, the work and `finish` callback do not run. If `finish` |
| 79 | +raises a `BaseException` that is not an `Exception`, that failure can replace |
| 80 | +the workload error. |
| 81 | + |
| 82 | +If a worker raises a `BaseException`, the error can reach the caller after its |
| 83 | +worker pool exits. If a DAG run records a terminal error, SQLMesh stops |
| 84 | +scheduling nodes and waits for running workers to exit before it raises that |
| 85 | +error. Parallel value operations also wait for their workers to finish before |
| 86 | +raising a worker's `BaseException`. Blocking callbacks and process termination |
| 87 | +are outside the observer failure guarantee. SQLMesh does not log observer |
| 88 | +failures. |
| 89 | + |
| 90 | +Nested workload calls produce nested observations. Callbacks for concurrent |
| 91 | +workers can interleave, and SQLMesh provides no global callback order. Observers |
| 92 | +must support concurrent calls and return promptly. SQLMesh allocates no |
| 93 | +execution IDs, timestamps, durations, or parent IDs. Consumers supply those |
| 94 | +values when needed. |
| 95 | + |
| 96 | +While SQLMesh delivers a callback, it suppresses observations triggered by that |
| 97 | +callback and ignores calls to `update_execution()`. The guard covers callback |
| 98 | +delivery only. Normal model work can still contain observations for physical |
| 99 | +operations, audits, and queries. |
| 100 | + |
| 101 | +## Native facts and limits |
| 102 | + |
| 103 | +Every facts mapping contains `kind` and `action`. Other fields depend on the |
| 104 | +observed operation and can be absent when work exits early or fails. |
| 105 | + |
| 106 | +| Kind | Action | Native facts | |
| 107 | +| --- | --- | --- | |
| 108 | +| `stage` | Plan-stage class name | `native_plan_id` | |
| 109 | +| `model` | `evaluate`, `audit_only` | `snapshot`, `interval`, `batch_index`, `execution_time`, `audit_only` | |
| 110 | +| `physical` | `create`, `materialize`, `schema_migration` | `snapshot`; `target` when known; `creating` for materialization; `existed` for schema migration when known | |
| 111 | +| `virtual` | `promote`, `demote` | `snapshot`; `target` when known; `source_table` for promotion when known | |
| 112 | +| `audit` | `audit` | `audit_name`; `audit_result` at finish if produced | |
| 113 | +| `query` | `execute` | `engine` dialect string | |
| 114 | + |
| 115 | +[`Snapshot`](../concepts/architecture/snapshots.md) and `AuditResult` values are |
| 116 | +borrowed native objects. Consumers must |
| 117 | +treat these objects and other nested values as read-only and must not retain them |
| 118 | +beyond the matching `finish` callback. The mapping's read-only wrapper does not |
| 119 | +freeze nested objects. Facts are not a JSON event schema. SQLMesh does not |
| 120 | +interpret audit results as consumer statuses or inherit model facts into child |
| 121 | +mappings. Consumers that need facts after `finish` copy and normalize those |
| 122 | +values during the callback. |
| 123 | + |
| 124 | +SQLMesh calls `finish` for an audit before it runs later audits or completes |
| 125 | +write-audit-publish (WAP). A later audit or WAP publication can still fail. An audit |
| 126 | +query failure can produce a `finish` callback without an `AuditResult`. The |
| 127 | +[audits guide](../concepts/audits.md) describes audit behavior in plans and |
| 128 | +runs. |
| 129 | + |
| 130 | +For `physical/materialize`, `target` is the logical destination table from the |
| 131 | +snapshot. During WAP, SQLMesh writes to a staging destination, but the `target` |
| 132 | +fact remains the logical destination. The `finish` callback runs after the |
| 133 | +materialization's transaction and session exit. It does not report the result |
| 134 | +of later audits or WAP publication. |
| 135 | + |
| 136 | +An observed operation can do no work, issue multiple statements, or trigger |
| 137 | +internal driver retries. The observation does not predict how many statements |
| 138 | +the operation will issue or report each retry. |
| 139 | + |
| 140 | +The query observation surrounds the base adapter's existing SQL logging and |
| 141 | +`_execute` call. It ends before a surrounding transaction commits and before |
| 142 | +later fetch or result consumption. Some adapter operations do not call this |
| 143 | +query hook. BigQuery session queries, BigQuery dataframe loads, and ClickHouse |
| 144 | +dataframe inserts may not produce query observations. These hooks do not provide |
| 145 | +a complete warehouse activity history or guarantee transaction success. |
| 146 | + |
| 147 | +SQL logging retains its existing behavior, including VALUES redaction for |
| 148 | +non-query expressions. The callback receives no SQL text, adapter objects, or |
| 149 | +warehouse query IDs. The `start` callback runs before SQL logging. Consumers can |
| 150 | +use that ordering to associate existing log entries with the invocation. |
| 151 | + |
| 152 | +## Worker context |
| 153 | + |
| 154 | +When an observer is active outside callback delivery, SQLMesh copies the current |
| 155 | +Python context for each task in scheduler and snapshot DAG work, snapshot schema creation, and |
| 156 | +data-object listing. That context carries Python's [context variables](https://docs.python.org/3/library/contextvars.html). |
| 157 | +Each task gets its own context copy, including during serial execution. DAG |
| 158 | +successors use the context captured for the run. They do not inherit changes |
| 159 | +made by earlier workers. Reusing a DAG executor captures a fresh context for |
| 160 | +each run. |
| 161 | + |
| 162 | +Context copies are shallow. Mutable objects stored in `ContextVar` values remain |
| 163 | +shared across copies. A parent identity stored in a consumer-owned `ContextVar` |
| 164 | +is available to each worker. SQLMesh uses these copies in the scheduler and |
| 165 | +snapshot DAG, schema creation, and data-object listing. Arbitrary consumer |
| 166 | +threads and other concurrency helpers do not receive these copies. |
| 167 | + |
| 168 | +With no observer registered, these operations do not copy contexts. The concurrency |
| 169 | +helpers retain their existing `ContextVar` behavior for serial and worker |
| 170 | +execution. |
0 commit comments