Node.js 生产级日志系统设计与 log4js 实战

Node.js 应用开发中,日志系统是排查问题、监控运行状态的核心设施。完善的日志系统需要支持分级、归档、染色、性能监控。这篇记录怎么用 log4js 搭建企业级的 Node.js 日志系统,包括配置设计、分级策略、日志轮转、性能优化这些环节。

日志系统架构设计

为什么需要专业的日志系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌─────────────────────────────────────────────────────────────────────┐
│ 生产环境日志系统架构 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 应用日志 │
│ │ │
│ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 控制台输出 │ │ 文件存储 │ │ 外部系统 │ │
│ │ (开发环境) │ │ (生产环境) │ │ (ELK/Sentry)│ │
│ └─────────────┘ └──────┬──────┘ └─────────────┘ │
│ │ │
│ ┌────────┴────────┐ │
│ ▼ ▼ │
│ ┌─────────┐ ┌──────────┐ │
│ │ 按级别分 │ │ 按日期分 │ │
│ │ debug │ │ 2024-01 │ │
│ │ info │ │ 2024-02 │ │
│ │ error │ │ 2024-03 │ │
│ └─────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
场景 需求 解决方案
开发调试 实时查看、颜色区分 Console Appender + chalk
生产排错 持久化、按级别分类 File Appender + 分级
审计追踪 长期保存、不可篡改 DateFile + 归档
异常监控 实时告警、聚合分析 对接 Sentry/ELK

log4js 基础配置

安装

1
2
npm install log4js
npm install moment chalk # 可选:用于日期格式化和控制台染色

核心配置结构

log4js 的配置分为两个核心概念:appenders(输出目标)和 categories(日志分类)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// logger.js
'use strict';

const log4js = require('log4js');

log4js.configure({
appenders: {
// 控制台输出
console: {
type: 'console'
},
// 按日期分割的调试日志
debugFile: {
type: 'dateFile',
filename: 'logs/debug-',
pattern: 'yyyy-MM-dd.log',
maxLogSize: 10 * 1000 * 1000, // 10MB
numBackups: 3,
alwaysIncludePattern: true
},
// 错误日志(单独文件)
errorFile: {
type: 'dateFile',
filename: 'logs/error-',
pattern: 'yyyy-MM-dd.log',
maxLogSize: 10 * 1000 * 1000,
numBackups: 3,
alwaysIncludePattern: true
}
},
categories: {
// 默认分类:输出到控制台,级别 info
default: {
appenders: ['console'],
level: 'info'
},
// 调试分类:输出到控制台 + 文件
debug: {
appenders: ['console', 'debugFile'],
level: 'debug'
},
// 错误分类:只输出错误到专用文件
error: {
appenders: ['errorFile'],
level: 'error'
}
}
});

module.exports = log4js;

Appender 类型详解

Appender 类型 用途 适用场景
console 输出到控制台 开发环境
file 写入单一文件 简单日志
dateFile 按日期分割文件 生产环境
stdout 标准输出 Docker 环境
stderr 标准错误 错误日志
1
2
3
4
5
6
7
8
9
10
11
// dateFile 配置参数详解
{
type: 'dateFile',
filename: 'logs/app-', // 文件名前缀
pattern: 'yyyy-MM-dd.log', // 日期格式后缀
maxLogSize: 10 * 1024 * 1024, // 单文件最大 10MB
numBackups: 7, // 保留 7 个历史文件
alwaysIncludePattern: true, // 文件名包含日期模式
compress: false, // 是否压缩历史文件
keepFileExt: true // 保持文件扩展名
}

日志分级与使用

日志级别说明

log4js 支持以下日志级别,从低到高:

1
TRACE < DEBUG < INFO < WARN < ERROR < FATAL
级别 数值 用途 示例
TRACE 5000 最详细的跟踪信息 函数入参、返回值
DEBUG 10000 调试信息 中间计算结果
INFO 20000 正常运行信息 请求处理完成
WARN 30000 警告信息 参数异常但可恢复
ERROR 40000 错误信息 数据库连接失败
FATAL 50000 致命错误 服务启动失败

自定义日志封装

在生产环境中,通常需要封装一层日志工具,增加时间戳、染色、动态级别控制等功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// utils/logger.js
'use strict';

const moment = require('moment');
const chalk = require('chalk');
const log4js = require('log4js');

// 配置 log4js
log4js.configure({
appenders: {
console: { type: 'console' },
debugFile: {
type: 'dateFile',
filename: 'logs/debug-',
pattern: 'yyyy-MM-dd.log',
alwaysIncludePattern: true
},
errorFile: {
type: 'dateFile',
filename: 'logs/error-',
pattern: 'yyyy-MM-dd.log',
alwaysIncludePattern: true
}
},
categories: {
default: { appenders: ['console'], level: 'info' },
debug: { appenders: ['console', 'debugFile'], level: 'debug' },
error: { appenders: ['console', 'errorFile'], level: 'error' }
}
});

// 日志级别常量
const LOG_LEVEL = {
OFF: -1,
DEBUG: 1,
INFO: 2,
WARN: 3,
ERROR: 4
};

// 从配置文件读取日志级别
const config = require('../config.js');
const currentLevel = config.log_level || LOG_LEVEL.DEBUG;

// 获取格式化时间
function now() {
return '[' + moment().format('YYYY-MM-DD HH:mm:ss.SSS') + ']';
}

// 获取 Logger 实例
const debugLogger = log4js.getLogger('debug');
const errorLogger = log4js.getLogger('error');

class Logger {
/**
* 调试日志(带黄色分隔线)
*/
static debug(msg, showLog = true) {
if (currentLevel === LOG_LEVEL.OFF || !showLog) return;
if (currentLevel <= LOG_LEVEL.DEBUG) {
debugLogger.info(chalk.cyan(now() + ' [DEBUG] ' + msg));
debugLogger.info(chalk.yellow(
'--------------------------------------------------------------------------------------'
));
}
}

/**
* 信息日志
*/
static info(msg) {
if (currentLevel === LOG_LEVEL.OFF) return;
if (currentLevel <= LOG_LEVEL.INFO) {
debugLogger.info(chalk.green(now() + ' [INFO] ' + msg));
}
}

/**
* 警告日志
*/
static warn(msg) {
if (currentLevel === LOG_LEVEL.OFF) return;
if (currentLevel <= LOG_LEVEL.WARN) {
debugLogger.warn(chalk.yellow(now() + ' [WARN] ' + msg));
}
}

/**
* 错误日志
*/
static error(msg, err) {
if (currentLevel === LOG_LEVEL.OFF) return;
if (currentLevel <= LOG_LEVEL.ERROR) {
const errorMsg = err ? `${msg}\n${err.stack}` : msg;
errorLogger.error(chalk.red(now() + ' [ERROR] ' + errorMsg));
}
}
}

module.exports = { Logger, LOG_LEVEL };

全局日志挂载

在应用入口文件中挂载全局日志对象:

1
2
3
4
5
6
7
8
9
10
// app.js
const { Logger } = require('./utils/logger.js');

// 挂载到全局
global.Log = Logger;

// 使用示例
Log.info('服务器启动成功,监听端口 3000');
Log.debug('用户登录请求参数:', { userId: 1001, token: 'xxx' });
Log.error('数据库连接失败', new Error('Connection timeout'));

高级配置与最佳实践

日志轮转与归档

生产环境需要配置日志自动轮转,防止磁盘占满:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 使用 pm2-logrotate 配合 log4js
// 或者使用 log4js 内置的 maxLogSize

log4js.configure({
appenders: {
app: {
type: 'dateFile',
filename: 'logs/application.log',
pattern: '.yyyy-MM-dd',
numBackups: 30, // 保留 30 天
compress: true, // 压缩历史日志
keepFileExt: true
}
},
categories: {
default: { appenders: ['app'], level: 'info' }
}
});

对接 PM2 集群模式

在 PM2 集群模式下,需要配置 disableClustering 或使用独立的日志文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ecosystem.config.js
module.exports = {
apps: [{
name: 'api-server',
script: './server.js',
instances: 'max',
exec_mode: 'cluster',
// 合并日志(避免每个进程一个文件)
merge_logs: true,
log_file: './logs/combined.log',
out_file: './logs/out.log',
error_file: './logs/error.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z'
}]
};

性能优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌─────────────────────────────────────────────────────────────────────┐
│ 日志系统性能优化策略 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 异步写入 │
│ • log4js 默认异步写入,不会阻塞主线程 │
│ • 避免在循环中频繁调用日志方法 │
│ │
│ 2. 级别控制 │
│ • 生产环境建议设置为 info 级别 │
│ • debug 日志使用 showLog 开关控制 │
│ │
│ 3. 批量写入 │
│ • 使用缓冲区批量写入磁盘 │
│ • 配置合理的 bufferSize(默认 0,立即写入) │
│ │
│ 4. 避免大对象序列化 │
│ • 不要直接打印整个请求对象或数据库结果集 │
│ • 只记录关键字段 │
│ │
└─────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
// 反例:打印大对象
Log.debug('用户数据:', userData); // userData 可能包含大量嵌套数据

// 正例:只记录关键字段
Log.debug('用户数据:', {
userId: userData.id,
name: userData.name,
loginTime: userData.loginTime
});

生产环境完整配置示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// config/logger.production.js
const path = require('path');

module.exports = {
appenders: {
// 访问日志
access: {
type: 'dateFile',
filename: path.join(__dirname, '../logs/access.log'),
pattern: '.yyyy-MM-dd',
numBackups: 30,
compress: true
},
// 应用日志
application: {
type: 'dateFile',
filename: path.join(__dirname, '../logs/app.log'),
pattern: '.yyyy-MM-dd',
numBackups: 30,
compress: true
},
// 错误日志
error: {
type: 'dateFile',
filename: path.join(__dirname, '../logs/error.log'),
pattern: '.yyyy-MM-dd',
numBackups: 90, // 错误日志保留更久
compress: true
},
// 控制台(Docker 环境使用 stdout)
console: {
type: 'stdout'
}
},
categories: {
default: {
appenders: ['console', 'application'],
level: 'info'
},
access: {
appenders: ['access'],
level: 'info'
},
error: {
appenders: ['console', 'error'],
level: 'error'
}
},
// PM2 集群模式需要
pm2: true,
pm2InstanceVar: 'INSTANCE_ID'
};

总结

Node.js 日志系统设计的一些经验:

  1. 分级管理:用 TRACE/DEBUG/INFO/WARN/ERROR/FATAL 六级日志,生产环境建议 info 级别
  2. 多目标输出:开发用控制台,生产用文件,错误单独归档
  3. 自动轮转:按日期或大小分割日志,配置压缩和保留策略
  4. 性能优化:异步写入、避免大对象序列化、使用缓冲区
  5. 集群兼容:PM2 集群模式下配置 merge_logs 或独立实例变量
  6. 全局挂载:在应用入口挂载全局 Log 对象,方便各模块调用

合理的日志系统设计能大幅提升问题排查效率,为应用稳定运行提供保障。