feat(actions): Pluggable DQ Actions & Alerting#1289
Open
mwojtyczka wants to merge 90 commits into
Open
Conversation
Co-authored-by: Isaac
Co-authored-by: Isaac
Co-authored-by: Isaac
…ment extensibility Co-authored-by: Isaac
…tion) Co-authored-by: Isaac
Add six new exception classes to errors.py (TerminalActionError, PipelineFailedError, InvalidConditionError, InvalidActionError, AlertDeliveryError, UnsafeWebhookUrlError), and new config dataclasses DQSecret, TableActionsStorageConfig, LakebaseActionsStorageConfig, ActionEventsConfig to config.py; add actions_location field to RunConfig. Co-authored-by: Isaac
- LakebaseActionsStorageConfig.instance_name changed from str|None=None to a required str field; required fields (location, instance_name) now precede defaulted ones; dead-code guard removed from _split_location. - TableActionsStorageConfig and ActionEventsConfig __post_init__ now validate mode is 'append' or 'overwrite', matching LakebaseActionsStorageConfig. - All inline imports in test_action_config.py hoisted to module level to satisfy pylint C0415. Co-authored-by: Isaac
Introduces databricks.labs.dqx.actions package with ConditionEvaluator that gates DQ actions on metric expressions using a safe AST walker — no eval/exec. Supports arithmetic, comparison, boolean, and literal nodes; raises InvalidConditionError for any other node type or unknown metric. Co-authored-by: Isaac
…p operator errors - Add _validate_tree() that uses ast.walk to visit every AST node and reject disallowed types before any evaluation begins; called unconditionally at the top of both validate() and evaluate() so short-circuit evaluation cannot bypass the allowlist. - Add _ALLOWED_NODE_TYPES frozenset (single definition, reused by the pre-pass); includes ast.Load and abstract base types produced by ast.walk on valid conditions. - Wrap operator application in _eval_binop, _eval_compare, and _eval_unaryop with try/except (ZeroDivisionError, TypeError, OverflowError) and re-raise as InvalidConditionError. - Remove redundant type(node.op) not in _BOOL_OPS guard in _eval_boolop (unreachable — only And/Or are BoolOp ops). - Add 6 new tests: full-tree pre-pass coverage and operator-error wrapping; total 58 tests, all passing. Co-authored-by: Isaac
Add AlertMessage frozen dataclass and StandardMessageBuilder to actions/message.py; builder takes primitives to avoid a circular import with ActionContext (Task 5). 26 unit tests cover the TDD RED/GREEN cycle. Co-authored-by: Isaac
…ey collision - Change test_is_frozen to assert dataclasses.FrozenInstanceError specifically instead of the overly broad Exception. - Prefix per-metric entries in the fields dict with "metric." (e.g. "metric.error_row_count") so a metric named after a reserved key (condition, run_id, run_time, table) cannot silently overwrite or be overwritten by the metadata entries. observed_metrics remains un-prefixed. - Update existing field-assertion tests to use the new "metric.<name>" keys. - Add TestStandardMessageBuilderReservedKeyCollision test that verifies both fields["metric.condition"] and fields["condition"] coexist correctly. Co-authored-by: Isaac
Adds SecretResolver to the actions package, resolving plain strings as-is and DQSecret references via ws.dbutils.secrets.get at delivery time. API failures are wrapped in InvalidParameterError without leaking the resolved secret value. Co-authored-by: Isaac
Introduces the foundational building blocks for the DQX actions & alerting subsystem: ActionStatus enum, ActionContext / ActionResult / ActionServices frozen dataclasses, the Action abstract base class, and DQAction (condition + action binding with eager validation). WebhookClient and SparkSession are guarded behind TYPE_CHECKING to keep the module importable without delivery.py or PySpark present. Co-authored-by: Isaac
Move sys import to top-level, replace abstract-instantiation test with inspect.isabstract check, remove unused error re-exports from base.py, and delete the test_action_base.py per-file pylint override block. Co-authored-by: Isaac
Implements WebhookAuth, validate_webhook_url (SSRF guard), and WebhookClient (urllib-only, no-redirect opener, exponential-backoff retry, no secrets in errors). Co-authored-by: Isaac
…able 4xx Match stdlib redirect_request signature and type the opener param so mypy needs no overrides; catch OSError once (HTTPError subclass) with a single last_exc assignment to satisfy pylint and remove duplicated backoff; fail fast on non-retryable 4xx (not 429); avoid type-ignores in tests. Co-authored-by: Isaac
Co-authored-by: Isaac
Co-authored-by: Isaac
Adds CallbackDQAlertDestination, an in-process destination that invokes a user-supplied Python callable on delivery. Not serializable (Task 11 skips it); validate() enforces non-empty name and a callable callback. Co-authored-by: Isaac
Co-authored-by: Isaac
Implements DQAlert (with DQAlertFrequency/NotifyOn enums) for concurrent multi-destination alerting with per-destination error isolation, and FailPipeline which raises PipelineFailedError to terminate the DQX run. Co-authored-by: Isaac
… type-ignores - DQAlert.validate() now rejects duplicate destination names (they would silently clobber entries in ActionResult.destination_errors). - Drop spurious '# type: ignore[import-untyped]' on the typed WebhookClient import and replace a None-defaulted list field with field(default_factory). - Broaden the CWE-117 sanitization test to cover tab/ANSI/null control chars. Co-authored-by: Isaac
Co-authored-by: Isaac
…ept in event-store test Add explicit Callable return type (AGENTS all-annotations rule), remove the redundant try/except around DROP TABLE IF EXISTS in the integration cleanup fixture, and add an exact-1h HOURLY boundary test. Co-authored-by: Isaac
Implements ActionSerializer (with registry-based OCP design for actions/destinations, DQSecret tagged-dict round-trip, CallbackDQAlertDestination skip with warning), TableActionsStorageHandler and LakebaseActionsStorageHandler, ActionsStorageHandlerFactory, and DQActionManager for save/load of DQAction definitions to UC Delta or Lakebase. Adds 23 unit tests (all passing) and integration tests for the UC Delta path. Co-authored-by: Isaac
… fully OCP Validate run_config_name via re.fullmatch before Delta replaceWhere interpolation (mirrors checks_storage.py guard; raises UnsafeSqlQueryError for unsafe chars). Extracted into pure helper build_replace_where_predicate for unit-testability. Added _ACTION_SERIALIZERS and _DESTINATION_SERIALIZERS registries to serializer.py so both serialize and deserialize sides are registry-driven (no isinstance chains). Adding a new action/destination type now requires only one registry entry per side. Co-authored-by: Isaac
delivery.py exists and is fully typed since Task 6, so the TYPE_CHECKING import no longer needs the suppression. Co-authored-by: Isaac
Replace bare operator.* references (typed as object) in _UNARY_OPS, _BIN_OPS, _CMP_OPS, and _EVALUATORS with thin typed wrapper functions (lhs/rhs/val params, cast to float for numeric ops) and precise Callable/dict value types. Node-evaluator functions now share a uniform (ast.AST, metrics) -> object signature via internal cast. Zero inline type: ignore remain; pylint 10/10, mypy and ruff clean, 58 tests pass. Co-authored-by: Isaac
Co-authored-by: Isaac
…re-abort Remove dead _healthy_result helper, update module docstring, and add two new tests: one proving deferred[0] (first terminal error) is raised rather than the last, and one proving alert actions execute and record before a subsequent terminal action aborts the pipeline. Co-authored-by: Isaac
1 task
1 task
… UTC handling - event_storage: skip rows with a NULL run_time on read (both Delta and Lakebase stores, load_latest and load_last_fired) so a corrupt or externally-inserted timestamp can no longer crash seed() via to_utc(None) - conditions: InvalidConditionError now references the user's original condition string rather than the operator-normalized form; _normalize_operators docstring uses italics instead of backticks per the docstring convention - tests: cover all to_utc branches (naive->UTC, UTC-aware unchanged, non-UTC-aware converted via astimezone) Co-authored-by: Isaac
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an extensible actions subsystem to DQX. An action runs when data checked by DQX violates an (optional) condition evaluated against the summary metrics produced by
DQMetricsObserver. The concrete actions are:DQAlert— sends a notification to one or more destinations: Slack, Microsoft Teams, generic HTTPS webhook, Log (driver logger, no external system) or an in-process callback.FailPipeline— raisesPipelineFailedErrorto stop the current pipeline run.NoOpAction— fires with no side effect (records the event only). Serves as an observe-only / dry-run mode and as a side-effect-free building block for testing action semantics.Actions can be defined programmatically (DQX classes) or declaratively as metadata (YAML/JSON dicts), and are passed to
DQEngine(ws, observer=..., actions=[...])— they fire automatically on the save-to-table methods (batch and streaming), or explicitly viaengine.evaluate_actions(...). Action definitions can be stored/loaded from UC or Lakebase tables (or local YAML/JSON files) viaDQActionManager, and action events can be persisted to a UC/Lakebase events table so frequency/status-change suppression survives engine restarts. In the installed DQX Workflows and the parallel multi-run-config runner, eachRunConfigcan point at its ownactions_location(definitions) andaction_events_location(history) so actions are auto-loaded and applied per run config.What's included
databricks.labs.dqx.actions—DQAction,ActionABC,DQAlert,FailPipeline,NoOpAction;AlertDestinationhierarchy (Slack / Teams / webhook / log / callback); a safe AST condition evaluator; standard message builder; webhook delivery with retry/backoff and an SSRF guard;SecretResolver(DQSecret);ActionStateStore+ UC/Lakebase event stores;ActionEvaluatororchestrator;ActionSerializer+ UC/Lakebase definition storage;DQActionManager. Destinations and actions are Pydantic discriminated unions, so adding a new type is a small, isolated change (OCP).actions=onDQEngine/DQEngineCore(acceptsDQActioninstances or raw metadata dicts), batch + streaming firing,evaluate_actions(...), and optionalaction_events_configfor persistent state/history.DQActionManager.load_actions_from_local_file/save_actions_in_local_fileround-trip them to disk.DQEngine.apply_checks_and_save_in_tables(and the installed quality-checker workflow) auto-load eachRunConfig.actions_locationand fire those actions for that run. Each run config is applied through a dedicated engine (its own actions, a fresh observer, and an optional event store), keeping the parallel runner thread-safe.action_events_locationpersists event history and durable alert suppression across runs.DQSecret,TableActionsStorageConfig,LakebaseActionsStorageConfig,ActionEventsConfig,RunConfig.actions_location(definitions) andRunConfig.action_events_location(event history).demos/dqx_demo_alerting.pydemonstrating alerting via the log destination (with optional Slack), wired into the e2e demo runner.Known follow-ups (out of scope here)
StreamingQueryListenerthread (not the micro-batch data path), so synchronous delivery blocks only that callback, and alerts are rate-limited (HOURLY/DAILY) and fire only on threshold crossings — the documented small webhook timeout/retry budget keeps the callback fast. Off-thread delivery would only help high-throughput streaming that fires alerts frequently against slow endpoints (where a slow callback could let Spark drop progress events). Documented; deferred unless such a use case appears.FailPipelinein streaming is best-effort. It aborts immediately on the batch path, but in streaming it is raised inside the listener callback and Spark generally isolates/logs listener exceptions rather than stopping the query — so it is not guaranteed to hard-stop a streaming query. Documented; a guaranteed streaming stop (e.g. signallingquery.stop()from the listener) is a possible follow-up that needs live verification.Linked issues
Resolves #204 #610 #1214
Tests
Documentation and Demos