Documentación/Sistema de Audit de DBFlux
Sistema de Audit de DBFlux
DBFlux registra todas las operaciones significativas en un audit trail unificado almacenado en SQLite. Esto cubre la ejecución de queries, el ciclo de vida de las conexiones, la ejecución de hooks, las ejecuciones de scripts, las decisiones de governance de MCP y los cambios de configuración.
Ubicación de almacenamiento
Todos los audit events se almacenan en la base de datos unificada:
~/.local/share/dbflux/dbflux.db
Tabla: aud_audit_events
La misma base de datos almacena todo el resto del estado en tiempo de ejecución (profiles, history, sessions). El schema lo gestiona el sistema de migraciones en dbflux_storage/src/migrations/.
Estructura de un evento
Cada audit event es un EventRecord (dbflux_core/src/observability/types.rs) con estos campos:
| Campo | Tipo | Descripción |
|---|---|---|
id | i64 | Asignado automáticamente al insertar |
ts_ms | i64 | Timestamp Unix en milisegundos |
level | EventSeverity | trace, debug, info, warn, error, fatal |
category | EventCategory | Dominio del evento (ver abajo) |
action | String | Identificador específico de la acción (p. ej., query_execute) |
outcome | EventOutcome | success, failure, cancelled, pending |
actor_type | EventActorType | Quién disparó el evento |
actor_id | Option<String> | Identidad del actor (client ID de MCP, nombre del hook, etc.) |
source_id | EventSourceId | Dónde se originó el evento |
connection_id | Option<String> | ID del connection profile |
database_name | Option<String> | Nombre de la base de datos destino |
driver_id | Option<String> | ID del driver (p. ej., postgres, mongodb) |
object_type | Option<String> | Tipo de objeto afectado (p. ej., table, collection) |
object_id | Option<String> | ID/nombre del objeto específico |
summary | String | Descripción legible por humanos |
details_json | Option<String> | Contexto estructurado adicional como objeto JSON |
error_code | Option<String> | Código de error en caso de fallo |
error_message | Option<String> | Mensaje de error en caso de fallo |
duration_ms | Option<i64> | Tiempo de ejecución en milisegundos |
session_id | Option<String> | ID de correlación de sesión |
correlation_id | Option<String> | ID de correlación entre componentes |
Categorías de eventos
| Categoría | String | Qué captura |
|---|---|---|
Query | query | Ejecución de SQL, queries de MongoDB, operaciones de scan |
Connection | connection | Ciclo de vida de connect, disconnect, reconnect |
Hook | hook | PreConnect, PostConnect, PreDisconnect, PostDisconnect |
Script | script | Ejecución de scripts Lua, Python, Bash |
Mcp | mcp | Llamadas a tools de un AI client y decisiones de policy |
Governance | governance | Resultados de evaluación de policy |
Config | config | Cambios de profile, modificaciones de settings |
System | system | Arranque de la aplicación, panics, migraciones |
ObjectStorage | object_storage | Eventos de CRUD/mutación de object storage (upload, delete, presign, rename, create bucket/folder, save-back edit) |
Tipos de actor
| Tipo | String | Significado |
|---|---|---|
User | user | Humano operando la GUI de DBFlux |
System | system | Operación de sistema en segundo plano |
App | app | Aplicación actuando de forma autónoma |
McpClient | mcp_client | AI agent vía el protocolo MCP |
Hook | hook | Script de un lifecycle hook |
Script | script | Script escrito por el usuario |
Campos obligatorios por categoría
La validación la aplica AuditService::validate_event() antes de almacenar:
| Categoría | Obligatorio además de action + summary |
|---|---|
Query | connection_id, driver_id, duration_ms (para eventos de ejecución) |
Connection | connection_id |
Hook | object_type, object_id, connection_id |
Script | object_type, object_id |
Mcp | actor_id, object_id (nombre de la tool) |
Config | object_type, object_id |
ObjectStorage | connection_id, object_type, object_id |
Governance, System | Sin campos adicionales |
Privacidad y redacción
Por defecto, AuditService se ejecuta con estos ajustes:
redact_sensitive = true: los valores sensibles (contraseñas, tokens, connection strings) endetails_jsonyerror_messagese reemplazan por[REDACTED]antes de almacenarse.capture_query_text = false: el texto completo de la query nunca se almacena. En su lugar, se guardan un fingerprint SHA256 más la longitud original como[FINGERPRINT:<16-char-hex>]conquery_length. Esto evita que datos sensibles en las queries se filtren al audit log.max_detail_bytes = 65536: los payloads mayores de 64 KiB se rechazan para prevenir el crecimiento descontrolado del almacenamiento.
Estos ajustes se pueden cambiar en tiempo de ejecución vía los métodos AuditService::set_*(). El servidor MCP expone algunos de ellos a través de la configuración de governance.
Ver los audit events
En la UI de DBFlux
Navega a Workspace → Audit. La vista unificada de audit soporta:
- Filtrado por actor, tool/action, rango de fechas, decision, category
- Exportar los resultados filtrados a CSV o JSON
El mismo shell de UI AuditDocument se reutiliza también para los event streams externos respaldados por un driver, cuando un driver los declara a través de abstracciones genéricas del core (CollectionPresentation, CollectionChildInfo, EventStreamTarget). La UI no debe hacer casos especiales para drivers concretos al abrir o renderizar esos streams.
Directamente vía SQLite
La base de datos es un archivo SQLite estándar. Consúltala directamente:
sqlite3 ~/.local/share/dbflux/dbflux.db
Queries útiles:
-- All events in the last 24 hours
SELECT id, datetime(ts_ms/1000, 'unixepoch') as ts, level, category, action, outcome, actor_id, summary
FROM aud_audit_events
WHERE ts_ms > (unixepoch('now') - 86400) * 1000
ORDER BY ts_ms DESC;
-- MCP tool calls only
SELECT id, datetime(ts_ms/1000, 'unixepoch') as ts, actor_id, object_id as tool, outcome, summary
FROM aud_audit_events
WHERE category = 'mcp'
ORDER BY ts_ms DESC;
-- All failed operations
SELECT id, datetime(ts_ms/1000, 'unixepoch') as ts, category, action, actor_id, error_message
FROM aud_audit_events
WHERE outcome = 'failure'
ORDER BY ts_ms DESC
LIMIT 50;
-- Query events by connection
SELECT id, datetime(ts_ms/1000, 'unixepoch') as ts, action, driver_id, duration_ms, summary
FROM aud_audit_events
WHERE category = 'query' AND connection_id = 'your-connection-id'
ORDER BY ts_ms DESC;
-- Events grouped by category and outcome
SELECT category, outcome, count(*) as count
FROM aud_audit_events
GROUP BY category, outcome
ORDER BY category, outcome;
Vía MCP Tools (AI clients)
La superficie de tools de MCP expone tres audit tools (clasificadas dentro de la execution class read):
query_audit_logs — Filter events by actor, tool, date range, decision
get_audit_entry — Retrieve a single event by ID
export_audit_logs — Export filtered results as CSV or JSON
Vía la API de Rust
use dbflux_audit::{AuditService, AuditQueryFilter, AuditExportFormat};
let service = AuditService::new_sqlite_default()?;
// Query recent events
let filter = AuditQueryFilter {
category: Some("mcp".to_string()),
start_epoch_ms: Some(start_ms),
limit: Some(100),
..Default::default()
};
let events = service.query(&filter)?;
// Export to CSV
let csv = service.export(&filter, AuditExportFormat::Csv)?;
// Export extended (all fields including details_json)
let json = service.export_extended(&filter, AuditExportFormat::Json)?;
Generar audit events
Desde las capas de servicio
Usa el trait EventSink. Todos los componentes que emiten audit events aceptan un Arc<dyn EventSink>:
use dbflux_core::observability::{
EventOrigin, EventRecord, EventSink,
types::{EventCategory, EventSeverity, EventOutcome},
actions,
};
// Build the event
let event = EventRecord::new(
now_epoch_ms(),
EventSeverity::Info,
EventCategory::Query,
EventOutcome::Success,
)
.with_typed_action(actions::QUERY_EXECUTE)
.with_summary("SELECT executed on users table")
.with_actor_id("my-actor-id")
.with_origin(EventOrigin::local())
.with_connection_context("my-profile-id", "mydb", "postgres")
.with_object_ref("table", "users")
.with_duration_ms(42);
// Emit through the sink (injected via constructor or DI)
event_sink.record(event)?;
Constantes canónicas de acción
Las cadenas de action se definen en dbflux_core/src/observability/actions.rs. Usa las constantes en lugar de strings sueltos:
| Constante | String | Categoría |
|---|---|---|
QUERY_EXECUTE | query_execute | Query |
QUERY_EXECUTE_FAILED | query_execute_failed | Query |
CONNECTION_CONNECT | connection_connect | Connection |
CONNECTION_DISCONNECT | connection_disconnect | Connection |
HOOK_EXECUTE | hook_execute | Hook |
HOOK_EXECUTE_FAILED | hook_execute_failed | Hook |
SCRIPT_EXECUTE | script_execute | Script |
SCRIPT_EXECUTE_FAILED | script_execute_failed | Script |
MCP_AUTHORIZE | mcp_authorize | Mcp |
MCP_APPROVE_EXECUTION | mcp_approve_execution | Mcp |
MCP_REJECT_EXECUTION | mcp_reject_execution | Mcp |
MCP_TOOL_EXECUTE | mcp_tool_execute | Mcp |
MCP_TOOL_EXECUTE_FAILED | mcp_tool_execute_failed | Mcp |
SYSTEM_PANIC | system_panic | System |
Checklist de campos obligatorios
Antes de llamar a record(), asegúrate de que:
actionestá definido y no está vacío (usa una constante deactions)summaryestá definido y no está vacío (legible por humanos, una frase)- Los campos específicos de la categoría están presentes (ver la tabla de arriba)
details_jsones un objeto JSON válido si se proporciona — no un array ni un primitivodetails_jsonocupa menos de 64 KiB
Eventos de fallo
Para los fallos, establece outcome a EventOutcome::Failure y rellena error_code y error_message:
let event = EventRecord::new(ts_ms, EventSeverity::Error, EventCategory::Query, EventOutcome::Failure)
.with_typed_action(actions::QUERY_EXECUTE_FAILED)
.with_summary("Query failed: syntax error")
.with_connection("profile-id", Some("mydb"), Some("postgres"))
.with_error("42601", "syntax error at or near \"SELEC\"");
error_message se redacta si contiene patrones sensibles. Usa error_code para identificadores de error estables y legibles por máquina.
Retención y purga
Los eventos se pueden purgar según una política de retención:
// Delete events older than 90 days, in batches of 500
let stats = service.purge_old_events(90, 500)?;
println!("Deleted {} events in {} batches", stats.deleted_count, stats.batches);
La purga se ejecuta en lotes para evitar transacciones de escritura largas. No se ejecuta automáticamente — añádela a una tarea programada en segundo plano o a un runbook de operaciones.
Puente de Tracing a Audit
El puente de tracing captura los eventos estructurados emitidos por las macros log::*! y tracing::*! en todos los crates de DBFlux y los escribe en la misma tabla aud_audit_events sin requerir migración de los call sites.
Flujo del evento
Categoría permitida por el puente
Todos los eventos capturados a través del puente se asignan a la categoría System. Esta es la resolución de la V1: los eventos de log de formato libre no llevan los campos estructurados (connection_id, object_type, object_id) que otras categorías requieren, así que enrutarlos a Connection o Config haría que validate_event los rechazara. El PREFIX_CATEGORY_MAP en dbflux_core/src/observability/tracing_bridge/category.rs mapea prefijos de módulo a categorías previstas con fines documentales, pero todas las categorías resueltas se coaccionan a System en tiempo de ejecución.
Umbral de captura
Solo se escriben en el audit store los eventos con nivel igual o superior al log_capture_min_level configurado. TRACE y DEBUG se filtran de forma estricta — nunca se escriben, sin importar el umbral configurado.
El umbral se almacena como un ordinal u8 en un Arc<AtomicU8> y se actualiza sin reinicializar el subscriber. El mapeo es:
| Severidad | Ordinal |
|---|---|
| Trace | 0 |
| Debug | 1 |
| Info | 2 |
| Warn | 3 |
| Error | 4 |
El umbral por defecto es Info (ordinal 2).
Configurar el umbral
En la UI de DBFlux: desplegable Settings → Audit → Log Capture → Minimum Level. Al seleccionar un nivel y pulsar Save se persiste en cfg_audit_settings.log_capture_min_level (columna añadida por la migración 014) y se aplica al puente de forma atómica — no requiere reiniciar.
Directamente en SQLite:
UPDATE cfg_audit_settings SET log_capture_min_level = 'warn';
Valores válidos: trace, debug, info, warn, error.
Contador de descartes
Cuando el canal acotado está lleno (512 eventos por defecto, configurable vía BridgeConfig::queue_capacity), el puente descarta el evento entrante en lugar de bloquear, e incrementa un contador de descartes Arc<AtomicU64>. Esto evita que la ruta de audit introduzca contrapresión en el código de la aplicación. El contador de descartes actual es accesible vía BridgeHandle::drop_count() y se expone a través de AuditService::dropped_log_event_count() para observabilidad, pero no se persiste ni se muestra en la UI en la V1.
Ventana de arranque
Existe un breve intervalo entre el inicio del proceso y la instalación del sink durante el cual los eventos se capturan en el canal de drenaje pero aún no se vuelcan a SQLite — el sink se instala después de que se construye AppState y se completa la primera lectura de audit settings. Los eventos en tránsito durante esta ventana se retienen en el canal acotado y se entregan una vez instalado el sink. Si el canal se llena durante la ventana de arranque, los eventos se descartan y se contabilizan.
Guardia de recursión
Los eventos emitidos desde dbflux_core::observability::tracing_bridge quedan excluidos del puente para evitar bucles de realimentación en los que los diagnósticos del propio puente se retroalimenten a sí mismos. Esto se aplica mediante la constante BRIDGE_INTERNAL_TARGET, comprobada en AuditLayer::on_event.
Lista de permitidos por target
Solo los eventos cuyo target empiece por dbflux se reflejan en el audit store. Dependencias upstream como gpui, blade_graphics, naga, wgpu, hyper y tokio emiten trazas verbosas de nivel INFO (ciclo de vida de texturas y buffers del render-loop, modo de presentación de superficie, ciclo de vida de peticiones HTTP, etc.) que de otro modo inundarían el audit log de ruido operacional sin ningún valor para el diagnóstico posterior.
Estos eventos siguen fluyendo a través de la capa fmt y permanecen visibles en stderr (o en el archivo de log) según RUST_LOG. El gate vive en passes_target_gate, en layer.rs, y se ejecuta antes de construir el record.
Para auditar un evento de una fuente que no sea dbflux, envuelve la emisión en un módulo de dbflux y reemítela con un target de dbflux — el puente intencionalmente no deja pasar targets upstream.
Campos de tracing con nombre
El puente reconoce estos campos con nombre en los eventos de tracing y los mapea a campos de EventRecord:
| Campo de tracing | Campo de EventRecord |
|---|---|
message | summary |
category | category (coaccionado a System) |
actor_type | actor_type |
actor_id | actor_id |
connection_id | connection_id |
database_name | database_name |
driver_id | driver_id |
action | action |
outcome | outcome |
details_json | details_json |
Los campos desconocidos se acumulan en details_json como un objeto JSON. Si el mensaje excede los 512 caracteres, se trunca con … y el mensaje completo se guarda en details_json["message"].
El puente también mapea correlation_id directamente a EventRecord.correlation_id (no dentro de details_json), lo que permite la correlación entre componentes entre los toasts de error orientados al usuario y sus audit records correspondientes.
Eventos de error orientados al usuario
Los errores orientados al usuario (fallos de storage, errores de driver, problemas de red, fallos al persistir la configuración) se reportan a través de report_error / report_error_async desde dbflux_ui_base::user_error. Cada llamada emite un evento de tracing que fluye a través del puente y además dispara una notificación toast.
La forma del evento de tracing:
| Campo de tracing | Valor |
|---|---|
target | dbflux_ui::user_error |
action | user_error |
outcome | failure |
kind | ErrorKind como string (storage, network, auth, hook, driver, user, config) |
correlation_id | UUID v7 que vincula el toast con el audit record |
message | El resumen legible por humanos mostrado en el toast |
El campo correlation_id lo extrae AuditFieldVisitor hacia EventRecord.correlation_id. Nótese que el visitor enruta tanto record_str (sigilo Display %val) como record_debug (sigilo Debug ?val) a través del mismo dispatcher record_string_by_name, de forma que los nuevos slots tipados que se añadan en el futuro se recogen sin importar qué sigilo use quien llama.
Hay dos caminos desde la UI de vuelta al audit document:
- Acción “View in Audit” por toast — emite
OpenAuditRequested(Some(correlation_id)). El workspace abre (o enfoca) el Audit document y aplica el filtro de correlation coincidente para que el usuario vea exactamente el único evento vinculado al toast. - Clic en el badge de error de la status bar — emite
OpenAuditRequested(None). El workspace abre el Audit document con el filtro de user-error por defecto (target = dbflux_ui::user_errorsobre una ventana de tiempo reciente) para que el usuario pueda explorar todos los fallos orientados al usuario recientes.
Ambos eventos fluyen a través de AppStateEntity::request_open_audit para que el workspace se suscriba una sola vez.
Mapeo de severidad desde EventSeverity:
EventSeverity::InfoyEventSeverity::Warn— emitidos a nivelWARN; regulados (throttled) (token bucket de 5, 1 recarga cada 2 segundos, por severidad)EventSeverity::ErroryEventSeverity::Fatal— emitidos a nivelERROR; evitan el throttle
Activar el puente
El puente se activa compilando dbflux_core con el feature tracing-bridge (activado por defecto para dbflux, dbflux_mcp_server). Llama a init_tracing(BridgeConfig { .. }) una vez al arrancar el proceso:
use dbflux_core::observability::tracing_bridge::{init_tracing, BridgeConfig, FmtWriter};
let handle = init_tracing(BridgeConfig {
include_audit_layer: true,
fmt_writer: FmtWriter::Stderr,
env_filter_default: "info",
..BridgeConfig::default()
})?;
// Later, after AuditService is ready:
handle.install_sink(Arc::new(audit_service));
dbflux_driver_host usa include_audit_layer: false porque los procesos de driver host son efímeros y no tienen acceso a la base de datos SQLite de audit.
Archivos clave
| Archivo | Rol |
|---|---|
crates/dbflux_core/src/observability/tracing_bridge/mod.rs | init_tracing, BridgeHandle, BridgeConfig, LevelCode |
crates/dbflux_core/src/observability/tracing_bridge/layer.rs | AuditLayer, AuditFieldVisitor, level gate |
crates/dbflux_core/src/observability/tracing_bridge/category.rs | PREFIX_CATEGORY_MAP, resolve_category, BRIDGE_INTERNAL_TARGET |
crates/dbflux_storage/src/migrations/mod_014_audit_settings_log_capture_min_level.rs | Añade la columna log_capture_min_level a cfg_audit_settings |
Emisión externa de audit (drivers RPC y auth providers)
Los drivers RPC externos (protocolo v1.2+) y los auth providers (protocolo v1.3+) pueden emitir audit events de vuelta al host como frames de respuesta intermedios. El host aplica una sanitización estricta antes de escribir en aud_audit_events.
Política controlada por el host
El host posee todos los campos de identidad, correlation y rate-limiting. Un servicio externo nunca puede falsificar su propia identidad ni reclamar una audit category para la que no tiene permiso.
| Campo | Origen |
|---|---|
actor_type | Siempre ExternalDriver o ExternalAuthProvider |
source_id | Siempre ExternalDriver / ExternalAuthProvider con el socket_id registrado |
actor_id | Siempre rpc:<socket_id> |
connection_id | Suministrado por el host desde el contexto de sesión (puede ser None) |
database_name | Suministrado por el host desde el contexto de sesión (puede ser None) |
driver_id | Siempre rpc:<socket_id> |
correlation_id | Generado por el host; uno por sesión para drivers, uno por request para auth providers |
ts_ms | Suministrado por el servicio, pero limitado si la desviación respecto al reloj de pared del host supera los cinco minutos |
correlation_id está garantizado estructuralmente a ser generado por el host porque AuditEventEmitDto (el tipo de payload IPC) no tiene ningún campo correlation_id. Los servicios externos no pueden suministrar uno — el campo se omitió intencionalmente del DTO en el momento del diseño (ADR-3) en lugar de aceptarse y validarse después. Como resultado, el escenario “el DTO del driver lleva un correlation_id falsificado y el host lo sobrescribe en tiempo de ejecución” es imposible a nivel de tipos; el valor almacenado siempre lo produce la lógica de asignación de correlation-id del host.
Lista blanca de categorías
Los drivers pueden emitir eventos Connection, Query y System. Los auth providers solo pueden emitir eventos Connection. Cualquier frame con una categoría no permitida se descarta silenciosamente.
Rate limiting
Cada servicio externo (por socket_id) está limitado a 100 eventos cada 60 segundos mediante un token-bucket. Los frames que superan el presupuesto se descartan y se contabilizan en AuditService::external_audit_dropped_count().
Flags de opt-in
- Drivers: el driver debe incluir
DriverCapability::AuditEmiten su respuesta hello (protocolo v1.2+). Los frames enviados por drivers que no anunciaron esta capacidad se descartan silenciosamente. - Auth providers: el provider debe establecer
audit_emit_opt_in: trueen su respuesta hello (protocolo v1.3+). Los frames de providers que no optaron por esto se descartan silenciosamente.
Campos obligatorios en cada frame emitido
Un AuditEventEmitDto emitido debe tener action y summary no vacíos. Los frames que no cumplan esta comprobación se descartan silenciosamente.
Mecanismo de transporte
Los frames emitidos llegan como frames intermedios done=false dentro de una secuencia de respuesta normal. La capa de transporte (RpcClient en dbflux_driver_ipc, RpcAuthProvider::dispatch_request_loop en dbflux_ipc) los intercepta antes de que lleguen a quien hizo la llamada. Quien llama solo llega a ver el frame terminal.
Archivos clave
| Archivo | Rol |
|---|---|
crates/dbflux_ipc/src/audit.rs | AuditEventEmitDto, trait ExternalAuditEmitter, ExternalAuditSource |
crates/dbflux_app/src/rpc_services/external_audit.rs | ExternalAuditSink, rate limiter de token-bucket, pipeline de sanitización |
crates/dbflux_driver_ipc/src/transport.rs | RpcClient::send_raw intercepta los frames de emisión del driver |
crates/dbflux_ipc/src/auth_provider_client.rs | dispatch_request_loop intercepta los frames de emisión del auth provider |
Arquitectura
[Service layers]
| emit EventRecord via EventSink trait
v
AuditService (dbflux_audit/src/lib.rs)
| validate → fingerprint query text → redact sensitive values → enforce size limit
v
SqliteAuditStore (dbflux_audit/src/store/sqlite.rs)
| delegates to AuditRepository
v
AuditRepository (dbflux_storage/src/repositories/audit.rs)
| inserts into aud_audit_events
v
~/.local/share/dbflux/dbflux.db
Archivos clave:
| Archivo | Rol |
|---|---|
crates/dbflux_core/src/observability/types.rs | EventRecord, todos los tipos enum |
crates/dbflux_core/src/observability/actions.rs | Constantes canónicas de cadenas de action |
crates/dbflux_audit/src/lib.rs | AuditService — validate, preprocess, record |
crates/dbflux_audit/src/query.rs | AuditQueryFilter |
crates/dbflux_audit/src/export.rs | Export a CSV/JSON (básico y extendido) |
crates/dbflux_audit/src/redaction.rs | Lógica de redacción de valores sensibles |
crates/dbflux_audit/src/purge.rs | Purga de eventos basada en retención |
crates/dbflux_audit/src/store/sqlite.rs | Adaptador del store de SQLite |
crates/dbflux_storage/src/repositories/audit.rs | AuditRepository + AuditEventDto |