日志与告警体系
没有日志 = 排查靠猜;没有告警 = 用户发现 bug 比你早。上线第一天就要搭好。
一、避坑指南
日志和监控是两件事:
- 监控 = 实时数值(错误率、QPS、CPU)
- 日志 = 详细事件记录(某用户某时刻做了什么)
- 监控告诉你「有异常」,日志告诉你「为什么」
日志必须带
requestId:// ❌ 没法串联
{ "msg": "用户登录失败" }
{ "msg": "查询订单失败" }
// ✅ 一个 request 一条链
{ "requestId": "abc-123", "userId": "u1", "msg": "用户登录失败" }
{ "requestId": "abc-123", "userId": "u1", "msg": "查询订单失败" }敏感字段必须脱敏:
// ❌ 直接打印
console.log("user info:", user);
// 泄露:手机号、身份证、token
// ✅ 脱敏后
console.log("user info:", {
...user,
phone: user.phone.replace(/(\d{3})\d{4}/, "$1****"),
idCard: user.idCard.replace(/(\d{4})\d+(\d{4})/, "$1****$2"),
token: "***",
});不要把日志写死:
// ❌ console.log 散布代码里
console.log("进入登录页");
console.log("调登录接口", params);
// ✅ 统一 logger
import { logger } from "@/lib/logger";
logger.info("login_page_view", { userId });
logger.error("login_api_error", { code, message });告警要分级,否则全员麻木:
P0 业务中断(支付失败、登录不可用) → 电话 + 短信 + 钉钉
P1 核心功能降级(部分用户报错) → 钉钉 @on-call
P2 异常率超过阈值(错误率 1% → 3%) → 钉钉
P3 性能下降(LCP 从 1.5s → 3s) → 邮件 / 看板
P4 信息提示(新版本上线、流量峰值) → 看板避免告警风暴:
触发: 数据库挂了 → 1000 个接口超时 → 1000 条告警
解决: 用告警「合并 / 抑制 / 静默」机制
二、日志架构
2.1 前端日志 vs 后端日志
| 维度 | 前端日志 | 后端日志 |
|---|---|---|
| 采集方式 | navigator.sendBeacon | stdout / 文件 |
| 落地位置 | 日志服务(ES / ClickHouse) | 同左 |
| 重要价值 | 用户行为还原、复现 bug | 服务端 trace、问题定位 |
| 主要问题 | 网络丢包、性能开销 | 磁盘爆、采集 agent 漏数据 |
| 典型工具 | 自研 / Sentry | Filebeat / Fluentd / Promtail |
2.2 日志系统选型
| 方案 | 优势 | 劣势 | 适用 |
|---|---|---|---|
| ELK(Elasticsearch + Logstash + Kibana) | 生态成熟、查询强 | 重、资源占用大、运维贵 | 中大型团队 |
| Loki + Grafana | 轻量、与 Prometheus 集成好 | 查询能力弱(仅标签 + grep) | 云原生 / K8s 场景 |
| ClickHouse | 列式存储、OLAP 强 | 不是专业日志系统 | 超大数据量分析 |
| 自研 + Sentry | 与前端监控一体化 | 需要服务端开发 | 前端团队推荐 |
2025+ 趋势:前端团队选 Sentry(错误 + 性能 + 日志一站式),后端团队选 Loki + Grafana(轻量云原生)。
2.3 必备字段
interface LogEntry {
// 必备
timestamp: number; // 毫秒时间戳
level: "info" | "warn" | "error";
message: string;
service: string; // 服务名(多服务时区分)
env: "dev" | "staging" | "production";
requestId: string; // 链路追踪
// 用户/会话
userId?: string;
sessionId?: string;
deviceId?: string;
// 上下文
url?: string; // 当前页面
appVersion?: string; // 客户端版本
userAgent?: string;
networkType?: string; // 4g / wifi
// 业务
module?: string; // 模块名
action?: string; // 动作
extra?: Record<string, any>;
}
三、统一 Logger 实现
// lib/logger.ts
type Level = "debug" | "info" | "warn" | "error";
interface LoggerOptions {
endpoint: string;
appVersion: string;
env: string;
sampleRate?: number; // 采样率 0-1
}
class Logger {
private queue: any[] = [];
private timer: number | null = null;
private sessionId = crypto.randomUUID();
constructor(private opts: LoggerOptions) {}
setUser(userId: string) {
this.userId = userId;
}
private send(level: Level, message: string, extra?: any) {
if (Math.random() > (this.opts.sampleRate ?? 1)) return;
this.queue.push({
timestamp: Date.now(),
level,
message,
sessionId: this.sessionId,
userId: this.userId,
url: location.href,
appVersion: this.opts.appVersion,
env: this.opts.env,
userAgent: navigator.userAgent,
extra,
});
if (this.queue.length >= 10) this.flush();
if (!this.timer) {
this.timer = window.setTimeout(() => this.flush(), 5000);
}
}
debug(msg: string, extra?: any) { this.send("debug", msg, extra); }
info(msg: string, extra?: any) { this.send("info", msg, extra); }
warn(msg: string, extra?: any) { this.send("warn", msg, extra); }
error(msg: string, extra?: any) { this.send("error", msg, extra); }
flush() {
if (!this.queue.length) return;
const data = JSON.stringify({ events: this.queue });
if (navigator.sendBeacon) {
navigator.sendBeacon(this.opts.endpoint, data);
} else {
fetch(this.opts.endpoint, { body: data, method: "POST", keepalive: true });
}
this.queue = [];
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
}
export const logger = new Logger({
endpoint: "/api/log",
appVersion: process.env.APP_VERSION || "0.0.0",
env: process.env.NODE_ENV || "development",
sampleRate: 0.1, // 10% 采样
});
// 页面卸载前 flush
window.addEventListener("beforeunload", () => logger.flush());
四、告警体系
4.1 告警分级标准
| 等级 | 触发条件 | 响应时间 | 通知方式 |
|---|---|---|---|
| P0 | 支付/登录不可用,5xx > 10% | 5 分钟 | 电话 + 短信 + 钉钉 |
| P1 | 核心功能报错率 > 3% | 15 分钟 | 钉钉 @on-call |
| P2 | 非核心错误率 > 1%,业务指标异常 | 30 分钟 | 钉钉 |
| P3 | 性能劣化(LCP / INP 超阈值) | 当天 | 邮件 / 看板 |
| P4 | 信息性(流量峰值、版本发布) | 不响应 | 看板 |
4.2 推荐告警渠道
| 工具 | 适用 | 价格 |
|---|---|---|
| 钉钉机器人 | 国内团队首选 | 免费 |
| 飞书机器人 | 字节系 / 协同场景 | 免费 |
| 企业微信机器人 | 腾讯系 | 免费 |
| PagerDuty | 海外 / 7×24 on-call | 付费 |
| OpsGenie | Atlassian 生态 | 付费 |
// 钉钉机器人示例
async function sendDingtalk(message: string, atMobiles: string[] = []) {
await fetch(`https://oapi.dingtalk.com/robot/send?access_token=${TOKEN}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
msgtype: "markdown",
markdown: {
title: "🚨 线上告警",
text: `**${message}**\n时间: ${new Date().toISOString()}\n服务: my-web\n负责人: @<at here>`,
},
at: { atMobiles, isAtAll: false },
}),
});
}
4.3 告警治理三剑客
告警治理的核心:少而准。一堆告警 = 没告警。
抑制 (Inhibit) 高优先级抑制低优先级(DB 挂了,订单告警自动抑制)
静默 (Silence) 已知维护窗口静默(升级、演练)
合并 (Group) 同一根因的告警聚合(1000 条 → 1 条)
Prometheus 告警规则示例(后端):
groups:
- name: myapp
rules:
# 抑制:DB 挂了 → 自动屏蔽订单告警
- alert: DatabaseDown
expr: up{job="postgres"} == 0
for: 1m
labels: { severity: critical }
annotations:
summary: "数据库宕机"
- alert: HighErrorRate
expr: |
sum(rate(http_requests_total{status=~"5.."}[5m]))
/ sum(rate(http_requests_total[5m])) > 0.03
for: 5m
labels: { severity: warning }
annotations:
summary: "5xx 错误率超过 3%"
4.4 On-call 轮值
| 模式 | 适用 | 优点 | 缺点 |
|---|---|---|---|
| 固定排班 | 中小团队 | 责任明确 | 节假日难调 |
| Follow the Sun | 全球团队 | 跨时区不间断 | 需多团队 |
| 值班池 | 大团队 | 灵活 | 责任分散 |
国内实践:周一~周五工作时间内 1 人值班,夜间和周末 on-call 轮流,值班给加班费。
五、On-call 工具链
| 工具 | 用途 |
|---|---|
| 钉钉群 + 机器人 | 实时告警 + 沟通 |
| 值班日历 | 谁在值班一目了然 |
| Runbook | 按图索骥排查 |
| 事故复盘文档 | 沉淀经验 |
| 状态页(status page) | 告诉用户我们出问题了(主动) |
状态页推荐:statuspage.io、自建(阿里云 / 腾讯云都有)
六、面试高频
Q1:前端怎么打日志?怎么避免丢日志?
A:统一 logger 抽象 + navigator.sendBeacon(页面卸载也能发)。批量 + 定时 flush,避免每次都发请求。日志落 localStorage 兜底,下次启动重发。
Q2:日志和监控有什么区别?
A:
- 监控:实时数值型指标(CPU、QPS、错误率),告诉你"有异常"
- 日志:详细事件记录(用户操作链路、错误堆栈),告诉你"为什么"
- 两者配合:监控发现 → 日志定位
Q3:怎么避免告警风暴?
A:① 告警合并(同一根因聚合);② 告警抑制(上游故障自动屏蔽下游);③ 告警静默(维护窗口关闭告警);④ 阈值合理(不要 1% 波动就告警)。
Q4:On-call 怎么轮值?
A:常用固定排班,工作时间 1 人值班、夜间周末 on-call 轮。配套:① Runbook(按图索骥);② 值班日历;③ 升级机制(P0 自动升级到主管);④ 复盘机制(每个事故写 post-mortem)。
Q5:状态页重要吗?
A:非常重要。出事时主动告诉用户「我们正在修复」,比让用户在 Twitter 上骂强一万倍。statuspage.io 是事实标准。