feat: log request body (masked, debug level) and drop headers from request logs

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
liuyun 2026-10-10 22:13:33 +08:00
parent 2c4f3008cf
commit c680fb9dc7
3 changed files with 58 additions and 0 deletions

View File

@ -6,6 +6,7 @@ import { AuthModule } from './auth/auth.module.js';
import { JwtAuthGuard } from './auth/jwt-auth.guard.js'; import { JwtAuthGuard } from './auth/jwt-auth.guard.js';
import { PermissionsGuard } from './auth/permissions.guard.js'; import { PermissionsGuard } from './auth/permissions.guard.js';
import { RequestContextInterceptor } from './auth/request-context.interceptor.js'; import { RequestContextInterceptor } from './auth/request-context.interceptor.js';
import { BodyLoggingInterceptor } from './logging/body-logging.interceptor.js';
import { TransformInterceptor } from './common/interceptors/transform.interceptor.js'; import { TransformInterceptor } from './common/interceptors/transform.interceptor.js';
import { AllExceptionsFilter } from './common/filters/http-exception.filter.js'; import { AllExceptionsFilter } from './common/filters/http-exception.filter.js';
import { TenantsModule } from './tenants/tenants.module.js'; import { TenantsModule } from './tenants/tenants.module.js';
@ -41,6 +42,7 @@ import { AppService } from './app.service.js';
{ provide: APP_GUARD, useClass: JwtAuthGuard }, { provide: APP_GUARD, useClass: JwtAuthGuard },
{ provide: APP_GUARD, useClass: PermissionsGuard }, { provide: APP_GUARD, useClass: PermissionsGuard },
{ provide: APP_INTERCEPTOR, useClass: RequestContextInterceptor }, { provide: APP_INTERCEPTOR, useClass: RequestContextInterceptor },
{ provide: APP_INTERCEPTOR, useClass: BodyLoggingInterceptor },
{ provide: APP_INTERCEPTOR, useClass: TransformInterceptor }, { provide: APP_INTERCEPTOR, useClass: TransformInterceptor },
{ provide: APP_FILTER, useClass: AllExceptionsFilter }, { provide: APP_FILTER, useClass: AllExceptionsFilter },
], ],

View File

@ -0,0 +1,44 @@
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { InjectPinoLogger, PinoLogger } from 'nestjs-pino';
const SENSITIVE_KEYS = new Set([
'password',
'passwordhash',
'oldpassword',
'newpassword',
'confirmpassword',
'refreshtoken',
'token',
'secret',
'authorization',
]);
/** 递归脱敏:命中的字段值替换为 ***。 */
function mask(value: unknown): unknown {
if (value === null || value === undefined) return value;
if (Array.isArray(value)) return value.map(mask);
if (typeof value === 'object') {
const out: Record<string, unknown> = {};
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
out[k] = SENSITIVE_KEYS.has(k.toLowerCase()) ? '***' : mask(v);
}
return out;
}
return value;
}
/** 记录请求 body(POST/PUT/PATCH/DELETE),敏感字段脱敏,级别 debug。 */
@Injectable()
export class BodyLoggingInterceptor implements NestInterceptor {
constructor(@InjectPinoLogger(BodyLoggingInterceptor.name) private readonly logger: PinoLogger) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const request = context.switchToHttp().getRequest();
const { method, url, body } = request;
if (body && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method) && Object.keys(body).length > 0) {
this.logger.debug({ method, url, body: mask(body) }, 'request body');
}
return next.handle();
}
}

View File

@ -47,6 +47,18 @@ export function createLoggerModule(): DynamicModule {
pinoHttp: { pinoHttp: {
level, level,
transport: { targets }, transport: { targets },
serializers: {
req: (req: any) => ({
id: req.id,
method: req.method,
url: req.url,
query: req.query,
params: req.params,
remoteAddress: req.remoteAddress,
remotePort: req.remotePort,
}),
res: (res: any) => ({ statusCode: res.statusCode }),
},
mixin() { mixin() {
const src = getCaller(); const src = getCaller();
return src ? { src } : {}; return src ? { src } : {};