# 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<string, any>) {
    this.logger.info(message, {
      context: this.context,
      correlationId: this.correlationId,
      ...data,
    });
  }

  error(message: string, trace?: string, data?: Record<string, any>) {
    this.logger.error(message, {
      context: this.context,
      correlationId: this.correlationId,
      stack: trace,
      ...data,
    });
  }

  warn(message: string, data?: Record<string, any>) {
    this.logger.warn(message, {
      context: this.context,
      correlationId: this.correlationId,
      ...data,
    });
  }

  debug(message: string, data?: Record<string, any>) {
    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<UserEntity>,
  ) {
    this.logger.setContext(UserService.name);
  }

  async create(dto: CreateUserDto): Promise<UserEntity> {
    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<UserEntity> {
    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<any> {
    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<any> {
    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<string, any>;
  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<string, any>) {
    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<string, any>) {
    this.log('debug', message, context, data);
  }

  info(message: string, context?: string, data?: Record<string, any>) {
    this.log('info', message, context, data);
  }

  warn(message: string, context?: string, data?: Record<string, any>) {
    this.log('warn', message, context, data);
  }

  error(message: string, context?: string, data?: Record<string, any>) {
    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<Props, State> {
  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 || <div>Something went wrong</div>;
    }
    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<string, any>): Record<string, any> {
  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
