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
664 lines
17 KiB
Markdown
664 lines
17 KiB
Markdown
# 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
|