From 151411d5eb6fbf107819970dee1e4ee1e5fa02aa Mon Sep 17 00:00:00 2001 From: Daniel Job Gonsum Date: Thu, 29 Jan 2026 15:03:27 +0000 Subject: [PATCH] logger --- src/app.module.ts | 12 +- src/auth/auth.service.ts | 123 ++++++++----- src/common/logging/correlation-id.ts | 33 ++++ src/common/logging/logger.service.ts | 203 ++++++++++++++++------ src/common/logging/logging.config.ts | 159 +++++++++++++++++ src/common/logging/logging.interceptor.ts | 44 +++-- src/common/logging/logging.middleware.ts | 22 +-- src/common/logging/logging.module.ts | 34 ++-- src/main.ts | 5 +- 9 files changed, 483 insertions(+), 152 deletions(-) create mode 100644 src/common/logging/correlation-id.ts create mode 100644 src/common/logging/logging.config.ts diff --git a/src/app.module.ts b/src/app.module.ts index 5d9c3704..d56a77a6 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -4,6 +4,7 @@ import { ThrottlerModule } from '@nestjs/throttler'; import { ScheduleModule } from '@nestjs/schedule'; import { TerminusModule } from '@nestjs/terminus'; import { BullModule } from '@nestjs/bull'; +import { APP_INTERCEPTOR } from '@nestjs/core'; // Core & Database import { PrismaModule } from './database/prisma/prisma.module'; @@ -14,7 +15,7 @@ import valuationConfig from './config/valuation.config'; // Logging import { LoggingModule } from './common/logging/logging.module'; -import { LoggingMiddleware } from './common/logging/logging.middleware'; +import { LoggingInterceptor } from './common/logging/logging.interceptor'; // Redis import { RedisModule } from './common/services/redis.module'; @@ -84,13 +85,16 @@ import { AuthRateLimitMiddleware } from './auth/middleware/auth.middleware'; ValuationModule, DocumentsModule, ], + providers: [ + { + provide: APP_INTERCEPTOR, + useClass: LoggingInterceptor, + }, + ], }) export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer - // Correlation ID & structured logging for all routes - .apply(LoggingMiddleware) - .forRoutes('*') // Auth rate limiting .apply(AuthRateLimitMiddleware) .forRoutes('/auth*'); diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index a708989e..591697c6 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -10,22 +10,34 @@ import { CreateUserDto } from '../users/dto/create-user.dto'; import * as bcrypt from 'bcrypt'; import { RedisService } from '../common/services/redis.service'; import { v4 as uuidv4 } from 'uuid'; +import { StructuredLoggerService } from '../common/logging/logger.service'; @Injectable() export class AuthService { constructor( - private userService: UserService, - private jwtService: JwtService, - private configService: ConfigService, - private redisService: RedisService, - ) {} + private readonly userService: UserService, + private readonly jwtService: JwtService, + private readonly configService: ConfigService, + private readonly redisService: RedisService, + private readonly logger: StructuredLoggerService, + ) { + this.logger.setContext('AuthService'); + } async register(createUserDto: CreateUserDto) { - const user = await this.userService.create(createUserDto); - await this.sendVerificationEmail(user.id, user.email); - return { - message: 'User registered successfully. Please check your email for verification.', - }; + try { + const user = await this.userService.create(createUserDto); + await this.sendVerificationEmail(user.id, user.email); + this.logger.logAuth('User registration successful', { userId: user.id }); + return { + message: 'User registered successfully. Please check your email for verification.', + }; + } catch (error) { + this.logger.error('User registration failed', error.stack, { + email: createUserDto.email, + }); + throw error; + } } async login(credentials: { @@ -36,38 +48,49 @@ export class AuthService { }) { let user: any; - if (credentials.email && credentials.password) { - user = await this.validateUserByEmail( - credentials.email, - credentials.password, - ); - } else if (credentials.walletAddress) { - user = await this.validateUserByWallet( - credentials.walletAddress, - credentials.signature, - ); - } else { - throw new BadRequestException( - 'Email/password or wallet address/signature required', - ); - } + try { + if (credentials.email && credentials.password) { + user = await this.validateUserByEmail( + credentials.email, + credentials.password, + ); + } else if (credentials.walletAddress) { + user = await this.validateUserByWallet( + credentials.walletAddress, + credentials.signature, + ); + } else { + throw new BadRequestException( + 'Email/password or wallet address/signature required', + ); + } - if (!user) { - throw new UnauthorizedException('Invalid credentials'); - } + if (!user) { + this.logger.warn('Invalid login attempt', { email: credentials.email }); + throw new UnauthorizedException('Invalid credentials'); + } - return this.generateTokens(user); + this.logger.logAuth('User login successful', { userId: user.id }); + return this.generateTokens(user); + } catch (error) { + this.logger.error('User login failed', error.stack, { + email: credentials.email, + }); + throw error; + } } async validateUserByEmail(email: string, password: string): Promise { const user = await this.userService.findByEmail(email); if (!user || !user.password) { + this.logger.warn('Email validation failed: User not found', { email }); throw new UnauthorizedException('Invalid credentials'); } const isPasswordValid = await bcrypt.compare(password, user.password); if (!isPasswordValid) { + this.logger.warn('Email validation failed: Invalid password', { email }); throw new UnauthorizedException('Invalid credentials'); } @@ -89,6 +112,7 @@ export class AuthService { firstName: 'Web3', lastName: 'User', }); + this.logger.logAuth('New Web3 user created', { walletAddress }); } const { password: _, ...result } = user as any; @@ -103,6 +127,9 @@ export class AuthService { const user = await this.userService.findById(payload.sub); if (!user) { + this.logger.warn('Refresh token validation failed: User not found', { + userId: payload.sub, + }); throw new UnauthorizedException('User not found'); } @@ -110,35 +137,44 @@ export class AuthService { `refresh_token:${payload.sub}`, ); if (storedToken !== refreshToken) { + this.logger.warn('Refresh token validation failed: Invalid token', { + userId: payload.sub, + }); throw new UnauthorizedException('Invalid refresh token'); } + this.logger.logAuth('Token refreshed successfully', { userId: user.id }); return this.generateTokens(user); - } catch { + } catch (error) { + this.logger.error('Token refresh failed', error.stack); throw new UnauthorizedException('Invalid refresh token'); } } async logout(userId: string) { await this.redisService.del(`refresh_token:${userId}`); + this.logger.logAuth('User logged out successfully', { userId }); return { message: 'Logged out successfully' }; } async forgotPassword(email: string) { const user = await this.userService.findByEmail(email); if (!user) { + this.logger.log('Forgot password request for non-existent user', { email }); return { message: 'If email exists, a reset link has been sent' }; } const resetToken = uuidv4(); - const resetTokenExpiry = Date.now() + 3600000; + const resetTokenExpiry = Date.now() + 3600000; // 1 hour + // Save reset token and expiry in Redis await this.redisService.set( `password_reset:${resetToken}`, JSON.stringify({ userId: user.id, expiry: resetTokenExpiry }), ); await this.sendPasswordResetEmail(user.email, resetToken); + this.logger.log('Password reset email sent', { email }); return { message: 'If email exists, a reset link has been sent' }; } @@ -148,6 +184,7 @@ export class AuthService { ); if (!resetData) { + this.logger.warn('Invalid or expired password reset token received'); throw new BadRequestException('Invalid or expired reset token'); } @@ -155,12 +192,14 @@ export class AuthService { if (Date.now() > expiry) { await this.redisService.del(`password_reset:${resetToken}`); + this.logger.warn('Expired password reset token used', { userId }); throw new BadRequestException('Reset token has expired'); } await this.userService.updatePassword(userId, newPassword); await this.redisService.del(`password_reset:${resetToken}`); + this.logger.log('Password reset successfully', { userId }); return { message: 'Password reset successfully' }; } @@ -170,6 +209,7 @@ export class AuthService { ); if (!verificationData) { + this.logger.warn('Invalid or expired email verification token'); throw new BadRequestException('Invalid or expired verification token'); } @@ -177,6 +217,7 @@ export class AuthService { await this.userService.verifyUser(userId); await this.redisService.del(`email_verification:${token}`); + this.logger.log('Email verified successfully', { userId }); return { message: 'Email verified successfully' }; } @@ -185,22 +226,18 @@ export class AuthService { const accessToken = this.jwtService.sign(payload, { secret: this.configService.get('JWT_SECRET'), - expiresIn: this.configService.get( - 'JWT_EXPIRES_IN', - '15m', - ) as any, + expiresIn: this.configService.get('JWT_EXPIRES_IN', '15m') as any, }); const refreshToken = this.jwtService.sign(payload, { secret: this.configService.get('JWT_REFRESH_SECRET'), - expiresIn: this.configService.get( - 'JWT_REFRESH_EXPIRES_IN', - '7d', - ) as any, + expiresIn: this.configService.get('JWT_REFRESH_EXPIRES_IN', '7d') as any, }); this.redisService.set(`refresh_token:${user.id}`, refreshToken); + this.logger.debug('Generated new tokens for user', { userId: user.id }); + return { access_token: accessToken, refresh_token: refreshToken, @@ -215,16 +252,22 @@ export class AuthService { private async sendVerificationEmail(userId: string, email: string) { const verificationToken = uuidv4(); + + // Save token in Redis + const expiry = Date.now() + 3600000; // 1 hour await this.redisService.set( `email_verification:${verificationToken}`, - JSON.stringify({ userId }), + JSON.stringify({ userId, expiry }), ); + + this.logger.log(`Verification email sent to ${email}`, { userId }); console.log( `Verification email sent to ${email} with token: ${verificationToken}`, ); } private async sendPasswordResetEmail(email: string, resetToken: string) { + this.logger.log(`Password reset email sent to ${email}`); console.log( `Password reset email sent to ${email} with token: ${resetToken}`, ); diff --git a/src/common/logging/correlation-id.ts b/src/common/logging/correlation-id.ts new file mode 100644 index 00000000..362dcb81 --- /dev/null +++ b/src/common/logging/correlation-id.ts @@ -0,0 +1,33 @@ +import { createNamespace, Namespace } from 'cls-hooked'; + +/** + * Manages correlation IDs for request tracking using async_hooks (via cls-hooked) + */ +export const CORRELATION_ID_KEY = 'correlationId'; + +// Create a namespace for correlation IDs +const ns: Namespace = createNamespace('propchain-request'); + +/** + * Get the correlation ID for the current request context + */ +export const getCorrelationId = (): string | undefined => { + return ns.get(CORRELATION_ID_KEY); +}; + +/** + * Run a function within a request context and set the correlation ID + */ +export const withCorrelationId = (fn: () => void, correlationId: string): void => { + ns.run(() => { + ns.set(CORRELATION_ID_KEY, correlationId); + fn(); + }); +}; + +/** + * Get the underlying namespace + */ +export const getNamespace = (): Namespace => { + return ns; +}; diff --git a/src/common/logging/logger.service.ts b/src/common/logging/logger.service.ts index 7ef0e666..5787bfc6 100644 --- a/src/common/logging/logger.service.ts +++ b/src/common/logging/logger.service.ts @@ -1,80 +1,177 @@ -import { Injectable, LoggerService as NestLoggerService } from '@nestjs/common'; +import { Injectable, Scope, LoggerService as NestLoggerService } from '@nestjs/common'; import * as winston from 'winston'; -import 'winston-daily-rotate-file'; +import { ConfigService } from '@nestjs/config'; +import { createWinstonLogger, LOG_CATEGORIES } from './logging.config'; +import { getCorrelationId } from './correlation-id'; -@Injectable() +/** + * Structured logging service with Winston + * Provides centralized logging with correlation IDs, log levels, and sensitive data filtering + */ +@Injectable({ scope: Scope.TRANSIENT }) export class StructuredLoggerService implements NestLoggerService { private logger: winston.Logger; private context?: string; - private readonly sensitiveKeys = ['password', 'privatekey', 'token', 'secret', 'mnemonic']; - - constructor() { - this.logger = winston.createLogger({ - level: 'info', - format: winston.format.combine( - winston.format.timestamp(), - this.redactFormat()(), // Added extra () to execute the format - winston.format.json(), - ), - transports: [ - new winston.transports.Console({ - format: winston.format.combine( - winston.format.colorize(), - winston.format.simple(), - ), - }), - new winston.transports.DailyRotateFile({ - filename: 'logs/application-%DATE%.log', - datePattern: 'YYYY-MM-DD', - maxFiles: '14d', - }), - ], - }); + constructor(private readonly configService: ConfigService) { + const environment = this.configService.get('NODE_ENV', 'development'); + this.logger = createWinstonLogger(environment); } + /** + * Set the context (category) for log messages + */ setContext(context: string) { this.context = context; } - log(message: any, ...params: any[]) { - this.logger.info(message, { context: this.context, ...params }); + /** + * Get the current context + */ + getContext(): string | undefined { + return this.context; } - error(message: any, stack?: string) { - this.logger.error(message, { stack, context: this.context }); + /** + * Info level logging + */ + log(message: string, metadata?: Record) { + this.logger.info(message, this.buildLogMetadata(metadata)); } - warn(message: any, ...params: any[]) { - this.logger.warn(message, { context: this.context, ...params }); + /** + * Error level logging + */ + error(message: string, stack?: string, metadata?: Record) { + this.logger.error(message, { + ...this.buildLogMetadata(metadata), + stack, + category: LOG_CATEGORIES.ERROR, + }); } - debug(message: any, ...params: any[]) { - this.logger.debug(message, { context: this.context, ...params }); + /** + * Warning level logging + */ + warn(message: string, metadata?: Record) { + this.logger.warn(message, this.buildLogMetadata(metadata)); } - // Adding these to satisfy the new interface from the upstream merge - verbose(message: any, ...params: any[]) { - this.logger.verbose(message, { context: this.context, ...params }); + /** + * Debug level logging + */ + debug(message: string, metadata?: Record) { + this.logger.debug(message, this.buildLogMetadata(metadata)); } - fatal(message: any, ...params: any[]) { - this.logger.error(message, { context: this.context, fatal: true, ...params }); + /** + * Verbose level logging (detailed debug info) + */ + verbose(message: string, metadata?: Record) { + this.logger.verbose(message, this.buildLogMetadata(metadata)); } - private redactFormat() { - return winston.format((info) => { - const redact = (obj: any): any => { - if (typeof obj !== 'object' || obj === null) return obj; - const newObj = { ...obj }; - for (const key in newObj) { - if (this.sensitiveKeys.includes(key.toLowerCase())) { - newObj[key] = '[REDACTED]'; - } - } - return newObj; - }; - return redact(info); + /** + * Fatal error logging + */ + fatal(message: string, stack?: string, metadata?: Record) { + this.logger.error(message, { + ...this.buildLogMetadata(metadata), + stack, + level: 'fatal', + category: LOG_CATEGORIES.ERROR, }); } + + /** + * Log HTTP request + */ + logRequest(method: string, path: string, metadata?: Record) { + this.logger.info(`${method} ${path}`, { + ...this.buildLogMetadata(metadata), + category: LOG_CATEGORIES.HTTP, + type: 'REQUEST', + }); + } + + /** + * Log HTTP response + */ + logResponse(method: string, path: string, statusCode: number, duration: number, metadata?: Record) { + const logLevel = statusCode >= 400 ? 'warn' : 'info'; + this.logger[logLevel](`${method} ${path} ${statusCode} (${duration}ms)`, { + ...this.buildLogMetadata(metadata), + category: LOG_CATEGORIES.HTTP, + type: 'RESPONSE', + statusCode, + duration, + }); + } + + /** + * Log authentication event + */ + logAuth(action: string, metadata?: Record) { + this.logger.info(`Authentication: ${action}`, { + ...this.buildLogMetadata(metadata), + category: LOG_CATEGORIES.AUTH, + }); + } + + /** + * Log database operation + */ + logDatabase(operation: string, duration: number, metadata?: Record) { + this.logger.debug(`Database ${operation} (${duration}ms)`, { + ...this.buildLogMetadata(metadata), + category: LOG_CATEGORIES.DATABASE, + duration, + }); + } + + /** + * Log blockchain operation + */ + logBlockchain(operation: string, metadata?: Record) { + this.logger.info(`Blockchain: ${operation}`, { + ...this.buildLogMetadata(metadata), + category: LOG_CATEGORIES.BLOCKCHAIN, + }); + } + + /** + * Log transaction + */ + logTransaction(action: string, transactionId?: string, metadata?: Record) { + this.logger.info(`Transaction ${action}`, { + ...this.buildLogMetadata(metadata), + category: LOG_CATEGORIES.TRANSACTION, + transactionId, + }); + } + + /** + * Log validation error + */ + logValidation(field: string, error: string, metadata?: Record) { + this.logger.warn(`Validation Error: ${field}`, { + ...this.buildLogMetadata(metadata), + category: LOG_CATEGORIES.VALIDATION, + field, + error, + }); + } + + /** + * Build consistent metadata structure for all logs + */ + private buildLogMetadata(metadata?: Record): Record { + const correlationId = getCorrelationId(); + return { + correlationId, + context: this.context, + timestamp: new Date().toISOString(), + ...metadata, + }; + } } \ No newline at end of file diff --git a/src/common/logging/logging.config.ts b/src/common/logging/logging.config.ts new file mode 100644 index 00000000..5dae0dbd --- /dev/null +++ b/src/common/logging/logging.config.ts @@ -0,0 +1,159 @@ +import * as winston from 'winston'; +import 'winston-daily-rotate-file'; + +/** + * Logging configuration with structured JSON format, log rotation, and sensitive data filtering + */ + +const SENSITIVE_KEYS = [ + 'password', + 'privatekey', + 'private_key', + 'token', + 'secret', + 'mnemonic', + 'seed', + 'apikey', + 'api_key', + 'authorization', + 'auth', + 'creditcard', + 'ssn', + 'pin', +]; + +/** + * Filter sensitive data from log objects + */ +export const filterSensitiveData = (obj: any): any => { + if (obj === null || obj === undefined) return obj; + + if (typeof obj !== 'object') return obj; + + if (Array.isArray(obj)) { + return obj.map(item => filterSensitiveData(item)); + } + + const newObj = { ...obj }; + + for (const key in newObj) { + const lowerKey = key.toLowerCase(); + + if (SENSITIVE_KEYS.some(sensitiveKey => lowerKey.includes(sensitiveKey))) { + newObj[key] = '[REDACTED]'; + } else if (typeof newObj[key] === 'object') { + newObj[key] = filterSensitiveData(newObj[key]); + } + } + + return newObj; +}; + +/** + * Custom format for sensitive data redaction + */ +const redactFormat = () => { + return winston.format((info) => { + // Redact common sensitive fields + const redactedInfo = { ...info }; + + // Redact nested data in 'meta' or 'data' fields + if (redactedInfo.meta) { + redactedInfo.meta = filterSensitiveData(redactedInfo.meta); + } + if (redactedInfo.data) { + redactedInfo.data = filterSensitiveData(redactedInfo.data); + } + if (redactedInfo.body) { + redactedInfo.body = filterSensitiveData(redactedInfo.body); + } + + // Redact direct properties + for (const key in redactedInfo) { + const lowerKey = key.toLowerCase(); + if (SENSITIVE_KEYS.some(sensitiveKey => lowerKey.includes(sensitiveKey))) { + redactedInfo[key] = '[REDACTED]'; + } + } + + return redactedInfo; + }); +}; + +/** + * Create Winston logger with structured JSON format + */ +export const createWinstonLogger = (environment: string): winston.Logger => { + const isProduction = environment === 'production'; + + return winston.createLogger({ + level: process.env.LOG_LEVEL || (isProduction ? 'info' : 'debug'), + format: winston.format.combine( + winston.format.timestamp({ + format: 'YYYY-MM-DD HH:mm:ss.SSS', + }), + redactFormat()(), + winston.format.json(), + ), + defaultMeta: { service: 'propchain-api', environment }, + transports: [ + // Console transport (always enabled) + new winston.transports.Console({ + format: winston.format.combine( + winston.format.colorize(), + winston.format.printf(({ timestamp, level, message, ...meta }) => { + const metaStr = Object.keys(meta).length ? JSON.stringify(meta, null, 2) : ''; + return `[${timestamp}] ${level.toUpperCase()}: ${message} ${metaStr}`; + }), + ), + }), + + // Error logs (separate file) + new winston.transports.DailyRotateFile({ + filename: 'logs/error-%DATE%.log', + datePattern: 'YYYY-MM-DD', + level: 'error', + maxFiles: '30d', + maxSize: '20m', + zippedArchive: true, + }), + + // Combined logs (all levels) + new winston.transports.DailyRotateFile({ + filename: 'logs/application-%DATE%.log', + datePattern: 'YYYY-MM-DD', + maxFiles: '14d', + maxSize: '20m', + zippedArchive: true, + }), + ], + }); +}; + +/** + * Log levels configuration + */ +export const LOG_LEVELS = { + ERROR: 'error', + WARN: 'warn', + INFO: 'info', + DEBUG: 'debug', + VERBOSE: 'verbose', +}; + +/** + * Log categories for consistent tagging + */ +export const LOG_CATEGORIES = { + HTTP: 'HTTP', + AUTH: 'AUTH', + DATABASE: 'DATABASE', + BLOCKCHAIN: 'BLOCKCHAIN', + TRANSACTION: 'TRANSACTION', + PROPERTY: 'PROPERTY', + USER: 'USER', + VALIDATION: 'VALIDATION', + ERROR: 'ERROR', + CACHE: 'CACHE', + EXTERNAL_API: 'EXTERNAL_API', +}; diff --git a/src/common/logging/logging.interceptor.ts b/src/common/logging/logging.interceptor.ts index 04f942fc..eb97ad4f 100644 --- a/src/common/logging/logging.interceptor.ts +++ b/src/common/logging/logging.interceptor.ts @@ -1,8 +1,16 @@ -import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, +} from '@nestjs/common'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; import { StructuredLoggerService } from './logger.service'; +/** + * Interceptor for logging incoming requests and outgoing responses + */ @Injectable() export class LoggingInterceptor implements NestInterceptor { constructor(private readonly logger: StructuredLoggerService) { @@ -11,28 +19,32 @@ export class LoggingInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable { const request = context.switchToHttp().getRequest(); - const { method, url } = request; - // Extract correlationId safely - const correlationId = request['correlationId']; - const now = Date.now(); + const { method, url, headers, body } = request; + const requestStartTime = Date.now(); + + // Log incoming request + this.logger.logRequest(method, url, { + userAgent: headers['user-agent'], + body, + }); return next.handle().pipe( tap({ next: () => { const response = context.switchToHttp().getResponse(); - const duration = Date.now() - now; - - this.logger.log( - `${method} ${url} ${response.statusCode} - ${duration}ms`, - { correlationId } - ); + const { statusCode } = response; + const duration = Date.now() - requestStartTime; + + this.logger.logResponse(method, url, statusCode, duration); }, - error: (err: any) => { - const duration = Date.now() - now; - // FIXED: Only passing 2 arguments to match your LoggerService + error: (error: any) => { + const duration = Date.now() - requestStartTime; this.logger.error( - `${method} ${url} Failed - ${duration}ms | Error: ${err.message}`, - err.stack + `${method} ${url} Failed in ${duration}ms`, + error.stack, + { + error, + }, ); }, }), diff --git a/src/common/logging/logging.middleware.ts b/src/common/logging/logging.middleware.ts index a303826a..6aaf04dc 100644 --- a/src/common/logging/logging.middleware.ts +++ b/src/common/logging/logging.middleware.ts @@ -1,28 +1,20 @@ import { Injectable, NestMiddleware } from '@nestjs/common'; import { Request, Response, NextFunction } from 'express'; import { v4 as uuidv4 } from 'uuid'; +import { withCorrelationId } from './correlation-id'; /** - * This middleware acts as a gatekeeper. - * It assigns a unique "Correlation ID" to every incoming request. + * Middleware to generate and manage correlation IDs for request tracking + * Assigns a unique correlation ID to every incoming request */ @Injectable() export class LoggingMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { - // 1. Check if the request already has an ID from the frontend, - // otherwise, generate a brand new unique ID (UUID). const correlationId = (req.headers['x-correlation-id'] as string) || uuidv4(); - - // 2. Attach this ID to the 'req' (request) object so our other - // files (Services and Interceptors) can see it. - req['correlationId'] = correlationId; - - // 3. Send the ID back to the client in the response header. - // This is helpful for debugging if the user reports an error. res.setHeader('x-correlation-id', correlationId); - - // 4. Important: Tell NestJS to move to the next step in the process. - // If you forget this, the request will hang forever! - next(); + + withCorrelationId(() => { + next(); + }, correlationId); } } \ No newline at end of file diff --git a/src/common/logging/logging.module.ts b/src/common/logging/logging.module.ts index bb3a6604..aac725af 100644 --- a/src/common/logging/logging.module.ts +++ b/src/common/logging/logging.module.ts @@ -1,26 +1,16 @@ -import { Module, Global } from '@nestjs/common'; -import { RedisModule } from '@liaoliaots/nestjs-redis'; -import { ConfigModule, ConfigService } from '@nestjs/config'; +import { Module, Global, MiddlewareConsumer, NestModule } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; import { StructuredLoggerService } from './logger.service'; -import { RedisService } from '../services/redis.service'; +import { LoggingMiddleware } from './logging.middleware'; -@Global() // This means you don't have to import it in every other file +@Global() @Module({ - imports: [ - RedisModule.forRootAsync({ - imports: [ConfigModule], - useFactory: (configService: ConfigService) => ({ - config: { - host: configService.get('REDIS_HOST', 'localhost'), - port: configService.get('REDIS_PORT', 6379), - password: configService.get('REDIS_PASSWORD'), - db: configService.get('REDIS_DB', 0), - }, - }), - inject: [ConfigService], - }), - ], - providers: [StructuredLoggerService, RedisService], - exports: [StructuredLoggerService, RedisService], + imports: [ConfigModule], + providers: [StructuredLoggerService], + exports: [StructuredLoggerService], }) -export class LoggingModule {} \ No newline at end of file +export class LoggingModule implements NestModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(LoggingMiddleware).forRoutes('*'); + } +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 6239d370..75abea27 100644 --- a/src/main.ts +++ b/src/main.ts @@ -23,8 +23,9 @@ async function bootstrap() { const configService = app.get(ConfigService); // Use our new StructuredLoggerService - const logger = app.get(StructuredLoggerService); - app.useLogger(logger); + const logger = await app.resolve(StructuredLoggerService); + app.useLogger(logger); + // Security middleware app.use(helmet());