Sentry 告警 → 飞书通知 完整实现方案

模拟告警项目地址:https://github.com/keepongo/sentry-test-app.git
中继服务项目地址:https://github.com/keepongo/sentry-relay.git

一、学什么:整条链路概念

1.1 Sentry 是什么

Sentry 是一个错误/异常监控平台。你的应用在运行中产生异常(NullPointerException、业务错误等),Sentry 会:

  • 收集:通过 SDK/logback 把异常事件上报
  • 聚合:相同异常合并为一个 Issue,记录触发次数、影响范围
  • 告警:根据你配置的规则(如"首次出现"“1 小时内出现 10 次”),触发告警动作

1.2 DSN 是什么

DSN = Data Source Name,是 Sentry 项目的"上报地址",格式如:

https://<公钥>@<Sentry域名>/<项目ID>
  • SaaS 版示例:https://abc123@o123456.ingest.sentry.io/456789
  • 自建版示例:https://abc123@sentry.yourcompany.com/2

你在环境变量里配的 LOG_SENTRY_DSN 就是这个。

1.3 整条链路

上报异常

触发告警\n发 Webhook POST

转成飞书格式\nPOST

显示告警消息

你的 Java 应用\n(logback + DSN)

Sentry\n(收集/聚合/告警)

中继服务\n(Spring Boot)

飞书机器人\n(群聊 Webhook)

飞书群

1.4 为什么需要「中继」

  • Sentry Webhook 发出的 JSON 格式(含 event、issue、triggered_rule 等)和飞书机器人要求的 JSON 格式(msg_type、content 等)完全不同
  • 不能直接把飞书 Webhook 填到 Sentry 告警里,Sentry 发过去飞书不认
  • 中继服务做的事:收 Sentry JSON → 解析 → 拼成飞书 JSON → 转发

二、准备工作:注册 Sentry

2.1 注册 Sentry SaaS(推荐,免费,自学用)

  1. 打开 https://sentry.io → Sign Up(GitHub / Google 登录即可)
  2. 创建 Organization,我登录的时候已经填写组织,这里自动创建了
    image-20260228151551156
  3. 创建 Project:
  • 点 “Create Project”

  • Platform 选 Java(或搜索 “Logback”,选 Java - Logback

  • 点 “Create Project”
    在这里插入图片描述

  1. 创建完成后,页面会显示 DSN,形如:
    https://abc123def456@o123456.ingest.sentry.io/789012
    这就是 LOG_SENTRY_DSN 的值。复制保存下来

在这里插入图片描述

或者

  • 左侧 Settings → Projects → 点 java-logback
  • 左侧找到 Client Keys (DSN)
  • 复制那个完整的 DSN(注意不要多复制空格或少复制字符)
  • 粘贴到 E:\sentry-test-app\src\main\resources\logback.xml 里替换掉现在的 DSN

在这里插入图片描述

在这里插入图片描述

  1. 免费版:每月 5,000 事件额度,自学足够

2.2 Sentry 界面关键页面(先认识一下)

登录后左侧菜单:

  • Issues:所有收集到的异常列表,相同异常会自动聚合为一个 Issue
  • Alerts:告警规则配置(后面第四节详细讲)
  • Settings → Projects → 你的项目:DSN 在这里可以再次查看
  • Settings → Integrations:配置 Webhook 等第三方集成

三、应用侧配置:测试项目验证 Sentry 上报

测试项目地址

3.1 Sentry 上报原理

Sentry 服务端 Sentry SDK logback-spring.xml 你的应用 Sentry 服务端 Sentry SDK logback-spring.xml 你的应用 logger.error("xxx") 匹配 root appender 转给 SentryAppender(ERROR级别命中) 封装为 Sentry Event(含堆栈、MDC上下文等) HTTP POST 到 DSN 地址 存储、聚合为 Issue 检查 Alert Rules 是否满足

核心就三步:

  1. 依赖:项目引入 sentry-logback
 <dependency>
            <groupId>io.sentry</groupId>
            <artifactId>sentry-logback</artifactId>
            <version>7.6.0</version>
 </dependency>
  1. logback 配置:加一个 SentryAppender,通过环境变量 LOG_SENTRY_DSN 读取 DSN

这是简单的配置

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

  <!-- 从系统环境变量读取,供下面的 appender 引用 -->
  <springProperty scope="context" name="LOG_SENTRY_DSN" source="LOG_SENTRY_DSN" defaultValue=""/>
  <springProperty scope="context" name="SENTRY_ENVIRONMENT" source="SENTRY_ENVIRONMENT" defaultValue="dev"/>

  <!-- ==================== 控制台输出 ==================== -->
  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <!-- ==================== Sentry Appender ==================== -->
  <appender name="SENTRY" class="io.sentry.logback.SentryAppender">
    <options>
      <dsn>${LOG_SENTRY_DSN}</dsn>
      <environment>${SENTRY_ENVIRONMENT}</environment>
      <debug>true</debug>
    </options>
    <minimumEventLevel>ERROR</minimumEventLevel>
    <minimumBreadcrumbLevel>WARN</minimumBreadcrumbLevel>
  </appender>

  <!-- 启动时打印 DSN 值,方便确认是否生效(调试用,确认后可删) -->
  <statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener" />

  <root level="INFO">
    <appender-ref ref="CONSOLE" />
    <appender-ref ref="SENTRY" />
  </root>

</configuration>

高级的配置可以这样

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <!-- ==================== 控制台输出 ==================== -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- ==================== Sentry Appender ==================== -->
    <!--
        通过环境变量 LOG_SENTRY_DSN 传入 Sentry 的 DSN 地址。
        如果不设置或为空,Sentry 不会初始化,不影响应用运行。
    -->
<!-- 从环境变量读取 Sentry 配置 -->
<springProperty scope="context" name="LOG_SENTRY_DSN" source="LOG_SENTRY_DSN" defaultValue=""/>
<springProperty scope="context" name="SENTRY_ENVIRONMENT" source="SENTRY_ENVIRONMENT" defaultValue="dev"/>
<springProperty scope="context" name="LOG_SENTRY_BREADCRUMB_LEVEL" source="LOG_SENTRY_BREADCRUMB_LEVEL" defaultValue="INFO"/>
<springProperty scope="context" name="LOG_SENTRY_EVENT_LEVEL" source="LOG_SENTRY_EVENT_LEVEL" defaultValue="ERROR"/>
<!-- 新增:Sentry 高级配置 -->
<springProperty scope="context" name="SENTRY_TRACES_SAMPLE_RATE" source="SENTRY_TRACES_SAMPLE_RATE" defaultValue="1.0"/>
<springProperty scope="context" name="SENTRY_MAX_BREADCRUMBS" source="SENTRY_MAX_BREADCRUMBS" defaultValue="100"/>
<springProperty scope="context" name="SENTRY_SEND_DEFAULT_PII" source="SENTRY_SEND_DEFAULT_PII" defaultValue="false"/>
<appender name="SENTRY" class="io.sentry.logback.SentryAppender">
    <options>
        <dsn>${LOG_SENTRY_DSN}</dsn>
        <environment>${SENTRY_ENVIRONMENT}</environment>
        <tracesSampleRate>${SENTRY_TRACES_SAMPLE_RATE}</tracesSampleRate>
        <maxBreadcrumbs>${SENTRY_MAX_BREADCRUMBS}</maxBreadcrumbs>
        <sendDefaultPii>${SENTRY_SEND_DEFAULT_PII}</sendDefaultPii>
    </options>
    <minimumBreadcrumbLevel>${LOG_SENTRY_BREADCRUMB_LEVEL}</minimumBreadcrumbLevel>
    <minimumEventLevel>${LOG_SENTRY_EVENT_LEVEL}</minimumEventLevel>
</appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="SENTRY" />
    </root>

</configuration>
SENTRY_ENVIRONMENT=dev
LOG_SENTRY_EVENT_LEVEL=ERROR
LOG_SENTRY_BREADCRUMB_LEVEL=DEBUG
SENTRY_TRACES_SAMPLE_RATE=1.0
SENTRY_MAX_BREADCRUMBS=100  //面包屑级别
SENTRY_SEND_DEFAULT_PII=false 
  1. 环境变量:启动时设 LOG_SENTRY_DSN=你的DSN,不设就不上报、不影响运行

3.2 测试项目:sentry-test-app

独立的 Spring Boot 测试项目 E:\sentry-test-app,用来验证 Sentry 上报。

项目结构:

E:\sentry-test-app\
├── pom.xml                          -- Spring Boot 3.2 + sentry-logback 7.6.0
├── src/main/java/com/example/sentrytest/
│   ├── SentryTestApplication.java   -- 启动类
│   └── controller/
│       └── TestController.java      -- 5 个测试接口
└── src/main/resources/
    ├── application.yml              -- 端口 8080
    └── logback-spring.xml           -- CONSOLE + SENTRY Appender

关键文件说明:

pom.xml – 只需要两个依赖:

<!-- Web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Sentry + Logback 集成 -->
<dependency>
    <groupId>io.sentry</groupId>
    <artifactId>sentry-logback</artifactId>
    <version>7.6.0</version>
</dependency>

logback-spring.xml – 核心配置:

 <springProperty scope="context" name="LOG_SENTRY_DSN" source="LOG_SENTRY_DSN" defaultValue=""/>
  <springProperty scope="context" name="SENTRY_ENVIRONMENT" source="SENTRY_ENVIRONMENT" defaultValue="dev"/>

  <!-- ==================== Sentry Appender ==================== -->
  <appender name="SENTRY" class="io.sentry.logback.SentryAppender">
    <options>
      <dsn>${LOG_SENTRY_DSN}</dsn>
      <environment>${SENTRY_ENVIRONMENT}</environment>
      <debug>true</debug>
    </options>
    <minimumEventLevel>ERROR</minimumEventLevel>
    <minimumBreadcrumbLevel>WARN</minimumBreadcrumbLevel>
  </appender>

  <!-- 启动时打印 DSN 值,方便确认是否生效(调试用,确认后可删) -->
  <statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener" />

  <root level="INFO">
    <appender-ref ref="CONSOLE" />
    <appender-ref ref="SENTRY" />
  </root>

如果 LOG_SENTRY_DSN 不设置或为空,Sentry Appender 不会初始化,不影响应用正常运行

测试接口:

接口 作用 Sentry 效果
GET /test/ok 正常 INFO 日志 Sentry 不捕获
GET /test/warn WARN 日志 只记为 Breadcrumb
GET /test/error ERROR 日志 上报为 Sentry Event,出现在 Issues
GET /test/exception 抛未捕获异常 上报完整堆栈到 Sentry
GET /test/business-error?code=CNY 模拟业务异常 上报带堆栈的 ERROR

3.3 运行和验证

cd E:\sentry-test-app

# 设置 DSN(替换为你在 sentry.io 创建项目后拿到的 DSN)
set LOG_SENTRY_DSN=https://你的DSN@xxx.ingest.us.sentry.io/xxx

# 编译运行
mvn spring-boot:run

在项目的环境变量设置也行,然后启动项目

在这里插入图片描述

在这里插入图片描述

验证步骤:

  1. 访问 http://localhost:8080/test/error → 页面返回 “ERROR logged”
  2. 访问 http://localhost:8080/test/business-error?code=CNY → 页面返回 “Business error logged”
  3. 打开 sentry.io → 你的项目 → Issues → 应该能看到两条新 Issue
  4. 点进 Issue 可以看到完整的堆栈、环境信息(environment=dev)

在这里插入图片描述

在这里插入图片描述


四、Sentry 配置告警规则 + Webhook

这部分全在 Sentry 网页端 操作,不改项目代码。

4.1 启用 WebHooks 集成

  1. 登录 sentry.io → 你的项目
  2. 左侧 SettingsIntegrations
  3. 搜索 “WebHooks” → 点 Enable PluginConfigure

在这里插入图片描述

填写:

  • Name:Feishu Relay
  • Webhook URL:http://你的服务器IP:8090/api/sentry/webhook(中继服务地址,暂时可以先填 https://webhook.site 生成的临时 URL)
  • Permissions 部分:Issue & Event → 选 Read
  • 勾选 Alert Rule Action(这样在告警规则里就能选这个 Integration 了)
    在这里插入图片描述

保存

4.2 创建告警规则

  1. 左侧 AlertsCreate Alert Rule → 选 Issues(Issue Alert)

在这里插入图片描述

  1. Set Conditions(触发条件):
  • When:选 “A new issue is created”(首次出现新异常就告警)
  • 或者选 “The issue is seen more than {value} times in {interval}”(N 分钟内出现超过 X 次)
  • 示例:出现超过 2 次 in 1 hour
  1. Set Filters(可选过滤):
  • 如果你设了 APP_ENV,可以加条件 “The event’s environment is Prod”
  • 可以按 Issue 级别过滤:“The event’s level is equal to error”
  1. Set Actions(触发动作):
  • “Send a notification via an integration”
  • Integration 选 WebHooks
  • 这会把告警发到你在 4.1 步骤填的 Callback URL
  1. Action Interval(防刷间隔):
  • 建议选 “30 minutes” 或 “1 hour”,避免同一 Issue 反复发告警刷屏
  1. Rule Name:起个名字,如 “通知飞书-所有新异常”
  2. Save Rule

在这里插入图片描述

在这里插入图片描述

4.3 Sentry Webhook 发出的 payload 长什么样

当告警触发时,Sentry 会向你的 Callback URL 发一个 HTTP POST,body 是 JSON:

{
  "action": "triggered",
  "data": {
    "issue": {
      "id": "90175418",
      "title": "Unsupported currency codeCNY",
      "culprit": "com.redteamobile.einstein.services.DataPlanServiceImpl in getCurrencyRate",
      "permalink": "https://redteamobile-kz.sentry.io/issues/90175418/",
      "project": {
        "name": "einstein",
        "slug": "einstein"
      },
      "status": "unresolved",
      "count": "2",
      "metadata": {
        "type": "UnsupportedOperationException",
        "value": "Unsupported currency codeCNY"
      }
    },
    "triggered_rule": "通知飞书-所有新异常"
  },
  "actor": { "type": "application", "name": "Sentry" }
}

当 Issue 被 resolve(手动或自动标记为已解决)时,会发:

{
  "action": "resolved",
  "data": {
    "issue": { ... 同上 ... }
  }
}

中继服务只需要从这个 JSON 里取出关键字段,转成飞书格式即可。


五、创建飞书自定义机器人

  1. 打开飞书 → 创建一个测试群(或用现有群)
  2. 群设置 → 群机器人添加机器人自定义机器人
  3. 名称填 “Sentry告警” → 完成
  4. 记录生成的 Webhook 地址,形如:
    https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
  5. 安全设置(可选但推荐):
  • 可以开启「签名校验」,会给你一个 secret,中继服务发消息时需要带签名
  • 自学阶段可以先不开,直接用即可
  1. 用 curl 验证机器人是否正常:
curl -X POST -H "Content-Type: application/json" -d "{\"msg_type\":\"text\",\"content\":{\"text\":\"Hello from curl!\"}}" https://open.feishu.cn/open-apis/bot/v2/hook/你的token

群里应该能看到机器人发出 “Hello from curl!”


六、中继服务实现(新建项目)

中继服务地址

5.1 项目结构

新建一个独立的 Spring Boot 项目 sentry-feishu-relay

sentry-feishu-relay/
  src/main/java/com/example/relay/
    RelayApplication.java           -- Spring Boot 启动类
    controller/
      SentryWebhookController.java  -- 接收 Sentry Webhook
    service/
      FeishuNotifyService.java      -- 发送飞书消息
    model/
      SentryPayload.java            -- Sentry Webhook 请求体模型
      FeishuMessage.java            -- 飞书消息模型
    config/
      RelayConfig.java              -- 配置类(飞书 Webhook URL 等)
  src/main/resources/
    application.yml                 -- 配置文件
  Dockerfile                        -- Docker 构建
  pom.xml                           -- Maven 依赖

5.2 中继服务详细工作流程

中继服务做的事情就是三步:接收 Sentry Webhook解析关键字段拼成飞书消息发出去。下面详细说明每一步。

飞书群 飞书机器人 Webhook 中继服务 (Spring Boot) Sentry 服务端 飞书群 飞书机器人 Webhook 中继服务 (Spring Boot) Sentry 服务端 1. POST /api/sentry/webhook(JSON payload) 2. Jackson 反序列化为 SentryPayload 对象 3. 从 payload 提取 action、title、issueId、permalink 等 4. 拼装飞书卡片消息 JSON 5. POST 飞书 Webhook URL(卡片 JSON) 6. 群里显示告警卡片 7. 返回 200 ok
第一步:接收 Sentry Webhook

Sentry 告警触发时,会向你配置的 Webhook URL 发一个 HTTP POST 请求,Content-Type 是 application/json

中继服务用 Spring Boot 的 @PostMapping 接收,Spring 自动用 Jackson 把 JSON body 反序列化为 Java 对象。

实际收到的 Sentry Internal Integration payload 结构(简化版):

{
  "action": "triggered",
  "installation": { "uuid": "xxx" },
  "data": {
    "event": {
      "event_id": "45c244dbdf504451...",
      "title": "ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer",
      "culprit": "com.example.sentrytest.controller.TestController in classCast",
      "web_url": "https://sentry.io/organizations/xxx/issues/7302285707/events/45c244db.../",
      "issue_id": "7302285707",
      "project": 4510972645933056,
      "metadata": {
        "type": "ClassCastException",
        "value": "class java.lang.String cannot be cast to class java.lang.Integer"
      }
    },
    "triggered_rule": "菜头-通知飞书-所有新异常"
  },
  "actor": { "type": "application", "name": "Sentry" }
}

注意: Sentry 有两种 Webhook 方式,payload 结构不同:

  • WebHooks Plugin(旧版插件):issue 信息在 data.issue
  • Internal Integration(新版,我们用的这种):issue 信息在 data.event 里,而且多了 installation 字段

中继服务必须兼容两种格式。

第二步:解析关键字段

不需要解析整个 payload(里面有巨大的堆栈信息),只需要提取这几个字段:

字段 来源 说明
action 根节点 "triggered"(告警触发)或 "resolved"(告警解决)
title data.event.title 异常的完整描述,如 "ClassCastException: ..."
issue_id data.event.issue_id Sentry Issue 的唯一 ID
web_url data.event.web_url 可以直接点击跳转到 Sentry Issue 详情页
culprit data.event.culprit 出错的类和方法,如 "TestController in classCast"
triggered_rule data.triggered_rule 触发的告警规则名称

解析代码的核心逻辑:

// SentryPayload 模型用 @JsonIgnoreProperties(ignoreUnknown = true)
// 这样 Sentry payload 里多余的字段不会报错

// 先尝试从 data.issue 取(旧版 WebHooks Plugin 格式)
Map<String, Object> issue = payload.getIssue();
if (issue != null) {
    title = issue.get("title");
    issueId = issue.get("id");
    permalink = issue.get("permalink");
} else {
    // 再从 data.event 取(Internal Integration 格式)
    Map<String, Object> event = payload.getEvent();
    title = event.get("title");
    issueId = event.get("issue_id");
    permalink = event.get("web_url");
}
第三步:拼装飞书消息并发送

飞书机器人 Webhook 要求的 JSON 格式和 Sentry 完全不同。飞书支持两种消息类型:

简单文本消息(msg_type: text):

{
  "msg_type": "text",
  "content": {
    "text": "einstein->告警触发\n●环境: Prod\n●ID: 7302285707\n●问题: ClassCastException..."
  }
}

交互式卡片消息(msg_type: interactive,我们用的这种,更美观):

{
  "msg_type": "interactive",
  "card": {
    "header": {
      "title": { "tag": "plain_text", "content": "einstein->告警触发" },
      "template": "red"
    },
    "elements": [
      { "tag": "markdown", "content": "**环境:** Prod" },
      { "tag": "markdown", "content": "**ID:** 7302285707" },
      { "tag": "markdown", "content": "**问题:** ClassCastException: ..." },
      { "tag": "markdown", "content": "**告警规则:** 菜头-通知飞书-所有新异常" },
      { "tag": "hr" },
      {
        "tag": "action",
        "actions": [{
          "tag": "button",
          "text": { "tag": "plain_text", "content": "查看详情" },
          "url": "https://sentry.io/organizations/xxx/issues/7302285707/...",
          "type": "primary"
        }]
      }
    ]
  }
}

卡片消息的效果:红色头部表示告警触发(绿色表示告警解决),下面是结构化的字段,底部有“查看详情”按钮可以直接跳转到 Sentry。

最后用 RestTemplate 把这个 JSON POST 到飞书的 Webhook URL:

restTemplate.postForEntity(feishuWebhookUrl, message, String.class);
// 飞书返回 {"StatusCode":0,"StatusMessage":"success"} 就表示发送成功
实践中踩过的坑
问题 原因 解决办法
Jackson 反序列化报错 Unrecognized field "installation" Sentry Internal Integration 的 payload 比旧版多了字段 模型类加 @JsonIgnoreProperties(ignoreUnknown = true)
解析出来 issue 为 null Internal Integration 的 issue 信息在 data.event 而不是 data.issue Controller 兼容两种格式,先找 issue 再找 event
Docker 容器里中文乱码 Alpine 镜像默认不是 UTF-8 Dockerfile 加 ENV LANG=C.UTF-8,启动参数加 -Dfile.encoding=UTF-8
RestTemplate 发飞书中文乱码 StringHttpMessageConverter 默认 ISO-8859-1 配置 RestTemplate 的 StringHttpMessageConverter 编码为 UTF-8
告警规则不重复触发 Sentry 的 Action Interval 防刷间隔,同一 Issue 在间隔内不会重复发 等待间隔过,或触发一个全新类型的异常

5.3 核心代码

SentryWebhookController.java – 接收和解析:

@RestController
@RequestMapping("/api/sentry")
@JsonIgnoreProperties(ignoreUnknown = true)  // payload 模型类上加这个
public class SentryWebhookController {

    private final FeishuNotifyService feishuNotifyService;

    @PostMapping("/webhook")
    public ResponseEntity<String> handleWebhook(@RequestBody SentryPayload payload) {
        String action = payload.getAction();  // "triggered" / "resolved"

        // 兼容两种格式:先尝试 data.issue,再尝试 data.event
        Map<String, Object> issue = payload.getIssue();
        if (issue != null) {
            // 旧版 WebHooks Plugin 格式
            issueId = issue.get("id");
            title = issue.get("title");
            permalink = issue.get("permalink");
        } else {
            // Internal Integration 格式
            Map<String, Object> event = payload.getEvent();
            title = event.get("title");
            issueId = event.get("issue_id");
            permalink = event.get("web_url");
        }

        feishuNotifyService.sendAlert(action, issueId, title, permalink, ...);
        return ResponseEntity.ok("ok");
    }
}

FeishuNotifyService.java – 拼成飞书卡片并发送:

@Service
public class FeishuNotifyService {

    private final RelayConfig relayConfig;
    private final RestTemplate restTemplate;

    public void sendAlert(String action, String issueId, String title,
                          String permalink, String count, String projectName,
                          String triggeredRule) {
        String statusLabel = "resolved".equals(action) ? "告警解决" : "告警触发";

        // 构建飞书卡片 JSON(红色头部=告警触发,绿色=告警解决)
        Map<String, Object> card = buildCard(statusLabel, issueId, title,
                                             permalink, count, projectName, triggeredRule);
        FeishuMessage message = FeishuMessage.card(card);

        // POST 到飞书 Webhook
        var response = restTemplate.postForEntity(webhookUrl, message, String.class);
        // 飞书返回 {"StatusCode":0,"msg":"success"} 表示成功
    }
}

application.yml

server:
  port: 8090

feishu:
  webhook:
    url: ${FEISHU_WEBHOOK_URL:https://open.feishu.cn/open-apis/bot/v2/hook/your-token}

5.3 pom.xml 关键依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.0</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

5.4 Dockerfile

FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY target/sentry-feishu-relay-*.jar app.jar
EXPOSE 8090
ENTRYPOINT ["java", "-jar", "app.jar"]

5.5 打包、Docker 构建和运行

cd E:\sentry-feishu-relay
mvn clean package -DskipTests
docker build -t sentry-feishu-relay .
docker run -d --name sentry-relay -p 8090:8090 -e FEISHU_WEBHOOK_URL=https://open.feishu.cn/open-apis/bot/v2/hook/你的token sentry-feishu-relay
第一步:打包 jar

如果本机没有安装 Maven 的情况下,用 IDEA 自带的 Maven 打包:

方式 A:IDEA 图形界面打包(推荐)

  1. IDEA 打开 E:\sentry-feishu-relay 项目
  2. 右侧边栏点 Maven 面板(如果没有,菜单 View → Tool Windows → Maven)
  3. 展开 Lifecycle
  4. 先双击 clean,等执行完
  5. 再双击 package(会自动编译 + 打包)
  6. 完成后在 E:\sentry-feishu-relay\target\ 下会生成 sentry-feishu-relay-1.0.0.jar

在这里插入图片描述

方式 B:命令行(用 IDEA 自带的 Maven)

如果命令行 mvn 提示不是命令,可以用 IDEA 内置 Maven 的完整路径:

cd E:\sentry-feishu-relay
"D:\IntelliJ IDEA Community Edition 2025.2.5\plugins\maven\lib\maven3\bin\mvn.cmd" clean package -DskipTests

路径根据你的 IDEA 实际安装目录调整。

方式 C:安装 Maven 到系统 PATH

下载 Maven:https://maven.apache.org/download.cgi ,解压后把 bin 目录加到系统环境变量 PATH 里,之后就可以直接用 mvn 命令了。

第二步:构建 Docker 镜像
cd E:\sentry-feishu-relay
docker build -t sentry-feishu-relay .

在这里插入图片描述

第三步:运行容器
docker run -d \
  --name sentry-relay \
  -p 8090:8090 \
  -e FEISHU_WEBHOOK_URL=https://open.feishu.cn/open-apis/bot/v2/hook/你的飞书token \
  sentry-feishu-relay

Windows CMD 如果不支持 \ 换行,写成一行:

docker run -d --name sentry-relay -p 8090:8090 -e FEISHU_WEBHOOK_URL=https://open.feishu.cn/open-apis/bot/v2/hook/你的飞书token sentry-feishu-relay

在这里插入图片描述

第四步:验证中继是否正常
# 健康检查
curl http://localhost:8090/api/sentry/webhook
# 应该返回 ok

# 模拟 Sentry Webhook,验证飞书群能否收到消息
curl -X POST http://localhost:8090/api/sentry/webhook -H "Content-Type: application/json" -d "{\"action\":\"triggered\",\"data\":{\"issue\":{\"id\":\"90175418\",\"title\":\"Unsupported currency codeCNY\",\"permalink\":\"https://sentry.io/issues/90175418/\",\"project\":{\"name\":\"einstein\"},\"count\":\"2\"},\"triggered_rule\":\"test-rule\"}}"

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

第五步:让 Sentry 能访问到中继(ngrok 内网穿透)

Sentry SaaS 在公网,访问不到你的 localhost。用 ngrok 临时暴露本地端口:

  1. 注册 https://ngrok.com (免费),下载 ngrok

在这里插入图片描述

在这里插入图片描述

2.运行:

ngrok http 8090
  1. 会输出类似:https://abc123.ngrok-free.app -> http://localhost:8090

在这里插入图片描述

在 Sentry 的 Webhook URL 里填:https://abc123.ngrok-free.app/api/sentry/webhook

在这里插入图片描述

注意:ngrok 免费版每次重启地址会变,需要重新去 Sentry 改 Webhook URL。正式环境应该用有公网 IP 的服务器。

常用 Docker 管理命令
# 查看容器日志
docker logs sentry-relay

# 停止容器
docker stop sentry-relay

# 重新启动
docker start sentry-relay

# 删除容器(需要先 stop)
docker rm sentry-relay

# 更新代码后重新构建部署
docker stop sentry-relay && docker rm sentry-relay
docker build -t sentry-feishu-relay .
docker run -d --name sentry-relay -p 8090:8090 -e FEISHU_WEBHOOK_URL=你的飞书URL sentry-feishu-relay

七、端到端测试

  1. 测试飞书:用 Postman 或 curl 直接 POST 飞书 Webhook,确认群里能收到消息
curl -X POST -H "Content-Type: application/json" -d "{\"msg_type\":\"text\",\"content\":{\"text\":\"Hello from curl!\"}}" https://open.feishu.cn/open-apis/bot/v2/hook/00867813-cf24-4942-8b7c-61b9ee8c08aa

在这里插入图片描述

  1. 测试中继:用 curl 模拟 Sentry Webhook,POST 到 http://localhost:8090/api/sentry/webhook,确认飞书群收到转发的消息
curl -X POST http://localhost:8090/api/sentry/webhook -H "Content-Type: application/json" -d "{\"action\":\"triggered\",\"data\":{\"issue\":{\"id\":\"90175418\",\"title\":\"Unsupported currency codeCNY\",\"permalink\":\"https://sentry.io/issues/90175418/\",\"project\":{\"name\":\"einstein\"},\"count\":\"2\"},\"triggered_rule\":\"test-rule\"}}"

在这里插入图片描述

在这里插入图片描述

  1. 测试全链路:在你的应用里故意抛一个异常 → 看 Sentry 是否收到 → 看告警是否触发 → 看飞书群是否收到通知

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


八、进阶:@ 指定人 & 告警解决通知

前面的基本流程只区分了「告警触发(红色卡片)」一种情况。实际生产中还需要:

  1. 告警触发时自动 @ 相关负责人
  2. 问题解决后收到 绿色卡片,清楚地告知「问题已解决」

下面分别说明原理和实现。


8.1 飞书卡片 @ 人的语法

飞书的 interactive 卡片消息 中,markdown 类型元素支持 <at> 标签:

写法 效果
<at id=ou_xxxxxx>张三</at> @ 指定用户(需要用 open_id)
<at id=all>所有人</at> @ 所有人

open_id 怎么拿?

  1. 打开飞书管理后台 → 通讯录 → 找到成员 → 复制 open_id(ou_ 开头的字符串)
  2. 或者通过飞书 API GET /open-apis/contact/v3/users/:user_id 获取
  3. 群机器人 webhook 不需要 token 就可以 @ 人,只要 open_id 正确
飞书开放平台和管理后台(需要权限)是两个东西,开放平台任何人都能访问:
1. 用你的飞书账号登录 https://open.feishu.cn
2. 点 创建企业自建应用(名字随便取,比如"查ID工具",不需要发布)
3. 进入应用 → 凭证与基础信息 → 记下 App ID 和 App Secret
4. 进入 权限管理 → 搜索 contact:user.base:readonly → 开通
5. 打开 API 调试台 → 选你的应用 → 用 用户身份 (user_access_token) 调用「获取登录用户信息」接口
6. 返回结果里就有你自己的 open_id
> 这个应用不需要管理员审批就能在调试台里使用,这里就不放截图了,步骤很详细,有不懂的可以问ai或博客哈

拼装到卡片 JSON 中的位置:

{
  "tag": "markdown",
  "content": "**通知:** <at id=ou_abc123>张三</at> <at id=ou_def456>李四</at>"
}

这个元素放在卡片的 elements 数组中即可。


8.2 告警解决通知

Sentry 在 Issue 被标记为 Resolved(手动或自动)时,同样会发 Webhook,区别在于 action 字段:

action 值 含义
"triggered" 新告警 / 再次触发
"resolved" 问题已被解决

中继服务只需要根据 action 字段做不同处理:

  • triggered → 红色卡片头 + 显示触发次数 + @ 负责人
  • resolved → 绿色卡片头 + 显示「已解决」+ 不 @ 人(免打扰)
Sentry Issue 触发        →  action="triggered"  →  🔴 红色卡片 + @ 人
Sentry Issue 手动 Resolve →  action="resolved"   →  🟢 绿色卡片(不 @)

8.3 配置方式

application.yml 中配置 @ 人员,支持全局按项目两种粒度:

feishu:
  webhook:
    url: ${FEISHU_WEBHOOK_URL:https://open.feishu.cn/...} # 改为你的token
  at:
    enabled: true            # 是否启用 @ 功能
    all: false               # true = @ 所有人(优先级最高)
    open-ids:                # 全局兜底的 open_id 列表
      - ou_global_001
      - ou_global_002
    project-map:             # 按项目名 @ 不同的人
      einstein:              #   Sentry 项目名(小写)
        - ou_abc123          #     负责人 A
        - ou_def456          #     负责人 B
      newton:
        - ou_ghi789
      sentry-test-app:
        - ou_test001

匹配优先级(从高到低):

1. at.all=true                →  直接 @所有人,忽略其他配置
2. at.project-map[项目名]     →  @ 该项目对应的负责人
3. at.open-ids               →  项目没有专属配置时,走全局兜底列表
4. 都没配                     →  不 @ 任何人

project-map 中的 key 就是 Sentry 上报时的项目名(如 einsteinnewton),
中继会自动用小写匹配,大小写不敏感。

也可以通过环境变量覆盖(Docker 部署时):

docker run -d --name sentry-relay \
  -p 8090:8090 \
  -e FEISHU_WEBHOOK_URL=你的飞书URL \
  -e FEISHU_AT_ENABLED=true \
  -e FEISHU_AT_ALL=false \
  -e FEISHU_AT_OPEN_IDS=ou_abc123,ou_def456 \
  sentry-feishu-relay

⚠️ project-map 是嵌套结构,通过环境变量配置较复杂,建议直接写在 application.yml 中。
如果一定要用环境变量,格式为:
FEISHU_AT_PROJECTMAP_EINSTEIN=ou_abc123,ou_def456(Spring Boot 宽松绑定)


8.4 核心代码改动

RelayConfig.javaAt 类新增 projectMap 字段:

public static class At {
    private boolean enabled = false;
    private boolean all = false;
    private List<String> openIds = Collections.emptyList();
    private Map<String, List<String>> projectMap = new HashMap<>();
    // getter/setter ...
}

Spring Boot 会自动把 YAML 里的 project-map 绑定到 Map<String, List<String>>

FeishuNotifyService.javabuildAtText 增加项目名参数,按优先级匹配:

private String buildAtText(boolean resolved, String projectName) {
    RelayConfig.At atConfig = relayConfig.getAt();
    if (!atConfig.isEnabled() || resolved) {
        return null;
    }

    // 优先级 1:@ 所有人
    if (atConfig.isAll()) {
        return "**通知:** <at id=all>所有人</at>";
    }

    // 优先级 2:按项目名查 project-map
    Map<String, List<String>> projectMap = atConfig.getProjectMap();
    List<String> ids = null;
    if (projectMap != null && projectName != null) {
        ids = projectMap.get(projectName.toLowerCase());  // 小写匹配
        if (ids == null) {
            ids = projectMap.get(projectName);             // 原始大小写兜底
        }
    }

    // 优先级 3:全局 open-ids 兜底
    if (ids == null || ids.isEmpty()) {
        ids = atConfig.getOpenIds();
    }

    if (ids == null || ids.isEmpty()) {
        return null;
    }

    String mentions = ids.stream()
            .filter(id -> id != null && !id.isBlank())
            .map(id -> "<at id=" + id.trim() + "></at>")
            .collect(Collectors.joining(" "));
    return mentions.isEmpty() ? null : "**通知:** " + mentions;
}

调用处改动(buildCard 方法中):

// 之前:buildAtText(resolved)
// 现在:buildAtText(resolved, projectName)  ← 多传了项目名
String atText = buildAtText(resolved, projectName);

8.5 两种卡片效果对比

告警触发(红色 + 按项目 @人):

假设 project-map 配置了 ein: [ou_abc, ou_def],当 ein 项目报错:

┌──────────────────────────────────────┐
│  🔴  ein → 告警触发 🔥            │
├──────────────────────────────────────┤
│  环境: Prod                           │
│  ID: 90175418                        │
│  问题: NullPointerException at ...    │
│  问题触发次数: 3                       │
│  告警规则: Send to relay              │
│  ─────────────────────────────────── │
│  通知: @张三 @李四   ← 只 @ einstein   │
│  ─────────────────────────────────── │       的负责人
│  [查看详情]                           │
└──────────────────────────────────────┘

当 news 项目报错(假设 news: [ou_ghi]):

┌──────────────────────────────────────┐
│  🔴  news → 告警触发 🔥              │
├──────────────────────────────────────┤
│  ...                                 │
│  ─────────────────────────────────── │
│  通知: @王五         ← 只 @ news    │
│  ─────────────────────────────────── │       的负责人
│  [查看详情]                           │
└──────────────────────────────────────┘

如果某个项目没有在 project-map 中配置,则走全局 open-ids 兜底。

告警解决(绿色,不 @):

┌──────────────────────────────────────┐
│  🟢  sentry-test-app → 告警解决 ✅    │
├──────────────────────────────────────┤
│  环境: Prod                           │
│  ID: 90175418                        │
│  问题: NullPointerException at ...    │
│  状态: 该问题已被标记为解决              │
│  ─────────────────────────────────── │
│  [查看Issue]                          │
└──────────────────────────────────────┘

8.6 测试 @ 人和解决通知

1. curl 模拟告警触发(带 @):

先在 application.yml 开启 at:

feishu:
  at:
    enabled: true
    all: true

重启中继后:

curl -X POST http://localhost:8090/api/sentry/webhook -H "Content-Type: application/json" -d "{\"action\":\"triggered\",\"data\":{\"issue\":{\"id\":\"90175418\",\"title\":\"Unsupported currency codeCNY\",\"permalink\":\"https://sentry.io/issues/90175418/\",\"project\":{\"name\":\"einstein\"},\"count\":\"2\"},\"triggered_rule\":\"test-rule\"}}"

飞书群应收到红色卡片 + “通知: @所有人”。

在这里插入图片描述

2. curl 模拟告警解决:

curl -X POST http://localhost:8090/api/sentry/webhook -H "Content-Type: application/json" -d "{\"action\":\"resolved\",\"data\":{\"issue\":{\"id\":\"123\",\"title\":\"Testresolve\",\"permalink\":\"https://sentry.io\",\"project\":{\"name\":\"demo\"},\"count\":\"0\"}}}"

飞书群应收到绿色卡片 + “该问题已被标记为解决”,且不 @ 任何人。

在这里插入图片描述

3. Sentry 真实触发 resolved:

在 Sentry 控制台找到一个 Issue → 点击 Resolve 按钮 → Sentry 会向你配置的 Webhook URL 发送 action="resolved" 的请求 → 中继转发绿色卡片到飞书。

在这里插入图片描述

在这里插入图片描述

⚠️ Sentry 的告警规则需要包含 resolved 条件才会发 webhook。检查方法:
Sentry → Alerts → 编辑规则 → 确认 “When” 条件里包含 A new issue is createdAn existing issue changes state from resolved to unresolved 等。


8.7 进阶(可选)

  • 签名校验:Sentry Webhook 支持配 secret,中继服务可以校验请求签名防伪造;飞书机器人也支持签名校验
  • 消息模板化:将卡片结构抽成 JSON 模板文件,运行时填充变量,方便非开发人员修改样式
  • 按环境区分:在 application.yml 中配置环境名,卡片中的"环境"字段从配置读取而不是写死 Prod
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐