import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common' import { Response } from 'express' import { debug, Debugger } from 'debug' import { isEmpty, omit } from 'lodash' import { Observable } from 'rxjs' import { catchError, tap } from 'rxjs/operators' import * as fastRedact from 'fast-redact' const HEADERS_OMITTED = ['accept-encoding', 'connection', 'content-length'] const ERROR_PROPERTIES_OMITTED = ['response', 'status'] const REDACT_PROPERTY_PATHS = ['input.credentials.password', '*.password'] @Injectable() export class LoggingInterceptor implements NestInterceptor { logger: Debugger redacter: fastRedact.redactFn constructor() { this.logger = debug('request') this.redacter = fastRedact({ paths: REDACT_PROPERTY_PATHS, }) } intercept(context: ExecutionContext, next: CallHandler): Observable { const ctx = context.switchToHttp() const req = ctx.getRequest() const res = ctx.getResponse() this.logger.extend('path')(req.path) if (req.headers) this.logger.extend('headers')(JSON.stringify(omit(req.headers, ...HEADERS_OMITTED))) if (req.body) this.logger.extend('body')(this.redacter(req.body)) if (req.params && !isEmpty(req.params)) this.logger.extend('params')(JSON.stringify(req.params)) return next.handle().pipe( tap(data => this.logResponse(data, res)), catchError(error => this.logError(error)), ) } private logResponse(data: any, res: Response): void { const responseLogger = this.logger.extend('response') if (data) responseLogger.extend('body')(this.redacter(data)) responseLogger.extend('status_code')(res.statusCode) } private logError(error: Error | any): any { if (!error) throw error const errorLogger = this.logger.extend('error') errorLogger.extend('message')(this.redacter(omit(error, ...ERROR_PROPERTIES_OMITTED))) errorLogger.extend('status_code')(error.status || 500) throw error } }