michangarrito/docs/97-adr/ADR-0008-audit-log-retention.md
rckrdmrd 2c916e75e5 [SIMCO-V4] feat: Agregar documentación SaaS, ADRs e integraciones
Nuevas Épicas (MCH-029 a MCH-033):
- Infraestructura SaaS multi-tenant
- Auth Social (OAuth2)
- Auditoría Empresarial
- Feature Flags
- Onboarding Wizard

Nuevas Integraciones (INT-010 a INT-014):
- Email Providers (SendGrid, Mailgun, SES)
- Storage Cloud (S3, GCS, Azure)
- OAuth Social
- Redis Cache
- Webhooks Outbound

Nuevos ADRs (0004 a 0011):
- Notifications Realtime
- Feature Flags Strategy
- Storage Abstraction
- Webhook Retry Strategy
- Audit Log Retention
- Rate Limiting
- OAuth Social Implementation
- Email Multi-provider

Actualizados:
- MASTER_INVENTORY.yml
- CONTEXT-MAP.yml
- HERENCIA-SIMCO.md
- Mapas de documentación

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 01:43:15 -06:00

5.4 KiB

id type title status decision_date updated_at simco_version stakeholders tags
ADR-0008 ADR Audit Log Retention Accepted 2026-01-10 2026-01-10 4.0.1
Equipo MiChangarrito
audit
retention
compliance
storage

ADR-0008: Audit Log Retention

Metadata

Campo Valor
ID ADR-0008
Estado Accepted
Fecha 2026-01-10
Autor Architecture Team
Supersede -

Contexto

MiChangarrito registra audit logs de todas las acciones del sistema para compliance, debugging y seguridad. Necesitamos definir:

  1. Cuanto tiempo mantener los logs
  2. Como archivar logs antiguos
  3. Como purgar datos sin perder trazabilidad

Decision

Implementamos politica de retencion configurable por tenant con archivado opcional a storage antes de purga.

  • Default: 90 dias en base de datos
  • Archivado: Opcional a S3/R2 antes de purga
  • Notificacion: 7 dias antes de purga
  • Configuracion: Por tenant segun plan

Alternativas Consideradas

Opcion 1: Retencion infinita

  • Pros:
    • Nunca se pierde informacion
    • Simple
  • Cons:
    • Crecimiento ilimitado de BD
    • Costos de almacenamiento
    • Performance degradada

Opcion 2: Retencion fija (30 dias)

  • Pros:
    • Simple de implementar
    • Predecible
  • Cons:
    • No flexible para diferentes necesidades
    • Puede no cumplir regulaciones

Opcion 3: Retencion configurable + archivado (Elegida)

  • Pros:
    • Flexible por tenant
    • Archivado economico
    • Cumple regulaciones
  • Cons:
    • Mas complejo
    • Necesita job de limpieza

Consecuencias

Positivas

  1. Compliance: Tenants pueden configurar segun sus necesidades
  2. Performance: BD no crece indefinidamente
  3. Economia: Archivado en storage es barato
  4. Flexibilidad: Diferentes politicas por plan

Negativas

  1. Complejidad: Job de limpieza y archivado
  2. Acceso: Logs archivados requieren restauracion

Implementacion

Configuracion por Plan

Plan Retencion BD Archivado Restauracion
Basic 30 dias No N/A
Pro 90 dias Si (1 ano) Bajo demanda
Enterprise 365 dias Si (7 anos) Autoservicio

Job de Limpieza

@Cron('0 3 * * *') // 3 AM diario
async cleanupAuditLogs() {
  const tenants = await this.tenantService.findAll();

  for (const tenant of tenants) {
    const config = await this.getRetentionConfig(tenant.id);
    const cutoffDate = subDays(new Date(), config.retentionDays);

    // 1. Archivar si esta habilitado
    if (config.archiveBeforeDelete) {
      await this.archiveLogs(tenant.id, cutoffDate);
    }

    // 2. Purgar logs antiguos
    await this.purgeLogs(tenant.id, cutoffDate);

    this.logger.log(`Cleaned audit logs for tenant ${tenant.id}`, {
      cutoffDate,
      archived: config.archiveBeforeDelete,
    });
  }
}

Archivado

async archiveLogs(tenantId: string, olderThan: Date) {
  const logs = await this.auditRepository.find({
    where: {
      tenantId,
      createdAt: LessThan(olderThan),
    },
  });

  if (logs.length === 0) return;

  // Comprimir y subir a storage
  const archive = await this.compressLogs(logs);
  const key = `audit-archives/${tenantId}/${format(new Date(), 'yyyy-MM')}.json.gz`;

  await this.storageService.upload(key, archive, {
    contentType: 'application/gzip',
    metadata: {
      tenantId,
      recordCount: logs.length.toString(),
      dateRange: `${logs[0].createdAt}-${logs[logs.length - 1].createdAt}`,
    },
  });
}

Notificacion Pre-Purga

@Cron('0 9 * * *') // 9 AM diario
async notifyUpcomingPurge() {
  const tenants = await this.tenantService.findAll();

  for (const tenant of tenants) {
    const config = await this.getRetentionConfig(tenant.id);
    if (!config.notifyBeforePurge) continue;

    const warningDate = subDays(new Date(), config.retentionDays - 7);
    const count = await this.countLogsOlderThan(tenant.id, warningDate);

    if (count > 0) {
      await this.notificationService.send(tenant.adminEmail, {
        template: 'audit-purge-warning',
        variables: {
          count,
          purgeDate: addDays(warningDate, 7),
          archiveEnabled: config.archiveBeforeDelete,
        },
      });
    }
  }
}

Formato de Archivo

Estructura

audit-archives/
└── {tenant_id}/
    ├── 2026-01.json.gz
    ├── 2026-02.json.gz
    └── ...

Contenido (descomprimido)

{
  "tenant_id": "550e8400-...",
  "exported_at": "2026-01-10T03:00:00Z",
  "record_count": 15000,
  "date_range": {
    "from": "2025-10-01T00:00:00Z",
    "to": "2025-10-31T23:59:59Z"
  },
  "records": [
    {
      "id": "...",
      "user_id": "...",
      "action": "UPDATE",
      "entity_type": "Product",
      "entity_id": "...",
      "old_value": {...},
      "new_value": {...},
      "created_at": "2025-10-15T10:30:00Z"
    }
  ]
}

Restauracion

API de Restauracion (Enterprise)

// POST /api/audit/restore
{
  "month": "2025-10",
  "reason": "Auditoria externa"
}

// Response
{
  "status": "processing",
  "estimatedTime": "5 minutes",
  "jobId": "restore-123"
}

Referencias


Fecha decision: 2026-01-10 Autores: Architecture Team