Skip to main content

日志与告警体系

没有日志 = 排查靠猜;没有告警 = 用户发现 bug 比你早。上线第一天就要搭好。

一、避坑指南

  1. 日志和监控是两件事

    • 监控 = 实时数值(错误率、QPS、CPU)
    • 日志 = 详细事件记录(某用户某时刻做了什么)
    • 监控告诉你「有异常」,日志告诉你「为什么
  2. 日志必须带 requestId

    // ❌ 没法串联
    { "msg": "用户登录失败" }
    { "msg": "查询订单失败" }

    // ✅ 一个 request 一条链
    { "requestId": "abc-123", "userId": "u1", "msg": "用户登录失败" }
    { "requestId": "abc-123", "userId": "u1", "msg": "查询订单失败" }
  3. 敏感字段必须脱敏

    // ❌ 直接打印
    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: "***",
    });
  4. 不要把日志写死

    // ❌ 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 });
  5. 告警要分级,否则全员麻木:

    P0 业务中断(支付失败、登录不可用)     → 电话 + 短信 + 钉钉
    P1 核心功能降级(部分用户报错) → 钉钉 @on-call
    P2 异常率超过阈值(错误率 1% → 3%) → 钉钉
    P3 性能下降(LCP 从 1.5s → 3s) → 邮件 / 看板
    P4 信息提示(新版本上线、流量峰值) → 看板
  6. 避免告警风暴

    触发: 数据库挂了 → 1000 个接口超时 → 1000 条告警
    解决: 用告警「合并 / 抑制 / 静默」机制

二、日志架构

2.1 前端日志 vs 后端日志

维度前端日志后端日志
采集方式navigator.sendBeaconstdout / 文件
落地位置日志服务(ES / ClickHouse)同左
重要价值用户行为还原、复现 bug服务端 trace、问题定位
主要问题网络丢包、性能开销磁盘爆、采集 agent 漏数据
典型工具自研 / SentryFilebeat / 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付费
OpsGenieAtlassian 生态付费
// 钉钉机器人示例
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 是事实标准。

七、相关文档