# log4js入门实例
30 秒速记
- 比
console.log多出来的东西:时间、级别、分类、输出位置,都能统一配置 - 三个核心概念:
level决定打不打,category标明谁打的,appender决定打到哪(控制台 / 文件 / 远端) - 和
Express整合用log4js.connectLogger()记录每个请求 - 版本差异大:
2.x之后删掉了logger.setLevel(),配置统一走log4js.configure();老文章的写法要按当前文档改 - 生产日志:结构化、带请求
id、敏感字段脱敏、文件要按天或大小轮转
log4js 解决的是 console.log 管不了的那些事:每条日志带时间和级别、能按模块分类、能按配置决定输出到哪里。 线上排查问题时,一条 console.log(err.message) 你连它几点打的、哪个模块打的都不知道;换成 log4js 后每条日志自带时间、级别和分类,配置里一改就能把 ERROR 单独写到一个文件、把 DEBUG 在生产关掉。要注意它的 API 在 2.x 前后变化很大,网上很多旧示例在新版本里已经跑不通了。
对于线上项目用来说,日志是非常重要的一环。log4js是使用得比较多的一个日志组件,经常跟Express一起配合使用。本文从入门实例开始,讲解log4js的使用,以及如何跟Express进行整合。
面试官追问
追问 1线上接口报错只打了 console.log(error.message),排查时缺什么?
缺时间、缺来源模块、缺调用栈、缺请求上下文。至少换成 logger.error(err) 把 stack 带上,再在日志里加请求 id,才能把一次请求的几条日志串起来。
追问 2项目有用户、订单、支付三个模块,日志分类怎么划?
按模块拿 logger:log4js.getLogger('order'),日志里就会带 [order],配置里还能让支付模块单独写一个文件。分到函数级别就太细了,配置会膨胀,没人维护。
追问 3服务跑在容器里,日志该写文件还是写 stdout?
容器里优先 stdout,由平台的日志采集统一收集,容器删了文件也就没了。非容器部署写文件就得配轮转,用 dateFile 或 file 的 maxLogSize + backups,不然迟早把磁盘写满。
追问 4logger.error(err) 和 logger.error(err.message) 有区别吗?
有。传 Error 对象,默认布局会把 stack 一起打出来;只传 message 就丢了调用栈,线上很难知道错误从哪抛出来的。
# 入门例子
30 秒速记
- 最小三步:
require('log4js')→getLogger()→logger.debug('hello world') - 输出格式:
[时间] [级别] [分类] - 消息,分类不传就是default - 时间、级别、分类都是
log4js补的,你只传了消息 - 新版本坑:
6.x起默认级别是OFF,不配置什么都不输出,要先logger.level = 'debug' - 消息支持
util.format占位符:logger.info('user %s login', name)
入门例子就是拿一个默认 logger 打一行日志,看到输出里自动带上了时间、级别和分类。 你只传了 'hello world',前面的 [2017-02-28 ...] [DEBUG] [default] 都是 log4js 按布局补上去的,default 是因为 getLogger() 没传分类名。这里有个版本坑要知道:老版本默认会输出,但 log4js 6.x 起默认分类的级别是 OFF,这段代码直接跑什么都看不到,要先写一句 logger.level = 'debug'。
输出日志如下,包括日志打印时间、日志级别、日志分类、日志内容。
// started.js
var log4js = require('log4js');
var logger = log4js.getLogger();
logger.debug('hello world');
// 输出:
// [2017-02-28 21:28:22.853] [DEBUG] [default] - hello world
面试官追问
追问 1照着教程写了 getLogger() 再 logger.debug('hello'),控制台什么都没有,为什么?
新版本里默认级别是 OFF,这是为了让库作者引入 log4js 时不会污染用户的输出。加一句 logger.level = 'debug',或者用 log4js.configure 配一个 default 分类和 stdout 输出。
追问 2输出里的时间、DEBUG、default 是从哪来的?
都是 log4js 的布局补上的,默认布局等价于 pattern 布局的 %d %p %c %m:时间、级别、分类、消息。改布局只影响输出格式,不影响你的调用代码。
追问 3想在日志里拼变量,logger.info('user ' + name) 和 logger.info('user %s', name) 选哪个?
选后者。占位符写法用的是 util.format,传对象时也能正确展开;级别被过滤掉时也省了一次字符串拼接。
追问 4监控同学想从这种文本日志里解析接口耗时,靠谱吗?
不靠谱,自由文本一改格式解析就崩。要统计就输出结构化日志,比如自定义一个 JSON 布局,把接口名、耗时、状态码作为独立字段,让日志平台直接按字段聚合。
# 日志级别
30 秒速记
- 级别从低到高:
TRACE<DEBUG<INFO<WARN<ERROR<FATAL,设了某一级,低于它的都不打 - 示例设
INFO:debug()被吞掉,info()和error()正常输出 - 过滤发生在
logger内部,低级别日志根本不会被格式化和写出 - 写法分版本:老版本
logger.setLevel('INFO'),2.x以后改成logger.level = 'info'或在configure的categories里配level - 一般开发用
DEBUG,生产用INFO,排障时临时下调
设置日志级别就是设一个门槛,只有大于等于这个级别的日志才会输出。 示例里门槛是 INFO,所以 debug() 那条直接被丢掉,info() 和 error() 能打出来。这个过滤是在 logger 内部做的,低于门槛的调用连格式化都不会走,所以生产环境放心留着 debug() 调用,开发时调低门槛就能看到。写法要注意版本,setLevel 是老 API,log4js 2.x 以后要写成 logger.level = 'info',或者在 configure 里按分类配。
logger.setLevel('INFO'); 表示想要打印的最低级别的日志是INFO,也就是说,调用类似logger.debug()等级别低于INFO的接口,日志是不会打印出来的。
var log4js = require('log4js');
var logger = log4js.getLogger();
logger.setLevel('INFO');
logger.debug('level: debug');
logger.info('level: info');
logger.error('level: error');
// 输出如下:
// [2017-02-28 21:50:45.372] [INFO] [default] - level: info
// [2017-02-28 21:50:45.376] [ERROR] [default] - level: error
面试官追问
追问 1级别设成 INFO 后,debug() 的日志是被打出来再被日志平台过滤掉的吗?
不是,在 logger 这一层就被拦下了,根本不会写到任何 appender。如果后面还有日志平台的过滤规则,那是另一层,和这里无关。
追问 2升级 log4js 后启动报 logger.setLevel is not a function,怎么改?
2.x 起删掉了这个方法。直接改成 logger.level = 'info';更推荐在 log4js.configure 的 categories 里写 default: { appenders: ['out'], level: 'info' },所有级别集中在配置里管。
追问 3线上出问题想临时看 DEBUG 日志,又不想重新发版,怎么做?
级别从环境变量读:logger.level = process.env.LOG_LEVEL || 'info',改环境变量重启就生效。要注意 DEBUG 日志量可能翻几倍,看完记得调回去,也要确认里面没打敏感信息。
追问 4构造日志参数本身很耗时,级别被关掉了还会执行吗?
会。logger.debug(JSON.stringify(bigObj)) 里的 JSON.stringify 在调用前就执行了。热点路径可以先判断:if (logger.isDebugEnabled()) logger.debug(...)。
