# PATRON DE LOGGING **Version:** 1.0.0 **Fecha:** 2025-12-08 **Prioridad:** RECOMENDADA - Seguir para consistencia **Sistema:** SIMCO + CAPVED --- ## PROPOSITO Definir patrones estandarizados de logging para todas las capas del sistema, asegurando trazabilidad, debugging efectivo y monitoreo en produccion. --- ## 1. NIVELES DE LOG ``` ╔══════════════════════════════════════════════════════════════════════╗ ║ NIVELES DE LOG (de menor a mayor severidad) ║ ╠══════════════════════════════════════════════════════════════════════╣ ║ ║ ║ TRACE → Detalle extremo (solo desarrollo) ║ ║ DEBUG → Informacion de debugging ║ ║ INFO → Eventos normales del sistema ║ ║ WARN → Situaciones anormales pero manejables ║ ║ ERROR → Errores que afectan funcionalidad ║ ║ FATAL → Errores criticos que detienen el sistema ║ ║ ║ ╚══════════════════════════════════════════════════════════════════════╝ ``` ### Cuando Usar Cada Nivel | Nivel | Uso | Ejemplo | |-------|-----|---------| | **TRACE** | Flujo detallado de ejecucion | `Entering method findById with id=123` | | **DEBUG** | Variables, estados internos | `User cache hit: userId=123` | | **INFO** | Eventos de negocio normales | `Order created: orderId=456` | | **WARN** | Situaciones inesperadas no criticas | `Retry attempt 2/3 for external API` | | **ERROR** | Errores que necesitan atencion | `Failed to process payment: timeout` | | **FATAL** | Sistema no puede continuar | `Database connection lost` | --- ## 2. ESTRUCTURA DE LOG ### Formato Estandar ```typescript { timestamp: "2025-12-08T10:30:45.123Z", // ISO 8601 level: "INFO", // Nivel context: "UserService", // Clase/modulo origen message: "User created successfully", // Mensaje descriptivo correlationId: "req-abc123", // ID de request/transaccion userId: "user-456", // Usuario (si aplica) data: { // Datos adicionales email: "user@example.com", action: "create" }, duration: 45 // Duracion en ms (si aplica) } ``` ### Campos Obligatorios | Campo | Descripcion | Siempre | |-------|-------------|---------| | `timestamp` | Fecha/hora ISO 8601 | SI | | `level` | Nivel del log | SI | | `context` | Origen del log | SI | | `message` | Descripcion del evento | SI | | `correlationId` | ID para trazar request | EN PRODUCCION | ### Campos Opcionales Recomendados | Campo | Cuando Usar | |-------|-------------| | `userId` | Cuando hay usuario autenticado | | `data` | Datos relevantes al evento | | `duration` | Para operaciones medibles | | `error` | Cuando es log de error | | `stack` | Stack trace en errores | --- ## 3. BACKEND (NestJS) ### Configuracion del Logger ```typescript // src/shared/logger/logger.service.ts import { Injectable, LoggerService, Scope } from '@nestjs/common'; import { Logger } from 'winston'; @Injectable({ scope: Scope.TRANSIENT }) export class AppLogger implements LoggerService { private context: string; private correlationId: string; constructor(private readonly logger: Logger) {} setContext(context: string) { this.context = context; } setCorrelationId(correlationId: string) { this.correlationId = correlationId; } log(message: string, data?: Record) { this.logger.info(message, { context: this.context, correlationId: this.correlationId, ...data, }); } error(message: string, trace?: string, data?: Record) { this.logger.error(message, { context: this.context, correlationId: this.correlationId, stack: trace, ...data, }); } warn(message: string, data?: Record) { this.logger.warn(message, { context: this.context, correlationId: this.correlationId, ...data, }); } debug(message: string, data?: Record) { this.logger.debug(message, { context: this.context, correlationId: this.correlationId, ...data, }); } } ``` ### Configuracion Winston ```typescript // src/shared/logger/winston.config.ts import * as winston from 'winston'; const { combine, timestamp, json, printf, colorize } = winston.format; // Formato para desarrollo const devFormat = combine( colorize(), timestamp(), printf(({ timestamp, level, message, context, ...meta }) => { return `${timestamp} [${context}] ${level}: ${message} ${ Object.keys(meta).length ? JSON.stringify(meta) : '' }`; }), ); // Formato para produccion (JSON estructurado) const prodFormat = combine( timestamp(), json(), ); export const winstonConfig: winston.LoggerOptions = { level: process.env.LOG_LEVEL || 'info', format: process.env.NODE_ENV === 'production' ? prodFormat : devFormat, transports: [ new winston.transports.Console(), // En produccion: agregar transports adicionales // new winston.transports.File({ filename: 'error.log', level: 'error' }), ], }; ``` ### Uso en Service ```typescript // src/modules/user/services/user.service.ts @Injectable() export class UserService { constructor( private readonly logger: AppLogger, private readonly repository: Repository, ) { this.logger.setContext(UserService.name); } async create(dto: CreateUserDto): Promise { this.logger.log('Creating new user', { email: dto.email }); try { const user = await this.repository.save(dto); this.logger.log('User created successfully', { userId: user.id, email: user.email, }); return user; } catch (error) { this.logger.error('Failed to create user', error.stack, { email: dto.email, errorCode: error.code, }); throw error; } } async findById(id: string): Promise { this.logger.debug('Finding user by ID', { userId: id }); const startTime = Date.now(); const user = await this.repository.findOne({ where: { id } }); const duration = Date.now() - startTime; if (!user) { this.logger.warn('User not found', { userId: id, duration }); throw new NotFoundException(`User ${id} not found`); } this.logger.debug('User found', { userId: id, duration }); return user; } } ``` ### Interceptor para Correlation ID ```typescript // src/shared/interceptors/correlation.interceptor.ts import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable } from 'rxjs'; import { v4 as uuidv4 } from 'uuid'; @Injectable() export class CorrelationInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable { const request = context.switchToHttp().getRequest(); // Usar header existente o generar nuevo const correlationId = request.headers['x-correlation-id'] || uuidv4(); // Guardar en request para uso posterior request.correlationId = correlationId; // Agregar a response headers const response = context.switchToHttp().getResponse(); response.setHeader('x-correlation-id', correlationId); return next.handle(); } } ``` ### Interceptor de Logging de Requests ```typescript // src/shared/interceptors/logging.interceptor.ts import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; @Injectable() export class LoggingInterceptor implements NestInterceptor { constructor(private readonly logger: AppLogger) { this.logger.setContext('HTTP'); } intercept(context: ExecutionContext, next: CallHandler): Observable { const request = context.switchToHttp().getRequest(); const { method, url, correlationId, user } = request; const startTime = Date.now(); this.logger.log('Incoming request', { method, url, correlationId, userId: user?.id, }); return next.handle().pipe( tap({ next: () => { const duration = Date.now() - startTime; this.logger.log('Request completed', { method, url, correlationId, duration, statusCode: context.switchToHttp().getResponse().statusCode, }); }, error: (error) => { const duration = Date.now() - startTime; this.logger.error('Request failed', error.stack, { method, url, correlationId, duration, errorMessage: error.message, }); }, }), ); } } ``` --- ## 4. FRONTEND (React) ### Logger Service ```typescript // src/shared/services/logger.service.ts type LogLevel = 'debug' | 'info' | 'warn' | 'error'; interface LogEntry { timestamp: string; level: LogLevel; message: string; context?: string; data?: Record; userId?: string; } class LoggerService { private isDev = process.env.NODE_ENV === 'development'; private userId: string | null = null; setUserId(userId: string | null) { this.userId = userId; } private log(level: LogLevel, message: string, context?: string, data?: Record) { const entry: LogEntry = { timestamp: new Date().toISOString(), level, message, context, data, userId: this.userId || undefined, }; // En desarrollo: console if (this.isDev) { const consoleMethod = level === 'error' ? console.error : level === 'warn' ? console.warn : level === 'debug' ? console.debug : console.log; consoleMethod(`[${entry.context}] ${message}`, data || ''); } // En produccion: enviar a servicio de logging if (!this.isDev && (level === 'error' || level === 'warn')) { this.sendToServer(entry); } } private async sendToServer(entry: LogEntry) { try { await fetch('/api/logs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(entry), }); } catch { // Silently fail - don't cause more errors } } debug(message: string, context?: string, data?: Record) { this.log('debug', message, context, data); } info(message: string, context?: string, data?: Record) { this.log('info', message, context, data); } warn(message: string, context?: string, data?: Record) { this.log('warn', message, context, data); } error(message: string, context?: string, data?: Record) { this.log('error', message, context, data); } } export const logger = new LoggerService(); ``` ### Uso en Componentes ```typescript // src/apps/web/pages/UsersPage.tsx import { logger } from '@/shared/services/logger.service'; export const UsersPage = () => { const { data, error, isLoading } = useUsers(); useEffect(() => { logger.info('Users page mounted', 'UsersPage'); }, []); useEffect(() => { if (error) { logger.error('Failed to load users', 'UsersPage', { errorMessage: error.message, }); } }, [error]); const handleDelete = async (userId: string) => { logger.info('Deleting user', 'UsersPage', { userId }); try { await deleteUser(userId); logger.info('User deleted successfully', 'UsersPage', { userId }); } catch (err) { logger.error('Failed to delete user', 'UsersPage', { userId, error: err.message, }); } }; return (/* ... */); }; ``` ### Error Boundary con Logging ```typescript // src/shared/components/ErrorBoundary.tsx import { Component, ErrorInfo, ReactNode } from 'react'; import { logger } from '@/shared/services/logger.service'; interface Props { children: ReactNode; fallback?: ReactNode; } interface State { hasError: boolean; } export class ErrorBoundary extends Component { state = { hasError: false }; static getDerivedStateFromError(): State { return { hasError: true }; } componentDidCatch(error: Error, errorInfo: ErrorInfo) { logger.error('React error boundary caught error', 'ErrorBoundary', { error: error.message, stack: error.stack, componentStack: errorInfo.componentStack, }); } render() { if (this.state.hasError) { return this.props.fallback ||
Something went wrong
; } return this.props.children; } } ``` --- ## 5. DATABASE ### Logging de Queries (TypeORM) ```typescript // src/shared/config/typeorm.config.ts import { TypeOrmModuleOptions } from '@nestjs/typeorm'; export const typeOrmConfig: TypeOrmModuleOptions = { // ... otras opciones logging: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], logger: 'advanced-console', // o custom logger maxQueryExecutionTime: 1000, // Log queries > 1s }; ``` ### Custom Query Logger ```typescript // src/shared/logger/typeorm.logger.ts import { Logger as TypeOrmLogger } from 'typeorm'; import { AppLogger } from './logger.service'; export class CustomTypeOrmLogger implements TypeOrmLogger { constructor(private readonly logger: AppLogger) { this.logger.setContext('TypeORM'); } logQuery(query: string, parameters?: any[]) { this.logger.debug('Query executed', { query: query.substring(0, 500), // Truncar queries largas parameters, }); } logQueryError(error: string, query: string, parameters?: any[]) { this.logger.error('Query failed', undefined, { error, query: query.substring(0, 500), parameters, }); } logQuerySlow(time: number, query: string, parameters?: any[]) { this.logger.warn('Slow query detected', { duration: time, query: query.substring(0, 500), parameters, }); } logSchemaBuild(message: string) { this.logger.info(message); } logMigration(message: string) { this.logger.info(message); } log(level: 'log' | 'info' | 'warn', message: any) { if (level === 'warn') { this.logger.warn(message); } else { this.logger.log(message); } } } ``` --- ## 6. QUE LOGUEAR Y QUE NO ### SI Loguear ``` ✅ Inicio/fin de operaciones importantes ✅ Errores y excepciones (con contexto) ✅ Eventos de autenticacion (login, logout, failed attempts) ✅ Operaciones de negocio criticas (pagos, cambios de estado) ✅ Llamadas a APIs externas (request/response resumido) ✅ Queries lentas (>1s) ✅ Warnings de recursos (memoria, conexiones) ✅ Cambios de configuracion en runtime ``` ### NO Loguear ``` ❌ Datos sensibles (passwords, tokens, tarjetas) ❌ PII sin necesidad (emails completos, nombres) ❌ Cada iteracion de loops ❌ Contenido completo de requests/responses grandes ❌ Logs de debug en produccion ❌ Informacion redundante ❌ Stack traces en logs INFO/DEBUG ``` ### Sanitizacion de Datos Sensibles ```typescript // src/shared/utils/log-sanitizer.ts const SENSITIVE_FIELDS = ['password', 'token', 'secret', 'authorization', 'credit_card']; export function sanitizeForLogging(data: Record): Record { const sanitized = { ...data }; for (const key of Object.keys(sanitized)) { if (SENSITIVE_FIELDS.some(field => key.toLowerCase().includes(field))) { sanitized[key] = '[REDACTED]'; } else if (typeof sanitized[key] === 'object' && sanitized[key] !== null) { sanitized[key] = sanitizeForLogging(sanitized[key]); } } return sanitized; } // Uso this.logger.log('User login attempt', sanitizeForLogging({ email: dto.email, password: dto.password, // Se convierte en [REDACTED] })); ``` --- ## 7. CONFIGURACION POR AMBIENTE ```typescript // src/shared/config/logger.config.ts export const loggerConfig = { development: { level: 'debug', format: 'pretty', includeTimestamp: true, colorize: true, }, staging: { level: 'info', format: 'json', includeTimestamp: true, colorize: false, }, production: { level: 'warn', format: 'json', includeTimestamp: true, colorize: false, // Enviar a servicio externo externalService: { enabled: true, endpoint: process.env.LOG_ENDPOINT, }, }, }; ``` --- ## 8. CHECKLIST DE LOGGING ``` Antes de hacer deploy: [ ] Logs no contienen datos sensibles [ ] Nivel de log apropiado para ambiente [ ] Errores tienen contexto suficiente para debug [ ] Correlation ID implementado [ ] Queries lentas se detectan [ ] Error boundary implementado en frontend En cada Service nuevo: [ ] Logger inyectado y contexto configurado [ ] Operaciones principales logueadas [ ] Errores logueados con stack trace [ ] Tiempos de operaciones criticas medidos ``` --- **Version:** 1.0.0 | **Sistema:** SIMCO | **Tipo:** Patron de Codigo