Ray Lee | 李宗叡
Learn or Die
Published in
5 min readApr 15, 2024

--

# 前言

在 Express.js 中建立 middleware 來 log request & response

# 目標 log 格式

目標是每個 request 都會記錄下 log, 格式如下:

label: {
requestId: xxx,
timestamp: xxx,
},
req: {
xxx: xxx,
yyy: yyy,
zzz: zzz
},
res: {
xxx: xxx,
yyy: yyy,
zzz: zzz
}

# Attach req log

首先, 我們經由一個 middleware, 將我們想要 log 的 request 資訊, 放到 global req 當中

const uuid = require("uuid")

exports.attachReqLog = (req, res, next) => {
const reqLog = {}
const labels = {}

labels.reqId = uuid.v4()
reqLog.body = req.body
reqLog.query = req.query
reqLog.params = req.params
reqLog.remoteIp = req.ip
reqLog.requestMethod = req.method
reqLog.requestUrl = req.url
reqLog.headers = req.headers

req.labels = labels
req.reqLog = reqLog

next()
}

# Attach res log

接著, 我們經由另一個 middleware, 抽換 res.write 以及 res.end, 因為只要我們有針對 exception 去做 catch, throw, and res 的機制, 在程式碼的最終, res.end 必定會被執行, 因此, 我們藉由在 res.write 以及 res.end method 中增加提取 res log 以及 write log 的邏輯, 這樣在最後 res.end 被執行時, 便會將先前放入 global req 的 reqLog, 以及 res log 都寫入檔案

const rfs = require('rotating-file-stream')
const path = require("path")
const dirName = process.env.NODE_ENV === 'stage' || process.env.NODE_ENV === 'production'
? '../../logs'
: '../logs'

const accessLogStream = rfs.createStream('request.log', {
interval: '1d',
path: path.join(__dirname, dirName),
compress: true,
mode: 0o660,
size: '10M',
maxFiles: 90
});

exports.attachResLog = (req, res, next) => {
let log = {}
const oldWrite = res.write,
oldEnd = res.end;

const chunks = [];

res.write = function (chunk) {
chunks.push(chunk);
oldWrite.apply(res);
};

res.end = function (chunk) {
if (chunk)
chunks.push(chunk);
j
log.labels = req.labels
log.req = req.reqLog

const body = Buffer.concat(chunks).toString('utf8');

log.res = {
code: res.statusCode,
body: body,
headers: res.getHeaders()
}

accessLogStream.write(JSON.stringify(log, '', 2))

oldEnd.apply(res, arguments);
};

next();
}

上面的程式碼中:

  • rfs 的目的是要將 log rotationally 寫入 log file
  • 我們先將原本的 res.write assign 給 oldWrite, 然後開始修改 res.write
  • chunk 代表任何 pass 到 write 的內容
  • 我們將 chunk 存到 chunks array, chunks array 的內容就是我們要的 res body
  • 我們從 global req 中取出早前放進去的 reqLog
  • 我們將 chunks 組合起來, 得到 res body
  • apply 代表將 arg1 指定為 oldEnd method 的 this 作用域, 並 return method 的結果, 具體一點來說, 簡如 res 的內容是 {statusCode: 200}, 那 oldEnd method 中的 this.statusCode 就會是 200, 細節我覺得 這篇文章 寫得很清楚
  • arguments 是 JS 中特別的用法, 可以直接取得 args
  • 最後我們寫入 log, 相當於改寫了 res.end, 讓其在 lifecycle 的最終執行時, 將 log 寫入 file

至此, 就可以將 req & res log 下來

--

--

Ray Lee | 李宗叡
Learn or Die

It's Ray. I do both backend and frontend, but more focus on backend. I like coding, and would like to see the whole picture of a product.