SentrySentry
Referência

API — sentry-storage

Referência do sentry-storage: pool Postgres, repositórios tipados, migrations, dedupe por payload_hash e LISTEN/NOTIFY

API — sentry-storage

sentry-storage é a persistência Postgres do Sentry: pool, migrations embutidas e repositórios tipados (events, incidents, ip_state, rules, routes, datasets) usados pelo daemon, pela CLI e pelo dashboard. Todas as queries usam sqlx::query() em runtime, então a crate compila sem um DATABASE_URL vivo. Declara #![forbid(unsafe_code)].

Pool — módulo pool

PgPool

pub struct PgPool { /* sqlx::PgPool */ }
pub async fn connect(cfg: &PostgresConfig) -> Result<Self>;
pub fn connect_lazy(url: &str) -> Result<Self>;
pub async fn listen(&self, channel: &str) -> Result<sqlx::postgres::PgListener>;
pub async fn notify(&self, channel: &str) -> Result<()>;

PgPool respeita max_connections e acquire_timeout_secs do [storage.postgres]; connect_lazy adia a conexão (tooling/testes). listen usa uma conexão dedicada fora do pool — o LISTEN pode bloquear indefinidamente sem starvation. Erros ficam em StorageError (Config/Connect/Query/Migrate).

Repositórios — módulo repo

Repo

Repo carrega o pool e empresta os repos: events(), incidents(), ip_state(), rules(), routes(), datasets(). Todos os métodos de IP canonizam o endereço (Event::canonical_ip).

Tipos de linha

Cada tabela tem seu struct FromRow/serde — são eles que atravessam a fronteira daemon ↔ CLI ↔ dashboard:

TipoTabelaNotas
EventRoweventsLinha completa: protocol/signals em JSONB, duration_ms/process_us
EventInserteventsPayload de insert em lote; payload_hash: Option<i64> alimenta o dedupe
IncidentRowincidentsclient_ip, acknowledged_at, resolved, notes
IpStateRowip_statestatus, reason, expires_at (bloqueio com TTL)
OffenderRowip_stateProjeção de strikes/total_violations/last_violation_at
RuleRowrulesmatch_expr textual (compilado pelo DSL no load_ruleset)
RouteRowroutespath + methods[]; implementa RouteLike
DatasetRowdatasetskind, source_url, action, enabled, entry_count

EventRepo

Repositório da tabela events (EventRepo).

Escrita e dedupe cross-node:

pub async fn insert_with_hash(&self, evt: &Event, risk_score: u8, risk_level: RiskLevel,
    verdict: Verdict, signals: &serde_json::Value, payload_hash: Option<i64>, process_us: Option<u64>) -> Result<()>;
pub async fn insert_batch_with_hash(&self, rows: &[EventInsert]) -> Result<()>;

insert_with_hash faz INSERT condicional: quando payload_hash já foi persistido por um nó irmão na janela de dedupe (10 s), a linha é pulada — o LRU local não enxerga eventos de outros nós (F4.7). insert_batch_with_hash é a lane de persistência em lote do overload response (1 multi-INSERT por lote). EventInsert::from_event monta a linha; protocolo que falha ao serializar vira JSON null — lote não perde evento por um payload ruim.

Leitura e agregações:

MétodoUso
recent / recent_sinceTUI e dashboard
recent_for_ipPopup de info de IP na TUI
update_verdictFeedback pós-AI/LLM
count_by_level_since / count_by_verdict_since/api/stats, relatórios
queries_per_hourSérie temporal
top_ips / top_paths / top_unknown_pathsRodapé da TUI, report --unknown-paths
posture_findingssentry posture: agrega signals JSONB filtrando posture_advisory, group by host+detail (sem migration)
incident_event_idsLabels de sentry model export --confirmed

IncidentRepo

Repositório da tabela incidents (IncidentRepo): create, get_or_create_for_event (idempotente por event_id — índice único parcial), open_incident_for_ip (coalescência: 1 incidente aberto por IP), unresolved, ack e resolve (round-trip dos webhooks com ack_url).

IpStateRepo

Repositório da tabela ip_state (IpStateRepo) — fonte da verdade de bloqueios e da memória de reincidentes:

MétodoPapel
block / unblockEscreve/remove bloqueio (com TTL)
is_blocked / blockedConsulta e listagem (pre-warm da BlockTable no startup)
record_violationStrike + total (janela configurável; strikes decaem fora da janela)
offender / recent_offenders / reset_offendersentry ip forgive e relatórios

RuleRepo

Repositório da tabela rules (RuleRepo): load_ruleset compila as linhas (match_expr via DSL) num RuleSet carregado no startup; upsert, delete, list e set_enabled servem à CLI/dashboard.

RouteRepo

Repositório da tabela routes (RouteRepo): list, insert e delete. RouteRow implementa a trait RouteLike do core, então entra direto no RouteValidator::merge (config ∪ DB).

DatasetRepo

Repositório dos datasets DB-backed (F7.7, DatasetRepo) — listas curadas user_agent/path/ja3 que viram regras sintéticas feed:<name> e alimentam o prefilter dinâmico:

pub async fn upsert(&self, name: &str, kind: &str, source_url: Option<&str>,
    action: &str, entries: &[String]) -> Result<()>;
pub async fn list(&self) -> Result<Vec<DatasetRow>>;
pub async fn entries(&self, name: &str) -> Result<Vec<String>>;
pub async fn set_enabled(&self, name: &str, enabled: bool) -> Result<()>;
pub async fn delete(&self, name: &str) -> Result<()>;

upsert substitui o dataset wholesale numa transação e dispara pg_notify no canal sentry_datasets_changed (constante); set_enabled e delete notificam igual — todos os nós hot-reloadam sem restart.

Migrations

Embutidas via sqlx::migrate! (migrations::run no startup). Arquivos em crates/sentry-storage/migrations/:

MigrationConteúdo
20260101000000_init.sqlTabelas events, incidents, rules, ip_state + índices base
20260102000000_routes.sqlTabela routes (hot-reloadable via LISTEN/NOTIFY)
20260821000000_offenders.sqlstrikes/total_violations/last_violation_at em ip_state (F2.12)
20260930000000_incident_feedback.sqlincidents.client_ip + acknowledged_at + índice único em event_id (F4.5)
20261001000000_ha.sqlevents.payload_hash + índice parcial (dedupe cross-node, F4.7)
20261002000000_datasets.sqlTabelas datasets/dataset_entries (F7.7)
20261003000000_event_timings.sqlevents.duration_ms/process_us

Dedupe e LISTEN/NOTIFY

O dedupe cross-node (F4.7) funciona assim: cada nó calcula o mesmo payload_hash que o LRU local usa (IP + método + path para HTTP, hash do raw caso contrário); o INSERT compara contra o índice parcial events_payload_hash_ts na janela de 10 s e pula duplicatas persistidas por irmãos. O índice é parcial (WHERE payload_hash IS NOT NULL) — eventos sem hash nunca pagam o lookup.

Canais de hot-reload por LISTEN/NOTIFY (produtores → tarefas LISTEN no daemon):

CanalDispara
sentry_rules_changedRecompila o ruleset (daemon)
sentry_datasets_changedReimporta datasets no pipeline e no prefilter (daemon; emitido pelo DatasetRepo)
sentry_blocks_changedRe-sincroniza a BlockTable entre nós (daemon)
sentry_model_changedTroca o ONNX a quente (daemon)

O productor typical é a CLI/dashboard: mutação no Postgres + NOTIFY via PgPool::notify. A exceção é sentry_datasets_changed, emitido dentro do próprio DatasetRepo na mesma transação da escrita.

Notas de operação

  • EventRepo::insert é o wrapper single-row sem dedupe (delega a insert_with_hash com payload_hash = None) — caminhos de baixo volume; o caminho quente do daemon usa a lane em lote.
  • Repo é Clone barato (só o handle do pool); cada repo emprestado também. Não há estado por repo — a fonte de verdade vive no Postgres, e caches locais (BlockTable, ruleset) são sincronizados pelos canais acima.
  • O módulo error re-exporta StorageError/Result definidos em pool.rs; toda falha de query vira StorageError::Query com o erro do sqlx no texto.

On this page