流水日志
4/21/2021, 1:31:59 PM 200 7 960b76c0a24c11eb851d71230b772f89 e48fb4d1a26211eb851d71230b772f89 h2 /article/a2e12610a0f811ebafc9c93cfc0a76ae/06-request.html
4/21/2021, 1:50:35 PM 200 9 7dbce4a0a26511eb851d71230b772f89 7de665a1a26511eb851d71230b772f89 h2 /article/a2e12610a0f811ebafc9c93cfc0a76ae/02-config.html
4/21/2021, 2:00:58 PM 200 13 f13972d0a26611eb851d71230b772f89 f1832600a26611eb851d71230b772f89 1.1 /?from=blog
流水日志输出如上,输出字段依次为时间戳、响应码、耗时(毫秒)、会话 ID、请求 ID、协议版本、请求路径。
http2 的优势之一就是,每个域只需建立一个连接,并基于此连接建立会话,后续所有请求都通过此会话来进行。为了跟踪方便,每个会话分配了唯一 ID,在所有日志(包括 debug 日志)输出中都会携带会话 ID,方便跟踪一个 session 上的所有活动。因为每个 session 会发送不受限制的请求数量,因此还需要一个请求 ID 来具体跟踪每一个请求。请求 ID 会在具体某一个请求过程中的所有日志中输出,方便跟踪具体请求的所有活动。
server.listen(config.port, config.host, onListening)
.on('connection', (socket /** net.Socket */) => {
// 分配连接 ID,也是会话 ID
socket.id = uuid.v1().replace(/-/g, '')
console.debug('server.onConnection', socket.id)
})
// onSession 回调函数中会读取连接 ID,并赋值给会话 ID
.on('session', onSession)
.on('request', (req, res) => {
const now = new Date()
req._context = {
timestamp: now.toLocaleString(),
startTime: now.getTime(),
path: req.url,
version: req.httpVersion,
sid: req.socket._parent.id,
// 分配请求 ID
reqid: uuid.v1().replace(/-/g, ''),
format() {
return `${this.timestamp}\t${this.code}\t${this.cost}\t${this.sid}\t${this.reqid}\t${this.version}\t${this.path}\n`
}
}
const next = (req, res, err) => {
req._context.endTime = Date.now()
req._context.code = res.statusCode
req._context.cost = req._context.endTime - req._context.startTime
console.debug('req.access.log', req._context)
fs.appendFile(config.accessLog, req._context.format(), (err) => {
if (err) {
console.error('req.access.log error', req._context.sid, err)
}
})
}
onRequest(req, res, next)
})
.on('stream', (stream, headers) => {
const now = new Date()
stream._context = {
timestamp: now.toLocaleString(),
startTime: now.getTime(),
path: headers[HTTP2_HEADER_PATH],
version: stream.session.alpnProtocol,
sid: stream.session.id,
// 分配请求 ID
reqid: uuid.v1().replace(/-/g, ''),
format() {
return `${this.timestamp}\t${this.code}\t${this.cost}\t${this.sid}\t${this.reqid}\t${this.version}\t${this.path}\n`
}
}
const next = (stream, headers, err) => {
stream._context.endTime = Date.now()
stream._context.code = stream.sentHeaders ? stream.sentHeaders[HTTP2_HEADER_STATUS] : HTTP_STATUS_INTERNAL_SERVER_ERROR
stream._context.cost = stream._context.endTime - stream._context.startTime
console.debug('stream.access.log', stream._context)
fs.appendFile(config.accessLog, stream._context.format(), (err) => {
if (err) {
console.error('stream.access.log error', stream._context.sid, err)
}
})
}
onStream(stream, headers, next)
})
上述代码关键部分已加上了注释,整体逻辑很简单。就是在启动服务器监听时,注册 request 及 stream 事件处理函数,并在这 2 个函数中定义 next 回调函数,由 next 回调函数最后完成流水日志的输出。next 函数会在向浏览器写回数据时调用。