前端日志管理:如何结构化收集前端日志?
大家好,我是全栈老李。今天咱们聊聊前端开发中一个容易被忽视但极其重要的环节——日志管理。想象一下,线上突然报了个诡异bug,用户反馈”页面白屏了”,你打开开发者工具一看——啥都没有!这时候要是有一套完善的日志系统,是不是就能快速定位问题了?
为什么需要结构化日志?
传统的前端日志收集方式就像把东西随便扔进抽屉:console.log('用户点击了按钮')
、console.error('接口报错')
… 等真出了问题,面对海量杂乱无章的日志,找线索就像大海捞针。
结构化日志的核心思想是给日志穿上统一制服。就像超市商品都有条形码,我们的每条日志也应该有标准格式:
{
"timestamp": "2023-08-20T14:30:00Z",
"level": "ERROR",
"message": "支付接口调用失败",
"context": {
"userId": "12345",
"orderId": "67890",
"apiUrl": "/api/payment",
"error": {
"code": 500,
"message": "Internal Server Error"
}
},
"environment": "production",
"device": {
"browser": "Chrome/115.0",
"os": "Windows 10"
}
}
(代码注释:全栈老李提醒 – 这个JSON结构包含了日志的基本要素,可以根据项目需求扩展)
实战:手写一个日志收集器
光说不练假把式,下面咱们用TypeScript实现一个轻量级日志收集器:
// 日志级别类型
type LogLevel = 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'FATAL';
// 日志接口定义
interface StructuredLog {
timestamp: string;
level: LogLevel;
message: string;
context?: Record<string, any>;
stack?: string;
}
class Logger {
private readonly serviceName: string;
constructor(serviceName: string) {
this.serviceName = serviceName;
}
// 核心日志方法
private log(level: LogLevel, message: string, context?: object): StructuredLog {
const logEntry: StructuredLog = {
timestamp: new Date().toISOString(),
level,
message,
context: {
service: this.serviceName,
...context,
},
};
// 如果是错误级别,捕获调用栈
if (level === 'ERROR' || level === 'FATAL') {
logEntry.stack = new Error().stack;
}
// 实际项目中这里应该发送到日志服务器
this.sendToServer(logEntry);
// 开发环境同时在控制台输出
if (process.env.NODE_ENV === 'development') {
console[level.toLowerCase()](logEntry);
}
return logEntry;
}
// 发送到日志服务器(伪代码)
private sendToServer(log: StructuredLog): void {
// 全栈老李提示:实际项目中可以用fetch或axios
// 注意要处理网络错误和重试逻辑
navigator.sendBeacon?.('/log-collector', JSON.stringify(log));
}
// 快捷方法
debug(message: string, context?: object) {
return this.log('DEBUG', message, context);
}
info(message: string, context?: object) {
return this.log('INFO', message, context);
}
// ...其他级别方法类似
}
// 使用示例
const logger = new Logger('checkout-page');
logger.info('用户进入结算页', { userId: 'u123' });
(代码注释:全栈老李友情提示 – 生产环境记得添加日志采样和限流机制,避免日志洪水)
日志收集的三大场景
用户行为追踪
记录关键操作流,比如:
// 用户点击加入购物车
logger.info('ADD_TO_CART', {
productId: 'p789',
sku: 'red-xl',
price: 299
});
异常监控
用window.onerror捕获全局错误:
window.addEventListener('error', (event) => {
logger.error('UNCAUGHT_ERROR', {
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno
});
});
性能指标
通过Performance API收集:
const paintTiming = performance.getEntriesByType('paint');
logger.info('PERF_METRICS', {
firstPaint: paintTiming.find(e => e.name === 'first-paint')?.startTime,
fcp: paintTiming.find(e => e.name === 'first-contentful-paint')?.startTime
});
日志收集的进阶技巧
上下文关联
给每个请求添加唯一traceId,方便追踪完整链路:
// 生成唯一ID
const traceId = crypto.randomUUID();
// 所有相关日志带上这个ID
axios.interceptors.request.use(config => {
config.headers['X-Trace-Id'] = traceId;
return config;
});
敏感信息过滤
千万别把用户密码记到日志里!可以写个过滤器:
const sensitiveFields = ['password', 'token', 'creditCard'];
function sanitizeContext(context) {
return Object.fromEntries(
Object.entries(context).map(([key, value]) =>
[key, sensitiveFields.includes(key) ? '***' : value])
);
}
采样策略
高流量场景下全量日志太浪费,可以按比例采样:
function shouldSample(sampleRate = 0.1) {
return Math.random() < sampleRate;
}
课后作业:实现日志去重
面试题:实现一个带有去重功能的日志收集器,相同的错误在5分钟内只上报一次。要求:
interface DedupLogger {
log(error: Error): void;
}
// 示例用法:
const logger = createDedupLogger(5 * 60 * 1000); // 5分钟去重窗口
logger.log(new Error('API timeout')); // 上报
logger.log(new Error('API timeout')); // 5分钟内不上报
要求:在评论区留下你的实现代码,我会随机抽几位同学的答案进行点评哦~全栈老李会在下期专栏公布参考答案和详细解析!
最后说两句:日志就像前端应用的”黑匣子”,平时可能感觉不到它的存在,但关键时刻能救命。建议大家尽早把日志系统搭起来,别等线上出事了才后悔莫及。我是全栈老李,咱们下期见!
🔥 必看面试题
【初级】前端开发工程师面试100题(一) 【初级】前端开发工程师面试100题(二) 【初级】前端开发工程师的面试100题(速记版)
我是全栈老李,一个资深Coder!
写码不易,如果你觉得本文有收获,点赞 + 收藏走一波!感谢鼓励🌹🌹🌹
暂无评论内容