Node.js 日志记录分析操作
一、原生 Node.js 操作日志
继前面的项目,实现日志操作 ▼
1、日志写入
Server 根目录下新建 logs 文件夹,存放几种类型的日志:
📁 server-project
|—— 📁 logs
|—— |—— 📜 access.log
|—— |—— 📜 error.log
|—— |—— 📜 event.log
添加操作日志的工具函数:
const fs = require('fs')
const path = require('path');
// 写日志通用函数
const writeLog = (writeStream, log) => {
writeStream.write(log + '\n')
}
// 生成 writeStream 函数
const createWriteStream = (file) => {
const fullFileName = path.join(__dirname, '../', '../', 'logs', file)
const writeStream = fs.createWriteStream(fullFileName, {
flags: 'a'
})
return writeStream
}
// 写访问日志
const accessWriteStream = createWriteStream('access.log')
const access = (log) => {
writeLog(accessWriteStream, log)
}
module.exports = {
access
}
在接口调用的位置执行日志函数:
const { access } = require('./src/utils/log')
const serverHandle = (req, res) => {
// 记录 access log
access(`${req.method} -- ${req.url} -- ${req.headers['user-agent']} -- ${Date.now()}`)
访问接口后,可以看到输出的日志文件内容如下:
2、日志拆分
由于日志内容会不断积累,都放在一个文件中不好处理,因此有必要对日志进行拆分,例如按时间拆分日志文件:2022-02-22.access.log
,可通过 linux 的 crontab 命令实现,即定时任务,步骤如下 ▼
- 设置定时任务,格式为
* * * * * command
- 将
access.log
拷贝并重命名为2022-02-22.access.log
- 清空
access.log
文件,继续积累日志。
具体实现如下,首先在 src/utils
下新建拷贝命名文件:
#!/bin/sh
cd /Users/xxx/node-web-server-blog/server-project/logs
cp access.log $(date +%Y-%m-%d).access.log
echo "" > access.log
在 src/utils
目录下运行命令:
sh copy.sh
运行完,可以看到 access.log
拷贝给了 2022-02-22.access.log
文件,并清空 access.log
文件的内容,说明该命令生效了。接下来运行 crontab 对该命令定时执行 ▼
# 任意目录下运行
crontab -e
# 复制刚刚 copy.sh 文件的路径,在输入框中输入,然后按 ESC 退出编辑模式
* 0 * * * sh /Users/xxx/test/node-web-server-blog/server-project/src/utils/copy.sh
# shift + : 然后输入 wq 回车(保存后退出)即可完成
wq
# 可通过以下命令查看当前系统有哪些任务
crontab -l
执行效果如下:
3、日志分析
例如对 access.log
日志分析 Chrome 的占比。
由于日志是按行存储的,因此日志分析可以使用 Node.js 的 readline 逐行读取来实现 ▼
const fs = require('fs')
const path = require('path')
const readline = require('readline')
// 文件名
const fileName = path.join(__dirname, '../', '../', 'logs', 'access.log')
// 读取日志
const readStream = fs.createReadStream(fileName)
// 创建 readline 对象
const rl = readline.createInterface({
input: readStream
})
let sum = 0 // 总访问次数
let chromeNum = 0 // 使用 Chrome 访问次数
// 逐行读取
rl.on('line', lineData => {
if (!lineData) {
return
}
// 记录总访问次数
sum++
const arr = lineData.split(' -- ')
if (arr[2] && arr[2].indexOf('Chrome') > 0) {
// 累加 Chrome 访问次数
chromeNum++
}
})
// 监听读取完成
rl.on('close', () => {
console.log('Chrome 占比:' + chromeNum / sum)
})
执行效果如下:
二、Express 框架操作日志
1、morgan 日志基本用法
Express 可直接通过 morgan 中间件操作日志:
const logger = require('morgan');
// 默认写法
app.use(logger('dev'));
// 默认 option 写法
app.use(logger('dev', {
stream: process.stdout
}));
2、morgan 写入日志
const logger = require('morgan');
const path = require('path');
const fs = require('fs');
// 日志操作
const ENV = process.env.NODE_ENV
if (ENV !== 'production') {
// 开发〡测试环境
app.use(logger('dev'));
} else {
// 线上环境
const logFileName = path.join(__dirname, 'logs', 'access.log')
const writeStream = fs.createWriteStream(logFileName, {
flags: 'a'
})
app.use(logger('combined', {
stream: writeStream
}));
}
3、morgan 预设日志
logger 的可用参数如下:
3-1、dev(默认)
dev 按响应状态着色的简明输出,供开发使用,状态颜色标识如下:
- 成功代码的
:status
标记为绿色; - 服务器错误代码为红色;
- 客户端错误代码为黄色;
- 重定向代码为青色;
- 信息代码为无色。
:method :url :status :response-time ms - :res[content-length]
# 示例
# GET /api/user/login-test 200 3.472 ms - 37
# GET /api/blog/count 200 33.869 ms - 21
# POST /api/blog/list 200 25.249 ms - 1331
# POST /api/blog/detail 200 11.286 ms - 160
3-2、combined
combined 使用标准 Apache 组合日志输出(线上环境常用)
:remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"
# 示例
# ::1 - - [01/Feb/2022:11:12:35 +0000] "GET /api/user/login-test HTTP/1.1" 200 37 "http://localhost:8001/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"
# ::1 - - [01/Feb/2022:11:12:35 +0000] "GET /api/blog/count HTTP/1.1" 200 21 "http://localhost:8001/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"
# ::1 - - [01/Feb/2022:11:12:35 +0000] "POST /api/blog/list HTTP/1.1" 200 1331 "http://localhost:8001/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"
# ::1 - - [01/Feb/2022:11:12:37 +0000] "POST /api/blog/detail HTTP/1.1" 200 160 "http://localhost:8001/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"
3-3、common
common 使用标准 Apache 通用日志输出。
:remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length]
# 示例
# ::1 - - [01/Feb/2022:11:14:05 +0000] "GET /api/user/login-test HTTP/1.1" 200 37
# ::1 - - [01/Feb/2022:11:14:05 +0000] "GET /api/blog/count HTTP/1.1" 200 21
# ::1 - - [01/Feb/2022:11:14:05 +0000] "POST /api/blog/list HTTP/1.1" 200 1331
# ::1 - - [01/Feb/2022:11:14:06 +0000] "POST /api/blog/detail HTTP/1.1" 200 160
3-4、short
short 比默认值更短,还包括响应时间。
:remote-addr :remote-user :method :url HTTP/:http-version :status :res[content-length] - :response-time ms
# 示例
# ::1 - GET /api/user/login-test HTTP/1.1 200 37 - 1.039 ms
# ::1 - GET /api/blog/count HTTP/1.1 200 21 - 8.093 ms
# ::1 - POST /api/blog/list HTTP/1.1 200 1331 - 2.931 ms
# ::1 - POST /api/blog/detail HTTP/1.1 200 160 - 2.624 ms
3-5、tiny
tiny 使用最小输出。
:method :url :status :res[content-length] - :response-time ms
# 示例
# GET /api/user/login-test 200 37 - 1.305 ms
# GET /api/blog/count 200 21 - 4.672 ms
# POST /api/blog/list 200 1331 - 4.320 ms
# POST /api/blog/detail 200 160 - 2.243 ms
在中间件中写 console.log
语句是比较糟糕的做法,因为 console.log
(包括其他同步的代码)会阻塞 Node.js 的异步事件循环,降低服务器的吞吐率。在实际生产中,使用 morgan 日志中间件来自定义日志是推荐的做法。
三、Koa 框架操作日志
Koa 通过 koa-morgan 中间件操作日志,用法与 Express 的 morgan 相同:
npm i koa-morgan --save
使用如下:
const logger = require('koa-logger')
const path = require('path')
const fs = require('fs')
const morgan = require('koa-morgan')
app.use(logger())
// logger
app.use(async (ctx, next) => {
const start = new Date()
await next()
const ms = new Date() - start
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
})
// 日志操作
const ENV = process.env.NODE_ENV
if (ENV !== 'production') {
// 开发〡测试环境
app.use(morgan('dev'));
} else {
// 线上环境
const logFileName = path.join(__dirname, 'logs', 'access.log')
const writeStream = fs.createWriteStream(logFileName, {
flags: 'a'
})
app.use(morgan('combined', {
stream: writeStream
}));
}