Nodejs 日志
系统日志记录了网站的使用情况,根据功能可以分为多种常见的日志
- 访问日志 access log(server 端最重要的日志),记录每次请求的情况
- 自定义日志,包括自定义事件、错误记录等
日志记录以文档形式存储在硬盘中(而不存储再 Redis,由于日志通常很大;一般不存储在 MySQL,由于日志数据结构比较简单,不需要 MySQL 进行优化存储),需要进行日志文件拆分方便存储管理,日志主要用于后续的内容分析。因此日志记录需要使用 Node.js 进行文件操作,一般通过 nodejs stream 方式提高性能。
文件操作
const fs = require('fs')
const path = require('path')
// 使用模块 path 拼接文件路径,以适配不同的系统环境下路径表示方式不同
// __dirname 是 nodejs 全局变量,表示当前文档所在目录
// 以下拼接得到文件 data.txt 相对于当前文档的路径
const fileName = path.resolve(__dirname, 'data.txt');
// 读取文件内容,异步操作
fs.readFile(fileName, (err, data) => {
if(err) {
console.error(err);
return
}
// data 是二进制类型,需要使用方法 .toString() 转换为字符串
console.log(data.toString());
});
// 写入文件
// 写入内容
const content = '这是新写入的内容\n';
// 配置写入方式
const opt = {
flag: 'a' // 追加写入,如果是覆盖写入就设置为 'w'
}
fs.writeFile(fileName, content, opt, (err) => {
if(err) {
console.error(err);
}
});
// 判断文件是否存在
fs.exists('data', (exist) => {
console.log(exist);
})
Tip
方法 fs.exists()
已过时,:thumbsup: 推荐使用方法 fs.stat(path[, options], callback)
查看文档/目录状态,或方法 fs.access(path[, mode], callback)
查看文档可访问的情况。
// 如果后续不需要对文档进行操作,推荐使用方法 access
// Check if the file exists in the current directory.
fs.access(file, fs.constants.F_OK, (err) => {
console.log(`${file} ${err ? 'does not exist' : 'exists'}`);
});
Stream
如果使用模块 fs
的方法 readFile
或 writeFile
分别读取或写入文件,每次读取或写入内容都要重新打开文档,如果频繁读取或写入内容时会十分消耗性能。
IO 操作(包括「网络IO」和「文件IO」)限制性能,因此应该采用流方式来执行读取和写入文档操作,每一个流的源 stream 通过管道 pipe 连接起来,数据就可以从一个源传递到另一个源。
Node.js 模块 http
的请求 req
和响应 res
都可以遵循水流管道模型,通过 pipe
连接来实现 stream 方式传输数据
const http = require('http')
const fs = require('fs')
const path = require('path')
const server = http.createServer((req, res) => {
const method = req.method;
// 请求文件,以 stream 方式返回响应
if (method ==='GET') {
const fileName = path.resolve(__dirname, 'data.text');
const stream = fs.createReadStream(fileName);
stream.pipe(res); // 将 res 作为 stream 的 dest
}
// 通过 stream 方式,以 pipe「串接」请求和响应,实现将 POST 请求直接作为响应返回
if (method === 'POST') {
req.pipe(res);
}
});
server.listen(8000);
使用 stream 方式操作文档
const http = require('http')
const fs = require('fs')
const path = require('path')
// 拷贝文件
// 两个文件的路径
const fileName1 = path.resolve(__dirname, 'data.txt');
const fileName2 = path.resovle(__dirname, 'bak.txt');
// 读取和写入文件的 stream 对象
const readStream = fs.createReadStream(fileName1);
const writeStream = fs.createWriteStream(fileName2);
// 通过 pipe 连接两个 stream 对象,执行拷贝
readStream.pipe(writeStream);
// 监听 data 事件,当通过 pipe 接收到数据流就会触发
readStream.on('data', chunk => {
// data 是二进制类型,需要转换为字符串再打印出每次接收的数据流内容
console.log((chunk.toString()));
});
// 监听 end 事件,当数据传输完成时触发,即拷贝完成
readStream.on('end', function() {
console.log('拷贝完成');
});
创建日志
访问日志 access.log
一般记录每一次请求 req
的信息,如请求方法、请求的地址、请求的浏览器信息、请求的时间等
const fs = require('fs');
// 实例化一个 write Stream
const accessWriteStream = fs.createWriteStream('access.log', {
flags: 'a' // append 追加模式
})
// 写日志
function access(log) {
accessWriteStream.write(log + '\n'); // 调用 Stream 对象的 write 方法,每次写入一行代码 log
}
module.exports = {
access
}
使用 morgan
access log 记录使用 morgan 模块。
该模块已经在使用 Express 脚手架搭建项目时,自动安装并引入到项目中
var logger = require('morgan');
// ...
app.use(logger('dev'));
默认以标准输出将访问日志打印在控制台上,输出格式采用 dev
模式,即 :method :url :status :response-time ms - :res[content-length]
。可以根据需求更改参数,如开发环境和生成环境(根据环境变量 process.env.NODE_ENV
判断)采用不同的模式以输出不同内容格式的访问日志。
const path = require('path');
const fs = require('fs');
var logger = require('morgan');
// ...
const ENV = process.env.NODE_ENV;
if(ENV !== 'production') {
// 开发环境
// 默认采用 dev 模式,日志直接输出到控制台
app.use(logger('dev'));
} else {
// 线上环境
const logFileName = path.join(__dirname, 'logs', 'access.log');
const writeStream = fs.createWriteStream(logFileName, {
flags: 'a'
});
// 采用 combined 模式,日志以数据流 stream 的形式写入到 access.log 文档中
app.use(logger('combined', {
stream: writeStream
}))
}
Tip
自定义日志使用 console.log
和 console.error
即可,再使用 pm2 工具将这些打印到控制台的自定义日志输出到文档中。
日志拆分
日志内容会慢慢类即,所有记录放在一个文件后期会不好处理,一般使用 shell 脚本按照一定的规则将日志拆分为不同文档。
拷贝日志文件并重命名为对应的格式,一般按时间划分日志文件,如按照 yyyy-dd.access.log
的规则来创建。
#!/bin/sh
cd /Users/project/logs
cp access.log ${date +%Y-%m-%d).access.log
echo "" > access.log
使用 Linux 的 crontab
命令实现定时任务,定时对日志进行拆分。crontab 定时任务的格式为 ***** command
每个 *
占位符依次表示 分钟、小时、日期、月份、星期(0~6);command
是一个拷贝日志等命令。
# 编辑 crontab 定时任务
crontab -e
每天 0 点执行日志拆分任务
* 0 * * * * sh /path/shellFile
Tip
可以在终端输入命令 crontab -l
查看所有 crontab 定时任务。
分析日志
日志内容一般是按行存储的,一行就是一条日志,可以使用 Node.js 的命令 readline
基于 stream 一行行地读取完整的日志,并获取所需的信息,用于后续的分析。
/**
* 分析所有请求中使用 Chrome 浏览器的占比
*/
const fs = require('fs');
const path = require('path');
const readline = require('readline');
// 日志文件路径
const fileName = path.join(__dirname, '../', '../', 'logs', 'access.log');
// 创建 read Stream 实例
const readStream = fs.createReadStream(fileName)
// 创建 readline 对象
const rl = readline.createInterface({
input: readStream
})
let chromeNum = 0;
let num = 0;
// 逐行读取
rl.on('line', (lineData) => {
// 排除空行
if(!lineData) {
return
}
// 记录总行数
num++;
const arr = lineData.split(' -- ');
if (arr[2] && arr[2].indexOf('Chrome') > 0) {
// 累加 chrome 的请求数量
chromeNum++
}
})
// 监听完成
rl.on('close', () => {
console.log('chrome 占比:' + chromeNum / num);
})