Skip to content

Commit f3fe96a

Browse files
committed
feat(common): add request tracking and http logging middlewares
1 parent 4f0dd1b commit f3fe96a

2 files changed

Lines changed: 100 additions & 0 deletions

File tree

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { AppLogger } from '#libs/logger/logger.service.js'
2+
import { Injectable, type NestMiddleware } from '@nestjs/common'
3+
import type { NextFunction, Request, Response } from 'express'
4+
5+
// ANSI color codes for status ranges, used only in dev
6+
const STATUS_COLORS: Record<string, string> = {
7+
'2': '\x1B[32m',
8+
'3': '\x1B[36m',
9+
'4': '\x1B[33m',
10+
'5': '\x1B[31m',
11+
}
12+
const ANSI_RESET = '\x1B[39m'
13+
14+
@Injectable()
15+
export class HttpLoggerMiddleware implements NestMiddleware {
16+
private readonly logger = new AppLogger('HTTP')
17+
private readonly isDev = process.env['NODE_ENV'] !== 'production'
18+
19+
private formatStatus(statusCode: number): string {
20+
const raw = statusCode.toString()
21+
if (!this.isDev) return raw
22+
const color = STATUS_COLORS[raw[0] ?? ''] ?? ''
23+
return `${color}${raw}${color ? ANSI_RESET : ''}`
24+
}
25+
26+
private writeLog(statusCode: number, message: string): void {
27+
if (statusCode >= 500) {
28+
this.logger.error(message)
29+
} else if (statusCode >= 400) {
30+
this.logger.warn(message)
31+
} else {
32+
this.logger.log(message)
33+
}
34+
}
35+
36+
use(req: Request, res: Response, next: NextFunction): void {
37+
const { method, originalUrl } = req
38+
const startTime = Date.now()
39+
40+
// Guard against double-logging when both finish and close fire
41+
let isLogged = false
42+
43+
const logRequest = (event: 'finish' | 'aborted'): void => {
44+
if (isLogged) return
45+
isLogged = true
46+
47+
const duration = Date.now() - startTime
48+
const { statusCode } = res
49+
const status = this.formatStatus(statusCode)
50+
51+
const message =
52+
event === 'aborted'
53+
? `${method} ${originalUrl} ABORTED - ${duration.toString()}ms`
54+
: `${method} ${originalUrl} ${status} - ${duration.toString()}ms`
55+
56+
if (event === 'aborted') {
57+
this.logger.warn(message)
58+
} else {
59+
this.writeLog(statusCode, message)
60+
}
61+
}
62+
63+
// finish: response fully sent (success or error handled by NestJS)
64+
res.on('finish', () => {
65+
logRequest('finish')
66+
})
67+
68+
// close: socket destroyed before response completed (client abort / timeout)
69+
res.on('close', () => {
70+
if (!res.writableEnded) {
71+
logRequest('aborted')
72+
}
73+
})
74+
75+
next()
76+
}
77+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { NestMiddleware } from '@nestjs/common'
2+
import { Injectable } from '@nestjs/common'
3+
import type { NextFunction, Request, Response } from 'express'
4+
import { randomUUID } from 'node:crypto'
5+
6+
/**
7+
* Propagates incoming `X-Request-ID` or generates a UUIDv4,
8+
* attaching it to both req/res for log correlation.
9+
*/
10+
@Injectable()
11+
export class RequestIdMiddleware implements NestMiddleware {
12+
use(req: Request, res: Response, next: NextFunction): void {
13+
const incoming = req.headers['x-request-id']
14+
const requestId =
15+
typeof incoming === 'string' && incoming.length > 0 ? incoming : randomUUID()
16+
17+
// Expose on the request object for downstream access (e.g. logging interceptors).
18+
req.headers['x-request-id'] = requestId
19+
res.setHeader('X-Request-ID', requestId)
20+
21+
next()
22+
}
23+
}

0 commit comments

Comments
 (0)