项目博客之日志
-
系统没有日志,就等于人没有眼睛
-
第一,访问日志 access log(server 端最重要的日志)
-
第二,自定义日志(包括自定义事件、错误记录等)
1. nodejs 文件操作
文件读取
基本的文件读取方法:
const fs = require("fs");
const path = require("path");
// 获取当前文件夹的 data.txt 所在路径
const filePath = path.resolve(__dirname, "data.txt");
// 读取文件内容
fs.readFile(filePath, (err, data) => {
if (err) {
console.error(err);
return;
}
// data 是二进制类型,需要转换为字符串
console.log(data.toString());
});
一口气读取了文件里的内容然后进行输出。这就带来了风险,如果要读取的这个文件特别大,内存吃不消。
文件写入
基本的文件写入方法:
// 写入文件
const content = "这是新写入的内容\n";
const opt = {
flag: "a",
};
// writeFile 四个参数:
// filePath: 目标文件路径 content: 要写入的内容
// opt: 写入方式 'a' 代表追加写入,覆盖用 'w'
// 最后一个参数是回调函数
fs.writeFile(filePath, content, opt, (err) => {
if (err) {
console.error(err);
}
});
每次写入的时候都得获取文件路径然后打开进行写入,这样就十分耗内存。另外,如果要写入一个特别大的内容,整个内容要存入内存里,内存吃不消。
判断文件是否存在
// 同步检查是否存在
console.log(fs.existsSync(filePath)); // true
// 异步检查是否存在
fs.exists(filePath, (exist) => {
console.log(exist);
});
2. stream
2.1 IO 操作的性能瓶颈
- IO 包括”网络 IO“和”文件 IO“
- 相比于 CPU 计算和内存读写,IO 的突出特点就是慢
- 如何在有限的硬件资源下提高 IO 操作效率
2.2 stream 通俗介绍
stream 就是流动,之前一口气读取文件内容出来就好比搬水桶,直接把整个水桶抬走了。但是大多数人并没有那力气。更好的方式是,接个水管,将水稳定地通过管道转移到其他地方,降低了成本(小孩子都能够完成)。stream 就类似于此,极大降低了硬件资源要求。
2.1 stream 演示
标准输入输出
process.stdin.pipe(process.stdout);
终端输入的内容将通过管道持续输出到终端中。
请求内容输出到响应内容
const http = require('http')
const server = http.createServer((req, res) => {
if (req.method === 'POST') {
req.pipe(res) // 最主要
}
})
server.listen(8000)
现在,req 和 res 通过 pipe 连接,一旦 req 接收到了东西,就会立刻稳定地输出到 res 中。
stream 拷贝文件
通过 fs.createReadStream
创建一个读取文件的 stream 对象,fs.createWriteStream
创建一个写入文件的 stream 对象。通过 pipe 连接使得数据流动。
// 两个文件路径
const fileName1 = path.resolve(__dirname, 'data.txt')
const fileName2 = path.resolve(__dirname, 'data-bak.txt')
// 读取文件的 stream 对象
const readStream = fs.createReadStream(fileName1)
// 写入文件的 stream 对象
const writeStream = fs.createWriteStream(fileName2)
// 通过 pipe 执行拷贝
readStream.pipe(writeStream)
// 可以监听数据传输时的内容
readStream.on('data', chunk => {
console.log(chunk.toString())
})
// 监听到读取完成后,执行回调
readStream.on('end', () => {
console.log('copy done')
})
stream 读取文件内容
发起一个 get 请求来读取文件,只要读取文件流通过管道输出到 response 里即可。
const http = require('http')
const fs = require('fs')
const path = require('path')
const fileName1 = path.resolve(__dirname, 'data.txt')
const server = http.createServer((req, res) => {
if (req.method === 'GET') {
const readStream = fs.createReadStream(fileName1)
readStream.pipe(res)
}
})
server.listen(8000)
3. 写日志
3.1 创建写日志的相关方法
在项目里创建 logs 文件夹,里边存储日志文件。同时在 src 目录创建 utils 用于放常用的工具方法,里边创建 log.js,里面写了写日志相关方法。
const fs = require("fs");
const path = require("path");
// 写日志
const writeLog = (writeStream, log) => {
// 写入信息并换行
writeStream.write(log + "\n");
};
// 生成 writeStream
const createWriteStream = (fileName) => {
const fullFileName = path.resolve(__dirname, "../", "../", "logs", fileName);
const writeStream = fs.createWriteStream(fullFileName, {
// 追加写入
flags: "a",
});
return writeStream;
};
// 写访问日志,error log 和 event log 同理
const accessWriteStream = createWriteStream("access.log");
const access = (log) => {
writeLog(accessWriteStream, log);
};
module.exports = {
access,
};
3.2 每次接收到请求后写入日志
app.js 里的 serverHandle 每次调用的时候便是接收到请求的时候,此时写入访问日志即可。其他的日志,例如 error 和 event 在合适的时候写入即可。
const serverHandle = async (req, res) => {
// 记录访问信息
access(
`${req.method} -- ${req.url} -- ${
req.headers["user-agent"]
} -- ${Date.now()}`
);
......其他主要代码
}
实现效果,访问的时候成功写入了 log 文件:
4. 日志拆分
- 日志内容会慢慢积累,放在一个文件中不好处理
- 按时间划分日志文件,方便定位
- 实现方式:Linux 的 crontab 命令,即定时任务
crontab
- 设置定时任务格式:
(分钟) (小时) (日期) (月份) (星期) command
,不需要填的用星号 * 代替 - 当时间到了,将 access.log 拷贝并重命名为当前时间的 log,如
- 清空 access.log 文件,继续积累日志
4.1 实现方法
以下流程适用于 Linux 和 macOS。
写 sh 脚本
写一个脚本 copy.sh 用于产生具有日期标注的日志,命令行执行 sh coppy.sh
查看效果:
#!/bin/sh
cd logs文件夹的绝对路径
# 复制文件
cp access.log $(date +%Y-%m-%d).access.log
# 清空 access.log
echo "" > access.log
理想情况是,生成新的带有日期的日志文件,并且 access.log 里的内容被清空。
使用 crontab
在命令行中执行 crontab -e
进入编辑模式,写入以下内容,意思为每天的第 0 小时执行 sh 命令。
* 0 * * * sh copy.sh的绝对路径
查看 crontab 任务
crontab -l
可以查看当前系统的定时任务。
5. 日志分析
- 如针对 access.log 日志,分析 Chrome 的占比
- 日志是按行存储的,一行就是一条日志
- 使用 nodejs 的 readline 一行一行读取(基于 stream,效率高)
比如,如下的日志内容,前四个是 Safari 浏览器跑的,后两个是 Chrome 浏览器。
GET -- /api/blog/detail?id=1 -- Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15 -- 1655271842104
GET -- /favicon.ico -- Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15 -- 1655271843024
GET -- /api/blog/detail?id=1 -- Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15 -- 1655271853955
GET -- /api/blog/detail?id=2 -- Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15 -- 1655271854350
GET -- /api/blog/detail?id=1 -- Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.61 Safari/537.36 -- 1655271857708
GET -- /api/blog/detail?id=1 -- Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.61 Safari/537.36 -- 1655271859548
可以看出,Safari 浏览器发送请求时,里面没有 Chrome 标记。就可以依据这个来判别 Chrome 的占比(当然比较不严谨,演示用)
计算 Chrome 占比的脚本如下:
// utils/readline.js
const fs = require("fs");
const path = require("path");
const readline = require("readline");
// 文件路径
const fullFileName = path.resolve(
__dirname,
"../",
"../",
"logs",
"access.log"
);
// 创建 readStream
const readStream = fs.createReadStream(fullFileName);
// 创建 readline 对象
const rl = readline.createInterface({
input: readStream,
});
let chromeNum = 0;
let sum = 0;
// 逐行读取
rl.on("line", (lineData) => {
if (!lineData) {
return;
}
// 记录总行数
sum++;
const [method, url, userAgent] = lineData.split(" -- ");
if (userAgent?.includes("Chrome")) {
// 累加 Chrome 数量
chromeNum++;
}
});
// 监听读取完成
rl.on("close", () => {
console.log(`Chrome 占比:${chromeNum / sum}`);
});
运行结果:
Chrome 占比:0.3333333333333333
6. 总结
- 日志对 server 端的重要性,相当于人的眼睛
- IO 性能瓶颈,使用 stream 提高性能
- 使用 crontab 拆分日志文件,使用 readline 分析日志内容