Analysis and Documentation: - Add ANALISIS-ALINEACION-WORKSPACE-2025-12-08.md with comprehensive gap analysis - Document SIMCO v3.2 system with 20+ directives - Identify alignment gaps between orchestration and projects New SaaS Products Structure: - Create apps/products/pos-micro/ - Ultra basic POS (~100 MXN/month) - Target: Mexican informal market (street vendors, small stores) - Features: Offline-first PWA, WhatsApp bot, minimal DB (~10 tables) - Create apps/products/erp-basico/ - Austere ERP (~300-500 MXN/month) - Target: SMBs needing full ERP without complexity - Features: Inherits from erp-core, modular pricing SaaS Layer: - Create apps/saas/ structure (billing, portal, admin, onboarding) - Add README.md and CONTEXTO-SAAS.md documentation Vertical Alignment: - Verify HERENCIA-ERP-CORE.md exists in all verticals - Add HERENCIA-SPECS-CORE.md to verticals - Update orchestration inventories Updates: - Update WORKSPACE-STATUS.md with new products and analysis - Update suite inventories with new structure 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
17 KiB
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