我们团队做的系统不是面向 C 端,而是一个带中后台的 B 端 SaaS 平台。用户不多,但业务复杂,页面交互、权限、多层组件嵌套,出了 bug 你靠一句 "你重试一下" 根本没用。
于是我们真的下功夫写了一个"日志系统"出来。
不是埋点,不是上报平台,是"调试用的日志" ,你平时 console.log()
的东西,全收编、格式化、可查、可过滤、可追踪、能自动上报。
这篇文章讲的是我们怎么一步一步把它做到今天这样:
👉背景
项目里日志是这样的:
ts
console.log('开始加载')
console.log(res.data)
console.log('执行完毕')
大家也都知道"上线不能打 console",但:
- 不打看不到数据,出问题无从下手;
- 打了又怕污染 console 或带上敏感数据;
- 测试环境 / 线上环境根本无法还原用户路径;
- 日志没有结构,过滤都做不到;
而且一个页面一旦包含 iframe、micro-frontend、甚至 webview,调试日志就像打进水里,一点回响都没有。
我们痛定思痛,真的重新设计了整个前端日志体系。
🧩 日志系统的设计目标
✅ 所有日志归一管理,不再满屏 console
✅ 每条日志带上下文(页面路径 / 用户 / 请求 ID)
✅ 支持日志分级(debug / info / warn / error)
✅ 开发时输出到控制台,线上时输出到远程服务(带缓冲)
✅ 支持动态开启日志过滤(调试指定用户 / 页面)
✅ 不影响性能,不干扰 UI 渲染
🔧 日志系统模块结构
我们把日志系统分成五个部分:
模块名 | 作用描述 |
---|---|
logger.ts |
日志核心:提供 debug/info/warn/error 方法 |
context.ts |
上下文注入:获取当前页面、用户、traceId 等 |
transport.ts |
日志发送器:控制本地输出还是上报服务端 |
filters.ts |
日志过滤器:比如调试指定用户、关键词 |
proxy-console.ts |
可选:接管 console.log() 重定向到 logger |
这五个模块都可以独立测试、替换,完全不是"封一层函数"那么简单。
🧱 核心实现结构
1️⃣ 日志结构标准化
ts
interface LogItem {
level: 'debug' | 'info' | 'warn' | 'error'
message: string
time: string
traceId?: string
userId?: string
route?: string
detail?: any
}
所有输出都走这个结构。
2️⃣ 核心输出入口 logger.ts
ts
import { getContext } from './context'
import { transport } from './transport'
function baseLog(level: LogItem['level'], msg: string, detail?: any) {
const ctx = getContext()
const item: LogItem = {
level,
message: msg,
time: new Date().toISOString(),
traceId: ctx.traceId,
userId: ctx.userId,
route: ctx.route,
detail,
}
transport(item)
}
export const logger = {
debug: (msg: string, detail?: any) => baseLog('debug', msg, detail),
info: (msg: string, detail?: any) => baseLog('info', msg, detail),
warn: (msg: string, detail?: any) => baseLog('warn', msg, detail),
error: (msg: string, detail?: any) => baseLog('error', msg, detail),
}
🧠 上下文注入模块(context)
这个模块统一获取日志需要的信息,支持你换掉 router、用户管理、TraceID 模式而不影响 logger。
ts
export function getContext() {
const route = window.location.pathname
const userId = localStorage.getItem('uid') || ''
const traceId = sessionStorage.getItem('trace_id') || generateTraceId()
return { route, userId, traceId }
}
每次刷新都会自动生成 traceId,调试的时候一查就知道是哪一批日志。
🚀 日志发送模块(transport)
ts
export function transport(item: LogItem) {
const isDev = import.meta.env.MODE === 'development'
if (isDev) {
console[item.level === 'error' ? 'error' : 'log']('[log]', item)
return
}
// 缓存后批量发送
logBuffer.push(item)
if (logBuffer.length >= 10) flushLogs()
}
function flushLogs() {
const payload = logBuffer.splice(0, logBuffer.length)
navigator.sendBeacon('/api/log', JSON.stringify(payload))
}
🔍 动态开启调试过滤(filters)
我们为了方便调试指定用户或页面,会加一个 window.__debug_filter__
:
ts
window.__debug_filter__ = {
userId: 'u12345',
route: '/dashboard',
includeLevel: ['warn', 'error']
}
在 transport 中判断,如果命中条件就输出,否则静默处理,调试体验超强。
📦 日志系统上线 2 个月后的真实收益
- 线上报错排查时间从半小时缩短到 5 分钟;
- 日志数据也作为我们用户使用路径分析的补充;
- 后端反馈请求 ID 后,我们能顺藤摸瓜还原整个日志链;
- 项目中的 console.log 基本为 0,日志系统真正变成了"工具链";
✍️ 感想
一个简单可控的日志系统,不仅能帮你调试 bug,更能撑起一个中型前端项目的可观测性、可靠性、可追溯性。