codigo0/docs/LOGS_AUDITORIA.md
planetazuzu 5d7a6500fe refactor: Fase 1 - Clean Architecture, refactorización modular y eliminación de duplicidades
-  Ticket 1.1: Estructura Clean Architecture en backend
-  Ticket 1.2: Schemas Zod compartidos
-  Ticket 1.3: Refactorización drugs.ts (1362 → 8 archivos modulares)
-  Ticket 1.4: Refactorización procedures.ts (3583 → 6 archivos modulares)
-  Ticket 1.5: Eliminación de duplicidades (~50 líneas)

Cambios principales:
- Creada estructura Clean Architecture en backend/src/
- Schemas Zod compartidos en backend/src/shared/schemas/
- Refactorización modular de drugs y procedures
- Utilidades genéricas en src/utils/ (filter, validation)
- Eliminados scripts obsoletos y documentación antigua
- Corregidos errores: QueryClient, import test-error-handling
- Build verificado y funcionando correctamente
2026-01-25 21:09:47 +01:00

11 KiB

📊 Sistema de Logs y Auditoría

🎯 Objetivo

Definir qué información registrar en logs y qué datos de relevancia médica almacenar (sin datos sensibles del paciente).


🔒 Principio: NO Datos Sensibles

NO Registrar:

  • Nombres de pacientes
  • DNI/NIE de pacientes
  • Direcciones de pacientes
  • Teléfonos de pacientes
  • Historial médico completo
  • Cualquier información que identifique a un paciente

SÍ Registrar:

  • Acciones del sistema
  • Cambios en contenido médico
  • Validaciones realizadas
  • Errores críticos
  • Estadísticas agregadas (sin identificar pacientes)
  • Metadatos de operaciones

📋 Tipos de Logs

1. Audit Logs (Auditoría)

Propósito: Rastrear quién hizo qué y cuándo

interface AuditLog {
  id: string;
  userId: string;
  userRole: string;
  action: 'create' | 'update' | 'delete' | 'submit' | 'approve' | 'reject' | 'publish';
  entityType: 'content' | 'drug' | 'protocol' | 'glossary' | 'media';
  entityId: string;
  changes?: {
    field: string;
    oldValue: unknown;
    newValue: unknown;
  }[];
  metadata?: Record<string, unknown>;
  ipAddress?: string;
  userAgent?: string;
  timestamp: Date;
}

Ejemplos:

// Crear contenido
{
  userId: 'user-123',
  userRole: 'editor',
  action: 'create',
  entityType: 'content',
  entityId: 'rcp-adulto-svb',
  timestamp: '2025-01-25T10:00:00Z'
}

// Aprobar contenido
{
  userId: 'user-456',
  userRole: 'reviewer',
  action: 'approve',
  entityType: 'content',
  entityId: 'rcp-adulto-svb',
  changes: [
    { field: 'status', oldValue: 'in_review', newValue: 'approved' }
  ],
  metadata: {
    reviewId: 'review-789',
    clinicalSources: ['ERC Guidelines 2021']
  },
  timestamp: '2025-01-25T11:00:00Z'
}

2. Validation Logs (Validaciones)

Propósito: Registrar todas las validaciones médicas realizadas

interface ValidationLog {
  id: string;
  validationType: 'dose' | 'protocol' | 'content';
  contentId?: string;
  drugId?: string;
  protocolId?: string;
  result: 'valid' | 'invalid' | 'warning';
  errors: string[];
  warnings: string[];
  context: {
    ageGroup?: string;
    weightRange?: string;
    route?: string;
  };
  validatedBy?: string;
  timestamp: Date;
}

Ejemplos:

// Validación de dosis
{
  validationType: 'dose',
  drugId: 'adrenalina',
  result: 'invalid',
  errors: ['Dosis excede máximo seguro'],
  warnings: [],
  context: {
    ageGroup: 'adulto',
    weightRange: '70-80kg',
    route: 'IV'
  },
  timestamp: '2025-01-25T12:00:00Z'
}

// Validación de protocolo
{
  validationType: 'protocol',
  protocolId: 'rcp-adulto-svb',
  result: 'warning',
  errors: [],
  warnings: ['Paso crítico 4 no ejecutado'],
  timestamp: '2025-01-25T13:00:00Z'
}

3. Error Logs (Errores)

Propósito: Registrar errores críticos del sistema

interface ErrorLog {
  id: string;
  level: 'error' | 'critical';
  errorCode: string;
  errorMessage: string;
  stack?: string;
  context: {
    userId?: string;
    contentId?: string;
    action?: string;
  };
  timestamp: Date;
}

Ejemplos:

// Error crítico de dosis
{
  level: 'critical',
  errorCode: 'DOSE_LETHAL',
  errorMessage: 'Dosis letal detectada: 10mg de adrenalina IV',
  context: {
    userId: 'user-123',
    drugId: 'adrenalina',
    action: 'administer_drug'
  },
  timestamp: '2025-01-25T14:00:00Z'
}

4. Performance Logs (Rendimiento)

Propósito: Monitorear rendimiento del sistema

interface PerformanceLog {
  endpoint: string;
  method: string;
  duration: number; // ms
  statusCode: number;
  timestamp: Date;
}

🗄️ Esquema de Base de Datos

-- Tabla de audit logs (ya existe, mejorar)
CREATE TABLE tes_content.audit_logs (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  user_id UUID NOT NULL REFERENCES tes_content.users(id),
  user_role VARCHAR(50) NOT NULL,
  action VARCHAR(50) NOT NULL,
  entity_type VARCHAR(50) NOT NULL,
  entity_id VARCHAR(100) NOT NULL,
  changes JSONB,
  metadata JSONB,
  ip_address INET,
  user_agent TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  
  CONSTRAINT chk_action CHECK (
    action IN ('create', 'update', 'delete', 'submit', 'approve', 'reject', 'publish', 'archive')
  )
);

CREATE INDEX idx_audit_logs_user ON tes_content.audit_logs(user_id);
CREATE INDEX idx_audit_logs_entity ON tes_content.audit_logs(entity_type, entity_id);
CREATE INDEX idx_audit_logs_action ON tes_content.audit_logs(action);
CREATE INDEX idx_audit_logs_created ON tes_content.audit_logs(created_at DESC);

-- Tabla de validation logs
CREATE TABLE tes_content.validation_logs (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  validation_type VARCHAR(50) NOT NULL,
  content_id VARCHAR(100),
  drug_id UUID,
  protocol_id VARCHAR(100),
  result VARCHAR(20) NOT NULL,
  errors TEXT[],
  warnings TEXT[],
  context JSONB,
  validated_by UUID REFERENCES tes_content.users(id),
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  
  CONSTRAINT chk_validation_type CHECK (
    validation_type IN ('dose', 'protocol', 'content')
  ),
  CONSTRAINT chk_result CHECK (
    result IN ('valid', 'invalid', 'warning')
  )
);

CREATE INDEX idx_validation_logs_type ON tes_content.validation_logs(validation_type);
CREATE INDEX idx_validation_logs_result ON tes_content.validation_logs(result);
CREATE INDEX idx_validation_logs_created ON tes_content.validation_logs(created_at DESC);

-- Tabla de error logs
CREATE TABLE tes_content.error_logs (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  level VARCHAR(20) NOT NULL,
  error_code VARCHAR(100) NOT NULL,
  error_message TEXT NOT NULL,
  stack TEXT,
  context JSONB,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  
  CONSTRAINT chk_level CHECK (level IN ('error', 'critical'))
);

CREATE INDEX idx_error_logs_level ON tes_content.error_logs(level);
CREATE INDEX idx_error_logs_code ON tes_content.error_logs(error_code);
CREATE INDEX idx_error_logs_created ON tes_content.error_logs(created_at DESC);

🔧 Implementación

Service de Logging

// infrastructure/services/LoggingService.ts
export class LoggingService {
  async logAudit(params: {
    userId: string;
    userRole: string;
    action: string;
    entityType: string;
    entityId: string;
    changes?: Array<{ field: string; oldValue: unknown; newValue: unknown }>;
    metadata?: Record<string, unknown>;
    ipAddress?: string;
    userAgent?: string;
  }): Promise<void> {
    await db.query(
      `INSERT INTO tes_content.audit_logs (
        user_id, user_role, action, entity_type, entity_id,
        changes, metadata, ip_address, user_agent
      ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
      [
        params.userId,
        params.userRole,
        params.action,
        params.entityType,
        params.entityId,
        JSON.stringify(params.changes || []),
        JSON.stringify(params.metadata || {}),
        params.ipAddress,
        params.userAgent
      ]
    );
  }
  
  async logValidation(params: {
    validationType: 'dose' | 'protocol' | 'content';
    contentId?: string;
    drugId?: string;
    protocolId?: string;
    result: 'valid' | 'invalid' | 'warning';
    errors: string[];
    warnings: string[];
    context?: Record<string, unknown>;
    validatedBy?: string;
  }): Promise<void> {
    await db.query(
      `INSERT INTO tes_content.validation_logs (
        validation_type, content_id, drug_id, protocol_id,
        result, errors, warnings, context, validated_by
      ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
      [
        params.validationType,
        params.contentId,
        params.drugId,
        params.protocolId,
        params.result,
        params.errors,
        params.warnings,
        JSON.stringify(params.context || {}),
        params.validatedBy
      ]
    );
  }
  
  async logError(params: {
    level: 'error' | 'critical';
    errorCode: string;
    errorMessage: string;
    stack?: string;
    context?: Record<string, unknown>;
  }): Promise<void> {
    await db.query(
      `INSERT INTO tes_content.error_logs (
        level, error_code, error_message, stack, context
      ) VALUES ($1, $2, $3, $4, $5)`,
      [
        params.level,
        params.errorCode,
        params.errorMessage,
        params.stack,
        JSON.stringify(params.context || {})
      ]
    );
    
    // Si es crítico, también notificar
    if (params.level === 'critical') {
      await this.notifyCriticalError(params);
    }
  }
  
  private async notifyCriticalError(error: {
    errorCode: string;
    errorMessage: string;
    context?: Record<string, unknown>;
  }): Promise<void> {
    // Enviar notificación a administradores
    // Email, Slack, etc.
  }
}

📊 Datos de Relevancia Médica (Sin Identificar Pacientes)

Estadísticas Agregadas

interface MedicalStatistics {
  // Validaciones de dosis
  doseValidations: {
    total: number;
    valid: number;
    invalid: number;
    warnings: number;
    byDrug: Record<string, number>;
    byAgeGroup: Record<string, number>;
  };
  
  // Protocolos ejecutados
  protocolExecutions: {
    total: number;
    byProtocol: Record<string, number>;
    averageStepsCompleted: number;
    criticalErrorsDetected: number;
  };
  
  // Contenido médico
  contentStats: {
    total: number;
    byStatus: Record<string, number>;
    averageReviewTime: number; // horas
    approvalRate: number; // porcentaje
  };
  
  // Errores críticos
  criticalErrors: {
    total: number;
    byType: Record<string, number>;
    blockedActions: number;
  };
}

Casos de Uso

Caso 1: Registrar creación de contenido

await loggingService.logAudit({
  userId: req.user.id,
  userRole: req.user.role,
  action: 'create',
  entityType: 'content',
  entityId: content.id,
  ipAddress: req.ip,
  userAgent: req.get('user-agent')
});

Caso 2: Registrar validación de dosis

await loggingService.logValidation({
  validationType: 'dose',
  drugId: 'adrenalina',
  result: validation.valid ? 'valid' : 'invalid',
  errors: validation.errors,
  warnings: validation.warnings,
  context: {
    ageGroup: 'adulto',
    weightRange: '70-80kg'
  }
});

Caso 3: Registrar error crítico

await loggingService.logError({
  level: 'critical',
  errorCode: 'DOSE_LETHAL',
  errorMessage: 'Dosis letal detectada',
  context: {
    drugId: 'adrenalina',
    dose: 10,
    userId: user.id
  }
});

🔍 Consultas Útiles

Obtener historial de cambios de contenido

SELECT * FROM tes_content.audit_logs
WHERE entity_type = 'content' AND entity_id = $1
ORDER BY created_at DESC;

Estadísticas de validaciones

SELECT 
  validation_type,
  result,
  COUNT(*) as count
FROM tes_content.validation_logs
WHERE created_at >= NOW() - INTERVAL '30 days'
GROUP BY validation_type, result;

Errores críticos recientes

SELECT * FROM tes_content.error_logs
WHERE level = 'critical'
  AND created_at >= NOW() - INTERVAL '24 hours'
ORDER BY created_at DESC;

Fin del documento