7e89950358
- feat(logging): log all http requests and responses - the auto logger does not log the body, to ensure sensitive payloads are not logged. Unfortunately this means that error messages are not logged either, so need to be manually logged. - fix(logging): 400 errors should be info not error severity logging
87 lines
2.8 KiB
TypeScript
87 lines
2.8 KiB
TypeScript
import { logger } from './logging'
|
|
import { randomUUID } from 'crypto'
|
|
import HttpLogger from 'pino-http'
|
|
import { IncomingMessage } from 'http'
|
|
import { NextFunction, Response } from 'express'
|
|
import pino, { SerializedResponse } from 'pino'
|
|
import { GenReqId } from 'pino-http'
|
|
|
|
const REQUEST_ID_HEADER = 'x-request-id'
|
|
|
|
const GenerateRequestId: GenReqId = (req: IncomingMessage) => DetermineRequestId(req)
|
|
|
|
const DetermineRequestId = (
|
|
req: IncomingMessage,
|
|
uuidGenerator: () => string = randomUUID
|
|
): string => {
|
|
const headers = req.headers[REQUEST_ID_HEADER]
|
|
if (!Array.isArray(headers)) return headers || uuidGenerator()
|
|
return headers[0] || uuidGenerator()
|
|
}
|
|
|
|
export const LoggingExpressMiddleware = HttpLogger({
|
|
logger,
|
|
autoLogging: true,
|
|
genReqId: GenerateRequestId,
|
|
customLogLevel: (req, res, err) => {
|
|
if (res.statusCode >= 400 && res.statusCode < 500) {
|
|
return 'info'
|
|
} else if (res.statusCode >= 500 || err) {
|
|
return 'error'
|
|
} else if (res.statusCode >= 300 && res.statusCode < 400) {
|
|
return 'silent'
|
|
}
|
|
return 'info'
|
|
},
|
|
|
|
// we need to redact any potential sensitive data from being logged.
|
|
// as we do not know what headers may be sent in a request by a user or client
|
|
// we have to allow list selected headers
|
|
serializers: {
|
|
req: pino.stdSerializers.wrapRequestSerializer((req) => {
|
|
return {
|
|
id: req.raw.id,
|
|
method: req.raw.method,
|
|
path: req.raw.url?.split('?')[0], // Remove query params which might be sensitive
|
|
// Allowlist useful headers
|
|
headers: {
|
|
host: req.raw.headers.host,
|
|
'user-agent': req.raw.headers['user-agent'],
|
|
'x-request-id': req.raw.headers[REQUEST_ID_HEADER],
|
|
referer: req.raw.headers.referer
|
|
}
|
|
}
|
|
}),
|
|
res: pino.stdSerializers.wrapResponseSerializer((res) => {
|
|
const resRaw = res as SerializedResponse & {
|
|
raw: {
|
|
headers: Record<string, string>
|
|
}
|
|
}
|
|
return {
|
|
statusCode: res.raw.statusCode,
|
|
// Allowlist useful headers
|
|
headers: {
|
|
'content-length': resRaw.raw.headers['content-length'],
|
|
'content-type': resRaw.raw.headers['content-type'],
|
|
'retry-after': resRaw.raw.headers['retry-after'],
|
|
'x-ratelimit-remaining': resRaw.raw.headers['x-ratelimit-remaining'],
|
|
'x-ratelimit-reset': resRaw.raw.headers['x-ratelimit-reset'],
|
|
'x-request-id': resRaw.raw.headers['x-request-id'],
|
|
'x-speckle-meditation': resRaw.raw.headers['x-speckle-meditation']
|
|
}
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
export const DetermineRequestIdMiddleware = (
|
|
req: IncomingMessage,
|
|
res: Response,
|
|
next: NextFunction
|
|
) => {
|
|
const id = DetermineRequestId(req)
|
|
res.setHeader(REQUEST_ID_HEADER, id)
|
|
next()
|
|
}
|