Nodejs 日志

backend

Nodejs 日志

系统日志记录了网站的使用情况,根据功能可以分为多种常见的日志

  • 访问日志 access log(server 端最重要的日志),记录每次请求的情况
  • 自定义日志,包括自定义事件、错误记录等

日志记录以文档形式存储在硬盘中(而不存储再 Redis,由于日志通常很大;一般不存储在 MySQL,由于日志数据结构比较简单,不需要 MySQL 进行优化存储),需要进行日志文件拆分方便存储管理,日志主要用于后续的内容分析。因此日志记录需要使用 Node.js 进行文件操作,一般通过 nodejs stream 方式提高性能。

文件操作

js
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) 查看文档可访问的情况。

js
// 如果后续不需要对文档进行操作,推荐使用方法 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 的方法 readFilewriteFile 分别读取或写入文件,每次读取或写入内容都要重新打开文档,如果频繁读取或写入内容时会十分消耗性能。

IO 操作(包括「网络IO」和「文件IO」)限制性能,因此应该采用方式来执行读取和写入文档操作,每一个流的源 stream 通过管道 pipe 连接起来,数据就可以从一个源传递到另一个源。

Node.js 模块 http 的请求 req 和响应 res 都可以遵循水流管道模型,通过 pipe 连接来实现 stream 方式传输数据

js
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 方式操作文档

js
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 的信息,如请求方法、请求的地址、请求的浏览器信息、请求的时间等

js
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 脚手架搭建项目时,自动安装并引入到项目中

js
var logger = require('morgan');
// ...

app.use(logger('dev'));

默认以标准输出将访问日志打印在控制台上,输出格式采用 dev 模式,即 :method :url :status :response-time ms - :res[content-length]。可以根据需求更改参数,如开发环境和生成环境(根据环境变量 process.env.NODE_ENV 判断)采用不同的模式以输出不同内容格式的访问日志。

js
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.logconsole.error 即可,再使用 pm2 工具将这些打印到控制台的自定义日志输出到文档中。

日志拆分

日志内容会慢慢类即,所有记录放在一个文件后期会不好处理,一般使用 shell 脚本按照一定的规则将日志拆分为不同文档。

拷贝日志文件并重命名为对应的格式,一般按时间划分日志文件,如按照 yyyy-dd.access.log 的规则来创建。

sh
#!/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 是一个拷贝日志等命令。

bash
# 编辑 crontab 定时任务
crontab -e

每天 0 点执行日志拆分任务

bash
* 0 * * * * sh /path/shellFile
Tip

可以在终端输入命令 crontab -l 查看所有 crontab 定时任务。

分析日志

日志内容一般是按行存储的,一行就是一条日志,可以使用 Node.js 的命令 readline 基于 stream 一行行地读取完整的日志,并获取所需的信息,用于后续的分析。

js
/**
 * 分析所有请求中使用 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);
})

Copyright © 2024 Ben

Theme BlogiNote

Icons from Icônes