在上一篇文章中,我们创建了一个 @vue/cli3
插件,并将 ssr
服务整合到插件中。
这篇文章中,让我们来为插件中的 ssr
服务创建日志系统。
我们将从如下几个方面来逐步进行:
- 选择日志工具库
- 将日志工具集成到
ssr
插件中 - 日志支持区分环境、日志支持分类、日志支持等级
- 日志切割
选择日志工具库
基于 nodejs 有一些日志工具库可供选择:
这里我们从中选择 winston
作为基础日志工具,接入我们的 ssr
工程
将日志工具集成到 ssr
插件中
我们打开 winston
的 ,参照 一节,来开始创建我们的 logger
创建 logger
打开我们在上一篇文章中创建的 vue-cli-plugin-my_ssr_plugin_demo
工程
安装 winston
yarn add winston复制代码
在根目录文件夹下的 app
中创建文件夹 lib
,并在 lib
中创建 logger.js
文件,我们在这个文件中定制自己的 logger
目录结构如下:
├── app│ ├── lib│ │ ├── logger.js│ ├── middlewares│ │ ├── dev.ssr.js│ │ ├── dev.static.js│ │ └── prod.ssr.js│ └── server.js...复制代码
logger.js
的内容如下:
const winston = require('winston')const logger = winston.createLogger({ transports: [ new winston.transports.Console(), new winston.transports.File({ filename: 'combined.log' }) ]})module.exports = { logger}复制代码
然后打开我们的 app/server.js
,在服务启动的过程中,为全局对象挂载上我们刚创建的 logger
...const { logger } = require('./lib/logger.js')...app.listen(port, host, () => { logger.info(`[${process.pid}]server started at ${host}:${port}`)})...复制代码
启动服务,除了在终端看到输出外,还会发现在根目录下多了一个 combined.log
文件,里面的内容与终端输出一致
{ "message":"[46071]server started at 127.0.0.1:3000","level":"info"}复制代码
至此,我们已经为服务端接入了最基础的日志功能,接下来,让我们考虑一下实际的日志场景。
日志支持区分环境、日志支持分类、日志支持等级
简单起见,我们将环境区分、日志分类、日志等级简化为以下几个具体要求:
- 日志需要写入到文件中。
- 日志需要支持自定义级别,级别由大到小依次是:
error
、warning
、notice
、info
、debug
。 - 开发环境,日志的输出最好能带颜色,格式能更加方便在终端阅读。
- 增加用户请求日志
access
类型,此日志需要写入到单独的文件中,与其他类型的日志区分开。
关于第一条要求,我们在上一个例子中,已经通过 winston.transports.File
实现了。
对于第二、三条要求,我们打开 lib/logger.js
添加相关的代码,最终代码如下:
const winston = require('winston')const options = { // 我们在这里定义日志的等级 levels: { error: 0, warning: 1, notice: 2, info: 3, debug: 4 }, transports: [ // 文件中我们只打印 warning 级别以上的日志(包含 warning) new winston.transports.File({ filename: 'combined.log', level: 'warning' }) ]}// 开发环境,我们将日志也输出到终端,并设置上颜色if (process.env.NODE_ENV === 'development') { options.format = winston.format.combine( winston.format.colorize(), winston.format.json() ) // 输出到终端的信息,我们调整为 simple 格式,方便看到颜色; // 并设置打印 debug 以上级别的日志(包含 debug) options.transports.push(new winston.transports.Console({ format: winston.format.simple(), level: 'debug' }))}const logger = winston.createLogger(options)module.exports = { logger}复制代码
我们在 app/servier.js
中输入以下代码:
...logger.error('this is the error log')logger.warning('this is the warning log')logger.notice('this is the info log')logger.info('this is the info log')logger.debug('this is the debug log')...复制代码
在发开环境启动服务后,能看到终端打印出如下内容:
error: this is the error logwarning: this is the warning lognotice: this is the info loginfo: this is the info logdebug: this is the debug log复制代码
而日志文件 combined.log
中的内容为:
{ "message":"this is the error log","level":"\u001b[31merror\u001b[39m"}{ "message":"this is the warning log","level":"\u001b[31mwarning\u001b[39m"}复制代码
在测试和产品环境启动服务后,日志并不会输出到终端,只输出到文件中:
{ "message":"this is the error log","level":"error"}{ "message":"this is the warning log","level":"warning"}复制代码
接下来我们来看第四条要求:
增加用户请求日志
access
类型,此日志需要写入到单独的文件中,与其他类型的日志区分开。
如果我们需要增加一个 access
日志类型,并将它的内容输出到独立的文件中,最简单的方式就是再创建一个 logger
实例:
...winston.loggers.add('access', { levels: { access: 0 }, level: 'access', format: winston.format.combine( winston.format.json() ), transports: [ new winston.transports.File({ filename: 'access.log', level: 'access' }) ]})...复制代码
我们在 app/servier.js
中添加打印 access
日志的代码:
const { logger, accessLogger } = require('./log.js')...accessLogger.access('this is the access log')复制代码
在开发环境启动服务后,我们发现除了 combined.log
日志文件外,又多了一个 access.log
文件,内容为:
{ "message":"this is the access log","level":"access"}复制代码
至此,我们的日志中还没有自动记录当前的时间,我们在 lib/logger.js
中为两类日志都添加上时间,添加后的代码如下:
const winston = require('winston')const options = { // 我们在这里定义日志的等级 levels: { error: 0, warning: 1, notice: 2, info: 3, debug: 4 }, format: winston.format.combine( winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }) ), transports: [ // 文件中我们只打印 warning 级别以上的日志(包含 warning) new winston.transports.File({ filename: 'combined.log', level: 'warning' }) ]}// 开发环境,我们将日志也输出到终端,并设置上颜色if (process.env.NODE_ENV === 'development') { options.format = winston.format.combine( winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }), winston.format.colorize(), winston.format.json() ) // 输出到终端的信息,我们调整为 simple 格式,方便看到颜色; // 并设置打印 debug 以上级别的日志(包含 debug) options.transports.push(new winston.transports.Console({ format: winston.format.simple(), level: 'debug' }))}winston.loggers.add('access', { levels: { access: 0 }, level: 'access', format: winston.format.combine( winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }), winston.format.json() ), transports: [ new winston.transports.File({ filename: 'access.log', level: 'access' }) ]})const logger = winston.createLogger(options)module.exports = { logger, accessLogger: winston.loggers.get('access')}复制代码
在开发环境启动服务后,我们发现日志携带了当前的时间信息,终端内容为:
error: this is the error log { "timestamp":"2019-06-06 17:02:36.736"}warning: this is the warning log { "timestamp":"2019-06-06 17:02:36.740"}notice: this is the info log { "timestamp":"2019-06-06 17:02:36.741"}info: this is the info log { "timestamp":"2019-06-06 17:02:36.741"}debug: this is the debug log { "timestamp":"2019-06-06 17:02:36.741"}复制代码
``文件的内容为:
{ "message":"this is the error log","level":"\u001b[31merror\u001b[39m","timestamp":"2019-06-06 17:02:36.736"}{ "message":"this is the warning log","level":"\u001b[31mwarning\u001b[39m","timestamp":"2019-06-06 17:02:36.740"}复制代码
``文件的内容为:
{ "message":"this is the access log","level":"access","timestamp":"2019-06-06 17:02:36.741"}复制代码
日志切割
将来我们的服务部署上线后,服务器端记录的日志会不断得往同一个文件中写入日志内容,这对于长时间运行的服务来说,是有日志过大隐患的。
这里,我们按照每小时分割日志,将每个小时内的日志内容,写入不同的文件中。
另外,因为部署产品服务会有多个 worker
进程服务,所以,我们为每个进程分配一个独立的文件夹(以进程id+日期区分),来存储此进程的全部日志:
logs├──your_project_name│ ├──pid_1236_2019_01_01│ │ ├──access-2019-01-01-23.log│ │ └──combined-2019-01-01-23.log│ └──pid_1237_2019_01_01│ ├──access-2019-01-01-23.log│ └──combined-2019-01-01-23.log复制代码
为了实现我们预计的日志切割功能,我们需要引入一个库:winston-daily-rotate-file
yarn add winston-daily-rotate-file复制代码
安装完后,我们在 lib/logger.js
中引入
require('winston-daily-rotate-file')复制代码
引入后,我们可以通过 winston.transports.DailyRotateFile
创建拥有自动切割功能的 tracsports 实例
const _getToday = (now = new Date()) => `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}`let dirPath += '/pid_' + pid + '_' + _getToday() + '/'let accessTransport = new (winston.transports.DailyRotateFile)({ filename: dirPath + 'access-%DATE%.log', // 日志文件存储路径 + 日志文件名称 datePattern: 'YYYY-MM-DD-HH', // 日志文件切割的粒度,这里为每小时 zippedArchive: true, // 是否压缩 maxSize: '1g', // 每个日志文件最大的容量,如果达到此容量则触发切割 maxFiles: '30d' // 日志文件保留的时间,这里为 30 天,30天之前的日志会被删除掉})复制代码
添加切割功能后的 lib/logger.js
内容如下:
const winston = require('winston')const { format } = winstonconst { combine, timestamp, json } = formatconst _getToday = (now = new Date()) => `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}`const rotateMap = { 'hourly': 'YYYY-MM-DD-HH', 'daily': 'YYYY-MM-DD', 'monthly': 'YYYY-MM'}module.exports = (dirPath = './', rotateMode = '') => { if (!~Object.keys(rotateMap).indexOf(rotateMode)) rotateMode = '' let accessTransport let combineTransport if (rotateMode) { require('winston-daily-rotate-file') const pid = process.pid dirPath += '/pid_' + pid + '_' + _getToday() + '/' const accessLogPath = dirPath + 'access-%DATE%.log' const combineLogPath = dirPath + 'combine-%DATE%.log' const datePattern = rotateMap[rotateMode] || 'YYYY-MM' accessTransport = new (winston.transports.DailyRotateFile)({ filename: accessLogPath, datePattern: datePattern, zippedArchive: true, maxSize: '1g', maxFiles: '30d' }) combineTransport = new (winston.transports.DailyRotateFile)({ filename: combineLogPath, datePattern: datePattern, zippedArchive: true, maxSize: '500m', maxFiles: '30d' }) } const options = { // 我们在这里定义日志的等级 levels: { error: 0, warning: 1, notice: 2, info: 3, debug: 4 }, format: combine( timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }) ), transports: rotateMode ? [ combineTransport ] : [] } // 开发环境,我们将日志也输出到终端,并设置上颜色 if (process.env.NODE_ENV === 'development') { options.format = combine( timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }), winston.format.colorize(), json() ) // 输出到终端的信息,我们调整为 simple 格式,方便看到颜色; // 并设置打印 debug 以上级别的日志(包含 debug) options.transports.push(new winston.transports.Console({ format: format.simple(), level: 'debug' })) } winston.loggers.add('access', { levels: { access: 0 }, level: 'access', format: combine( timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }), json() ), transports: rotateMode ? [ accessTransport ] : [] }) const logger = winston.createLogger(options) return { logger: logger, accessLogger: winston.loggers.get('access') }}复制代码
在 app/server.js
中引入 lib/logger.js
也需要调整为以下方式:
const { logger, accessLogger } = require('./lib/logger.js')('./', 'hourly')复制代码
在开发环境启动服务,我们会发现除了终端输出了日志外,我们的日志文件变成了如下的结构:
./pid_48794_2019-6-6├── access-2019-06-06-18.log└── combine-2019-06-06-18.log复制代码
最终,插件 vue-cli-plugin-my_ssr_plugin_demo
的完整目录结构如下:
├── app│ ├── middlewares│ │ ├── dev.ssr.js│ │ ├── dev.static.js│ │ └── prod.ssr.js│ ├── lib│ │ └── logger.js│ └── server.js├── generator│ ├── index.js│ └── template│ ├── src│ │ ├── App.vue│ │ ├── assets│ │ │ └── logo.png│ │ ├── components│ │ │ └── HelloWorld.vue│ │ ├── entry-client.js│ │ ├── entry-server.js│ │ ├── main.js│ │ ├── router│ │ │ └── index.js│ │ ├── store│ │ │ ├── index.js│ │ │ └── modules│ │ │ └── book.js│ │ └── views│ │ ├── About.vue│ │ └── Home.vue│ └── vue.config.js├── index.js└── package.json复制代码
至此,我们的日志系统完成了。下一篇文章,我们讲如何为 vue-cli-plugin-my_ssr_plugin_demo
设计并集成监控系统。
水滴前端团队招募伙伴,欢迎投递简历到邮箱:fed@shuidihuzhu.com