znlgis 博客

GIS开发与技术分享 — GDAL · GeoServer · PostGIS · QGIS · OpenLayers · Cesium · FreeCAD · NPOI

目录

第八章:告警规则与通知体系

Sentry 的告警与通知体系是整个错误监控平台的核心闭环之一——仅有数据采集和展示远远不够,关键在于在正确的时机、通过正确的渠道、将正确的信息触达正确的人。本章深入分析 Sentry 告警系统的三层架构:告警规则定义(Issue Alert / Metric Alert)、通知路由分发(NotificationController)、通知聚合与渠道投递(Digests / Integrations)。

目录

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]

RuleBasesrc/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 中配置的参数。

EventConditionsrc/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              # 是否升级

EventFiltersrc/sentry/rules/filters/base.py:7 签名与 Condition 完全相同,passes(event, state) -> bool。过滤器和条件的区别在于语义和执行顺序——条件先判断、过滤器后筛选。

EventActionsrc/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 行)负责:

  1. 遍历 rule.data["actions"],通过 instantiate_action 实例化每个动作对象
  2. 调用 action_inst.after(event, notification_uuid) 收集所有 CallbackFuture
  3. 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 规则注册机制

规则组件通过 RuleRegistrysrc/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 的数据模型更复杂,涉及 AlertRuleDetectorWorkflow 等多个模型。不过,在通知分发层面,Metric Alert 最终会复用 Issue Alert 的通知基础设施。BaseMetricAlertHandlersrc/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),包含 ACTIVERESOLVEDWARNINGCRITICAL 等状态。每个 Alert Rule 可以有多个 Trigger(条件分支),每个 Trigger 绑定一组 Actions。

4. 告警条件详解

4.1 First Seen / Every Event

FirstSeenEventConditionsrc/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)分别配置告警策略十分有用。

EveryEventConditionsrc/sentry/rules/conditions/every_event.py:6)更加简单——passes() 永远返回 True,但通过重写 is_enabled() 返回 False 来在 UI 中隐藏该条件。它主要用于内部默认规则或程序化创建的规则。

4.2 Event Frequency(事件频率)

频率条件是最复杂也最常用的条件类型,由 BaseEventFrequencyConditionsrc/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(标签匹配)

TaggedEventConditionsrc/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 支持逗号分隔的多值列表匹配。

TaggedEventFiltersrc/sentry/rules/filters/tagged_event.py:4)直接继承 TaggedEventCondition,仅改变 rule_type"filter/event"——这体现了”相同逻辑,不同语义”的设计:作为 Condition 决定是否触发,作为 Filter 决定在触发后是否排除。

4.4 Assigned To(分配人过滤)

AssignedToFiltersrc/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/ 下。Notificationsrc/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, ...)

消息内容由 SlackIssuesMessageBuildersrc/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。两者的验证逻辑在通知行动注册表中:

  • PagerDutyaction_validation.py:168):通过 PagerDutyNotifyServiceForm 验证服务配置,从 organization_integrations 配置中获取 pagerduty_services 列表。
  • Opsgenieaction_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 的子类实现,通过 TicketingIssueAlertHandlernotification_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) 注册到全局 registrysrc/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 四级作用域

NotificationControllersrc/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,记录为何该参与者应收到通知

参与者来源按优先级:

  1. Issue Owners(代码所有者):通过 ProjectOwnership 规则匹配事件的文件路径(如 src/auth/login.py 匹配到 @auth-team 规则),找到对应的团队或用户。使用 RoleBasedRecipientStrategysrc/sentry/notifications/notifications/strategies/role_based_recipient_strategy.py:20)按角色筛选组织成员。
  2. 直接订阅者(Subscribers):主动订阅了该 Issue 的用户,通过 GroupSubscription 模型查询。
  3. 分配人(Assignee):当前被分配处理该 Issue 的团队成员,通过 GroupAssignee 查询。
  4. 提交者(Committers):通过 get_serialized_event_file_committers 获取相关代码提交者,关联 Release/Commit 数据。
  5. 活跃成员(Active Members):曾经参与过该 Issue 活动的成员(如评论、状态变更)。

ParticipantMap 还支持:

  • delete_participant_by_id():移除已取消订阅的用户
  • update(other):合并两个 ParticipantMap
  • split_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 行):

  1. 内层:每个 Rule 下的 Group 按 (事件数, 用户数) 降序排列
  2. 外层:Rule 按包含的 Group 数量降序排列

这确保了对用户影响最大的 Issue 排在摘要的最前面

DigestNotificationsrc/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 集成。

SlackMessagingSpecsrc/sentry/integrations/slack/spec.py)定义了 Slack 消息的规范格式,确保所有 Slack 通知的一致性。

对于 Metric Alert 场景,通知中会包含事件数量、受影响用户数、时间范围等指标细节。

8.2 Microsoft Teams / Discord

Microsoft Teams 和 Discord 集成遵循与 Slack 类似的架构模式:

  • TeamsMSTeamsActionValidatorHandleraction_validation.py:87)处理验证,消息格式使用 Markdown([{text}]({url}))。
  • DiscordDiscordActionValidatorHandleraction_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 动作

通过 NotifyEventServiceActionsrc/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,第三方开发者可以:

  1. 在 Sentry App 清单中定义 UI Schema(文本字段、下拉框等)
  2. 用户在告警规则创建时配置这些自定义字段
  3. 触发时通过 Webhook 接收完整的 Issue 或 Event 数据以及用户配置的 settings

方式四:通用 Webhook Action

通过 Action 类型 Action.Type.WEBHOOKaction_validation.py:285),WebhookActionValidatorHandler 负责验证配置并通过 NotifyEventServiceForm 管理服务选择。app_service.find_alertable_services() 提供可用的 Webhook 服务列表。

10. 告警最佳实践

减少告警噪音

  1. 合理设置频率条件:使用 Event Frequency 条件而非 Every Event 条件。”每 5 分钟出现超过 10 次”比”每次出现都通知”更有价值。新 Issue 的第一个事件在 value > 1 时永远不会触发频率条件(passes() 第 175 行),这也是一种内置的噪音抑制机制。
  2. 使用 Digest 聚合:对于高频 Issue,启用 Digest 功能。默认 minimum_delay 为 5 分钟,maximum_delay 为 30 分钟,每次新事件递增 30 秒——这意味着如果事件在 25 分钟内持续到来,通知只会发送一次摘要。
  3. 利用 Filters 过滤:通过 TaggedEventFilter 按环境过滤(如只监控 production),通过 AssignedToFilter 过滤已分配出去的 Issue,通过 IssueCategoryFilter 过滤特定类型的 Issue(Error / Performance 等)。
  4. 应用 RuleSnoozeRuleSnooze 模型允许临时静默特定规则(如部署期间)。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%),避免告警在阈值附近反复触发和解除。

值班轮转

  1. Issue Owners(推荐):通过 CODEOWNERS 文件或 ProjectOwnership 规则(位于 Project Settings > Ownership),将文件路径映射到团队。这样每个 Issue 会自动路由到正确的团队,使用 ActionTargetType.ISSUE_OWNERS 作为 targetType。
  2. Fallthrough 策略:当 Issue Owners 无法匹配时(没有所有权规则或无人匹配),FallthroughChoiceType 决定通知行为——发送给所有团队成员(ALL_MEMBERS)还是仅发送给活跃成员(ACTIVE_MEMBERS)。设置 FALLTHROUGH_NOTIFICATION_LIMIT = 20 限制最大通知人数。
  3. 通知偏好管理:利用 NotificationController 的四级作用域,让团队成员精细控制通知偏好:
    • Organization 级别:为新成员设定默认通知策略
    • Project 级别:按项目复杂度调整通知频率
    • User 级别:个人设置免打扰时段或渠道偏好
    • Team 级别:团队统一的通知标准

监控告警体系本身

  1. 告警分析:Sentry 内置了 AlertSentEventsrc/sentry/analytics/events/alert_sent.py)分析事件,记录每次告警发送的 provideralert_idalert_typenotification_uuid,可用于构建告警趋势仪表盘。
  2. Digest 任务监控:关注 SENTRY_DIGESTS_OPTIONS 中配置的调度任务执行频率。如果出现大量卡在 READY 状态的时间线(maintenance 方法触发频繁),说明 digest 任务队列出现积压,需要扩展 consumer 或调整调度频率。
  3. Action 失败处理:所有 action 执行都包裹在 invoke_future_with_error_handling()notification_action/types.py:78)中:
    • ApiError(网络故障等):自动通过 RetryTaskError 重试
    • IntegrationFormError / InvalidIdentity(配置错误 / 认证失效):静默忽略并记录日志
    • 未预期的异常:通过 sentry_sdk.capture_exception 上报到 Sentry 自身
  4. 通知设置验证validate() 函数(notifications/helpers.py:66)确保每个 NotificationSettingEnum 类型只接受 VALID_VALUES_FOR_KEY 中定义的合法值,防止错误配置导致通知静默丢失。