zcatsql/docs/AUDIT_LOG_DESIGN.md
reugenio c5e6cec4a6 refactor: rename zsqlite to zcatsql
Consistent naming with zcat ecosystem (zcatui, zcatgui, zcatsql).
All lowercase per Zig naming conventions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-09 02:19:52 +01:00

441 lines
14 KiB
Markdown

# Audit Log System - Diseño de Arquitectura
## Visión General
Sistema de log histórico externo para zcatsql que permite:
1. **v1.0**: Auditoría completa de operaciones con integridad verificable
2. **v2.0**: Navegación temporal (time travel) a cualquier punto en el historial
## Arquitectura
```
┌─────────────────────────────────────────────────────────────────────┐
│ Aplicación │
│ ┌─────────────┐ │
│ │ AuditCtx │ ◀── user_id, app_name (proporcionado por app) │
│ └─────────────┘ │
└────────┬────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ AuditLog │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ Hooks: pre-update, update, commit, rollback │ │
│ │ Captura: SQL + valores antes/después + metadatos │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ Entry Buffer (en memoria hasta commit) │ │
│ │ - Agrupa operaciones por transacción │ │
│ │ - Descarta si rollback │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ LogWriter │ │
│ │ - Escribe a archivo │ │
│ │ - Gestiona rotación │ │
│ │ - Calcula hash chain │ │
│ └──────────────────────────────────────────────────────────────┘ │
└────────┬────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ Sistema de Archivos │
│ │
│ mydb_audit_0001.log ──▶ mydb_audit_0002.log ──▶ ... │
│ (hash chain) (hash chain) │
│ │
│ mydb_audit_index.json ◀── Índice de archivos y rangos de tiempo │
└─────────────────────────────────────────────────────────────────────┘
```
---
## Formato de Entrada de Log
Cada operación se registra en formato JSON Lines (una línea por entrada):
```json
{
"seq": 1,
"ts": "2025-12-08T14:32:15.123456Z",
"tx_id": 42,
"ctx": {
"user": "alice",
"app": "simifactu",
"host": "workstation-01",
"pid": 12345
},
"op": "UPDATE",
"table": "invoices",
"rowid": 157,
"sql": "UPDATE invoices SET status = 'paid' WHERE id = 157",
"before": {
"id": 157,
"status": "pending",
"amount": 1500.00
},
"after": {
"id": 157,
"status": "paid",
"amount": 1500.00
},
"prev_hash": "a1b2c3d4e5f6...",
"hash": "f6e5d4c3b2a1..."
}
```
### Campos
| Campo | Tipo | Descripción |
|-------|------|-------------|
| `seq` | u64 | Número de secuencia global (nunca se repite) |
| `ts` | ISO8601 | Timestamp con microsegundos |
| `tx_id` | u64 | ID de transacción (agrupa operaciones) |
| `ctx` | object | Contexto: user, app, host, pid |
| `op` | string | INSERT, UPDATE, DELETE |
| `table` | string | Nombre de la tabla |
| `rowid` | i64 | SQLite rowid |
| `sql` | string | SQL ejecutado (si está disponible) |
| `before` | object? | Valores antes (NULL para INSERT) |
| `after` | object? | Valores después (NULL para DELETE) |
| `prev_hash` | string | Hash de la entrada anterior |
| `hash` | string | SHA-256 de esta entrada (sin el campo hash) |
---
## Estructura de Archivos
```
mydb_audit/
├── index.json # Índice maestro
├── 0001_20251208_143200.log # Archivo de log
├── 0002_20251209_000000.log # Siguiente archivo
└── ...
```
### index.json
```json
{
"version": 1,
"db_name": "mydb.sqlite",
"created": "2025-12-08T14:32:00Z",
"files": [
{
"id": 1,
"filename": "0001_20251208_143200.log",
"seq_start": 1,
"seq_end": 15420,
"ts_start": "2025-12-08T14:32:00Z",
"ts_end": "2025-12-08T23:59:59Z",
"entries": 15420,
"bytes": 8523410,
"first_hash": "000000...",
"last_hash": "a1b2c3...",
"closed": true
},
{
"id": 2,
"filename": "0002_20251209_000000.log",
"seq_start": 15421,
"seq_end": null,
"ts_start": "2025-12-09T00:00:00Z",
"ts_end": null,
"entries": 342,
"bytes": 182340,
"first_hash": "a1b2c3...",
"last_hash": "d4e5f6...",
"closed": false
}
],
"rotation": {
"max_bytes": 104857600,
"max_age_days": 30
}
}
```
---
## Rotación de Archivos
Criterios (configurable, se aplica el primero que se cumpla):
1. **Por tamaño**: cuando alcanza `max_bytes` (default: 100MB)
2. **Por tiempo**: cuando pasa `max_age_days` (default: 30 días)
3. **Manual**: API para forzar rotación
Al rotar:
1. Cerrar archivo actual (marcar `closed: true`)
2. Crear nuevo archivo
3. Primer hash del nuevo = último hash del anterior (continuidad)
4. Actualizar index.json
---
## Hash Chain (Integridad)
```
Entry 1: prev_hash = "0000...0000" (genesis)
hash = SHA256(entry_1_without_hash)
Entry 2: prev_hash = hash(Entry 1)
hash = SHA256(entry_2_without_hash)
Entry N: prev_hash = hash(Entry N-1)
hash = SHA256(entry_N_without_hash)
```
### Verificación
```zig
pub fn verifyChain(log_dir: []const u8) !VerifyResult {
// 1. Leer index.json
// 2. Para cada archivo:
// - Verificar que first_hash == last_hash del anterior
// - Para cada entrada:
// - Recalcular hash
// - Verificar prev_hash == hash anterior
// 3. Reportar: OK o primera entrada corrupta
}
```
---
## API v1.0 - Auditoría
```zig
const AuditLog = @import("zcatsql").audit.AuditLog;
// Configuración
const config = AuditLog.Config{
.log_dir = "./mydb_audit",
.rotation = .{
.max_bytes = 100 * 1024 * 1024, // 100MB
.max_age_days = 30,
},
.context = .{
.app_name = "simifactu",
.user_id = null, // Se puede cambiar dinámicamente
},
.capture = .{
.sql = true,
.before_values = true,
.after_values = true,
},
};
// Inicializar
var audit = try AuditLog.init(allocator, &db, config);
defer audit.deinit();
// Cambiar usuario dinámicamente (ej: después de login)
audit.setUser("alice");
// El audit log captura automáticamente via hooks
try db.exec("INSERT INTO users (name) VALUES ('Bob')");
// Forzar rotación manual
try audit.rotate();
// Verificar integridad
const result = try audit.verify();
if (!result.valid) {
std.debug.print("Corrupto en seq {}\n", .{result.first_invalid_seq});
}
// Estadísticas
const stats = audit.stats();
std.debug.print("Entries: {}, Files: {}\n", .{stats.total_entries, stats.file_count});
```
---
## API v2.0 - Time Travel
```zig
const TimeMachine = @import("zcatsql").audit.TimeMachine;
// Inicializar con log existente
var tm = try TimeMachine.init(allocator, "./mydb_audit");
defer tm.deinit();
// Obtener estado en un momento específico
var snapshot_db = try tm.getStateAt("2025-12-08T15:30:00Z");
defer snapshot_db.close();
// snapshot_db es una DB en memoria con el estado de ese momento
// O navegar por secuencia
var snapshot_db2 = try tm.getStateAtSeq(1000);
// Replay: reconstruir DB desde cero
var rebuilt_db = try tm.replay(":memory:");
defer rebuilt_db.close();
// Replay parcial: desde snapshot hasta punto
var partial_db = try tm.replayRange(
"base_snapshot.sqlite", // Punto de partida
5000, // seq_start
8000, // seq_end
);
// Undo: revertir últimas N operaciones
try tm.undoLast(&db, 5);
// Redo: re-aplicar operaciones revertidas
try tm.redo(&db, 3);
// Ver historial de una fila específica
const history = try tm.getRowHistory("invoices", 157);
defer allocator.free(history);
for (history) |entry| {
std.debug.print("{s}: {s} -> {s}\n", .{entry.ts, entry.op, entry.after});
}
```
---
## Consideraciones de Rendimiento
### v1.0
- **Overhead mínimo**: hooks son muy ligeros
- **Buffering**: acumular en memoria hasta commit
- **Async write**: opción de escribir en thread separado
- **Compresión**: opcional, gzip por archivo cerrado
### v2.0
- **Snapshots periódicos**: cada N operaciones o tiempo
- Reduce tiempo de replay
- `snapshot_0001.sqlite` + logs desde ahí
- **Índices en memoria**: para búsqueda rápida por tiempo/tabla/rowid
- **Lazy loading**: cargar solo los logs necesarios
---
## Flujo de Datos
### Captura (v1.0)
```
1. App ejecuta SQL
2. Pre-update hook captura valores ANTES
3. SQLite ejecuta la operación
4. Update hook captura operación + rowid
5. Guardar en buffer de transacción
├── Commit hook ──▶ Escribir buffer a archivo
└── Rollback hook ──▶ Descartar buffer
```
### Replay (v2.0)
```
1. Encontrar snapshot más cercano anterior al tiempo deseado
2. Cargar snapshot a memoria
3. Leer logs desde snapshot hasta tiempo deseado
4. Para cada entrada:
- INSERT: ejecutar INSERT con valores `after`
- UPDATE: ejecutar UPDATE con valores `after`
- DELETE: ejecutar DELETE
5. Retornar DB en estado deseado
```
### Undo
```
1. Leer entrada a revertir
2. Según operación:
- INSERT: DELETE WHERE rowid = X
- UPDATE: UPDATE SET (valores `before`) WHERE rowid = X
- DELETE: INSERT con valores `before`
3. Registrar operación de undo en el log (para auditoría)
```
---
## Seguridad (Futuro)
- **Encriptación**: AES-256-GCM por archivo
- **Firma digital**: además de hash, firmar con clave privada
- **Acceso**: permisos de lectura separados de escritura
---
## Módulos a Implementar
```
src/
├── audit/
│ ├── mod.zig # Exports
│ ├── log.zig # AuditLog principal
│ ├── writer.zig # Escritura a archivo
│ ├── entry.zig # Estructura de entrada
│ ├── context.zig # Contexto (user, app, host)
│ ├── rotation.zig # Gestión de rotación
│ ├── index.zig # Índice de archivos
│ ├── hash.zig # Hash chain
│ ├── verify.zig # Verificación de integridad
│ └── time_machine.zig # Time travel (v2.0)
```
---
## Fases de Implementación
### Fase 1: Core (v1.0)
1. Estructura de entrada y serialización JSON
2. Writer con rotación básica
3. Hash chain
4. Hooks para captura
5. Contexto y metadatos
### Fase 2: Integridad (v1.0)
1. Verificación de cadena
2. Index.json completo
3. Estadísticas
### Fase 3: Time Travel (v2.0)
1. Replay hacia adelante
2. Undo/Redo
3. getStateAt()
4. getRowHistory()
### Fase 4: Optimización (v2.0)
1. Snapshots automáticos
2. Índices en memoria
3. Compresión de archivos cerrados
---
## Preguntas Resueltas
| Pregunta | Decisión |
|----------|----------|
| ¿Qué capturar? | SQL + valores antes/después |
| ¿Cómo identificar contexto? | Mixto: auto-detectar host/pid, app proporciona user/app_name |
| ¿Cuándo rotar? | Configurable: por defecto 100MB o 30 días |
| ¿Granularidad time travel? | Cualquier operación (seq exacto) |