workspace-v1/orchestration/patrones/PATRON-LOGGING.md
rckrdmrd 66161b1566 feat: Workspace-v1 complete migration with NEXUS v3.4
Sistema NEXUS v3.4 migrado con:

Estructura principal:
- core/orchestration: Sistema SIMCO + CAPVED (27 directivas, 28 perfiles)
- core/catalog: Catalogo de funcionalidades reutilizables
- shared/knowledge-base: Base de conocimiento compartida
- devtools/scripts: Herramientas de desarrollo
- control-plane/registries: Control de servicios y CI/CD
- orchestration/: Configuracion de orquestacion de agentes

Proyectos incluidos (11):
- gamilit (submodule -> GitHub)
- trading-platform (OrbiquanTIA)
- erp-suite con 5 verticales:
  - erp-core, construccion, vidrio-templado
  - mecanicas-diesel, retail, clinicas
- betting-analytics
- inmobiliaria-analytics
- platform_marketing_content
- pos-micro, erp-basico

Configuracion:
- .gitignore completo para Node.js/Python/Docker
- gamilit como submodule (git@github.com:rckrdmrd/gamilit-workspace.git)
- Sistema de puertos estandarizado (3005-3199)

Generated with NEXUS v3.4 Migration System
EPIC-010: Configuracion Git y Repositorios
2026-01-04 03:37:42 -06:00

17 KiB

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

{
  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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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)

// 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

// 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

// 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

// 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