|
1 | 1 | import { randomUUID } from "node:crypto"; |
2 | | -import type { IncomingMessage, ServerResponse } from "node:http"; |
3 | | -import type { Request, RequestHandler, Response } from "express"; |
4 | | -import { StatusCodes, getReasonPhrase } from "http-status-codes"; |
5 | | -import type { LevelWithSilent } from "pino"; |
6 | | -import { type CustomAttributeKeys, type Options, pinoHttp } from "pino-http"; |
| 2 | +import type { NextFunction, Request, Response } from "express"; |
| 3 | +import { StatusCodes } from "http-status-codes"; |
| 4 | +import pino from "pino"; |
| 5 | +import pinoHttp from "pino-http"; |
7 | 6 |
|
8 | 7 | import { env } from "@/common/utils/envConfig"; |
9 | 8 |
|
10 | | -enum LogLevel { |
11 | | - Fatal = "fatal", |
12 | | - Error = "error", |
13 | | - Warn = "warn", |
14 | | - Info = "info", |
15 | | - Debug = "debug", |
16 | | - Trace = "trace", |
17 | | - Silent = "silent", |
18 | | -} |
| 9 | +const logger = pino({ |
| 10 | + level: env.isProduction ? "info" : "debug", |
| 11 | + transport: env.isProduction ? undefined : { target: "pino-pretty" }, |
| 12 | +}); |
19 | 13 |
|
20 | | -type PinoCustomProps = { |
21 | | - request: Request; |
22 | | - response: Response; |
23 | | - error: Error; |
24 | | - responseBody: unknown; |
| 14 | +const getLogLevel = (status: number) => { |
| 15 | + if (status >= StatusCodes.INTERNAL_SERVER_ERROR) return "error"; |
| 16 | + if (status >= StatusCodes.BAD_REQUEST) return "warn"; |
| 17 | + return "info"; |
25 | 18 | }; |
26 | 19 |
|
27 | | -const requestLogger = (options?: Options): RequestHandler[] => { |
28 | | - const pinoOptions: Options = { |
29 | | - enabled: env.isProduction, |
30 | | - customProps: customProps as unknown as Options["customProps"], |
31 | | - redact: [], |
32 | | - genReqId, |
33 | | - customLogLevel, |
34 | | - customSuccessMessage, |
35 | | - customReceivedMessage: (req) => `request received: ${req.method}`, |
36 | | - customErrorMessage: (_req, res) => `request errored with status code: ${res.statusCode}`, |
37 | | - customAttributeKeys, |
38 | | - ...options, |
39 | | - }; |
40 | | - return [responseBodyMiddleware, pinoHttp(pinoOptions)]; |
41 | | -}; |
| 20 | +const addRequestId = (req: Request, res: Response, next: NextFunction) => { |
| 21 | + const existingId = req.headers["x-request-id"] as string; |
| 22 | + const requestId = existingId || randomUUID(); |
| 23 | + |
| 24 | + // Set for downstream use |
| 25 | + req.headers["x-request-id"] = requestId; |
| 26 | + res.setHeader("X-Request-Id", requestId); |
42 | 27 |
|
43 | | -const customAttributeKeys: CustomAttributeKeys = { |
44 | | - req: "request", |
45 | | - res: "response", |
46 | | - err: "error", |
47 | | - responseTime: "timeTaken", |
| 28 | + next(); |
48 | 29 | }; |
49 | 30 |
|
50 | | -const customProps = (req: Request, res: Response): PinoCustomProps => ({ |
51 | | - request: req, |
52 | | - response: res, |
53 | | - error: res.locals.err, |
54 | | - responseBody: res.locals.responseBody, |
| 31 | +const httpLogger = pinoHttp({ |
| 32 | + logger, |
| 33 | + genReqId: (req) => req.headers["x-request-id"] as string, |
| 34 | + customLogLevel: (_req, res) => getLogLevel(res.statusCode), |
| 35 | + customSuccessMessage: (req) => `${req.method} ${req.url} completed`, |
| 36 | + customErrorMessage: (_req, res) => `Request failed with status code: ${res.statusCode}`, |
| 37 | + // Only log response bodies in development |
| 38 | + serializers: { |
| 39 | + req: (req) => ({ |
| 40 | + method: req.method, |
| 41 | + url: req.url, |
| 42 | + id: req.id, |
| 43 | + }), |
| 44 | + }, |
55 | 45 | }); |
56 | 46 |
|
57 | | -const responseBodyMiddleware: RequestHandler = (_req, res, next) => { |
58 | | - const isNotProduction = !env.isProduction; |
59 | | - if (isNotProduction) { |
| 47 | +const captureResponseBody = (req: Request, res: Response, next: NextFunction) => { |
| 48 | + if (!env.isProduction) { |
60 | 49 | const originalSend = res.send; |
61 | | - res.send = (content) => { |
62 | | - res.locals.responseBody = content; |
63 | | - res.send = originalSend; |
64 | | - return originalSend.call(res, content); |
| 50 | + res.send = function (body) { |
| 51 | + res.locals.responseBody = body; |
| 52 | + return originalSend.call(this, body); |
65 | 53 | }; |
66 | 54 | } |
67 | 55 | next(); |
68 | 56 | }; |
69 | 57 |
|
70 | | -const customLogLevel = (_req: IncomingMessage, res: ServerResponse<IncomingMessage>, err?: Error): LevelWithSilent => { |
71 | | - if (err || res.statusCode >= StatusCodes.INTERNAL_SERVER_ERROR) return LogLevel.Error; |
72 | | - if (res.statusCode >= StatusCodes.BAD_REQUEST) return LogLevel.Warn; |
73 | | - if (res.statusCode >= StatusCodes.MULTIPLE_CHOICES) return LogLevel.Silent; |
74 | | - return LogLevel.Info; |
75 | | -}; |
76 | | - |
77 | | -const customSuccessMessage = (req: IncomingMessage, res: ServerResponse<IncomingMessage>) => { |
78 | | - if (res.statusCode === StatusCodes.NOT_FOUND) return getReasonPhrase(StatusCodes.NOT_FOUND); |
79 | | - return `${req.method} completed`; |
80 | | -}; |
81 | | - |
82 | | -const genReqId = (req: IncomingMessage, res: ServerResponse<IncomingMessage>) => { |
83 | | - const existingID = req.id ?? req.headers["x-request-id"]; |
84 | | - if (existingID) return existingID; |
85 | | - const id = randomUUID(); |
86 | | - res.setHeader("X-Request-Id", id); |
87 | | - return id; |
88 | | -}; |
89 | | - |
90 | | -export default requestLogger(); |
| 58 | +export default [addRequestId, captureResponseBody, httpLogger]; |
0 commit comments