「企业项目集成Sentry 告警 进 飞书群聊:中继方案与实现」!!!附全部源码!!!
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 整条链路
1.4 为什么需要「中继」
- Sentry Webhook 发出的 JSON 格式(含 event、issue、triggered_rule 等)和飞书机器人要求的 JSON 格式(msg_type、content 等)完全不同
- 不能直接把飞书 Webhook 填到 Sentry 告警里,Sentry 发过去飞书不认
- 中继服务做的事:收 Sentry JSON → 解析 → 拼成飞书 JSON → 转发
二、准备工作:注册 Sentry
2.1 注册 Sentry SaaS(推荐,免费,自学用)
- 打开 https://sentry.io → Sign Up(GitHub / Google 登录即可)
- 创建 Organization,我登录的时候已经填写组织,这里自动创建了
image-20260228151551156 - 创建 Project:
-
点 “Create Project”
-
Platform 选 Java(或搜索 “Logback”,选 Java - Logback)
-
点 “Create Project”

- 创建完成后,页面会显示 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


- 免费版:每月 5,000 事件额度,自学足够
2.2 Sentry 界面关键页面(先认识一下)
登录后左侧菜单:
- Issues:所有收集到的异常列表,相同异常会自动聚合为一个 Issue
- Alerts:告警规则配置(后面第四节详细讲)
- Settings → Projects → 你的项目:DSN 在这里可以再次查看
- Settings → Integrations:配置 Webhook 等第三方集成
三、应用侧配置:测试项目验证 Sentry 上报
3.1 Sentry 上报原理
核心就三步:
- 依赖:项目引入
sentry-logback
<dependency>
<groupId>io.sentry</groupId>
<artifactId>sentry-logback</artifactId>
<version>7.6.0</version>
</dependency>
- 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
- 环境变量:启动时设
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
在项目的环境变量设置也行,然后启动项目


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


四、Sentry 配置告警规则 + Webhook
这部分全在 Sentry 网页端 操作,不改项目代码。
4.1 启用 WebHooks 集成
- 登录 sentry.io → 你的项目
- 左侧 Settings → Integrations
- 搜索 “WebHooks” → 点 Enable Plugin 或 Configure

填写:
- 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 创建告警规则
- 左侧 Alerts → Create Alert Rule → 选 Issues(Issue Alert)

- Set Conditions(触发条件):
- When:选 “A new issue is created”(首次出现新异常就告警)
- 或者选 “The issue is seen more than {value} times in {interval}”(N 分钟内出现超过 X 次)
- 示例:出现超过 2 次 in 1 hour
- Set Filters(可选过滤):
- 如果你设了 APP_ENV,可以加条件 “The event’s environment is Prod”
- 可以按 Issue 级别过滤:“The event’s level is equal to error”
- Set Actions(触发动作):
- 选 “Send a notification via an integration”
- Integration 选 WebHooks
- 这会把告警发到你在 4.1 步骤填的 Callback URL
- Action Interval(防刷间隔):
- 建议选 “30 minutes” 或 “1 hour”,避免同一 Issue 反复发告警刷屏
- Rule Name:起个名字,如 “通知飞书-所有新异常”
- 点 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 里取出关键字段,转成飞书格式即可。
五、创建飞书自定义机器人
- 打开飞书 → 创建一个测试群(或用现有群)
- 群设置 → 群机器人 → 添加机器人 → 自定义机器人
- 名称填 “Sentry告警” → 完成
- 记录生成的 Webhook 地址,形如:
https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - 安全设置(可选但推荐):
- 可以开启「签名校验」,会给你一个 secret,中继服务发消息时需要带签名
- 自学阶段可以先不开,直接用即可
- 用 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 → 解析关键字段 → 拼成飞书消息发出去。下面详细说明每一步。
第一步:接收 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 图形界面打包(推荐)
- IDEA 打开
E:\sentry-feishu-relay项目 - 右侧边栏点 Maven 面板(如果没有,菜单 View → Tool Windows → Maven)
- 展开 Lifecycle
- 先双击 clean,等执行完
- 再双击 package(会自动编译 + 打包)
- 完成后在
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 临时暴露本地端口:
- 注册 https://ngrok.com (免费),下载 ngrok


2.运行:
ngrok http 8090
- 会输出类似:
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
七、端到端测试
- 测试飞书:用 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

- 测试中继:用 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\"}}"


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




八、进阶:@ 指定人 & 告警解决通知
前面的基本流程只区分了「告警触发(红色卡片)」一种情况。实际生产中还需要:
- 告警触发时自动 @ 相关负责人
- 问题解决后收到 绿色卡片,清楚地告知「问题已解决」
下面分别说明原理和实现。
8.1 飞书卡片 @ 人的语法
飞书的 interactive 卡片消息 中,markdown 类型元素支持 <at> 标签:
| 写法 | 效果 |
|---|---|
<at id=ou_xxxxxx>张三</at> |
@ 指定用户(需要用 open_id) |
<at id=all>所有人</at> |
@ 所有人 |
open_id 怎么拿?
- 打开飞书管理后台 → 通讯录 → 找到成员 → 复制 open_id(
ou_开头的字符串)- 或者通过飞书 API
GET /open-apis/contact/v3/users/:user_id获取- 群机器人 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 上报时的项目名(如
einstein、newton),
中继会自动用小写匹配,大小写不敏感。
也可以通过环境变量覆盖(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.java — At 类新增 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.java — buildAtText 增加项目名参数,按优先级匹配:
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 created 和 An existing issue changes state from resolved to unresolved 等。
8.7 进阶(可选)
- 签名校验:Sentry Webhook 支持配 secret,中继服务可以校验请求签名防伪造;飞书机器人也支持签名校验
- 消息模板化:将卡片结构抽成 JSON 模板文件,运行时填充变量,方便非开发人员修改样式
- 按环境区分:在
application.yml中配置环境名,卡片中的"环境"字段从配置读取而不是写死 Prod
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)