本文基于 SQLMesh 官方 Notifications guide 编写,所有示例均在
sqlmesh 0.236.3上实际跑通验证(含邮件发信、Webhook 推送、审计失败触发)。示例只涉及 SQL 模型 ,不涉及 Python 模型;自定义 Webhook 目标所需的少量 Python 代码放在 配置文件 里,与建模语言无关。
摘要
SQLMesh 内置了通知(Notification)机制,可以在 plan 应用、run 执行、audit 审计失败时自动发出告警。它开箱支持 SMTP 邮件 、Slack Webhook 、Slack API 三种通道,并通过 GenericNotificationTarget 支持把告警推送成结构化 JSON 到你自己的业务系统。
本文解决的问题是「告警能真正落地」:
- 最小可用 :只加几行
config.yaml,sqlmesh run失败就自动发邮件; - 精确找人 :利用模型的
owner+users[].email,让审计失败只通知模型负责人,而不是全组群发; - 推到自有系统 :用一个自定义通知目标,把
apply_failure / run_failure / audit_failure事件 POST 成 JSON,接入自建工单、值班、IM 机器人; - 别把告警变成新的故障:内置 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 个条件才会通知到人:
- 模型上填了
owner; - 模型挂了至少一个审计;
- 该
owner在users里配了用户级通知目标; - 该目标的
notify_on包含audit_failure; - 审计失败发生在
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 |
判别字段 |
三个来自源码、必须知道的细节:
- 只有隐式 SSL :内置实现写死了
smtplib.SMTP_SSL(host, port)。若你的企业邮箱只开放 587(STARTTLS),内置目标无法使用,必须走第五节的自定义目标。 - 密钥校验 :
is_configured要求host、user、password、sender四者齐全,缺一会静默不发。 - 没有连接超时 :
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] 数据质量告警"
三个容易踩的坑:
recipients必须等于email。User模型有校验器,用户级 SMTP 目标的收件人集合必须与user.email完全相等,多写一个地址就启动失败。- 必须
email字段 。只写recipients而不写email一样会失败。 - 审计失败只在
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
也就是说:
- 通知异常会向上抛出 ,直接中断
sqlmesh plan/sqlmesh run,并且掩盖掉真正的业务失败原因(原计划里是审计失败,最终看到的却是 SMTP DNS 错误); - 没有连接超时,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
七、总结
核心结论
- SQLMesh 的通知 =
notification_targets(目标) +notify_on(事件) + 全局/用户(作用域),三者配齐才会真正发出去。 - 邮件 是最低成本的生产告警通道:一条
type: smtp配置即可在run_failure时发信;用env_var()管理密钥,用subject区分环境;记住内置实现只支持隐式 SSL(465)且没有连接超时。 - 审计失败要落到人 ,必须
模型 owner↔users[].username对齐,且用户级 SMTP 目标的recipients必须与该用户email完全一致;审计失败通知只对prod环境生效。 - 推送到自建业务系统 用
GenericNotificationTarget:覆写notify_apply_failure / notify_run_failure / notify_audit_failure构造结构化 JSON,自带重试与退避,用event字段在下游做路由。坑点是type_必须保持内置标签generic。 - 告警失败绝不能影响主流程:内置 SMTP 目标会抛出异常并掩盖真实故障原因。生产环境务必用"加固版"目标(超时 + try/except),或直接使用自定义目标。
- 自定义目标需要
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 的故意失败审计各跑一次 |
延伸阅读