2. Console日志基础:从console.log到堆栈追踪

说起Node.js调试,大家最先接触的肯定是console。我刚开始写Node.js那会儿,console.log就是我的救命稻草。没有它,我可能连第一个接口都调不通。今天咱们就好好聊聊console家族的那些事儿。

2.1 console.log / console.error / console.warn:日志三兄弟

这三个方法,说白了就是往控制台输出信息。但它们的区别,可不仅仅是颜色不同。

核心区别:

  • console.log:标准输出,打印普通信息
  • console.error:错误输出,打印错误信息(stderr)
  • console.warn:警告输出,打印警告信息(stderr)

你可能会问:「不都是打印吗?有啥区别?」嗯,这里有个坑。我在生产环境排查问题时,就吃过这个亏。

// 普通日志
console.log('用户登录成功:', userId);

// 错误日志
console.error('数据库连接失败:', err.message);

// 警告日志
console.warn('接口响应时间超过阈值:', responseTime);

为什么要把error和warn输出到stderr?因为生产环境里,我们通常会把stdout和stderr分开重定向。比如:

node app.js >> app.log 2>> error.log

这样,普通日志和错误日志就分开了。我曾经见过一个团队,所有日志都用console.log,结果排查线上问题的时候,错误信息淹没在茫茫日志海里,找得人想哭。

我的习惯:

  • 业务逻辑用 console.log
  • 异常捕获用 console.error
  • 性能告警用 console.warn

2.2 console.time:性能测量的利器

「这段代码到底跑了多久?」——这是每个开发者都会问的问题。console.time就是专门干这个的。

// 开始计时
console.time('数据库查询');

// 模拟数据库查询
setTimeout(() => {
  // 结束计时
  console.timeEnd('数据库查询');
}, 1500);

// 输出:数据库查询: 1.502s

我个人习惯用console.time来快速验证性能假设。比如,我怀疑某个接口慢是因为数据库查询太慢,就会在查询前后加上time和timeEnd。

这里有个小技巧:你可以同时启动多个计时器,只要标签不同就行。

console.time('总耗时');
console.time('步骤1');

// 步骤1
setTimeout(() => {
  console.timeEnd('步骤1');  // 步骤1: 1.002s
  
  console.time('步骤2');
  // 步骤2
  setTimeout(() => {
    console.timeEnd('步骤2');  // 步骤2: 0.503s
    console.timeEnd('总耗时');  // 总耗时: 1.508s
  }, 500);
}, 1000);

注意:console.time和console.timeEnd的标签必须完全一致,否则会报错。我曾经手滑打错了标签,排查了半天才发现是这个问题。

2.3 console.trace:堆栈追踪的艺术

「这个函数是谁调用的?」——console.trace就是回答这个问题的。

说白了,console.trace会打印当前代码的执行堆栈。这在调试复杂调用链时特别有用。

function getUser(id) {
  console.trace('getUser被调用');
  return { id, name: '张三' };
}

function handleRequest(req) {
  const user = getUser(req.params.id);
  return user;
}

handleRequest({ params: { id: 1 } });

输出结果会显示完整的调用链:

Trace: getUser被调用
    at getUser (app.js:2:11)
    at handleRequest (app.js:7:20)
    at Object.<anonymous> (app.js:11:1)

你看,从getUser一路追溯到入口文件,清清楚楚。我在项目中遇到过一个问题:某个中间件莫名其妙地修改了请求对象。用console.trace一查,立马就找到了罪魁祸首。

什么时候用console.trace?

  • 函数被多次调用,想知道调用来源
  • 事件监听器被触发,想追踪触发链
  • 对象属性被意外修改,想找到修改者

2.4 格式化输出:让日志更清晰

console还支持类似C语言的格式化输出,这个功能很多人不知道。

const name = 'Node.js';
const version = '18.0.0';

// 格式化输出
console.log('当前环境:%s %s', name, version);
// 输出:当前环境:Node.js 18.0.0

// 对象格式化
const user = { id: 1, name: '张三' };
console.log('用户信息:%j', user);
// 输出:用户信息:{"id":1,"name":"张三"}

// 数字格式化
console.log('百分比:%d%%', 85);
// 输出:百分比:85%

格式化占位符一览:

占位符 说明 示例
%s 字符串 console.log('名称:%s', '张三')
%d 数字(整数) console.log('数量:%d', 100)
%i 数字(整数) console.log('ID:%i', 123)
%f 数字(浮点数) console.log('价格:%f', 19.99)
%j JSON对象 console.log('数据:%j', {a:1})
%o 对象(展开) console.log('详情:%o', obj)
%O 对象(完整展开) console.log('完整:%O', obj)

小建议:格式化输出在拼接复杂日志时特别好用。我一般会在记录请求参数时用%j,这样能保证JSON格式完整,不会因为特殊字符导致日志解析出错。

2.5 避坑指南:console的陷阱

用了这么多年console,我总结了几条血泪教训:

我曾经踩过的坑:

  • 不要在生产环境滥用console.log:console.log是同步操作,大量日志会阻塞事件循环。我见过一个项目因为日志太多,QPS直接掉了30%。
  • console.log是异步的?:嗯,这个问题有点复杂。在Node.js中,console.log在终端输出时是同步的,但在管道重定向时可能是异步的。别依赖它的执行顺序。
  • 对象引用问题:console.log打印对象时,显示的是引用。如果你在打印后修改了对象,控制台显示的可能不是打印时的状态。
// 坑:对象引用问题
const obj = { count: 1 };
console.log(obj);  // 显示:{ count: 2 } 而不是 { count: 1 }
obj.count = 2;

// 正确做法:打印快照
console.log(JSON.parse(JSON.stringify(obj)));

说白了,console是开发调试的好帮手,但别把它当成生产日志系统。生产环境还是得用专业的日志框架,比如winston、pino这些。不过那是后面章节的内容了,咱们先打好基础。

记住一句话:console.log是你的朋友,但不是你的全部。用好它,但别依赖它。