第八章:告警规则与通知体系
Sentry 的告警与通知体系是整个错误监控平台的核心闭环之一——仅有数据采集和展示远远不够,关键在于在正确的时机、通过正确的渠道、将正确的信息触达正确的人。本章深入分析 Sentry 告警系统的三层架构:告警规则定义(Issue Alert / Metric Alert)、通知路由分发(NotificationController)、通知聚合与渠道投递(Digests / Integrations)。
目录
- 1. Sentry 告警体系概览
- 2. Issue Alert Rules:条件、过滤器、动作
- 3. Metric Alert Rules(指标告警)
- 4. 告警条件详解
- 5. 告警动作详解
- 6. 通知路由与分发
- 7. 通知聚合(Digests)
- 8. 集成通知渠道
- 9. Webhook 自定义通知
- 10. 告警最佳实践
1. Sentry 告警体系概览
Sentry 的告警系统由两大核心类型构成:
| 告警类型 | 触发机制 | 数据源 | 典型场景 |
|---|---|---|---|
| Issue Alert Rules | 事件到达时实时评估 | 单个 Error/Issue 事件 | “新错误首次出现时通知我” |
| Metric Alert Rules | 定时聚合查询 Snuba | Snuba 时序数据库 | “5分钟内错误率超过 5% 则告警” |
两者共享同一套通知分发体系:条件匹配 → 动作执行 → 参与者计算 → 渠道路由 → 消息投递(支持聚合)。这套体系的代码入口分布在以下几个核心模块:
src/sentry/rules/ # 规则定义(条件、过滤器、动作的注册与基类)
src/sentry/notifications/ # 通知路由、参与者计算、通知对象定义
src/sentry/digests/ # 通知聚合(Digest)的后端与状态管理
src/sentry/integrations/ # 各集成渠道的通知发送实现
src/sentry/mail/ # 邮件通知发送
2. Issue Alert Rules:条件、过滤器、动作
Issue Alert 是”基于事件的告警”——每当有新事件被接收并完成处理后,Sentry 的 post_process 流程会触发规则评估。一个 Issue Alert Rule 由三部分组成,在数据模型中以 Rule.data JSON 字段存储:
{
"action_match": "any",
"filter_match": "all",
"conditions": [
{"id": "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition"}
],
"filters": [
{"id": "sentry.rules.filters.tagged_event.TaggedEventFilter", "key": "environment", "match": "eq", "value": "production"}
],
"actions": [
{"id": "sentry.mail.actions.MailAction", "targetType": "IssueOwners"}
],
"frequency": 30
}
三层配置的逻辑关系:
Conditions (OR/AND) → Filters (AND/OR/NONE) → Actions
- Conditions(条件):定义”什么情况下触发”,多个条件之间通过
action_match决定是any(任一满足)还是all(全部满足)。 - Filters(过滤器):在条件判断通过后进一步筛选,
filter_match决定all/any/none。 - Actions(动作):触发后的执行操作,如发送通知、创建工单等。
2.1 三层继承体系
Sentry 中所有规则组件都继承自 RuleBase,再分化为三类抽象基类:
RuleBase (src/sentry/rules/base.py)
├── EventCondition (src/sentry/rules/conditions/base.py)
│ rule_type = "condition/event"
│ 抽象方法: passes(event, state) -> bool
│
├── EventFilter (src/sentry/rules/filters/base.py)
│ rule_type = "filter/event"
│ 抽象方法: passes(event, state) -> bool
│
└── EventAction (src/sentry/rules/actions/base.py)
rule_type = "action/event"
抽象方法: after(event, notification_uuid) -> Generator[CallbackFuture]
RuleBase(src/sentry/rules/base.py:56) 是所有规则组件的基类,提供了:
class RuleBase(abc.ABC):
id: ClassVar[str] # 全局唯一标识
label: ClassVar[str] # 人类可读标签
rule_type: ClassVar[str] # 分类:"condition/event" / "filter/event" / "action/event"
def __init__(self, project, data=None, rule=None):
self.project = project
self.data = data or {} # 用户配置的参数
self.rule = rule # 关联的 Rule 模型实例
def get_option(self, key, default=None): # 从 self.data 读取配置项
def get_form_instance(self): # 返回 Django Form 用于 UI 渲染
def render_label(self): # 根据 data 动态渲染标签
每个规则组件通过三个类变量标识自身:id(全局唯一标识符,如 "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition")、label(人类可读标签)、rule_type(分类标识)。get_option() 方法从 self.data 字典中读取用户在 UI 中配置的参数。
EventCondition(src/sentry/rules/conditions/base.py:17) 核心方法是 passes(event, state),接收事件与事件状态对象,返回布尔值表示条件是否满足。EventState 携带关于事件的上下文信息:
class EventState:
is_new: bool # 是否为全新 issue
is_regression: bool # 是否为回归
is_new_group_environment: bool # 是否为该环境下的新 issue
has_reappeared: bool # 是否重现
has_escalated: bool # 是否升级
EventFilter(src/sentry/rules/filters/base.py:7) 签名与 Condition 完全相同,passes(event, state) -> bool。过滤器和条件的区别在于语义和执行顺序——条件先判断、过滤器后筛选。
EventAction(src/sentry/rules/actions/base.py:31) 只有一个核心方法:
@abc.abstractmethod
def after(self, event, notification_uuid=None) -> Generator[CallbackFuture]:
"""规则匹配后执行,应 yield CallbackFuture 实例"""
CallbackFuture 是一个 namedtuple,封装了回调函数及其参数:
CallbackFuture = namedtuple("CallbackFuture", ["callback", "kwargs", "key"])
这个设计使得动作可以返回延迟执行的回调链——由 RuleProcessor 收集所有 Future 后统一执行,实现了条件判断与副作用执行的分离。这意味着即使某个 Action 发送通知失败,其他 Action 仍然可以正常执行。
2.2 Rule Processor:规则匹配引擎
规则匹配引擎的入口在 src/sentry/rules/processing/processor.py。核心函数 activate_downstream_actions(第 66 行)负责:
- 遍历
rule.data["actions"],通过instantiate_action实例化每个动作对象 - 调用
action_inst.after(event, notification_uuid)收集所有CallbackFuture - 按
key分组返回{key: (callback, [RuleFuture])}映射
条件/过滤器的分离在 split_conditions_and_filters(第 52 行)中完成——通过检查 rule_type 是否为 "condition/event" 来区分。特别地,频率条件(event_frequency)被标记为”慢条件”(第 16 行),因为它们需要查询 Snuba 数据库,Sentry 会先评估快条件再评估慢条件以优化性能。
匹配函数(第 19 行)支持三种逻辑:
def get_match_function(match_name):
if match_name == "all": return all
elif match_name == "any": return any
elif match_name == "none": return lambda bool_iter: not any(bool_iter)
2.3 规则注册机制
规则组件通过 RuleRegistry(src/sentry/rules/registry.py)管理和发现:
class RuleRegistry:
def __init__(self):
self._rules: dict[str, list[type[RuleBase]]] = defaultdict(list)
self._map: dict[str, type[RuleBase]] = {}
def add(self, rule: type[RuleBase]):
self._map[rule.id] = rule
self._rules[rule.rule_type].append(rule)
初始化时,init_registry()(src/sentry/rules/__init__.py:17)遍历 _SENTRY_RULES 常量列表,通过 import_string 动态加载每个规则类并注册。全局单例 rules 在第 29 行创建。这种基于注册表的设计允许 Sentry 的第三方插件和 Sentry App 注册自定义的条件/过滤器/动作。
3. Metric Alert Rules(指标告警)
3.1 架构概览
Metric Alert(指标告警)与 Issue Alert 的根本区别在于:Issue Alert 在每个事件到达时评估,而 Metric Alert 通过定时任务周期性地查询 Snuba 聚合数据。这意味着 Metric Alert 能够处理跨事件的聚合逻辑(如”过去 5 分钟内错误数超过 100”),而 Issue Alert 每次只能看到一个事件。
Metric Alert 的数据模型更复杂,涉及 AlertRule、Detector、Workflow 等多个模型。不过,在通知分发层面,Metric Alert 最终会复用 Issue Alert 的通知基础设施。BaseMetricAlertHandler(src/sentry/notifications/notification_action/types.py:433)定义了对 Metric Alert 通知的处理接口:
class BaseMetricAlertHandler(ABC):
@classmethod
def send_alert(cls, notification_context, alert_context,
metric_issue_context, open_period_context,
trigger_status, notification_uuid,
organization, project) -> None:
raise NotImplementedError
四个上下文对象封装了 Metric Alert 触发时的完整状态:
| 上下文 | 内容 |
|---|---|
NotificationContext |
通知渠道配置(sentry_app_id, integration_id 等) |
AlertContext |
告警规则基本信息(action_identifier_id, name 等) |
MetricIssueContext |
指标状态(open_period_identifier, new_status, session_status) |
OpenPeriodContext |
开放周期信息(date_started, date_closed) |
3.2 双阈值迟滞设计
Metric Alert 的一个关键设计是双阈值迟滞(Hysteresis)。这是为了防止指标在阈值附近抖动导致的”告警风暴”。典型配置:
| 阈值类型 | 值 | 作用 |
|---|---|---|
| Critical | 错误率 > 10% | 触发 Critical 告警 |
| Warning | 错误率 > 5% | 触发 Warning 告警 |
| Resolution | 错误率 < 3% | 自动解除告警 |
Resolution 阈值(3%)低于 Warning 阈值(5%),形成一个”迟滞区间”——指标必须从高于 5% 降到低于 3% 才能解除告警,而非在 5% 附近来回波动时反复触发和解除。
从代码层面,告警的状态切换由 TriggerStatus 枚举管理(src/sentry/incidents/models/incident.py),包含 ACTIVE、RESOLVED、WARNING、CRITICAL 等状态。每个 Alert Rule 可以有多个 Trigger(条件分支),每个 Trigger 绑定一组 Actions。
4. 告警条件详解
4.1 First Seen / Every Event
FirstSeenEventCondition(src/sentry/rules/conditions/first_seen_event.py:11)是最简单的条件之一:
class FirstSeenEventCondition(EventCondition):
id = "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition"
label = "A new issue is created"
def passes(self, event, state):
if self.rule.environment_id is None:
return state.is_new # 全局首次出现
else:
return state.is_new_group_environment # 该环境内首次出现
当关联了环境过滤时,条件判断变为”该 Issue 是否在指定环境中首次出现”,这对于按环境(staging vs production)分别配置告警策略十分有用。
EveryEventCondition(src/sentry/rules/conditions/every_event.py:6)更加简单——passes() 永远返回 True,但通过重写 is_enabled() 返回 False 来在 UI 中隐藏该条件。它主要用于内部默认规则或程序化创建的规则。
4.2 Event Frequency(事件频率)
频率条件是最复杂也最常用的条件类型,由 BaseEventFrequencyCondition(src/sentry/rules/conditions/event_frequency.py:131)及其子类实现:
| 子类 | ID 后缀 | 行为 |
|---|---|---|
EventFrequencyCondition |
EventFrequencyCondition |
Issue 在 {interval} 内出现超过 {value} 次 |
EventUniqueUserFrequencyCondition |
EventUniqueUserFrequencyCondition |
受 Issue 影响的用户数在 {interval} 内超过 {value} |
EventFrequencyPercentCondition |
EventFrequencyPercentCondition |
Issue 影响超过 {value}% 的会话 |
标准时间间隔(第 35 行):
STANDARD_INTERVALS = {
"1m": ("one minute", timedelta(minutes=1)),
"5m": ("5 minutes", timedelta(minutes=5)),
"15m": ("15 minutes", timedelta(minutes=15)),
"1h": ("one hour", timedelta(hours=1)),
"1d": ("one day", timedelta(hours=24)),
"1w": ("one week", timedelta(days=7)),
"30d": ("30 days", timedelta(days=30)),
}
频率条件支持两种比较模式(ComparisonType,第 58 行):
- COUNT(绝对计数):”过去 1 小时内出现超过 50 次”。
- PERCENT(百分比增长):”过去 1 小时内出现次数比之前 5 分钟增加了 50% 以上”。
核心的 passes() 方法(第 169 行)流程:
def passes(self, event, state):
# 新 Issue 的第一个事件不可能超过阈值(value > 1 时)
if state.is_new and value > 1:
return False
comparison_type = self.get_option("comparisonType", "count")
comparison_interval = COMPARISON_INTERVALS[comparison_interval_option][1]
# 调用 get_rate() 查询 Snuba 获取当前值
current_value = self.get_rate(
duration=duration,
comparison_interval=comparison_interval,
event=event,
environment_id=self.rule.environment_id,
comparison_type=comparison_type,
)
return current_value > value
get_rate()(第 291 行)是实际的 Snuba 查询入口:
def get_rate(self, duration, comparison_interval, event, environment_id, comparison_type):
start, end = self.get_query_window(end=current_time, duration=duration)
with self.disable_consistent_snuba_mode(duration):
result = self.query(event, start, end, environment_id=environment_id)
if comparison_type == ComparisonType.PERCENT:
# 查询对比区间的数据
current_time -= comparison_interval
start, end = self.get_query_window(end=current_time, duration=duration)
comparison_result = self.query(event, start, end, environment_id=environment_id)
result = percent_increase(result, comparison_result)
return result
percent_increase()(第 1029 行)的计算逻辑:
def percent_increase(result, comparison_result):
if comparison_result <= 0:
return int(max(0, result) * 100) # 无基线时按 N*100% 计算
change = (result - comparison_result) / comparison_result * 100
return int(max(0, change))
对于大于等于 1 小时间隔的查询,disable_consistent_snuba_mode()(第 272 行)会禁用 Snuba 的写后读一致性(consistent: False),以提升查询性能和扩展性——对于长时间窗口的聚合查询,少量延迟写入的数据不会显著影响结果。
批量查询优化:batch_query_hook() 方法(第 258 行)支持同时查询多个 Group 的聚合数据,通过 chunked() 函数(每批 10000 个 group_id)分批查询,并在内部将 Error 类型的 Issue 和 Generic Issue 分开查询(因为它们使用不同的 TSDB model)。
4.3 Tagged Event(标签匹配)
TaggedEventCondition(src/sentry/rules/conditions/tagged_event.py:42)根据事件的 Tag 键值对进行匹配。其 _passes() 方法(第 52 行)利用 match_values() 函数实现丰富的匹配语义:
def _passes(self, raw_tags):
option_key = self.get_option("key") # 如 "environment"
option_match = self.get_option("match") # 如 "eq"
option_value = self.get_option("value") # 如 "production"
# IS_SET / NOT_SET 只检查键的存在性
if option_match == MatchType.IS_SET:
return option_key in tag_keys
elif option_match == MatchType.NOT_SET:
return option_key not in tag_keys
# 获取匹配的 tag 值列表
tag_values = (v.lower() for k, v in raw_tags
if k.lower() == option_key or
tagstore.backend.get_standardized_key(k) == option_key)
return match_values(tag_values, option_value, option_match)
MatchType 枚举(src/sentry/rules/match.py:6)定义了 16 种匹配类型:
| 匹配类型 | 代码 | 描述 | 匹配类型 | 代码 | 描述 |
|---|---|---|---|---|---|
CONTAINS |
co |
包含 | NOT_CONTAINS |
nc |
不包含 |
EQUAL |
eq |
等于 | NOT_EQUAL |
ne |
不等于 |
STARTS_WITH |
sw |
以…开头 | NOT_STARTS_WITH |
nsw |
不以…开头 |
ENDS_WITH |
ew |
以…结尾 | NOT_ENDS_WITH |
new |
不以…结尾 |
IS_SET |
is |
已设置 | NOT_SET |
ns |
未设置 |
IS_IN |
in |
属于 | NOT_IN |
nin |
不属于 |
GREATER |
gt |
大于 | GREATER_OR_EQUAL |
gte |
大于等于 |
LESS |
lt |
小于 | LESS_OR_EQUAL |
lte |
小于等于 |
match_values()(src/sentry/rules/match.py:53)逐一实现每种匹配逻辑,对 IS_IN/NOT_IN 支持逗号分隔的多值列表匹配。
TaggedEventFilter(src/sentry/rules/filters/tagged_event.py:4)直接继承 TaggedEventCondition,仅改变 rule_type 为 "filter/event"——这体现了”相同逻辑,不同语义”的设计:作为 Condition 决定是否触发,作为 Filter 决定在触发后是否排除。
4.4 Assigned To(分配人过滤)
AssignedToFilter(src/sentry/rules/filters/assigned_to.py:21)根据 Issue 的分配状态进行过滤:
class AssignedToFilter(EventFilter):
id = "sentry.rules.filters.assigned_to.AssignedToFilter"
label = "The issue is assigned to {targetType}"
def _passes(self, group):
target_type = AssigneeTargetType(self.get_option("targetType"))
if target_type == AssigneeTargetType.UNASSIGNED:
return len(self.get_assignees(group)) == 0
target_id = self.get_option("targetIdentifier", None)
if target_type == AssigneeTargetType.TEAM:
for assignee in self.get_assignees(group):
if assignee.team_id and assignee.team_id == target_id:
return True
elif target_type == AssigneeTargetType.MEMBER:
for assignee in self.get_assignees(group):
if assignee.user_id and assignee.user_id == target_id:
return True
return False
分配人查询通过缓存优化(第 28 行),缓存 key 为 "group:{id}:assignees",TTL 60 秒,避免了每次规则评估都查询数据库。
5. 告警动作详解
动作是条件满足后的执行逻辑。所有动作都实现 EventAction.after() 方法,返回 CallbackFuture 生成器。动作继承体系:
EventAction
├── NotifyEventAction # 遗留插件(已废弃,仅保留注册以兼容旧规则)
├── MailAction # 邮件发送
├── SentryAppEventAction # Sentry App 动作基类
│ └── NotifyEventSentryAppAction # Sentry App Webhook
├── NotifyEventServiceAction # 通用 Webhook
├── IntegrationEventAction # 集成动作基类
│ ├── SlackNotifyServiceAction # Slack
│ ├── PagerDutyNotifyServiceAction# PagerDuty
│ ├── OpsgenieNotifyTeamAction # Opsgenie
│ ├── DiscordNotifyServiceAction # Discord
│ └── MSTeamsNotifyServiceAction # Microsoft Teams
└── TicketEventAction # 工单创建(Jira/Azure DevOps/GitHub)
5.1 Send Notification(邮件通知)
邮件通知动作通过 mail_adapter.notify() 将通知分发到邮件渠道。邮件发送的核心在 src/sentry/mail/notifications.py:
@register_notification_provider(ExternalProviders.EMAIL)
def send_notification_as_email(notification, recipients, shared_context, extra_context_by_actor):
for recipient in recipients:
msg = MessageBuilder(
subject=get_subject_with_prefix(notification, context),
context=context,
template=f"{notification.template_path}.html",
html_body=f"{notification.template_path}.html",
body=f"{notification.template_path}.txt",
headers=get_headers(notification, context),
reference=notification.reference,
)
msg.send_async([recipient.email])
邮件头部包含以下自定义元数据:
def get_headers(notification, context):
headers = {"X-SMTPAPI": orjson.dumps({"category": notification.metrics_key}).decode()}
headers["X-Sentry-Project"] = notification.project.slug
headers["X-Sentry-Logger"] = group.logger
headers["X-Sentry-Logger-Level"] = group.get_level_display()
headers["X-Sentry-Reply-To"] = group_id_to_email(group.id, group.project.organization_id)
X-SMTPAPI:SendGrid 分类标识,用于邮件分析X-Sentry-Reply-To:生成的唯一回复地址,格式为{group_id}@{organization_id}.{base_hostname},支持通过回复邮件直接与 Issue 交互
邮件模板通过 template_path 指定 HTML 和纯文本两套模板,位于 src/sentry/templates/ 下。Notification(src/sentry/plugins/base/structs.py)封装了事件和触发规则列表。
5.2 Slack 通知
Slack 通知有两个层面:
层面一:Issue Alert 动作(src/sentry/integrations/slack/actions/notification.py),SlackNotifyServiceAction 继承自 IntegrationEventAction:
class SlackNotifyServiceAction(IntegrationEventAction):
id = "sentry.integrations.slack.notify_action.SlackNotifyServiceAction"
provider = "slack"
integration_key = "workspace"
def after(self, event, notification_uuid=None):
integration = self.get_integration()
channel = self.get_option("channel_id") or self.get_option("channel")
blocks = self._build_notification_blocks(event, rules, ...)
client = SlackSdkClient(integration_id=integration.id)
client.chat_postMessage(channel=channel, blocks=blocks, ...)
消息内容由 SlackIssuesMessageBuilder(src/sentry/integrations/slack/message_builder/issues.py)构建,包含:
- Issue 标题和堆栈信息摘要
- 触发规则名称和条件
- Tags 摘要展示
- 操作按钮(Resolve、Ignore、Assign、Open in Sentry)
- 可选图表快照和 Seer Autofix 集成
层面二:通知分发(src/sentry/integrations/slack/notifications.py),通过 @register_notification_provider(ExternalProviders.SLACK) 注册到全局通知路由表。_send_slack_notification()(第 20 行)负责:
def _send_slack_notification(notification, recipients, shared_context, extra_context_by_actor, provider):
# 1. 获取每个接收者的集成和频道映射
data = get_integrations_by_channel_by_recipient(notification.organization, recipients, provider)
for recipient, integrations_by_channel in data.items():
# 2. 生成消息附件
attachments = service.get_attachments(notification, recipient, shared_context, extra_context_by_actor)
# 3. 遍历频道发送
for channel, integration in integrations_by_channel.items():
service.notify_recipient(notification=notification, recipient=recipient,
attachments=attachments, channel=channel, integration=integration, ...)
5.3 PagerDuty / Opsgenie
PagerDuty 和 Opsgenie 的动作类继承自 IntegrationEventAction。两者的验证逻辑在通知行动注册表中:
- PagerDuty(
action_validation.py:168):通过PagerDutyNotifyServiceForm验证服务配置,从organization_integrations配置中获取pagerduty_services列表。 - Opsgenie(
action_validation.py:201):类似模式,从team_table配置获取团队列表。
两者在 IntegrationEventAction.record_notification_sent() 中有专门的分析事件记录:
PROVIDER_TO_EVENT_CLASS = {
"pagerduty": PagerdutyIntegrationNotificationSent,
"opsgenie": OpsgenieIntegrationNotificationSent,
"slack": SlackIntegrationNotificationSent,
"discord": DiscordIntegrationNotificationSent,
"msteams": MSTeamsIntegrationNotificationSent,
"email": EmailNotificationSent,
}
5.4 Sentry App / Webhook
Sentry App 动作(src/sentry/rules/actions/sentry_apps/notify_event.py)允许第三方开发者构建自定义告警动作:
class NotifyEventSentryAppAction(SentryAppEventAction):
id = "sentry.rules.actions.notify_event_sentry_app.NotifyEventSentryAppAction"
actionType = "sentryapp"
def after(self, event, notification_uuid=None):
app = self._get_sentry_app(event)
component = self._get_alert_rule_component(app.id, app.name)
# 验证 UI Schema 中定义的自定义字段
for field in settings:
validate_field(setting["value"], field, app.name)
# 异步调用 Sentry App 的 Webhook
notify_sentry_app.delay(
installation_id=...,
event=...,
action_type=self.actionType,
settings=self.data.get("settings", []),
...
)
这类动作通过 Sentinel App 的 UI Schema 定义自定义配置字段(文本、下拉选择等),最终通过 Webhook 将完整的 Issue/Event 数据 POST 到第三方服务。
Webhook 动作(src/sentry/rules/actions/notify_event_service.py)提供通用 Webhook 发送能力。对于 Metric Alert 场景,send_incident_alert_notification()(第 57 行)构建标准化的 incident 附件:
def send_incident_alert_notification(...):
incident_attachment = build_incident_attachment(alert_context, metric_issue_context, ...)
send_metric_alert_webhook.delay(
sentry_app_id=..., new_status=..., incident_attachment_json=..., ...
)
5.5 Jira / Ticket 创建
Ticket 创建动作由 IntegrationEventAction 的子类实现,通过 TicketingIssueAlertHandler(notification_action/types.py:384)统一处理。build_rule_action_blob()(第 185 行)构建标准的行动 blob:
blob = {
"id": mapping["id"],
**cls.get_integration_id(action, mapping),
**cls.get_target_identifier(action, mapping, organization_id),
**cls.get_target_display(action, mapping),
**cls.get_additional_fields(action, mapping), # dynamic_form_fields + additional_fields
}
支持 Jira、Jira Server、Azure DevOps、GitHub、GitHub Enterprise 等 ticket 系统。每种 ticket 系统通过 ACTION_FIELD_MAPPINGS 映射表(workflow_engine/typings/notification_action.py)定义各自需要的配置字段。
6. 通知路由与分发
6.1 ExternalProviders 渠道体系
Sentry 的通知渠道由 ExternalProviders 枚举(src/sentry/integrations/types.py:8)定义:
class ExternalProviders(ValueEqualityEnum):
EMAIL = 100
SLACK = 110
SLACK_STAGING = 111 # Slack Staging 环境
MSTEAMS = 120
PAGERDUTY = 130
DISCORD = 140
OPSGENIE = 150
GITHUB = 200
GITHUB_ENTERPRISE = 201
GITLAB = 210
JIRA_SERVER = 300
CUSTOM = 700 # 已废弃,等待清理
每个通知发送函数通过 @register_notification_provider(ExternalProviders.XXX) 注册到全局 registry(src/sentry/notifications/notify.py:23)字典中:
registry: MutableMapping[ExternalProviders, NotifyCallable] = {}
def register_notification_provider(provider):
def wrapped(send_notification):
registry[provider] = send_notification
return send_notification
return wrapped
notify() 函数(第 46 行)作为统一入口,屏蔽了底层的渠道差异:
def notify(provider, notification, recipients, shared_context, extra_context_by_actor=None):
registry[provider](notification, recipients, shared_context, extra_context_by_actor)
6.2 NotificationController 四级作用域
NotificationController(src/sentry/notifications/notificationcontroller.py:53)是整个通知系统的心脏,它负责根据四级作用域计算每个参与者在每个渠道上的通知偏好:
ORGANIZATION (最宽泛)
└── USER
└── TEAM
└── PROJECT (最具体)
作用域按从宽泛到具体的顺序排列,后面的设置覆盖前面的:
def sort_settings_by_scope(setting):
if setting.scope_type == NotificationScopeEnum.PROJECT.value: return 4
if setting.scope_type == NotificationScopeEnum.ORGANIZATION.value: return 3
if setting.scope_type == NotificationScopeEnum.USER.value: return 2
if setting.scope_type == NotificationScopeEnum.TEAM.value: return 1
return 0
构造函数接收四个关键参数:
def __init__(self, recipients, project_ids=None, organization_id=None,
type=None, provider=None):
初始化时,它构建一个 Q 查询对象(第 93 行),同时查询四种作用域的设置:
def _get_query(self):
project_settings = Q(
(Q(user_id__in=user_ids) | Q(team_id__in=team_ids)),
scope_type=NotificationScopeEnum.PROJECT.value,
scope_identifier__in=self.project_ids,
)
org_settings = Q(
(Q(user_id__in=user_ids) | Q(team_id__in=team_ids)),
scope_type=NotificationScopeEnum.ORGANIZATION.value,
scope_identifier=self.organization_id,
)
user_settings = Q(
Q(user_id__in=user_ids),
scope_type=NotificationScopeEnum.USER.value,
scope_identifier__in=user_ids,
)
team_settings = Q(
Q(team_id__in=team_ids),
scope_type=NotificationScopeEnum.TEAM.value,
scope_identifier__in=team_ids,
)
return project_settings | org_settings | user_settings | team_settings
_get_layered_setting_options()(第 177 行)和 _get_layered_setting_providers()(第 234 行)两个核心方法负责查询并层层覆盖设置:
1. 查询范围内所有设置
2. 按 scope 排序(PROJECT=4 > ORG=3 > USER=2 > TEAM=1)
3. 后出现的(更具体)设置覆盖前面的(更宽泛)设置
4. 对缺失的设置填充默认值
get_combined_settings()(第 309 行)将 Options 和 Providers 进行协同过滤——先检查用户对该通知类型的整体偏好(ALWAYS / NEVER),再检查对具体渠道的偏好:
for recipient, recipient_options_map in setting_options_map.items():
for type in types_to_search:
option_value = recipient_options_map[type]
if option_value == NotificationSettingsOptionEnum.NEVER:
continue # 用户关闭了该通知类型
for provider, provider_value in provider_options_map.items():
if provider_value == NotificationSettingsOptionEnum.NEVER:
continue # 用户关闭了该渠道
result[recipient][type][provider] = option_value
最终 get_notification_recipients()(第 373 行)返回按渠道分组的参与者集合:
{
ExternalProviders.EMAIL: {Actor1, Actor2, Actor3},
ExternalProviders.SLACK: {Actor1, Actor3},
}
通知设置选项枚举(src/sentry/notifications/types.py:46):
| 选项 | 值 | 含义 |
|---|---|---|
DEFAULT |
default |
使用默认值 |
NEVER |
never |
从不通知 |
ALWAYS |
always |
始终通知 |
SUBSCRIBE_ONLY |
subscribe_only |
仅当明确订阅时通知 |
COMMITTED_ONLY |
committed_only |
仅当有代码提交时通知 |
6.3 参与者计算流程
参与者的计算入口在 get_send_to()(src/sentry/notifications/utils/participants.py)。ParticipantMap 类(第 55 行)是核心的内部数据结构:
class ParticipantMap:
_dict: MutableMapping[ExternalProviders, MutableMapping[Actor, int]]
# int 是 GroupSubscriptionReason,记录为何该参与者应收到通知
参与者来源按优先级:
- Issue Owners(代码所有者):通过
ProjectOwnership规则匹配事件的文件路径(如src/auth/login.py匹配到@auth-team规则),找到对应的团队或用户。使用RoleBasedRecipientStrategy(src/sentry/notifications/notifications/strategies/role_based_recipient_strategy.py:20)按角色筛选组织成员。 - 直接订阅者(Subscribers):主动订阅了该 Issue 的用户,通过
GroupSubscription模型查询。 - 分配人(Assignee):当前被分配处理该 Issue 的团队成员,通过
GroupAssignee查询。 - 提交者(Committers):通过
get_serialized_event_file_committers获取相关代码提交者,关联 Release/Commit 数据。 - 活跃成员(Active Members):曾经参与过该 Issue 活动的成员(如评论、状态变更)。
ParticipantMap 还支持:
delete_participant_by_id():移除已取消订阅的用户update(other):合并两个 ParticipantMapsplit_participants_and_context():按渠道拆分参与者并附带上下文信息(如订阅原因)
7. 通知聚合(Digests)
当短时间内有大量事件触发同一告警规则时,每事件一封通知会造成严重的”告警噪音”。Sentry 的 Digest 系统通过将多个通知聚合为一份摘要来解决这个问题。
7.1 摘要模式:”等待-就绪”状态机
Digest 的核心抽象是时间线(Timeline)——一条反向时间排序的记录序列。时间线在两种状态间切换:
+--------------------------+
| READY (就绪) |
| 可被 digest 并投递 |
+----------+---------------+
| 投递完成
v
+--------------------------+
| WAITING (等待) |
| 等待 delay 后再变为就绪 |
+----------+---------------+
| 有新事件加入
| --> 延长等待时间
| (上限为 maximum_delay)
v
延迟到期 --> 回到 READY
Backend 基类(src/sentry/digests/backends/base.py:36)定义了完整的 API:
class Backend(Service):
minimum_delay = 60 * 5 # 最小延迟:5 分钟
maximum_delay = 60 * 30 # 最大延迟:30 分钟
increment_delay = 30 # 每次新事件延长 30 秒
capacity = None # 时间线容量上限
truncation_chance = 0.0 # 截断概率
关键方法:
add(key, record, ...) -> bool(第 122 行):向时间线添加记录。首次添加时时间线立即变为 READY 状态,返回True(表示可立即 digest)。如果时间线处于 WAITING 状态,则此次添加会延长等待时间。digest(key, ...) -> ContextManager(第 144 行):以上下文管理器方式提取时间线内容并处理。如果处理成功(with 块正常退出),时间线回到 WAITING 状态;如果异常,所有记录保留不变,下次可以重新尝试。schedule(deadline, ...) -> Iterable[ScheduleEntry](第 179 行):检查所有 WAITING 状态的时间线,将到期的时间线移到 READY 状态。maintenance(deadline, ...)(第 190 行):处理卡在 READY 状态但任务丢失的时间线,将其移回 WAITING 状态重新调度——专为解决队列中任务丢失但时间线滞留在 READY 的边界情况。
digest 操作的原子性设计尤为重要:digest 方法使用上下文管理器,确保”提取记录 → 构建邮件 → 发送成功”要么全部成功(记录被删除,时间线回到 WAITING),要么全部回滚(记录保留,下次重新处理)。
7.2 Redis 后端实现
Redis 后端(src/sentry/digests/backends/redis.py)是生产环境中使用的实现:
- 时间线存储:使用 Redis Sorted Set,key 格式为
d:t:mail:p:{project_id}:{target_type}:{target_id},score 为时间戳,member 为 record key。 - 记录存储:单独存储每条记录,key 格式为
d:t:mail:p:{project_id}:r:{record_key},使用配置的 codec(默认CompressedPickleCodec)编码。 - 调度队列:两个 Sorted Set——
d:s:w(waiting)和d:s:r(ready),按时间戳排序,通过ZREVRANGEBYSCORE获取即将到期的项。 - Digest 操作:通过 Lua 脚本(
digests.lua)原子执行——将时间线 Sorted Set 重命名为 digest set,避免添加新记录和 digest 操作之间的竞态条件。 - 锁机制:使用 Redis 分布式锁(
RedisLockBackend)保护每个时间线的并发操作,锁的命名空间格式为d:l:{key}。
配置选项:
def __init__(self, **options):
self.namespace = options.pop("namespace", "d") # key 前缀
self.ttl = options.pop("ttl", 60 * 60) # 数据 TTL:1 小时
super().__init__(**options)
7.3 Digest 构建与排序
build_digest()(src/sentry/digests/notifications.py:221)将原始记录构建为结构化的 Digest:
原始 Record 集合
→ _bind_records: 关联 Group 和 Rule 对象,过滤已 Resolved 的 Group
→ _group_records: 按 (Rule, Group) 二级分组
→ _sort_digest: 按事件数和用户数排序
最终生成的 Digest 数据结构为三层嵌套:
# Digest = dict[Rule, dict[Group, list[RecordWithRuleObjects]]]
排序逻辑(_sort_digest(),第 136 行):
- 内层:每个 Rule 下的 Group 按
(事件数, 用户数)降序排列 - 外层:Rule 按包含的 Group 数量降序排列
这确保了对用户影响最大的 Issue 排在摘要的最前面。
DigestNotification(src/sentry/notifications/notifications/digest.py:50)是摘要通知的载体。它有一个关键优化(should_send_as_alert_notification()):
def should_send_as_alert_notification(context):
return len(context["counts"]) == 1
如果摘要中只有一个 Group(无论该 Group 匹配了多少条规则),则直接使用单条告警通知模板而非摘要模板,以提供更丰富的上下文信息。
8. 集成通知渠道
8.1 Slack
Slack 集成(src/sentry/integrations/slack/)是最复杂的通知渠道之一,包含以下核心组件:
| 组件 | 路径 | 职责 |
|---|---|---|
| 消息构建 | message_builder/issues.py |
将 Issue 数据转为 Slack Block Kit 格式 |
| 通知发送 | notifications.py |
通过 @register_notification_provider 注册为 Slack 渠道发送器 |
| 动作处理 | actions/notification.py |
Issue Alert 中”发送 Slack 通知”动作的具体实现 |
| 交互处理器 | handlers/ |
处理 Slack 按钮回调(Resolve/Assign/Ignore) |
| 频道线程 | threads/ |
同一 Issue 的通知聚合到 Slack 线程 |
| SDK 客户端 | sdk_client.py |
封装 slack_sdk API 调用 |
| 工作区管理 | workspace.py |
管理 Slack 工作区连接 |
通知消息通过 SlackIssuesMessageBuilder 构建为丰富的 Block 组件:Issue 标题和堆栈信息、触发规则名称和条件、Tags 摘要、操作按钮(Resolve / Ignore / Assign / Open in Sentry)、可选的图表快照和 Seer Autofix 集成。
SlackMessagingSpec(src/sentry/integrations/slack/spec.py)定义了 Slack 消息的规范格式,确保所有 Slack 通知的一致性。
对于 Metric Alert 场景,通知中会包含事件数量、受影响用户数、时间范围等指标细节。
8.2 Microsoft Teams / Discord
Microsoft Teams 和 Discord 集成遵循与 Slack 类似的架构模式:
- Teams:
MSTeamsActionValidatorHandler(action_validation.py:87)处理验证,消息格式使用 Markdown([{text}]({url}))。 - Discord:
DiscordActionValidatorHandler(action_validation.py:108)处理验证,同样使用 Markdown 链接格式。
两者都通过 @register_notification_provider 注册到全局通知路由表,并在 IntegrationEventAction.record_notification_sent() 中有各自的分析事件。
各个渠道的 URL 格式由 BaseNotification.provider_to_url_format 定义:
provider_to_url_format = {
ExternalProviders.SLACK: "<{url}|{text}>", # Slack 原生链接语法
ExternalProviders.MSTEAMS: "[{text}]({url})", # Markdown
ExternalProviders.DISCORD: "[{text}]({url})", # Markdown
}
9. Webhook 自定义通知
Webhook 是 Sentry App 生态的基础。开发者可以通过以下方式自定义通知:
方式一:Issue Alert Webhook 动作
通过 NotifyEventServiceAction(src/sentry/rules/actions/notify_event_service.py:95),配置一个外部服务 URL,当规则触发时 Sentry 会向该 URL POST JSON payload。Payload 包含事件详情、触发规则、项目信息等完整上下文。
方式二:Metric Alert Sentry App 通知
send_incident_alert_notification()(第 57 行)构建的 Payload 结构:
{
"metric_alert": {
"id": "123",
"identifier": "456",
"status": 1,
"title": "Error rate exceeded",
"date_detected": "2024-01-01T00:00:00Z",
...
},
"description_text": "Error rate is above 5% in the last 1 hour",
"description_title": "Critical: Error rate spike",
"web_url": "https://sentry.io/organizations/.../alerts/..."
}
该 Payload 通过 Celery 任务 send_metric_alert_webhook.delay() 异步投递,包含 status(WARNING/CRITICAL/RESOLVED)、data(聚合查询结果)和 date_detected 等关键字段。
方式三:Sentry App 自定义动作
通过 NotifyEventSentryAppAction,第三方开发者可以:
- 在 Sentry App 清单中定义 UI Schema(文本字段、下拉框等)
- 用户在告警规则创建时配置这些自定义字段
- 触发时通过 Webhook 接收完整的 Issue 或 Event 数据以及用户配置的
settings
方式四:通用 Webhook Action
通过 Action 类型 Action.Type.WEBHOOK(action_validation.py:285),WebhookActionValidatorHandler 负责验证配置并通过 NotifyEventServiceForm 管理服务选择。app_service.find_alertable_services() 提供可用的 Webhook 服务列表。
10. 告警最佳实践
减少告警噪音
- 合理设置频率条件:使用 Event Frequency 条件而非 Every Event 条件。”每 5 分钟出现超过 10 次”比”每次出现都通知”更有价值。新 Issue 的第一个事件在
value > 1时永远不会触发频率条件(passes()第 175 行),这也是一种内置的噪音抑制机制。 - 使用 Digest 聚合:对于高频 Issue,启用 Digest 功能。默认 minimum_delay 为 5 分钟,maximum_delay 为 30 分钟,每次新事件递增 30 秒——这意味着如果事件在 25 分钟内持续到来,通知只会发送一次摘要。
- 利用 Filters 过滤:通过 TaggedEventFilter 按环境过滤(如只监控 production),通过 AssignedToFilter 过滤已分配出去的 Issue,通过 IssueCategoryFilter 过滤特定类型的 Issue(Error / Performance 等)。
- 应用 RuleSnooze:
RuleSnooze模型允许临时静默特定规则(如部署期间)。snooze 检查集成在参与者计算流程中,确保被静默的规则不会触发通知。
分级策略
| 级别 | 条件类型 | 动作配置 | 典型场景 |
|---|---|---|---|
| Critical | Metric Alert + 高阈值 | PagerDuty / Opsgenie 紧急通知 | 核心 API 错误率 > 5%,支付服务宕机 |
| Warning | Event Frequency (> 10 次/15min) | Slack 频道通知 + 邮件 | 非核心服务异常增加,性能回归 |
| Info | First Seen + Tags 过滤 | 邮件摘要 | 新类型错误首次出现,低频偶发异常 |
对于 Metric Alert,使用双阈值迟滞设计——将 Resolution 阈值设为低于 Warning 阈值(如 Warning > 5%,Resolved < 3%),避免告警在阈值附近反复触发和解除。
值班轮转
- Issue Owners(推荐):通过
CODEOWNERS文件或ProjectOwnership规则(位于 Project Settings > Ownership),将文件路径映射到团队。这样每个 Issue 会自动路由到正确的团队,使用ActionTargetType.ISSUE_OWNERS作为 targetType。 - Fallthrough 策略:当 Issue Owners 无法匹配时(没有所有权规则或无人匹配),
FallthroughChoiceType决定通知行为——发送给所有团队成员(ALL_MEMBERS)还是仅发送给活跃成员(ACTIVE_MEMBERS)。设置FALLTHROUGH_NOTIFICATION_LIMIT = 20限制最大通知人数。 - 通知偏好管理:利用
NotificationController的四级作用域,让团队成员精细控制通知偏好:- Organization 级别:为新成员设定默认通知策略
- Project 级别:按项目复杂度调整通知频率
- User 级别:个人设置免打扰时段或渠道偏好
- Team 级别:团队统一的通知标准
监控告警体系本身
- 告警分析:Sentry 内置了
AlertSentEvent(src/sentry/analytics/events/alert_sent.py)分析事件,记录每次告警发送的provider、alert_id、alert_type和notification_uuid,可用于构建告警趋势仪表盘。 - Digest 任务监控:关注
SENTRY_DIGESTS_OPTIONS中配置的调度任务执行频率。如果出现大量卡在 READY 状态的时间线(maintenance方法触发频繁),说明 digest 任务队列出现积压,需要扩展 consumer 或调整调度频率。 - Action 失败处理:所有 action 执行都包裹在
invoke_future_with_error_handling()(notification_action/types.py:78)中:ApiError(网络故障等):自动通过RetryTaskError重试IntegrationFormError/InvalidIdentity(配置错误 / 认证失效):静默忽略并记录日志- 未预期的异常:通过
sentry_sdk.capture_exception上报到 Sentry 自身
- 通知设置验证:
validate()函数(notifications/helpers.py:66)确保每个NotificationSettingEnum类型只接受VALID_VALUES_FOR_KEY中定义的合法值,防止错误配置导致通知静默丢失。