- ✅ 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
494 lines
11 KiB
Markdown
494 lines
11 KiB
Markdown
# 📊 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
|
|
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
interface ErrorLog {
|
|
id: string;
|
|
level: 'error' | 'critical';
|
|
errorCode: string;
|
|
errorMessage: string;
|
|
stack?: string;
|
|
context: {
|
|
userId?: string;
|
|
contentId?: string;
|
|
action?: string;
|
|
};
|
|
timestamp: Date;
|
|
}
|
|
```
|
|
|
|
**Ejemplos:**
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
interface PerformanceLog {
|
|
endpoint: string;
|
|
method: string;
|
|
duration: number; // ms
|
|
statusCode: number;
|
|
timestamp: Date;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🗄️ Esquema de Base de Datos
|
|
|
|
```sql
|
|
-- 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
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
|
|
```typescript
|
|
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
|
|
```typescript
|
|
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
|
|
```typescript
|
|
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
|
|
```sql
|
|
SELECT * FROM tes_content.audit_logs
|
|
WHERE entity_type = 'content' AND entity_id = $1
|
|
ORDER BY created_at DESC;
|
|
```
|
|
|
|
### Estadísticas de validaciones
|
|
```sql
|
|
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
|
|
```sql
|
|
SELECT * FROM tes_content.error_logs
|
|
WHERE level = 'critical'
|
|
AND created_at >= NOW() - INTERVAL '24 hours'
|
|
ORDER BY created_at DESC;
|
|
```
|
|
|
|
---
|
|
|
|
**Fin del documento**
|