SQLMesh 告警落地实践:SQL 模型跑挂了,如何自动发邮件并推送自建业务系统

本文基于 SQLMesh 官方 Notifications guide 编写,所有示例均在 sqlmesh 0.236.3 上实际跑通验证(含邮件发信、Webhook 推送、审计失败触发)。

示例只涉及 SQL 模型 ,不涉及 Python 模型;自定义 Webhook 目标所需的少量 Python 代码放在 配置文件 里,与建模语言无关。


摘要

SQLMesh 内置了通知(Notification)机制,可以在 plan 应用、run 执行、audit 审计失败时自动发出告警。它开箱支持 SMTP 邮件 、Slack Webhook 、Slack API 三种通道,并通过 GenericNotificationTarget 支持把告警推送成结构化 JSON 到你自己的业务系统。

本文解决的问题是「告警能真正落地」:

  1. 最小可用 :只加几行 config.yaml,sqlmesh run 失败就自动发邮件;
  2. 精确找人 :利用模型的 owner + users[].email,让审计失败只通知模型负责人,而不是全组群发;
  3. 推到自有系统 :用一个自定义通知目标,把 apply_failure / run_failure / audit_failure 事件 POST 成 JSON,接入自建工单、值班、IM 机器人;
  4. 别把告警变成新的故障:内置 SMTP 目标会把异常抛回主流程、且没有连接超时------文中给出加固写法与实测证据。

一、先搞清 3 个概念:目标、事件、作用域

SQLMesh 的通知由三部分拼装而成,理解这三件事,配置就不会写错。

1.1 通知目标(notification target)

通知目标写在项目根目录的 config.yaml(或 config.py)中,可以配 全局 ,也可以配到 具体用户 下:

yaml 复制代码
# 全局:所有环境、所有事件都走这一组目标
notification_targets:
  - type: smtp
    ...

# 用户级:只发给对应的人(审计失败只发给模型 owner 时用)
users:
  - username: jen
    email: jen@example.com
    notification_targets:
      - type: smtp
        ...

内置的 type 一共 5 个(这也是配置校验允许的全部取值,写错会直接报 union_tag_invalid):

type 说明
smtp 邮件,字段见下文
slack_webhook Slack Incoming Webhook,只能发频道
slack_api Slack Web API,可以发频道或指定用户
console 打印到控制台,官方注释写明是给测试用的
generic 自定义目标的基类标签,用于扩展(见第四节)

1.2 触发事件(notify_on)

事件取值来自 NotificationEvent 枚举,只有下面这些:

事件 notify_on 取值 触发时机 通知正文
计划应用开始 apply_start sqlmesh plan 开始应用 Plan `{plan_id}` apply started for environment `{env}`.
计划应用结束 apply_end 应用成功 Plan `{plan_id}` apply finished for environment `{env}`.
计划应用失败 apply_failure 应用过程中抛异常(含模型执行失败、审计失败导致的中断) Plan {plan_id}in environment{env} apply failed. + 异常栈
调度开始 run_start sqlmesh run 开始 SQLMesh run started for environment `{env}`.
调度结束 run_end 调度成功 SQLMesh run finished for environment `{env}`.
调度失败 run_failure sqlmesh run 抛异常或存在失败模型 SQLMesh run failed. + 异常栈
审计失败 audit_failure 审计查询返回非 0 行 Audit failure. + 审计错误明细

实践中最该配的是 run_failure + audit_failure + apply_failure 三个"失败类"事件。run_start / apply_start 在生产环境通常只带来噪音。

1.3 作用域:全局 vs 用户

  • 全局目标 :会为所有环境 (dev、staging、prod)的同一事件发通知;
  • 用户级目标 :只在满足条件时生效,审计失败要同时满足 5 个条件才会通知到人:
    1. 模型上填了 owner;
    2. 模型挂了至少一个审计;
    3. 该 owner 在 users 里配了用户级通知目标;
    4. 该目标的 notify_on 包含 audit_failure;
    5. 审计失败发生在 prod 环境。

开发期防刷屏:顶层加 username: jen,SQLMesh 就只发这个人的用户级目标,其余目标全部静音。这个键也可以写在 ~/.sqlmesh/config.yaml 里,做成"本机开发静音"。


二、邮件通知:从最小配置到生产配置

2.1 目录结构

复制代码
my_project/
├── config.yaml            # 通知配置写在这里
├── models/
│   ├── raw_orders.sql
│   └── orders_daily.sql
└── audits/
    └── no_future_dates.sql

2.2 最小可用:run 失败就发邮件

yaml 复制代码
notification_targets:
  - type: smtp
    notify_on:
      - run_failure
    host: smtp.example.com
    port: 465
    user: alerts@example.com
    password: "{{ env_var('SMTP_PASSWORD') }}"
    sender: alerts@example.com
    recipients:
      - data-team@example.com

2.3 生产配置:环境变量 + 多收件人 + 自定义主题

官方推荐用 env_var() 把密钥留在环境里(YAML 中 {``{ ... }} 由 SQLMesh 的 Jinja 环境渲染),配合 .env 文件即可不把密码写进 Git:

yaml 复制代码
notification_targets:
  # ① 全组邮件:plan 应用失败 / run 调度失败
  - type: smtp
    notify_on:
      - apply_failure
      - run_failure
    host: "{{ env_var('SMTP_HOST') }}"
    port: 465
    user: "{{ env_var('SMTP_USER') }}"
    password: "{{ env_var('SMTP_PASSWORD') }}"
    sender: "{{ env_var('SMTP_SENDER') }}"
    recipients:
      - data-platform@example.com
      - data-oncall@example.com
    subject: "[SQLMesh][PROD] 调度异常"

  # ② 顺带推到 Slack 频道(Slack Webhook 只能发频道,不能 @ 人)
  - type: slack_webhook
    notify_on:
      - apply_failure
      - run_failure
      - audit_failure
    url: "{{ env_var('SLACK_WEBHOOK_URL') }}"

.env:

bash 复制代码
SMTP_HOST=smtp.exmail.qq.com
SMTP_USER=alerts@example.com
SMTP_PASSWORD=********
SMTP_SENDER=alerts@example.com
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/XXX/YYY/ZZZ

2.4 smtp 目标字段速查(对照源码)

字段 类型 默认值 说明
host str None SMTP 服务器
port int 465 仅使用隐式 SSL
user str None 登录用户名
password SecretStr None 登录密码,日志中自动脱敏
sender str None 发件人(From)
recipients setstr None 收件人列表
subject str SQLMesh Notification 邮件主题(固定值,正文里才带环境/异常)
type literal smtp 判别字段

三个来自源码、必须知道的细节:

  1. 只有隐式 SSL :内置实现写死了 smtplib.SMTP_SSL(host, port)。若你的企业邮箱只开放 587(STARTTLS),内置目标无法使用,必须走第五节的自定义目标。
  2. 密钥校验 :is_configured 要求 host、user、password、sender 四者齐全,缺一会静默不发。
  3. 没有连接超时 :SMTP_SSL 未传 timeout,网络黑洞下会长时间挂起------这是投产前必须加固的点。

2.5 SQL 模型怎么写(审计失败示例)

告警要有价值,前提是审计真的能挡住脏数据。下面两个模型是本文所有示例的基础:

sql 复制代码
-- models/raw_orders.sql
MODEL (
  name demo.raw_orders,
  kind FULL,
  owner: jen,              -- 关键:审计失败通知要靠它找人
  cron '@daily',
  audits (not_null(columns := (order_id, order_date)))
);

SELECT
  1 AS order_id,
  CAST('2025-01-01' AS DATE) AS order_date,
  42 AS customer_id,
  100.0 AS amount
sql 复制代码
-- models/orders_daily.sql
MODEL (
  name demo.orders_daily,
  kind FULL,
  owner: jen,
  cron '@daily',
  audits (no_future_dates)      -- 引用 audits/ 目录下的命名审计
);

SELECT
  order_date,
  COUNT(*) AS orders,
  SUM(amount) AS amount
FROM demo.raw_orders
GROUP BY order_date
sql 复制代码
-- audits/no_future_dates.sql
AUDIT (
  name no_future_dates,
  dialect duckdb
);

SELECT
  COUNT(*) AS future_rows
FROM @this_model
WHERE CAST(order_date AS DATE) > CURRENT_DATE

审计查询返回行数 > 0 即判定失败。首次应用计划时你会看到:

text 复制代码
**Failed models**
* `"demo"."demo"."orders_daily"`

  'no_future_dates' audit error: 1 row failed

此时 audit_failure 事件被触发------但只有把 owner 和用户级目标接上,才会真正发到人。


三、把审计失败只发给模型负责人

这是"审计失败通知"唯一的正确用法:模型 owner ↔ 用户 username 对齐,邮件只发给当事人。

yaml 复制代码
model_defaults:
  dialect: duckdb
  owner: jen          # 也可以按模型单独写 owner

users:
  - username: jen     # 必须与模型 owner 一致
    email: jen@example.com
    notification_targets:
      - type: smtp
        notify_on:
          - audit_failure
          - run_failure
        host: "{{ env_var('SMTP_HOST') }}"
        port: 465
        user: "{{ env_var('SMTP_USER') }}"
        password: "{{ env_var('SMTP_PASSWORD') }}"
        sender: "{{ env_var('SMTP_SENDER') }}"
        recipients:
          # ⚠️ 必须与该用户的 email 完全一致,否则配置校验直接报错:
          #    "Recipient emails do not match user email"
          - jen@example.com
        subject: "[SQLMesh][PROD] 数据质量告警"

三个容易踩的坑:

  1. recipients 必须等于 email 。User 模型有校验器,用户级 SMTP 目标的收件人集合必须与 user.email 完全相等,多写一个地址就启动失败。
  2. 必须 email 字段 。只写 recipients 而不写 email 一样会失败。
  3. 审计失败只在 prod 环境通知 。在 dev 里反复跑计划不会打扰任何人,这是官方有意设计。

需要一次通知多个人?把那些人各自建成 user(各自 email),或者用全局目标(全局目标不受该校验限制)。

实测配置解析结果:

复制代码
GLOBAL BasicSMTPNotificationTarget       ['apply_failure', 'run_failure']
GLOBAL SlackWebhookNotificationTarget    ['apply_failure', 'audit_failure', 'run_failure']
USER jen BasicSMTPNotificationTarget     ['audit_failure', 'run_failure']
owner default: jen

四、推到自建业务系统:Webhook 通知目标

邮件会被忽略,Slack 不能进工单。真正落地时,通常要把告警推给自己的系统(值班平台、工单、内部 IM 机器人、监控网关)。

SQLMesh 没有开箱的"通用 HTTP Webhook",但提供了扩展点:继承 GenericNotificationTarget 并覆写事件钩子。

4.1 一个前提

自定义通知目标必须是 Python 类,所以这个项目要用 config.py 而不是 config.yaml。

注意 :config.py 与 config.yaml 不能共存,否则 SQLMesh 报 Multiple configuration files found。

再强调一次:这只是配置文件,与"Python 模型 / Python 项目"无关,模型依旧全部是 SQL。

4.2 目录结构

复制代码
my_project/
├── config.py                  # 用 Python 配置来挂载自定义目标
├── notification_targets.py    # 自定义通知目标(纯告警基础设施)
├── models/*.sql               # 全部是 SQL 模型
└── audits/*.sql

4.3 自定义 Webhook 目标

python 复制代码
# notification_targets.py
from __future__ import annotations

import json
import os
import time
from dataclasses import dataclass

import requests
from sqlmesh.core.notification_target import (
    GenericNotificationTarget,
    NotificationEvent,
    NotificationStatus,
)


@dataclass(frozen=True)
class _Alert:
    event: str
    environment: str
    severity: str
    summary: str
    detail: str


class WebhookNotificationTarget(GenericNotificationTarget):
    """把 SQLMesh 事件推送成结构化 JSON,供自建业务系统消费。"""

    # ⚠️ 必须复用内置的 "generic" 标签:
    # Config 的判别式联合只认 smtp/generic/console/slack_api/slack_webhook,
    # 自己起名(例如 "webhook")会在配置校验阶段被拒绝。
    type_: str = "generic"

    url: str
    token: str = ""
    timeout: int = 10
    retries: int = 3

    # ---- 事件钩子:只覆写需要自定义 payload 的事件 ----
    def notify_apply_start(self, environment: str, plan_id: str, *args, **kwargs) -> None:
        self._post(_Alert(
            event=NotificationEvent.APPLY_START.value,
            environment=environment,
            severity="info",
            summary=f"Plan `{plan_id}` apply started",
            detail="",
        ))

    def notify_apply_end(self, environment: str, plan_id: str, *args, **kwargs) -> None:
        self._post(_Alert(
            event=NotificationEvent.APPLY_END.value,
            environment=environment,
            severity="success",
            summary=f"Plan `{plan_id}` apply finished",
            detail="",
        ))

    def notify_apply_failure(self, environment: str, plan_id: str, exc: str, *args, **kwargs) -> None:
        self._post(_Alert(
            event=NotificationEvent.APPLY_FAILURE.value,
            environment=environment,
            severity="critical",
            summary=f"Plan `{plan_id}` apply failed",
            detail=exc,
        ))

    def notify_run_failure(self, exc: str, *args, **kwargs) -> None:
        self._post(_Alert(
            event=NotificationEvent.RUN_FAILURE.value,
            environment=os.getenv("SQLMESH_ENV", "prod"),
            severity="critical",
            summary="SQLMesh run failed",
            detail=exc,
        ))

    def notify_audit_failure(self, audit_error, *args, **kwargs) -> None:
        self._post(_Alert(
            event=NotificationEvent.AUDIT_FAILURE.value,
            environment=os.getenv("SQLMESH_ENV", "prod"),
            severity="warning",
            summary="SQLMesh audit failed",
            detail=str(audit_error),
        ))

    # 兜底:未覆写的事件(run_start/apply_end 之外的新事件)走通用分支
    def send(self, notification_status: NotificationStatus, msg: str, **kwargs) -> None:
        detail = kwargs.get("exc") or ""
        if kwargs.get("audit_error") is not None:
            detail = str(kwargs["audit_error"])
        self._post(_Alert(
            event="sqlmesh_notification",
            environment=os.getenv("SQLMESH_ENV", "prod"),
            severity="critical" if notification_status.is_failure else "info",
            summary=msg,
            detail=detail,
        ))

    # ---- 真正的推送逻辑 ----
    def _post(self, alert: _Alert) -> None:
        payload = {
            "source": "sqlmesh",
            "event": alert.event,
            "environment": alert.environment,
            "severity": alert.severity,
            "summary": alert.summary,
            "detail": alert.detail[:4000],          # 防止异常栈撑爆请求体
            "occurred_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
        }
        headers = {"Content-Type": "application/json"}
        if self.token:
            headers["Authorization"] = f"Bearer {self.token}"

        last_error: Exception | None = None
        for attempt in range(1, self.retries + 1):
            try:
                resp = requests.post(
                    self.url,
                    headers=headers,
                    data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
                    timeout=self.timeout,
                )
                if resp.status_code < 400:
                    return
                last_error = RuntimeError(f"HTTP {resp.status_code}: {resp.text[:200]}")
            except requests.RequestException as e:   # 网络异常同样重试
                last_error = e
            time.sleep(min(2 ** attempt, 10))        # 2s / 4s / 8s 退避

        # 通知失败不影响 SQLMesh 主流程,只记日志
        print(f"[webhook] failed to notify {self.url}: {last_error}")

4.4 挂到 config.py

python 复制代码
# config.py
import os

from sqlmesh.core.config import (
    Config,
    DuckDBConnectionConfig,
    GatewayConfig,
    ModelDefaultsConfig,
)
from sqlmesh.core.user import User

from notification_targets import WebhookNotificationTarget

config = Config(
    # 换成你的生产连接(Snowflake / BigQuery / Trino ...)
    gateways={
        "local": GatewayConfig(connection=DuckDBConnectionConfig(database="demo.db")),
    },
    default_gateway="local",
    model_defaults=ModelDefaultsConfig(dialect="duckdb"),
    # 用户级目标(可选):审计失败只通知模型 owner
    users=[
        User(
            username="jen",
            email="jen@example.com",
            notification_targets=[
                WebhookNotificationTarget(
                    notify_on=["audit_failure"],
                    url=os.getenv("DATA_QUALITY_WEBHOOK_URL", ""),
                )
            ],
        )
    ],
    notification_targets=[
        WebhookNotificationTarget(
            notify_on=["apply_failure", "run_failure", "audit_failure"],
            url=os.getenv("ALERT_WEBHOOK_URL", "https://alert.internal.example.com/api/v1/sqlmesh"),
            token=os.getenv("ALERT_WEBHOOK_TOKEN", ""),
        )
    ],
)

4.5 实测:SQLMesh 真实触发时打出的 payload

下面的 JSON 是真实跑出来的(audit_failure 由审计查询返回 1 行触发,apply_failure 由计划应用中断触发):

json 复制代码
{
  "source": "sqlmesh",
  "event": "audit_failure",
  "environment": "prod",
  "severity": "warning",
  "summary": "SQLMesh audit failed",
  "detail": "'no_future_dates' audit error: 1 row failed",
  "occurred_at": "2026-10-07T15:04:00+0800"
}
json 复制代码
{
  "source": "sqlmesh",
  "event": "apply_failure",
  "environment": "dev",
  "severity": "critical",
  "summary": "Plan `84c0bdeec58d4b2e9ffa342caaa9421d` apply failed",
  "detail": "Traceback (most recent call last):\n ... sqlmesh.utils.errors.PlanError: Plan application failed.\n",
  "occurred_at": "2026-10-07T15:04:00+0800"
}

下游系统只要按 event 字段路由即可:

  • run_failure / apply_failure → 建 P1 工单 + 电话值班;
  • audit_failure → 建数据质量工单,指派给 owner(可把 owner 一并塞进 payload);
  • apply_end → 关闭对应的发布工单。

4.6 想只改邮件、不改通道?

同一条路也适用于"在邮件里补上下文"。比如把 SQLMesh 运行日志尾部一起发给值班同学:

python 复制代码
# notification_targets.py
from __future__ import annotations

import os
from pathlib import Path

from sqlmesh.core.notification_target import (
    BasicSMTPNotificationTarget,
    NotificationStatus,
)

LOG_FILE = Path(os.getenv("SQLMESH_LOG_FILE", "logs/sqlmesh.log"))
LOG_TAIL_LINES = 30


class ContextualEmailTarget(BasicSMTPNotificationTarget):
    """run_failure 时把日志尾部一起发给值班同学。"""

    def notify_run_failure(self, exc: str, *args, **kwargs) -> None:
        detail = exc
        if LOG_FILE.exists():
            lines = LOG_FILE.read_text(encoding="utf-8").splitlines()
            detail = (
                f"{exc}\n\n--- {LOG_FILE} (last {LOG_TAIL_LINES} lines) ---\n"
                + "\n".join(lines[-LOG_TAIL_LINES:])
            )
        self.send(NotificationStatus.FAILURE, "SQLMesh run failed.", exc=detail)

实测发出邮件的正文(SMTP 连接被替换为内存对象,EmailMessage 内容为真实产物):

复制代码
From    : alerts@example.com
To      : oncall@example.com, data-team@example.com
Subject : SQLMesh Notification
Body    :
SQLMesh run failed.
DuckDB Error: Catalog Error - table demo.raw_orders does not exist

--- logs\sqlmesh.log (last 30 lines) ---
2025-01-01 00:00:30 INFO model=demo.raw_orders step=30
...

五、生产加固:别让告警失败打断主流程

这一节是血泪教训,有实测证据。

5.1 内置 smtp 目标的两个问题

我把 config.yaml 的 SMTP 指向一个不存在的主机,然后跑 sqlmesh plan dev --auto-apply。模型本身的问题被"告警失败"盖住了------最终异常栈是:

复制代码
File ".../sqlmesh/core/notification_target.py", line 386, in send_text_message
    with smtplib.SMTP_SSL(host=self.host, port=self.port) as smtp:
...
socket.gaierror: [Errno 11001] getaddrinfo failed

也就是说:

  1. 通知异常会向上抛出 ,直接中断 sqlmesh plan / sqlmesh run,并且掩盖掉真正的业务失败原因(原计划里是审计失败,最终看到的却是 SMTP DNS 错误);
  2. 没有连接超时,SMTP 服务器被防火墙丢包时,调度任务可能长时间挂住。

同一场景下,自定义目标(第四节)因为自己吞掉了异常,prompt 返回的是正常的业务失败。

5.2 加固写法

python 复制代码
# notification_targets.py
from __future__ import annotations

import logging
import os
import smtplib
from email.message import EmailMessage

from sqlmesh.core.notification_target import (
    BasicSMTPNotificationTarget,
    NotificationStatus,
)

logger = logging.getLogger("sqlmesh.alerting")
SMTP_CONNECT_TIMEOUT = int(os.getenv("SMTP_CONNECT_TIMEOUT", "10"))


class SafeSMTPTarget(BasicSMTPNotificationTarget):
    """内置 smtp 目标的加固版本。

    与内置实现的差异:
    1. SMTP_SSL 显式传入 timeout,避免 TLS 握手/连接阶段无限期挂起;
    2. 发送过程中的任何异常只记日志,不再向上抛出(否则会掩盖真正的调度失败)。
    """

    def send_text_message(self, notification_status: NotificationStatus, msg: str) -> None:
        if not self.host:
            raise ValueError("Missing SMTP host for notification")

        email = EmailMessage()
        email["Subject"] = self.subject
        email["To"] = ",".join(self.recipients or [])
        email["From"] = self.sender
        email.set_content(msg)

        try:
            with smtplib.SMTP_SSL(
                host=self.host,
                port=self.port,
                timeout=SMTP_CONNECT_TIMEOUT,
            ) as smtp:
                if self.user and self.password:
                    smtp.login(user=self.user, password=self.password.get_secret_value())
                smtp.send_message(email)
            logger.info("alert sent to %s", email["To"])
        except Exception:
            # 绝不能让"发不出告警"变成"任务失败"
            logger.exception("failed to send SQLMesh alert via %s:%s", self.host, self.port)

5.3 需要 587 / STARTTLS 的企业邮箱

内置目标是隐式 SSL,走 587 的话直接用同一位置换成显式 TLS:

python 复制代码
        try:
            with smtplib.SMTP(host=self.host, port=self.port, timeout=SMTP_CONNECT_TIMEOUT) as smtp:
                smtp.ehlo()
                smtp.starttls()
                smtp.ehlo()
                if self.user and self.password:
                    smtp.login(user=self.user, password=self.password.get_secret_value())
                smtp.send_message(email)

实测加固效果:

复制代码
target type: SafeSMTPTarget
调用方没有被异常打断 -> 异常隔离生效
(异常栈被记录到日志,不会再冒泡到 SQLMesh CLI)

六、如何验证告警真的会发出去

告警最怕"以为配好了"。推荐两条实测路径。

6.1 发信链路空跑(不依赖模型失败)

用 SQLMesh Python API 直接载入配置、调用目标的 notify_* 方法,把 SMTP 连接换成内存对象,验证邮件内容与收件人是否正确:

python 复制代码
# verify_notification.py
import smtplib
from pathlib import Path

from sqlmesh.core.config import Config, load_config_from_paths


class FakeSMTP:
    """记录被"发送"的邮件,不做任何网络连接。"""

    sent = []

    def __init__(self, host, port, *args, **kwargs):
        self.host, self.port = host, port

    def __enter__(self):
        return self

    def __exit__(self, *args):
        return False

    def login(self, user, password):
        print(f"  login -> {user}")

    def send_message(self, email):
        FakeSMTP.sent.append((self.host, self.port, email))


smtplib.SMTP_SSL = FakeSMTP

cfg = load_config_from_paths(Config, project_paths=[Path(__file__).parent / "config.py"])
target = cfg.notification_targets[0]
print("target type:", type(target).__name__)
print("recipients :", sorted(target.recipients))

target.notify_run_failure("DuckDB Error: Catalog Error - table demo.raw_orders does not exist")

for host, port, email in FakeSMTP.sent:
    print("\n=== EMAIL SENT ===")
    print("smtp :", f"{host}:{port}")
    print("From :", email["From"])
    print("To   :", email["To"])
    print("Subj :", email["Subject"])
    print("Body :\n" + email.get_content())

只要替换收件人即可用真实邮箱做一次"真发信"冒烟测试,无需真的把模型跑挂。

6.2 端到端:让审计真的失败

审计查询返回 > 0 行即失败,所以写一个"故意失败"的审计就能稳定复现:

sql 复制代码
AUDIT (
  name always_fail,
  dialect duckdb
);

SELECT 1 AS failed_rows

把它挂到一个演示模型上,然后:

bash 复制代码
sqlmesh plan dev --auto-apply --no-prompts   # 触发 apply_failure
sqlmesh plan prod --auto-apply --no-prompts  # 审计失败 + owner 通知(prod 才发)

一个真实的失败输出:

复制代码
**Failed models**
* `"demo"."demo"."orders_daily"`

'no_future_dates' audit error: 1 row failed
复制代码

6.3 本地监听 Webhook

自建系统接入前,可以用一个本地 HTTP 服务顶替,直接看到 SQLMesh 推过来的原始 JSON:

bash 复制代码
# 终端 A:起一个打印请求体的服务
python -m http.server 8000   # 或用 ngrok / 内网网关
bash 复制代码
# 终端 B
export ALERT_WEBHOOK_URL=http://127.0.0.1:8000/alert
sqlmesh plan dev --auto-apply --no-prompts

七、总结

核心结论

  1. SQLMesh 的通知 = notification_targets(目标) + notify_on(事件) + 全局/用户(作用域),三者配齐才会真正发出去。
  2. 邮件 是最低成本的生产告警通道:一条 type: smtp 配置即可在 run_failure 时发信;用 env_var() 管理密钥,用 subject 区分环境;记住内置实现只支持隐式 SSL(465)且没有连接超时。
  3. 审计失败要落到人 ,必须 模型 owner ↔ users[].username 对齐,且用户级 SMTP 目标的 recipients 必须与该用户 email 完全一致;审计失败通知只对 prod 环境生效。
  4. 推送到自建业务系统 用 GenericNotificationTarget:覆写 notify_apply_failure / notify_run_failure / notify_audit_failure 构造结构化 JSON,自带重试与退避,用 event 字段在下游做路由。坑点是 type_ 必须保持内置标签 generic。
  5. 告警失败绝不能影响主流程:内置 SMTP 目标会抛出异常并掩盖真实故障原因。生产环境务必用"加固版"目标(超时 + try/except),或直接使用自定义目标。
  6. 自定义目标需要 config.py,而 config.py 与 config.yaml 互斥;但这只涉及配置,模型侧仍然是纯 SQL。

落地清单

项 建议
事件订阅 只订 apply_failure / run_failure / audit_failure,避免 *_start 噪音
通道 邮件给 owner + Webhook 给值班/工单系统,双通道冗余
密钥 全部走环境变量或 .env,不要把密码提交进 Git
超时 SMTP / HTTP 都要显式超时,HTTP 再加指数退避重试
异常 通知逻辑一律吞异常并记日志,禁止冒泡
开发期 顶层 username: <你自己> 静音其他人的目标
验证 上线前用 6.1 的空跑脚本 + 6.2 的故意失败审计各跑一次

延伸阅读

相关推荐
大大大大晴天1 小时前
每天认识一个组件:Apache Calcite
大数据
adinnet20261 小时前
保单、赔付与渠道问数:保险经营数据如何实现按需查询
大数据·数据库·人工智能
组工部管理能手李哥3 小时前
政务办公场景下,私有化知识库+智能体方案的技术实践与思考
大数据·人工智能
anxiao_m3 小时前
水利数字孪生怎么选?主流可视化渲染平台深度横向测评
大数据·前端·人工智能·图形渲染·云渲染
用户3610588626124 小时前
Flink Time 之时间语义深度剖析:从 Processing Time 到 Event Time 与 Watermark 机制
大数据·flink
梦想画家4 小时前
SQLMesh 宏实现循环完全指南:@EACH、Python 宏与 SQLGlot 表达式实战
大数据·数据开发·sqlmesh
跨境联盟5 小时前
行业思考|精准营养会成为社区健康驿站的核心竞争力吗?
大数据·人工智能·健康医疗·健康管理·精准营养
龙亘川5 小时前
长假大客流复盘|数字化助力城市交通与文旅态势智能管控
大数据·人工智能·智慧城市·开源软件·数据可视化
QYR-分析6 小时前
锂电制造核心装备:全球电芯焊接机市场格局与增长趋势研判
大数据·人工智能·制造