在JavaScript开发中,console.log()就像空气一样无处不在。几乎每个开发者都习惯性地在代码中插入各种console语句来调试程序。但当你面对一个复杂的项目时,这种调试方式很快就会变得力不从心。
我最近接手了一个遗留项目,代码库里有超过200个console.log调用。当需要追踪某个特定流程时,不得不在一大堆无关输出中寻找关键信息。更糟糕的是,有些console.log在生产环境忘记移除,导致用户浏览器控制台充斥着调试信息。
传统console.log的主要痛点:
优秀的日志工具应该支持不同级别的日志输出,比如:
javascript复制// 传统方式
console.log('用户登录开始');
console.log('获取用户数据...');
console.error('登录失败!');
// 现代日志工具
logger.debug('用户登录流程开始');
logger.info('获取用户数据', {userId: 123});
logger.warn('密码强度不足');
logger.error('登录失败', error);
现代日志工具会将日志信息结构化存储,通常包含:
bash复制# 传统console.log输出
用户数据加载完成 {name: "John", age: 30}
# 结构化日志
{
"timestamp": "2023-07-20T14:23:45.123Z",
"level": "INFO",
"message": "用户数据加载完成",
"context": "userService.js:42",
"data": {
"name": "John",
"age": 30
}
}
好的日志工具应该能:
Winston是Node.js生态最流行的日志库之一,特点包括:
安装与基础配置:
bash复制npm install winston
javascript复制const winston = require('winston');
const logger = winston.createLogger({
level: 'debug',
format: winston.format.json(),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'combined.log' })
]
});
logger.info('系统启动成功', { version: '1.0.0' });
Pino以极高性能著称,特别适合高并发场景:
基础使用示例:
javascript复制const pino = require('pino');
const logger = pino({
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
});
logger.info({ userId: 123 }, '用户登录成功');
对于浏览器端调试,loglevel是不错的选择:
使用示例:
javascript复制import log from 'loglevel';
log.setLevel('debug');
log.debug('调试信息');
log.info('常规信息');
log.warn('警告信息');
在微服务架构中,一个请求可能经过多个服务。为日志添加唯一请求ID可以帮助追踪完整调用链:
javascript复制// 使用cls-hooked实现请求上下文
const cls = require('cls-hooked');
const namespace = cls.createNamespace('app');
function logWithContext(message) {
const requestId = namespace.get('requestId');
logger.info({ requestId }, message);
}
// 中间件中设置请求ID
app.use((req, res, next) => {
namespace.run(() => {
namespace.set('requestId', uuid.v4());
next();
});
});
记录关键操作的执行时间:
javascript复制function timedLog(operationName) {
const start = process.hrtime();
return {
end: () => {
const diff = process.hrtime(start);
logger.debug(`${operationName} 耗时 ${diff[0]}秒 ${diff[1]/1e6}毫秒`);
}
};
}
const timer = timedLog('数据库查询');
// ...执行操作
timer.end();
防止意外记录密码等敏感信息:
javascript复制const sensitiveFields = ['password', 'token', 'creditCard'];
function sanitize(data) {
const result = {...data};
sensitiveFields.forEach(field => {
if(result[field]) result[field] = '***REDACTED***';
});
return result;
}
logger.info('用户数据', sanitize(user));
推荐方案:
javascript复制function track(event, data) {
const payload = {
event,
data,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent
};
// 使用sendBeacon确保可靠发送
navigator.sendBeacon('/api/log', JSON.stringify(payload));
}
// 防抖版本
const debouncedTrack = _.debounce(track, 1000);
ELK Stack经典方案:
现代替代方案:
防止团队成员继续使用console.log:
json复制{
"rules": {
"no-console": ["error", {
"allow": ["warn", "error"]
}]
}
}
使用jscodeshift自动化转换:
javascript复制// console-log-to-winston.js转换脚本
module.exports = function(fileInfo, api) {
const j = api.jscodeshift;
return j(fileInfo.source)
.find(j.CallExpression, {
callee: {
object: { name: 'console' },
property: { name: 'log' }
}
})
.replaceWith(p =>
j.callExpression(
j.identifier('logger.info'),
p.node.arguments
)
)
.toSource();
};
javascript复制// 好的日志示例
logger.error('支付处理失败', {
error: err.stack, // 完整错误堆栈
orderId: 12345, // 业务上下文
paymentMethod: 'creditCard',
// 自动过滤了cardNumber等敏感字段
});
// 不好的日志示例
console.log('出错啦!', err.message);
切换到专业日志工具初期可能需要一些适应,但一旦建立起完善的日志系统,你会发现调试效率确实能有数量级的提升。我在最近的项目中实测,通过结构化日志和级别过滤,定位问题的时间从平均30分钟缩短到了3分钟以内。