运行服务器端应用程序时,会发生很多事件,其中大多数事件都会写入日志文件中。日志文件可能是磁盘上文件的集合,也可能是数据库中的一系列记录。无论是哪一种情况,将诊断信息写入日志的做法都称为日志记录。
记录的事件包括:
数据库读取记录时记录的状态。
应用程序无法连接到数据库的时间。
有助于应用程序调试的信息。
日志将告诉我们发生了什么情况,当程序员诊断问题时,他们通常会先查看日志。虽然所有事件都会被记录下来,但只有最严重的系统事件才会广播到管理工具。
在本单元中,了解日志记录以及 Node 生态系统中两个最受欢迎的日志工具:Winston 和 Log4js。
日志记录所面临的难题
使用 SDK for Node.js? 创建一个 Cloud Foundry 应用
轻松开发、部署、扩展服务器端 JavaScript? 应用。IBM SDK for Node.js? 提供增强的性能、安全和服务。
将诊断信息写入控制台或日志文件并非难事。但困难的是,要在应用程序的整个生命周期内始终如一地这样做。
很遗憾,开发者对日志记录的用途有不同的设想,甚至有些人完全没有意识到日志记录的重要性。如果您多年来一直支持某个应用程序,您可能已经看到了后果:代码的某些部分被详尽记录,而其他部分则完全没有被记录。
当您需要调试应用程序时,不一致的日志记录(无论由何种原因导致)就会成为一个严重的问题。经验丰富的服务器端开发者通常都了解日志记录的价值,而前端开发者通常会忽略日志记录的价值。
选择出色的日志包
使用日志记录工具可提高日志记录的质量和一致性,幸运的是,Node 生态系统提供了许多出色的日志包以供您选择。选择的诀窍是您要了解自己的需求。
我的经验是,日志包必须是可配置的,这样您就可以进行定制。 其次,我希望获得以下必备功能:
应该支持多个日志通道,这样您就可以将日志发送到各种目标(控制台、文件、数据库等)。
应该支持灵活配置,以便支持可编程配置样式和外部配置样式。
应该支持多种日志优先级,因为某些类型的消息比其他类型的消息更重要。
应该支持不同的日志格式,这样程序员就可以进行配置和定制。
可以识别多个日志类别,因为不同的功能区域需要采用不同的方式进行记录。
本单元中介绍的两个日志包不仅具备所有上述功能,而且还具备其他功能。
Winston
Winston 是最受欢迎的日志包之一,每周下载近 200 万次。根据我在第 7 单元中介绍的三部分评分标准(质量、受欢迎程度和维护),Winston 的得分为 81(分别为 98/82/67)。
Winston 易于使用且极受欢迎,如果您是一名专业的 Node 开发者,您很可能会使用到 Winston。
Log4js
Log4js 是另一个受欢迎的日志包,每周下载超过 100 万次,三部分得分为 76(分别为 98/68/67)。
虽然它不像 Winston 那样受欢迎或广受好评,但 Log4js 具备我要从记录器中获得的所有功能。它还具有与 log4j 非常相似的外观,因此,对于从 Java 过渡到 Node.js 的开发者而言,它很受欢迎。
日志记录示例
在继续后面的学习之前,我们首先设置一些示例,然后通过这些示例来试用上面的两个记录器。可使用以下两种方法来访问源代码并运行本单元的示例:
浏览至
Course
存储库中的Unit-10
目录,然后运行示例。
2. 从一个空目录和一个空 package.json
文件开始。然后逐步创建并运行示例。
从终端访问源代码
如果选择第一个选项,需打开终端窗口或命令提示符,浏览至 Course
存储库中的 Unit-10
目录。在我的 Mac
上,完整路径为 ~/src/projects/IBM-Developer/Node.js/Course/Unit-10
。确保至少运行一次该目录中的 npm install
(但如果您遵循了每一节中的安装说明,那么肯定已经运行过)。
逐步创建源代码
如果您希望使用第二个选项,那么需要创建一个如下所示的 package.json
:
清单 1. 用于运行本单元中的示例的 package.json
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 | { "name": "unit-10", "version": "1.0.0", "main": "unit-10.js", "license": "Apache-2.0", "scripts": { "lint": "eslint .", "lint-fix": "npm run lint -- --fix", "pre-run": "npm run lint && mkdir -p logs", "example-1": "npm run pre-run && node ./example-1", "example-2": "npm run pre-run && node ./example-2", "example-3": "npm run pre-run && node ./example-3", "example-4": "npm run pre-run && node ./example-4", "example-5": "npm run pre-run && node ./example-5", "example-6": "npm run pre-run && node ./example-6", "example-7": "npm run pre-run && node ./example-7", "example-8": "npm run pre-run && node ./example-8", "example-9": "npm run pre-run && node ./example-9", "example-10": "npm run pre-run && node ./example-10", "example-11": "npm run pre-run && node ./example-11", "example-12": "npm run pre-run && node ./example-12", "example-13": "npm run pre-run && node ./example-13", "example-14": "npm run pre-run && node ./example-14", "example-15": "npm run pre-run && node ./example-15", "example-16": "npm run pre-run && node ./example-16", "example-17": "npm run pre-run && node ./example-17" }, "repository": { "type": "git", "url": "https://github.com/jstevenperry/node-modules" }, "devDependencies": { "babel-eslint": "^8.2.6", "eslint": "^5.2.0", "eslint-config-google": "^0.9.1" }, "eslintConfig": { "extends": [ "eslint:recommended", "google" ], "env": { "node": true }, "parserOptions": { "ecmaVersion": 6 }, "rules": { "max-len": [ 2, 120, 4, { "ignoreUrls": true } ], "spaced-comment": 0, "no-trailing-spaces": 0, "comma-dangle": 0, "object-curly-spacing": 0, "array-bracket-spacing": 0, "no-console": 0 } }, "eslintIgnore": [ "node_modules" ] } |
现在,运行 npm install
来安装 devDependencies
。
重要注意事项:务必将示例保存在 JavaScript 文件中,并且这些文件要与 package.json
所在目录中的示例编号相匹配。将 example-1
保存在名为 example-1.js
的文件中,将 example-2
保存在名为 example-2.js
的文件中,以此类推。
找到 linter
仔细查看清单 1,您可能会注意到,在每个示例运行之前,npm
脚本设置为对代码进行静态分析。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | { "name": "unit-10", "version": "1.0.0", . . "scripts": { "lint": "eslint .", "lint-fix": "npm run lint -- --fix", "pre-run": "npm run lint && mkdir -p logs", "example-1": "npm run pre-run && node ./example-1", "example-2": "npm run pre-run && node ./example-2", "example-3": "npm run pre-run && node ./example-3", . . } . . } |
在运行任何代码之前,我都会先对代码进行静态分析,以便尽快捕获潜在的错误。我建议大家都养成这种习惯。
运行 npm i winston
命令,安装 Winston。输出应如下所示(您安装的版本可能与我的版本不同):
1 2 3 4 5 6 | Ix:~/src/projects/IBM-Developer/Node.js/Course/Unit-10 sperry$ npm i winston npm WARN unit-10@1.0.0 No description + winston@3.0.0 added 194 packages from 214 contributors and audited 380 packages in 4.252s found 0 vulnerabilities |
在后续章节中,我将介绍 Winston 如何实现我在第三方日志包中推荐的各个核心功能。
Winston 中的多个日志通道(传输)
在 Winston 术语中,日志通道称为传输。内置传输的示例包括:
控制台
文件
HTTP
此外,在 Winston 项目外部还会维护一些传输 contrib 模块。
我将介绍 Winston 如何处理两个最常用的传输:控制台和文件。
控制台传输
顾名思义,控制台传输用于将记录器输出发送到控制台(也称为stdout
)。您将在记录器配置过程中创建控制台传输,然后调用 createLogger()
函数。然后,您将调用其中一个
Winston API 方法来记录消息。
清单 2 显示了用于记录消息的两种方法。
清单 2. Winston 控制台传输
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | const winston = require('winston'); // Logger configuration const logConfiguration = { 'transports': [ new winston.transports.Console() ] }; // Create the logger const logger = winston.createLogger(logConfiguration); // Log a message logger.log({ message: 'Hello, Winston!', level: 'info' }); // Log a message logger.info('Hello, Winston!'); |
要查看此示例的实际效果,可将以上代码保存到 example-1.js
,然后运行 npm run example-1
。运行 example-1
后,控制台输出如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | Ix:~/src/projects/IBM-Developer/Node.js/Course/Unit-10 sperry$ npm run example-1 > unit-10@1.0.0 example-1 /Users/sperry/home/development/projects/IBM-Developer/Node.js/Course/Unit-10 > npm run pre-run && node ./example-1 > unit-10@1.0.0 pre-run /Users/sperry/home/development/projects/IBM-Developer/Node.js/Course/Unit-10 > npm run lint && mkdir -p logs > unit-10@1.0.0 lint /Users/sperry/home/development/projects/IBM-Developer/Node.js/Course/Unit-10 > eslint . {"message":"Hello, Winston!","level":"info"} {"message":"Hello, Winston!","level":"info"} |
在控制台中进行日志记录的两种方法
注意以上输出中的最后两行。这两行完全相同,都显示了使用两种不同方法记录 example-1
中消息所产生的结果。
使用 Winston API 记录消息的第一种方法是使用 log()
函数,在其中将传递一个包含以下两个属性的对象:
message
:要记录的消息文本level
:消息级别,可为以下项之一:silly
debug
verbose
info
warn
error
暂时不必考虑这些级别,我们将在本单元后面的内容中讨论这些级别。
记录消息的第二种方法是使用与消息级别(info()
、warn()
等)相同的名称来调用 API
函数,然后只传递要记录的消息。
清单 2 显示了用于传递消息的两种方法。接下来,我将使用第二种方法,因为它更加简单。
文件传输
文件传输用于将输出发送到您在配置中指定的文件。只需在记录器配置过程中创建文件传输,并通过传递配置对象来调用 createLogger()
函数即可。要记录消息,可调用其中一个 Winston API 方法,如下所示:
清单 3. Winston 文件传输
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | const winston = require('winston'); // Logger configuration const logConfiguration = { 'transports': [ new winston.transports.File({ filename: './logs/example-2.log' }) ] }; // Create the logger const logger = winston.createLogger(logConfiguration); // Log a message logger.info('Hello, Winston!'); |
要查看此示例的实际效果,可将以上代码保存到 example-2.js
,然后运行 npm run example-2
。运行 example-2
后,控制台输出如下所示:
1 2 3 4 5 6 | Ix:~/src/projects/IBM-Developer/Node.js/Course/Unit-10 sperry$ npm run example-2 . . . > unit-10@1.0.0 lint /Users/sperry/home/development/projects/IBM-Developer/Node.js/Course/Unit-10 > eslint . |
不存在与日志记录相关的控制台输出,因为文件传输会将输出发送到文件(在此例中为 ./logs/example-2.log
):
1 | {"message":"Hello, Winston!","level":"info"} |
多个 Winston 传输
应用程序经常会将相同的日志输出发送到多个目标(也称为传输),如清单 4 所示。
清单 4. 同一个配置对象中的多个 Winston 传输
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 | const winston = require('winston'); // Logger configuration const logConfiguration = { transports: [ new winston.transports.Console({ level: 'verbose' }), new winston.transports.File({ level: 'error', filename: './logs/example-3.log' }) ] }; // Create the logger const logger = winston.createLogger(logConfiguration); // Log some messages logger.silly('Trace message, Winston!'); logger.debug('Debug message, Winston!'); logger.verbose('A bit more info, Winston!'); logger.info('Hello, Winston!'); logger.warn('Heads up, Winston!'); logger.error('Danger, Winston!'); |
注意,控制台传输和文件传输都会添加到配置对象中的 transports
数组中。
要查看此示例的实际效果,可将以上代码保存到 example-3.js
,然后运行 npm run example-3
。运行 example-3
后,控制台输出如下所示:
1 2 3 4 5 6 7 8 | Ix:~/src/projects/IBM-Developer/Node.js/Course/Unit-10 sperry$ npm run example-3 . . . {"message":"A bit more info, Winston!","level":"verbose"} {"message":"Hello, Winston!","level":"info"} {"message":"Heads up, Winston!","level":"warn"} {"message":"Danger, Winston!","level":"error"} |
控制台传输的 level
属性已设置为 verbose
,因此并非所有日志语句都会出现在控制台输出中。此外,文件传输的 level
属性已设置为 error
,因此只有一个日志语句出现在日志文件中。./logs/example-3.log
的内容如下所示:
1 2 | Ix:/tmp/IBM-Developer/Node.js/Course/Unit-10 sperry$ cat logs/example-3.log {"message":"Danger, Winston!","level":"error"} |
Ix:/tmp/IBM-Developer/Node.js/Course/Unit-10 sperry$ cat logs/example-3.log
{"message":"Danger, Winston!","level":"error"}
您很快就会更深入地了解级别。
Winston 中的灵活配置
在到目前为止的示例中,Winston 配置对象与使用它的记录器位于同一个模块中,但这不是强制要求。要从记录器中获得的一项功能是能够在使用它的代码外部进行配置。Winston 具备这项功能。
以下列配置为例。
清单 5. ./config/app-settings.js 中的外部配置
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 | const winston = require('winston'); const appSettings = { winston: { sillyLogConfig: { level: 'silly', transports: [ new winston.transports.File({ filename: './logs/silly.log' }), new winston.transports.Console() ] }, }, log4js: { traceLogConfig: { appenders: { fileAppender: { type: 'file', filename: './logs/trace.log'}, consoleAppender: { type: 'console' } }, categories: { default: { appenders: ['fileAppender', 'consoleAppender'], level: 'trace'} } } } }; module.exports = appSettings; |
如果要逐步创建源代码,那么需要创建一个名为 config
的目录,该目录是 package.json
所在位置的下级目录。然后,您将创建另一个名为 app-settings.js
的文件。将这两者粘贴到清单 5 的内容中。
要使用此外部配置,可以像使用任何其他 Node 模块一样使用 require()
,并将 sillyLogConfig
传递给
Winston 的 createLogger()
函数,如下所示。
清单 6. ./config/app-settings.js
中的外部配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | const winston = require('winston'); // App settings const { sillyLogConfig } = require('./config/app-settings').winston; // Create the logger const logger = winston.createLogger(sillyLogConfig); // Log some messages logger.silly('Trace message, Winston!'); logger.debug('Debug message, Winston!'); logger.verbose('A bit more info, Winston!'); logger.info('Hello, Winston!'); logger.warn('Heads up, Winston!'); logger.error('Danger, Winston!'); |
要查看此代码的实际效果,可将以上代码保存到 example-4.js
,然后运行 npm run example-4
。
运行 example-4
后,控制台输出如下所示:
1 2 3 4 5 6 7 8 9 10 | Ix:~/src/projects/IBM-Developer/Node.js/Course/Unit-10 sperry$ npm run example-4 . . . {"message":"Trace message, Winston!","level":"silly"} {"message":"Debug message, Winston!","level":"debug"} {"message":"A bit more info, Winston!","level":"verbose"} {"message":"Hello, Winston!","level":"info"} {"message":"Heads up, Winston!","level":"warn"} {"message":"Danger, Winston!","level":"error"} |
文件输出将发送到 ./logs/silly.log
,并且与控制台输出类似:
1 2 3 4 5 6 7 | Ix:/tmp/IBM-Developer/Node.js/Course/Unit-10 sperry$ cat logs/silly.log {"message":"Trace message, Winston!","level":"silly"} {"message":"Debug message, Winston!","level":"debug"} {"message":"A bit more info, Winston!","level":"verbose"} {"message":"Hello, Winston!","level":"info"} {"message":"Heads up, Winston!","level":"warn"} {"message":"Danger, Winston!","level":"error"} |
注意:在撰写本文时,Winston 配置对象必须是 JavaScript 对象。例如,您目前无法使用纯 JSON 来配置 Winston。但是,已发布了此问题,并且 Winston 正在积极维护中,敬请期待!
Winston 中的多种日志优先级
要从生产质量日志包中获得的另一项功能是能够支持多种日志优先级(也称为级别)。您已经在 example-3
和 example-4
中看到了这一点。现在,我们将更深入地了解 Winston 对日志级别的支持。
Winston 按照 RFC5424 的设计来定义日志级别,如清单 7 所示。
清单 7. Winston 的标准日志级别
1 2 3 4 5 6 7 8 | const levels = { error: 0, warn: 1, info: 2, verbose: 3, debug: 4, silly: 5 }; |
根据这一标准,将按反向数字顺序来定义级别。也就是说,消息越重要,优先级数字就越小。例如,error
消息比 debug
消息更重要,因此错误消息的优先级数字更小。
在我看来,这种顺序完全是颠倒了。虽然我赞同 Winston 应至少遵循一种标准,但我怀疑 RFC 5424 的日志级别定义是基于预先存在的实现 linux 内核日志优先级,而不是直观地诠释优先级的含义。
在创建自定义日志级别(我们很快就会这样做)时,这些信息将非常重要。
覆盖优先级级别
在清单 8 中,我们将创建一个 Winston 记录器,然后覆盖它的级别,这样我们就可以看到它的所有消息。
清单 8. 查看所有可用级别的消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | const winston = require('winston'); // Logger configuration const logConfiguration = { transports: [ new winston.transports.Console({ level: 'error' }) ] }; // Create the logger const logger = winston.createLogger(logConfiguration); // Let's see ALL the log levels logger.transports[0].level = 'silly'; // Log some messages logger.silly('Trace message, Winston!'); logger.debug('Debug message, Winston!'); logger.verbose('A bit more info, Winston!'); logger.info('Hello, Winston!'); logger.warn('Heads up, Winston!'); logger.error('Danger, Winston!'); |
注意,在此例中,只有一个控制台传输。
要查看此示例的实际效果,可将以上代码保存到 example-5.js
,然后运行 npm run example-5
。运行 example-5
后,控制台输出如下所示:
1 2 3 4 5 6 7 8 9 10 | Ix:~/src/projects/IBM-Developer/Node.js/Course/Unit-10 sperry$ npm run example-5 . . . {"message":"Trace message, Winston!","level":"silly"} {"message":"Debug message, Winston!","level":"debug"} {"message":"A bit more info, Winston!","level":"verbose"} {"message":"Hello, Winston!","level":"info"} {"message":"Heads up, Winston!","level":"warn"} {"message":"Danger, Winston!","level":"error"} |
自定义日志级别
正如我所指出,我们可以用自定义日志级别替换 Winston 的标准日志级别,如下所示。
清单 9. 在 Winston 中定义自定义日志级别
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 | 01 const winston = require('winston'); 02 03 // Custom log levels 04 const noteLevels = { 05 values: { 06 doe: 10, 07 ray: 20, 08 me: 30, 09 far: 40, 10 sew: 50, 11 la: 60, 12 tea: 70 13 } 14 }; 15 16 // Logger configuration 17 const logConfiguration = { 18 level: 'far', 19 levels: noteLevels.values, 20 transports: [ 21 new winston.transports.Console() 22 ] 23 }; 24 25 // Create the logger 26 const logger = winston.createLogger(logConfiguration); 27 28 /** 29 * 30 */ 31 function doLogging() { 32 // Log some messages 33 logger.tea('Tea, Winston!'); 34 logger.la('La, Winston!'); 35 logger.sew('Sew, Winston!'); 36 logger.far('Far, Winston!'); 37 logger.me('Me, Winston!'); 38 logger.ray('Ray, Winston!'); 39 logger.doe('Doe, Winston!'); 40 } 41 42 // Do some logging as the logger was setup 43 logger.doe(`Logging messages, current log level: ${logger.level}`); 44 doLogging(); 45 46 // Now modify the level 47 logger.level = 'tea'; 48 logger.doe(`Logging messages, current log level: ${logger.level}`); 49 doLogging(); 50 51 try { 52 logger.info('The previously used log methods no longer work!'); 53 } catch (err) { 54 logger.doe(`${err.message}`); 55 } |
要查看此示例的实际效果,可将以上代码保存到 example-6.js
(我先移除了行号),然后运行 npm run
example-6
。输出如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | Ix:~/src/projects/IBM-Developer/Node.js/Course/Unit-10 sperry$ npm run example-6 . . . {"message":"Logging messages, current log level: far","level":"doe"} {"message":"Far, Winston!","level":"far"} {"message":"Me, Winston!","level":"me"} {"message":"Ray, Winston!","level":"ray"} {"message":"Doe, Winston!","level":"doe"} {"message":"Logging messages, current log level: tea","level":"doe"} {"message":"Tea, Winston!","level":"tea"} {"message":"La, Winston!","level":"la"} {"message":"Sew, Winston!","level":"sew"} {"message":"Far, Winston!","level":"far"} {"message":"Me, Winston!","level":"me"} {"message":"Ray, Winston!","level":"ray"} {"message":"Doe, Winston!","level":"doe"} {"message":"logger.info is not a function","level":"doe"} |
关于 Winston 代码的注释
我们来逐行查看上面的代码。
首先,在第 4-14 行中定义自定义级别。
接下来,在第 17-23 行中创建 Winston 配置对象。特别注意第 19 行,在该行中设置了新定义的级别。这些级别将替换清单 7 中所示的默认级别。
另需注意,doLogging()
函数(第 31 行)被调用了两次:
第一次是在第 44 行中,目的是使用在创建配置对象时设置的日志级别进行日志记录。
第二次是在第 49 行中,在日志级别设置为
tea
(第 47 行)之后,此时将显示所有级别。
最后,当您定义自己的日志级别并使用它们来创建 Winston 配置对象时,默认级别便不再适用。实际上,如果您尝试使用默认级别,那么 Winston 将会报错;参阅第 51-55 行并注意最后一条日志消息:
1 | {"message":"logger.info is not a function","level":"doe"} |
在 Winston 中进行格式化
生产质量记录器应允许您对所记录的消息进行格式化。
在到目前为止的示例中,都采用 JSON 格式记录消息,但是 Winston 提供了许多其他的日志消息格式。下面介绍了其中两种格式:printf 和 splat。
在开始探讨这些格式之前,务必了解 Winston 的格式框架。首先要记住的是,Winston 消息是以 info
对象为中心,该对象具有以下两个属性:level
和 message
。
您通过 createLogger()
创建的每个记录器实例都有一个 format
属性,您可以将此属性设置为要使用的格式化程序类型。Winston 附带了几个内置的格式化程序,您可以组合使用这些格式化程序来处理所需的任何日志消息格式。
使用 printf 进行格式化
清单 10. 打印样式的格式化
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 | 01 const winston = require('winston'); 02 03 // The Logger Category (functional area) 04 const CATEGORY = 'example-7'; 05 06 // Logger configuration 07 const logConfiguration = { 08 transports: [ 09 new winston.transports.Console() 10 ], 11 format: winston.format.combine( 12 winston.format.label({ 13 label: CATEGORY 14 }), 15 winston.format.timestamp(), 16 winston.format.printf((info) => { 17 return `${info.timestamp} - ${info.label}:[${info.level}]: ${info.message}`; 18 }) 19 ) 20 }; 21 22 // Create the logger 23 const logger = winston.createLogger(logConfiguration); 24 25 logger.info('Howdy, Winston!'); |
此示例中的格式化程序是在第 11-19 行中创建的,它是 Winston 内置的 label()
(第 12-14
行)、timestamp()
(第 15 行)和 printf()
(第 16-18 行)格式化程序的组合形式。
另需注意,第 17 行用于控制所记录的消息:
1 | return `${info.timestamp} - ${info.label}:[${info.level}]: ${info.message}`; |
如果与这些字段匹配,那么可以准确地了解消息的计算方式:
info.timestamp
来自第 15 行。info.label
来自第 12-14 行。info.level
是info
对象中的日志级别。info.message
是要记录的消息文本(第 25 行)。
要查看此示例的实际效果,可将以上代码保存到 example-7.js
。首先去掉行号,然后运行 npm run
example-7
。控制台输出如下所示:
1 2 3 4 5 | Ix:~/src/projects/IBM-Developer/Node.js/Course/Unit-10 sperry$ npm run example-7 . . . 2018-07-26T01:22:09.087Z - example-7:[info]: Howdy, Winston! |
使用 splat 和 simple 进行格式化
如果要在调用日志函数时控制格式化权限,那么可以组合使用 splat
和 simple
格式化程序。
splat()
根据 Node 的util.format()
函数来对level.message
进行格式化。simple()
将返回level.info
以及level.message
。
清单 11. 使用 splat
和 simple
进行格式化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | const winston = require('winston'); // The Logger Category (functional area) const CATEGORY = 'example-8'; // Logger configuration const logConfiguration = { transports: [ new winston.transports.Console() ], format: winston.format.combine( winston.format.splat(), winston.format.simple() ) }; // Create the logger const logger = winston.createLogger(logConfiguration); logger.warn('%d - %s: Howdy, Winston!', Date.now(), CATEGORY); |
要查看此示例的实际效果,可将以上代码保存到 example-8.js
,然后运行 npm run
example-8
。控制台输出如下所示:
1 2 3 4 5 | Ix:~/src/projects/IBM-Developer/Node.js/Course/Unit-10 sperry$ npm run example-8 . . . warn: 1532569853985 - example-8: Howdy, Winston! |
Winston 还提供了一些其他内置格式化程序,您可通过阅读 WinstonJS 文档和 Winston logform 文档来了解这些格式化程序。
注意: 在此例中,将在 warn
(即 level.info
的值)级别记录消息。info info
对象与 info
级别之间的名称冲突很容易引起混淆,但您在通读了 info
对象文档后就会豁然开朗。如果要开始使用 Winston,我建议您花一些时间来通读该文档。
Winston 中的日志类别
要从 Winston 中获得的最后一项功能是能够根据应用程序的功能区域来对记录器实例进行分段。Winston 通过"容器"这个概念来提供了这项功能。
考虑下面的示例,并注意我已移除了注释来节省空间。
首先,利用一个函数来创建 Winston 配置,如清单 12 所示。
清单 12. 创建 Winston 配置
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 | 01 const winston = require('winston'); 02 03 const DEFAULT_CATEGORY = 'DEFAULT'; 04 05 function createLoggerConfig(category) { 06 return { 07 level: 'info', 08 transports: [ 09 new winston.transports.Console() 10 ], 11 format: winston.format.combine( 12 winston.format.label({ 13 label: category 14 }), 15 winston.format.timestamp(), 16 winston.format.printf((info) => { 17 return `${info.timestamp} - ${info.label}:[${info.level}]: ${info.message}`; 18 }) 19 ) 20 }; 21 } 22 winston.loggers.add(DEFAULT_CATEGORY, createLoggerConfig(DEFAULT_CATEGORY)); 23 24 module.exports.defaultLogger = winston.loggers.get(DEFAULT_CATEGORY); 25 26 module.exports.createLoggerConfig = createLoggerConfig; |
如果要手动创建源代码,需去掉行号,然后将这个清单保存到 ./config/winston-config.js
中。另外,务必向 createLoggerConfig()
函数中添加一个 JSDoc 注释,否则 Linter 将会报错。(如果要避免 Linter 报错,可以禁用 package.json
中的静态分析规则,但我将该选项留作读者的练习。)
清单 12 中的模块定义了一个默认类别(第 3 行),并将一个记录器添加到 Winston 的内部 Container
存储器中(第 22
行),然后将其导出为 defaultLogger
(第 24 行)。另外,还导出了 createLoggerConfig()
函数(第
5-21 行),因此可以按如下所示来调用该函数:
清单 13. 日志类别
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 | 01 const winstonConfig = require('./config/winston-config'); 02 03 const winston = require('winston'); 04 05 let defaultLogger = winstonConfig.defaultLogger; 06 defaultLogger.info('Beginning program execution...'); 07 08 const MODULE = 'example-9'; 09 10 winston.loggers.add(MODULE, winstonConfig.createLoggerConfig(MODULE)); 11 const moduleLogger = winston.loggers.get(MODULE); 12 13 moduleLogger.info('Howdy, Winston!'); 14 moduleLogger.warn('WARNING, Winston!'); 15 moduleLogger.error('ERROR, Winston!'); 16 17 const FOO = 'foo'; 18 const BAR = 'bar'; 19 20 winston.loggers.add(FOO, winstonConfig.createLoggerConfig(FOO)); 21 winston.loggers.add(BAR, winstonConfig.createLoggerConfig(BAR)); 22 23 const fooLogger = winston.loggers.get(FOO); 24 fooLogger.info('Howdy, Winston!'); 25 26 const barLogger = winston.loggers.get(BAR); 27 barLogger.info('Howdy, Winston!'); 28 29 defaultLogger.info('Program terminated.'); |
关于代码的注释
在这个简单的示例中执行了很多操作,我们来看一看。
首先,使用默认记录器(第 5-6 行,它是从 ./config/winston-config.js
导出的)来输出一条消息。
接下来,定义新记录器类别的配置(第 8 行),并将其添加到 Winston 的 Container 存储器中(第 10 行),然后检索一个记录器对象(第 11
行)。此记录器特定于此模块 (example-9
),用于记录一些消息。
然后,创建并添加两个新类别配置(第 20-21 行),并采用相同的方式检索它们的记录器实例(第 23 行和第 26 行)。
要查看此示例的实际效果,可将以上代码保存到 example-9.js
(别忘了先去掉行号),然后运行 npm run
example-9
。控制台输出如下所示:
1 2 3 4 5 6 7 8 9 10 11 | Ix:~/src/projects/IBM-Developer/Node.js/Course/Unit-10 sperry$ npm run example-9 . . . 2018-07-26T02:10:19.402Z - DEFAULT:[info]: Beginning program execution... 2018-07-26T02:10:19.404Z - example-9:[info]: Howdy, Winston! 2018-07-26T02:10:19.404Z - example-9:[warn]: WARNING, Winston! 2018-07-26T02:10:19.404Z - example-9:[error]: ERROR, Winston! 2018-07-26T02:10:19.405Z - foo:[info]: Howdy, Winston! 2018-07-26T02:10:19.405Z - bar:[info]: Howdy, Winston! 2018-07-26T02:10:19.405Z - DEFAULT:[info]: Program terminated. |
注意,该类别显示为 label
,因为它就是这样配置的(参阅清单 12 中的第 13 行)。
以上就是对 Winston 的介绍(至少对于本单元来说是这样)。接下来,您将安装并试用 Log4js。
运行 npm i log4js
命令,安装 Log4js。输出应如下所示(您安装的版本可能与我的版本不同):
1 2 3 4 5 6 | Ix:~/src/projects/IBM-Developer/Node.js/Course/Unit-10 sperry$ npm i log4js npm WARN unit-10@1.0.0 No description + log4js@3.0.2 added 4 packages from 2 contributors and audited 400 packages in 1.916s found 0 vulnerabilities |
在下一节中,我们将了解 Log4js 如何实现要从日志包中获得的所有五个核心功能。
Log4js 中的多个日志通道(附加程序)
在 Log4js 术语中,日志通道称为附加程序。内置附加程序的示例包括:
控制台
文件
tcp
要了解更多附加程序,可参阅列表。
重申一下,我们将了解两个最常用的附加程序:控制台和文件。
控制台附加程序
顾名思义,控制台附加程序用于将记录器输出发送到控制台。Log4js 具有一种内置配置,其中包含一个控制台附加程序,因此您不需要显式添加控制台附加程序。通过调用 getLogger()
从 Log4js 请求一个记录器实例。确保设置了日志级别,然后通过调用其中一个 Log4js API 方法来记录消息:
清单 14. Log4js 控制台附加程序
1 2 3 4 5 6 7 8 | const log4js = require('log4js'); // Create the logger const logger = log4js.getLogger(); logger.level = 'info'; // Log a message logger.info('Hello, log4js!'); |
注意:默认情况下,日志级别为 OFF
。您必须设置记录器级别,否则不会记录任何内容。
要查看此示例的实际效果,可将以上代码保存到 example-10.js
,然后运行 npm run example-10
。运行 example-10
后,输出如下所示:
1 2 3 4 5 | Ix:~/src/projects/IBM-Developer/Node.js/Course/Unit-10 sperry$ npm run example-10 . . . [2018-07-25T22:06:00.154] [INFO] default - Hello, log4js! |
文件附加程序
文件附加程序用于将输出发送到您在配置中指定的文件。
清单 15. Log4js 文件传输
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | const log4js = require('log4js'); // Logger configuration log4js.configure({ appenders: { fileAppender: { type: 'file', filename: './logs/example-11.log' } }, categories: { default: { appenders: ['fileAppender'], level: 'info' } } }); // Create the logger const logger = log4js.getLogger(); logger.level = 'info'; // Log a message logger.info('Hello, log4js!'); |
注意对 log4js.configure()
的额外调用,如果要覆盖默认控制台附加程序以添加您自己的控制台附加程序,那么必须进行此调用。
要查看此示例的实际效果,可将以上代码保存到 example11.js
,然后运行 npm run
example-11
。控制台输出如下所示:
1 2 3 4 5 6 | x:~/src/projects/IBM-Developer/Node.js/Course/Unit-10 sperry$ npm run example-11 . . . > unit-10@1.0.0 lint /Users/sperry/home/development/projects/IBM-Developer/Node.js/Course/Unit-10 > eslint . |
不存在与日志记录相关的控制台输出,因为文件附加程序会将输出发送到文件。在此例中,文件为 ./logs/example-11.log
,其内容如下所示:
1 2 | Ix:/tmp/IBM-Developer/Node.js/Course/Unit-10 sperry$ cat logs/example-11.log [2018-07-26T21:52:29.801] [INFO] default - Hello, log4js! |
多个传输
应用程序经常会将同一个日志输出发送到多个目标。可以向 Log4js 配置对象添加多个附加程序,如下所示:
清单 16. 同一配置中的多个 Log4js 附加程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | const log4js = require('log4js'); // Logger configuration log4js.configure({ appenders: { fileAppender: { type: 'file', filename: './logs/example-12.log' }, console: { type: 'console' } }, categories: { default: { appenders: ['fileAppender', 'console'], level: 'error' } } }); // Create the logger const logger = log4js.getLogger(); // Log a message logger.trace('Trace, log4js!'); logger.debug('Debug, log4js!'); logger.info('Hello, log4js!'); logger.warn('Heads up, log4js!'); logger.error('Danger, log4js!'); logger.fatal('Fatal, log4js!'); |
要查看此示例的实际效果,可将以上代码保存到 example-12.js
,然后运行 npm run
example-12
。控制台输出如下所示:
1 2 3 4 5 6 | Ix:~/src/projects/IBM-Developer/Node.js/Course/Unit-10 sperry$ npm run example-12 . . . [2018-07-26T21:56:24.196] [ERROR] default - Danger, log4js! [2018-07-26T21:56:24.201] [FATAL] default - Fatal, log4js! |
控制台附加程序的 level
属性已设置为 info
(默认值),因此并非 example-12
中的所有日志语句都会出现在控制台输出中。
文件传输的 level
属性已设置为 error
,因此只有一个日志语句出现在日志文件中。./logs/example-12.log
的内容类似于控制台输出:
1 2 3 | Ix:/tmp/IBM-Developer/Node.js/Course/Unit-10 sperry$ cat logs/example-12.log [2018-07-26T21:56:24.196] [ERROR] default - Danger, log4js! [2018-07-26T21:56:24.201] [FATAL] default - Fatal, log4js! |
Log4js 中的灵活配置
在到目前为止的示例中,Log4js 配置对象与使用它的记录器位于同一个模块中,但这不是强制要求。与 Winston 一样,Log4js 可以在使用它的代码外部进行配置。
回顾一下清单 5 中 ./config/app-settings.js
内的以下配置代码片段:
清单 17. ./config/app-settings.js
中的 Log4js 外部配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | const appSettings = { . . log4js: { traceLogConfig: { appenders: { fileAppender: { type: 'file', filename: './logs/trace.log'}, consoleAppender: { type: 'console' } }, categories: { default: { appenders: ['fileAppender', 'consoleAppender'], level: 'trace'} } } } }; module.exports = appSettings; |
对于此外部配置,可以像使用任何其他 Node 模块一样使用 require()
,并将 traceLogConfig
传递给
Log4j 的 configure()
函数,如下所示:
清单 18. ./config/app-settings.js
中的外部配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | const log4js = require('Log4js'); // App settings const { traceLogConfig } = require('./config/app-settings').log4js; // Logger configuration log4js.configure(traceLogConfig); // Create the logger const logger = log4js.getLogger(); // Log a message logger.trace('Trace, Log4js!'); logger.debug('Debug, Log4js!'); logger.info('Hello, Log4js!'); logger.warn('Heads up, Log4js!'); logger.error('Danger, Log4js!'); logger.fatal('Fatal, Log4js!'); |
要查看此代码的实际效果,可将以上代码保存到 example-13.js
,然后运行 npm run
example-13
。控制台输出如下所示:
1 2 3 4 5 6 7 8 9 10 | Ix:~/src/projects/IBM-Developer/Node.js/Course/Unit-10 sperry$ npm run example-13 . . . [2018-07-26T21:58:28.084] [TRACE] default - Trace, Log4js! [2018-07-26T21:58:28.089] [DEBUG] default - Debug, Log4js! [2018-07-26T21:58:28.089] [INFO] default - Hello, Log4js! [2018-07-26T21:58:28.089] [WARN] default - Heads up, Log4js! [2018-07-26T21:58:28.089] [ERROR] default - Danger, Log4js! [2018-07-26T21:58:28.089] [FATAL] default - Fatal, Log4js! |
文件输出将发送到 ./logs/trace.log
,并且与控制台输出类似:
1 2 3 4 5 6 7 | Ix:~/src/projects/IBM-Developer/Node.js/Course/Unit-10 sperry$ cat ./logs/trace.log [2018-07-25T22:25:04.796] [TRACE] default - Trace, Log4js! [2018-07-25T22:25:04.800] [DEBUG] default - Debug, Log4js! [2018-07-25T22:25:04.801] [INFO] default - Hello, Log4js! [2018-07-25T22:25:04.801] [WARN] default - Heads up, Log4js! [2018-07-25T22:25:04.801] [ERROR] default - Danger, Log4js! [2018-07-25T22:25:04.801] [FATAL] default - Fatal, Log4js! |
另外,还可以使用纯 JSON 对象来配置 Log4js。考虑以下 JSON:
清单 19. log4js-config.json
中的纯 JSON 配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | { "appenders": { "consoleAppender": { "type": "console", "layout": { "type": "pattern", "pattern": "%d - %c:[%p]: %m" } } }, "categories": { "default": { "appenders": ["consoleAppender"], "level": "info" } } } |
如果要使用"逐步创建"选项,那么需要在 ./config
目录中创建名为 log4js-config.json
的新文件,并将其粘贴到清单 19 的内容中。
对于此外部配置,可以像使用任何其他 Node 模块一样使用 require()
,并将 traceLogConfig
传递给
Log4j 的 configure()
函数,如下所示:
清单 20. 纯 JSON 配置