监控日志与告警体系
可观测性建设指南:讲解三支柱(指标/日志/链路)、日志分级与结构化、采集与轮转、监控指标体系、前端错误追踪、告警分级与设计、故障排查与复盘,让问题被发现得早、定位得快。
一、可观测性三支柱
| 支柱 | 回答什么问题 | 典型工具 |
|---|---|---|
| Metrics(指标) | 「整体是否健康?」 | Prometheus、云监控、宝塔监控 |
| Logs(日志) | 「具体发生了什么?」 | 文件日志、ELK、Loki |
| Traces(链路) | 「慢在哪一环?」 | Jaeger、SkyWalking、OpenTelemetry |
三者的分工:
指标异常(CPU 90%、错误率 5%) ↓ 触发告警查日志(哪个接口、什么错误) ↓ 定位到具体请求看链路(请求经过了哪些服务、各耗时多少) ↓ 找到瓶颈小团队不必一次上齐三件套。最低配是「指标 + 结构化日志」,已经能覆盖 80% 的排查场景。
二、日志规范
2.1 日志分级
| 级别 | 含义 | 生产环境 | 示例 |
|---|---|---|---|
DEBUG |
调试细节 | ❌ 关闭 | 变量值、SQL 语句 |
INFO |
正常业务事件 | ✅ 开 | 用户登录、订单创建 |
WARN |
可恢复的异常 | ✅ 开 | 重试成功、降级触发 |
ERROR |
需要处理的错误 | ✅ 开 | 接口 500、第三方调用失败 |
FATAL |
进程无法继续 | ✅ 开 | 数据库连接池耗尽、启动失败 |
常见错误:全用 INFO,导致真正的错误淹没在噪音里;或 ERROR 满天飞,失去告警意义。
2.2 结构化日志(关键)
// ❌ 纯文本:无法检索与聚合console.log('用户 1024 下单失败,错误:库存不足')// ✅ 结构化(JSON):可查询、可统计logger.error({ event: 'order.create.failed', userId: 1024, orderId: 'ORD20260101001', reason: 'STOCK_INSUFFICIENT', requestId: ctx.state.requestId, duration: 156,})必须包含的字段:
| 字段 | 作用 |
|---|---|
timestamp |
时间(含时区) |
level |
级别 |
event |
事件名(用点分层级,便于聚合) |
requestId |
贯穿一次请求的追踪 ID |
userId |
受影响用户(脱敏后) |
duration |
耗时(ms) |
error.stack |
错误堆栈(仅 ERROR) |
2.3 requestId:贯穿全链路
// 中间件:为每个请求生成唯一 IDapp.use(async (ctx, next) => { ctx.state.requestId = ctx.get('X-Request-Id') || crypto.randomUUID() ctx.set('X-Request-Id', ctx.state.requestId) await next()})价值:用户报障时提供 requestId,就能直接筛出这一次请求的所有相关日志,不必靠时间戳猜。
2.4 该记与不该记
| ✅ 应记录 | ❌ 禁止记录 |
|---|---|
| 登录成功/失败(IP、UA、时间) | 明文密码 |
| 权限变更、敏感操作 | 完整身份证、银行卡号、CVV |
| 关键状态流转(订单状态变化) | 完整 token / session 值 |
| 第三方调用(接口、耗时、结果) | 数据库连接串、密钥 |
| 异常堆栈(含 requestId) | 用户完整手机号(应脱敏) |
// 脱敏工具const maskPhone = (p) => p?.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')const maskIdCard = (s) => s?.replace(/(\d{4})\d{10}(\w{4})/, '$1**********$2')日志一旦写入磁盘、进入日志平台,就很难彻底删除。脱敏要在写入前完成,而不是事后处理。
三、日志采集与轮转
3.1 落盘位置规范
/var/log/app/├── app.log # 全量日志(INFO 及以上)├── app-error.log # 仅 ERROR└── archive/ # 归档 ├── app-2026-01-01.log.gz └── ...3.2 轮转(必须做,否则磁盘会满)
# /etc/logrotate.d/app/var/log/app/*.log { daily # 每天轮转 rotate 14 # 保留 14 天 compress # gzip 压缩 delaycompress missingok notifempty copytruncate # 不重启进程也能轮转}为什么必须轮转:单个日志文件增长到几 GB 后,会拖慢写入、占满磁盘,甚至导致服务崩溃。磁盘满是最常见的线上故障之一。
# 手动检查日志目录占用du -sh /var/log/app/du -h /var/log/app/* | sort -rh | head -103.3 应用层日志滚动(不依赖系统 logrotate)
// winston + 按天/按大小滚动const { createLogger, format, transports } = require('winston')require('winston-daily-rotate-file')const logger = createLogger({ format: format.combine(format.timestamp(), format.json()), transports: [ new transports.DailyRotateFile({ filename: '/var/log/app/app-%DATE%.log', datePattern: 'YYYY-MM-DD', maxSize: '100m', maxFiles: '14d', level: 'info', }), new transports.DailyRotateFile({ filename: '/var/log/app/app-error-%DATE%.log', datePattern: 'YYYY-MM-DD', level: 'error', maxFiles: '30d', }), ],})容器环境应输出到 stdout,由容器运行时统一收集,不要在容器内写文件。
四、日志分析常用命令
# 实时跟踪tail -f /var/log/app/app.log# 过滤错误并统计 TOP 10grep '"level":"error"' app.log | jq -r '.event' | sort | uniq -c | sort -rn | head -10# 按 requestId 追踪一次请求grep 'a1b2c3d4-xxxx' app.log# 统计各接口 P95 耗时(jq 处理 JSON 日志)jq -r 'select(.duration) | "\(.event) \(.duration)"' app.log \ | awk '{a[$1]=a[$1]" "$2} END {for (k in a) print k, a[k]}'# 统计每分钟错误数grep '"level":"error"' app.log | jq -r '.timestamp' | cut -c1-16 | uniq -c# Nginx 日志:TOP 10 访问 IPawk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10# Nginx 日志:TOP 10 慢接口awk '{print $NF, $7}' /var/log/nginx/access.log | sort -rn | head -10五、监控指标体系
5.1 四个黄金信号
| 信号 | 含义 | 关键指标 |
|---|---|---|
| 延迟(Latency) | 请求处理快慢 | P50/P95/P99 响应时间 |
| 流量(Traffic) | 负载大小 | QPS、并发连接数 |
| 错误(Errors) | 失败比例 | 5xx 率、业务失败率、超时率 |
| 饱和度(Saturation) | 资源使用程度 | CPU、内存、磁盘、连接池使用率 |
5.2 分层监控指标
系统层
| 指标 | 告警阈值建议 |
|---|---|
| CPU 使用率 | 持续 5 分钟 > 85% |
| 内存使用率 | > 90%,或 Swap 持续使用 |
| 磁盘使用率 | > 85%(写满会直接导致故障) |
| 负载(load) | > CPU 核数 × 2 |
| 网络带宽 | 接近上限的 80% |
应用层
| 指标 | 告警阈值建议 |
|---|---|
| 接口 P95 耗时 | > 目标值(如 500ms) |
| 5xx 错误率 | 5 分钟内 > 1% |
| 进程存活 | 进程消失立即告警 |
| 队列积压 | > 阈值或持续增长 |
| 数据库连接池 | 使用率 > 80% |
业务层(最容易被忽略,但最有价值)
| 指标 | 为什么重要 |
|---|---|
| 下单量 / 支付成功率 | 技术指标正常但业务指标暴跌,说明有隐藏故障 |
| 注册转化率 | 突然归零通常是注册链路坏了 |
| 关键接口调用量 | 掉零意味着上游没流量或路由错了 |
| 第三方回调到达量 | 掉零可能对方出问题或回调地址失效 |
技术监控与业务监控缺一不可:服务活着但「什么都办不成」,技术指标可能完全正常。
5.3 缓存与数据库专项指标
| 对象 | 指标 |
|---|---|
| Redis | 内存使用率、命中率、连接数、慢命令、被驱逐 key 数 |
| MySQL | 连接数、慢查询数、QPS、主从延迟、InnoDB 缓冲池命中率 |
缓存命中率掉到 80% 以下就要警惕——可能大量 key 过期或缓存服务异常。
六、前端错误监控
6.1 需要捕获的三类错误
// 1. JS 运行时错误window.addEventListener('error', (e) => { report({ type: 'js-error', message: e.message, source: e.filename, line: e.lineno, stack: e.error?.stack, url: location.href, })})// 2. 未处理的 Promise 拒绝window.addEventListener('unhandledrejection', (e) => { report({ type: 'unhandled-rejection', reason: String(e.reason) })})// 3. 资源加载失败(图片/JS/CSS)window.addEventListener('error', (e) => { if (e.target?.tagName) { report({ type: 'resource-error', tag: e.target.tagName, src: e.target.src || e.target.href }) }}, true) // 捕获阶段才能拿到资源错误6.2 上报要点
| 要点 | 说明 |
|---|---|
| 采样 | 大流量下按比例采样(如 10%),否则上报本身成为负担 |
| 去重 | 相同错误按 message + stack 首行 聚合 |
| 带上版本号 | 区分是哪个版本引入的问题 |
| 带上用户环境 | UA、屏幕尺寸、网络类型 |
| 失败不影响业务 | 上报请求要 try/catch + 无阻塞,绝不因为上报失败影响页面 |
6.3 性能数据采集
// 采集 Core Web Vitalsnew PerformanceObserver((list) => { for (const entry of list.getEntries()) { report({ type: 'lcp', value: entry.startTime, url: location.href }) }}).observe({ type: 'largest-contentful-paint', buffered: true })七、告警体系
7.1 告警分级
| 级别 | 定义 | 通知方式 | 响应时限 |
|---|---|---|---|
| P0 | 服务不可用、数据丢失 | 电话 + 短信 + IM | 立即 |
| P1 | 核心功能受损、错误率高 | 短信 + IM | 30 分钟内 |
| P2 | 非核心功能异常、性能下降 | IM | 当天 |
| P3 | 潜在风险、容量预警 | 邮件/日报 | 本周 |
关键:级别定义要写明「什么情况算 P0」,否则所有告警都会变成 P0。
7.2 告警设计三原则
原则一:告警必须可执行
不可执行的告警等于噪音:
| ❌ 无效告警 | ✅ 有效告警 |
|---|---|
| CPU 瞬时 80% | CPU 持续 5 分钟 > 85% |
| 出现 1 次错误 | 5 分钟内 5xx 率 > 1% |
| 磁盘 60% | 磁盘 > 85% |
原则二:避免告警风暴
# 重复抑制:同一告警 30 分钟内只发一次# 告警聚合:同一时刻多条告警合并为一条# 依赖收敛:数据库挂了不要再报「接口报错」——根因优先原则三:分级降噪
- 生产环境才开高优先级告警
- 已知问题临时静默(并设置静默到期时间)
- 定期清理从未触发或永远在触发的告警规则
7.3 一个可用的最小告警集合
小项目只需先配这几条:
| 告警 | 条件 |
|---|---|
| 服务进程消失 | 进程数 = 0(持续 1 分钟) |
| 站点不可访问 | 健康检查连续 3 次失败 |
| 磁盘将满 | 使用率 > 85% |
| 内存吃紧 | 可用内存 < 10% 或 Swap 持续增长 |
| 错误率飙升 | 5xx 率 5 分钟内 > 1% |
| 证书即将过期 | 剩余天数 < 15 |
八、故障排查流程
8.1 标准顺序(由外到内)
1. 用户侧:确认现象(全站还是局部?所有用户还是部分?)2. DNS / 证书:域名解析是否正常、证书是否过期3. 网络 / 网关:能否连通、Nginx 是否在跑4. 应用进程:进程是否存活、端口是否监听5. 应用日志:最近 ERROR 是什么、有无异常堆栈6. 依赖服务:数据库、Redis、第三方接口是否可用7. 资源:CPU / 内存 / 磁盘 / 连接数8. 最近变更:是否有发布、配置修改、依赖升级8.2 十分钟快速定位清单
# 1. 端口与进程ss -tlnp | grep :80ps aux | grep -E "node|php-fpm|java" | grep -v grep# 2. 本机能否访问curl -s -o /dev/null -w "%{http_code} %{time_total}s\n" http://127.0.0.1/health# 3. 资源free -h; df -h; uptime# 4. 最近错误日志tail -100 /var/log/app/app-error.log# 5. 依赖服务redis-cli pingmysqladmin status -u user -p# 6. 最近变更git log --oneline -58.3 「最近改了什么」是第一嫌疑
统计上,线上故障大部分由变更引起。排查时优先确认:
- 今天是否发布过?
- 是否改过配置(Nginx、数据库参数、环境变量)?
- 是否升级过依赖或系统包?
- 是否改过 DNS / 证书?
九、值班与复盘
9.1 故障复盘模板
## 故障复盘:{标题}**时间**:2026-01-01 10:00 ~ 10:35(35 分钟)**影响**:{哪些功能、多少用户、是否数据丢失}**级别**:P1### 时间线- 10:00 监控告警触发(错误率 8%)- 10:03 值班确认,开始排查- 10:15 定位到 {根因}- 10:28 执行 {修复动作}- 10:35 指标恢复,确认无残留### 根因{一句话说明直接原因 + 深层原因}### 改进项| 项 | 负责人 | 截止 ||----|-------|------|| 补充 {某场景} 的监控告警 | — | — || 修复 {某代码缺陷} | — | — || 为该问题增加回归测试 | — | — |复盘的原则:讨论「系统为什么允许这个错误发生」,而不是「谁犯了错」。追责会让人隐瞒问题,改进项才是价值所在。
9.2 改进项要闭环
复盘最常见的失效模式:写了一份很好的报告,改进项没人跟进。
建议:
- 每个改进项必须有负责人 + 截止时间
- 下次复盘先检查上次改进项的完成情况
- 只在「同类问题重复出现」时才升级到流程层面
十、常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 磁盘突然满 | 日志未轮转 | 配 logrotate,清理归档;同时检查是否有异常大量日志 |
| 告警太多没人看 | 阈值过低、无抑制 | 收紧阈值 + 加抑制 + 分级 |
| 出问题查不到日志 | 只记 INFO、无 requestId | 改结构化日志 + 引入 requestId |
| 日志里发现明文密码 | 未脱敏 | 立即改代码,并评估是否需要轮换凭证 |
| 前端报错无法复现 | 无版本号与用户环境 | 上报时带版本、UA、URL |
| 监控显示正常但用户说不能用 | 只监控了技术指标 | 补业务指标(下单量、成功率) |
| 凌晨告警无人响应 | 无分级与值班安排 | 定义 P0 用电话,其余用 IM 次日处理 |
十一、检查清单
日志
- 日志分级使用正确,生产关闭 DEBUG
- 结构化 JSON 输出,含 timestamp/level/event/requestId
- 每个请求有唯一 requestId 并回传响应头
- 敏感字段已脱敏(密码、手机号、证件号、token)
- 已配置轮转(按天 + 按大小 + 压缩 + 保留天数)
- 日志目录占用有监控
监控
- 已覆盖四个黄金信号(延迟/流量/错误/饱和度)
- 系统指标:CPU、内存、磁盘、负载
- 应用指标:接口 P95、错误率、进程存活
- 依赖指标:数据库连接与慢查询、Redis 命中率
- 业务指标:核心业务量、成功率
- HTTPS 证书到期时间有监控
告警
- 告警分了 P0–P3 且明确定义触发条件
- 每条告警都可执行(持续时长 + 阈值明确)
- 有重复抑制与聚合,避免告警风暴
- P0 能触达本人(电话/短信)
前端
- JS 错误、Promise 拒绝、资源加载失败均已捕获
- 上报带版本号、页面 URL、用户环境
- 有采样策略,上报失败不影响业务
- Core Web Vitals 有采集
流程
- 有标准故障排查顺序(由外到内)
- 故障复盘有模板,改进项有负责人与截止时间
- 上次复盘的改进项已闭环
十二、总结
可观测性建设的优先级,从高到低是:
- 结构化日志 + requestId——成本最低、收益最直接,没有它排查基本靠猜。
- 核心指标监控 + 少量精准告警——先配「服务不可用、磁盘满、错误率飙升、证书过期」这几条,比堆几十条无效告警有用得多。
- 业务指标监控——技术指标正常但业务量暴跌,是最隐蔽也最危险的故障。
- 链路追踪——服务拆分到一定复杂度后才需要。
两条最容易忽视的原则:
- 告警必须可执行:不能指导行动的告警,是在训练大家忽略告警。
- 故障优先怀疑变更:先问「最近改了什么」,比从代码里找 bug 快得多。
完