Ir para o conteúdo

Observabilidade

O Quorum (quorum-sec-scan, v0.8.3) é uma ferramenta CLI/Docker efêmera: cada execução nasce com quorum scan <target>, roda um pool de scanners em paralelo, emite um relatório e morre. Não há daemon, servidor, banco de dados nem painel — o próprio root.go resume a filosofia: "Built for CI/CD: configure via flags, gate via exit code. No panel, no daemon." Por isso, a observabilidade do Quorum não segue o modelo de serviço de longa duração (métricas push, tracing distribuído, health checks, dashboards ao vivo). Em vez disso, ela se apoia em sinais de processo: logs estruturados em stderr (text ou json), status por scanner, exit codes, um arquivo de métricas Prometheus opt-in e o relatório (SARIF/JSON/XML) como artefato auditável.

Este documento descreve com precisão o que existe hoje (as-is), declara explicitamente o que é N/A para um CLI efêmero (com justificativa técnica) e oferece propostas futuras concretas e claramente separadas do estado atual.

Novidade desde a v0.2.3: os dois recursos que antes eram propostas — --log-format json (logs NDJSON em stderr) e --metrics <arquivo> (Prometheus textfile) — agora são reais e implementados. Eles saíram da §4 (propostas) e passaram a integrar o estado atual (§2.1 e §2.4).

Novidade desde a v0.7.4: a camada de aconselhamento opt-in (--advice) adiciona sua própria telemetria, com escopo bem delimitado — a série de métricas quorum_advice_* (§2.7) — emitida apenas quando --advice está ligado. Sem --advice, o arquivo de métricas e todas as demais saídas são byte-idênticos ao que eram antes. A camada de aconselhamento é somente de apresentação e nunca toca em correlationKey / fingerprint / confidence / severidade agregada / o gate --fail-on.


1. Modelo mental: observabilidade de um CLI efêmero vs. serviço

A maioria dos pilares clássicos de observabilidade (Logs, Métricas, Traces) pressupõe um processo de longa duração que pode ser raspado (scraped), consultado e correlacionado ao longo do tempo. O Quorum não é esse processo. Ele é um job batch: executa em segundos a minutos, normalmente dentro de um step de CI ou de um docker run, e termina.

flowchart LR
    subgraph "Serviço de longa duração (NÃO é o Quorum)"
        S[Processo sempre ativo] --> P[/metrics scrape/]
        S --> T[traces para coletor]
        S --> H[/healthz/]
    end
    subgraph "Quorum: job efêmero"
        I[quorum scan] --> L[logs em stderr text/json]
        I --> ST[status por scanner]
        I --> EC[exit code]
        I --> MF[arquivo .prom via --metrics]
        I --> R[relatório SARIF/JSON/XML]
    end
    L --> CI[CI logs]
    ST --> R
    EC --> Gate[gate do pipeline]
    MF --> TF[textfile collector / Pushgateway]
    R --> CS[Code Scanning / artefato]

Consequência prática: a "telemetria" do Quorum é consumida pela plataforma que o invoca (runner de CI, orquestrador de jobs, shell), não por um stack de observabilidade próprio. Os sinais são projetados para serem capturados pelos logs do step, pelo upload de artefatos, pela ingestão de SARIF (ex.: GitHub Code Scanning) e — quando --metrics é usado — por um coletor textfile ou Pushgateway ao fim do job.


2. Estado atual (as-is)

2.1 Logs estruturados em stderr (text ou json)

Toda a saída de progresso vai para stderr, mantendo o stdout limpo para o relatório (quando --output não é usado, o relatório sai em stdout — separar os streams é o que permite quorum scan img > report.sarif sem poluição).

Desde a v0.7.4 o formato do log é selecionável pela flag --log-format text|json (default text). No formato text cada evento é prefixado com [quorum]; no formato json cada evento vira uma linha JSON (NDJSON) em stderr. O logger é definido em cmd/quorum/scan.go (runScan):

logf := func(format string, args ...any) {
    if f.quiet {
        return
    }
    msg := fmt.Sprintf(format, args...)
    if f.logFormat == "json" {
        b, _ := json.Marshal(struct {
            TS    string `json:"ts"`
            Level string `json:"level"`
            Msg   string `json:"msg"`
        }{time.Now().UTC().Format(time.RFC3339), "info", msg})
        fmt.Fprintln(os.Stderr, string(b))
        return
    }
    fmt.Fprintf(os.Stderr, "[quorum] %s\n", msg)
}

Esse logf é injetado no orquestrador via orchestrator.Options.Logf, de modo que CLI e pipeline compartilham o mesmo canal e formato de log. Um valor inválido de --log-format é recusado com erro de uso (invalid --log-format …) antes de qualquer scanner rodar.

Exemplo de saída no formato json:

{"ts":"2026-07-04T10:00:01Z","level":"info","msg":"run  trivy (0.52.2) ..."}
{"ts":"2026-07-04T10:00:04Z","level":"info","msg":"done trivy: 12 findings in 3.14s"}

Eventos logados hoje (linhas reais do código; a mesma mensagem é emitida em ambos os formatos, mudando apenas o envelope):

Origem Evento Exemplo de mensagem (campo msg/texto)
scan.go contexto inicial target=… type=… crosswalk=N rules (…) offline=false
scan.go filtragem/supressão filtered: N suppressed by baseline (M entries), K below min-severity …
scan.go métricas gravadas metrics written to <path>
scan.go gate disparado gate: found HIGH finding >= --fail-on high → exit 1
scan.go aconselhamento (Fase 0) advice: knowledge=N entries (<dir>) → M findings enriched
scan.go aconselhamento (RAG) advice(rag): corpus=N chunks (…) → M findings gained OWASP references
scan.go aconselhamento (IA) advice(ai): provider=local model=… endpoint=… fix=suggest
scan.go aconselhamento (resultado IA) advice(ai): N findings gained an AI recommendation (fixes: V verified / P proposed)
orchestrator.go scanner desconhecido warning: unknown scanner "foo" ignored (known: …)
orchestrator.go skip por não-suporte skip <name>: does not support target <type>
orchestrator.go probe de versão skip <name>: version probe timed out after 60s …
orchestrator.go probe OOM skip <name>: version probe killed (likely OOM …)
orchestrator.go indisponível skip <name>: not installed/available
orchestrator.go início de execução run <name> (<ver>) ...
orchestrator.go falha de execução fail <name>: <erro>
orchestrator.go conclusão done <name>: N findings in <dur>

Características e limites (honestos):

  • No formato json, o envelope de cada linha é minimalista e estável: apenas ts (RFC 3339 UTC), level (hoje sempre "info") e msg (o texto humano). Não há, por evento, campos tipados como scanner, event ou durationMs — esses dados vivem no msg livre e no relatório. Ou seja, --log-format json torna o stream facilmente parseável linha a linha, mas ainda não é um esquema de eventos ricos por campo.
  • No formato text, as mensagens têm o prefixo estável [quorum] e um formato razoavelmente parseável, porém em texto livre — sem chave=valor garantida nem timestamps por linha.
  • --quiet / -q silencia todo o progresso e o summary (controlado por f.quiet), ortogonal ao --log-format: silencia ambos os formatos. Não há um modo --verbose/--debug com mais granularidade.
  • Não há rotação, sampling ou correlação de log — desnecessário para um processo efêmero; quem persiste é o runner de CI.

2.2 Status por scanner (o sinal central de "rodou de verdade")

O princípio de design "0 findings is not proof of safety" exige que o Quorum nunca confunda "zero vulnerabilidades" com "o scanner não rodou". Isso é materializado pelo ScannerRun.Status (em internal/orchestrator/orchestrator.go). Com 12 scanners disponíveis na v0.8.3 (trivy, grype, checkov, kics, dockle, kubescape, polaris, kube-score, terrascan, tfsec, regula e conftest), esse sinal é ainda mais importante: cada alvo aciona apenas o subconjunto que o suporta, e o status distingue "não se aplica" de "deveria rodar e falhou".

Status Significado Como é determinado
ran rodou e produziu resultado a.Run retornou sem erro
skipped não se aplica ao alvo !a.Supports(target)
unavailable binário ausente, probe estourou (timeout) ou foi morto (OOM) falha no a.Version durante o probe
error rodou mas falhou a.Run retornou erro que não é deadline
timeout excedeu --timeout por scanner runCtx.Err() == DeadlineExceeded

O probe de versão (Options.ProbeTime, default 60s) é um diagnóstico de observabilidade por si só: ele distingue timeout de killed (OOM) de não instalado, e gera mensagens acionáveis (ex.: "raise the container's memory limit", "scope --scanners"). Ver também a seção de status de scanner em DESIGN.md §14.

2.3 Summary no terminal

Após emitir o relatório, printSummary (scan.go) escreve em stderr um resumo legível por humano (suprimido por --quiet). O summary é sempre em texto tabular — ele não é afetado por --log-format (o --log-format json cobre o stream de progresso logf, não o bloco de resumo):

── quorum summary ───────────────────────────
  trivy      ran           12 findings
  grype      ran            9 findings
  checkov    unavailable    0 findings  (version probe killed — likely OOM …)
  ----------------------------------------
  18 findings after consensus  (7 multi-detected)
  CRIT 1  HIGH 4  MED 9  LOW 3  INFO 1
  elapsed 8.412s
  note: 0 findings is not proof of safety — see scanner statuses above.

Sinais embutidos no summary: status + contagem por scanner, total pós-consenso, quantos foram multi-detectados (DetectionCount > 1), distribuição por severidade, e duração total (res.Duration). A nota final reforça o princípio anti-falso-negativo.

2.4 Métricas Prometheus via --metrics <arquivo> (implementado)

Fiel ao modelo efêmero, o Quorum não expõe um endpoint /metrics. Em vez disso, a flag --metrics <path> (off por default) faz o binário gravar um arquivo no formato texto do Prometheus ao fim da execução — pronto para ser coletado pelo node_exporter --collector.textfile ou empurrado ao Pushgateway no encerramento do job de CI. Assim a telemetria numérica existe sem servidor nem daemon.

O arquivo é escrito por writeMetricsFile (cmd/quorum/scan.go), que chama report.WriteMetrics (internal/report/metrics.go). Detalhes de implementação relevantes:

  • Ordem de execução: o arquivo é gravado depois do relatório e do summary e antes do gate — portanto o .prom é produzido mesmo quando --fail-on vai disparar os.Exit(1).
  • Após gravar, um evento metrics written to <path> é logado via logf.
  • Permissões: 0644 (métricas são contagens não sensíveis, pensadas para serem raspadas), diferentemente do relatório, que é 0600 por poder conter detalhe sensível de finding. O caminho passa por filepath.Clean e os diretórios intermediários são criados com os.MkdirAll (0755).
  • Falha ao escrever o arquivo é fatal para a execução (writing metrics: …), pois o operador pediu explicitamente a telemetria.

Exemplo de arquivo .prom gerado (rótulos e nomes exatamente como emitidos pelo código):

# HELP quorum_scan_duration_seconds Total scan wall-clock time in seconds.
# TYPE quorum_scan_duration_seconds gauge
quorum_scan_duration_seconds 8.412
# HELP quorum_scanner_up 1 if the scanner ran, else 0 (skipped/unavailable/error/timeout).
# TYPE quorum_scanner_up gauge
quorum_scanner_up{scanner="trivy",status="ran"} 1
quorum_scanner_up{scanner="checkov",status="unavailable"} 0
# HELP quorum_scanner_findings Raw findings reported per scanner (pre-consensus).
# TYPE quorum_scanner_findings gauge
quorum_scanner_findings{scanner="trivy"} 12
# HELP quorum_scanner_duration_seconds Per-scanner duration in seconds.
# TYPE quorum_scanner_duration_seconds gauge
quorum_scanner_duration_seconds{scanner="trivy"} 3.14
# HELP quorum_findings_after_consensus Findings remaining after consensus merge.
# TYPE quorum_findings_after_consensus gauge
quorum_findings_after_consensus 18
# HELP quorum_findings_total Consensus findings by severity.
# TYPE quorum_findings_total gauge
quorum_findings_total{severity="CRITICAL"} 1
quorum_findings_total{severity="HIGH"} 4
quorum_findings_total{severity="MEDIUM"} 9
quorum_findings_total{severity="LOW"} 3
quorum_findings_total{severity="INFO"} 1
# HELP quorum_multi_detected Consensus findings corroborated by more than one scanner.
# TYPE quorum_multi_detected gauge
quorum_multi_detected 7

Métricas emitidas por report.WriteMetrics (todas do tipo gauge), com a fonte exata no código:

Métrica Rótulos Fonte no código
quorum_scan_duration_seconds res.Duration.Seconds()
quorum_scanner_up scanner, status 1 se ScannerRun.Status == "ran", senão 0 (um ponto por scanner)
quorum_scanner_findings scanner ScannerRun.Findings (findings brutos, pré-consenso)
quorum_scanner_duration_seconds scanner ScannerRun.Duration.Seconds()
quorum_findings_after_consensus len(res.Merged)
quorum_findings_total severity contagem de res.Merged por Severity (rótulos CRITICAL/HIGH/MEDIUM/LOW/INFO; UNKNOWN não é emitido)
quorum_multi_detected quantos res.Merged têm DetectionCount > 1

Observações de fidelidade importantes:

  • O rótulo severity usa os valores em maiúsculas de model.Severity (CRITICAL, HIGH, MEDIUM, LOW, INFO), pois é a serialização literal do enum.
  • Há uma distinção deliberada entre quorum_scanner_findings (bruto, por scanner, antes do consenso) e quorum_findings_after_consensus / quorum_findings_total (depois do merge de consenso). Não confunda os dois ao montar dashboards.
  • A saúde de cada scanner é exposta como quorum_scanner_up (booleano) mais o rótulo textual status, permitindo tanto sum(quorum_scanner_up) quanto o detalhe do motivo (status="timeout" etc.).
  • Não há métrica dedicada de gate (ex.: quorum_gate_triggered); o resultado do gate é observável pelo exit code (§2.5) e pelo evento de log gate: ….

2.5 Exit codes como sinal de máquina

Os exit codes são o sinal primário consumido por pipelines (contrato em scan.go e root.go):

Código Significado
0 OK — execução concluída e nenhum finding atingiu --fail-on
1 Gate — algum finding >= --fail-on disparou (os.Exit(1))
2 Erro de uso/runtime

Esse é o "monitoramento" mais barato e confiável de um job: o pipeline decide passar/falhar lendo um inteiro. Ver 06-interfaces-cli-e-formatos.md para o detalhamento das flags de gate (--fail-on, --min-severity).

2.6 Relatório como artefato auditável

O relatório é a trilha de auditoria persistente de cada execução. Em SARIF (formato primário) e JSON, o status de cada scanner é embutido no próprio documento — não fica só no log volátil:

  • SARIF (internal/report/sarif.go): a lista de scanners (scannerSummaryname/status/version) é gravada em properties.scanners do run; cada resultado carrega partialFingerprints["quorum/v1"] (= Fingerprint = sha256(correlationKey)), permitindo deduplicação e rastreio estável entre execuções e ferramentas a montante (ex.: GitHub Code Scanning).
  • JSON (internal/report/json.go): inclui scanners (os ScannerRun, com status, version, findings, durationMs, error) e os findings canônicos detalhados.
  • XML (internal/report/xml.go): expõe status por scanner como atributo.

Como o relatório carrega status + versão + duração por scanner + fingerprint determinístico, ele é auto-suficiente para auditoria post-mortem, mesmo que os logs do runner já tenham expirado. Ver 06-interfaces-cli-e-formatos.md.

2.7 Métricas da camada de aconselhamento (só com --advice)

Quando a camada de aconselhamento opt-in roda (--advice), report.WriteMetrics recebe um *report.AdviceMetrics não-nulo e anexa um bloco quorum_advice_* ao mesmo arquivo .prom. Essas séries só aparecem quando --advice está ligado; sem ela as métricas de aconselhamento estão ausentes e toda a saída é byte-idêntica. Elas são estritamente observacionais — a camada de aconselhamento é somente de apresentação e nunca afeta correlationKey, fingerprint, confidence, severidade agregada ou o gate --fail-on.

  • quorum_advice_enriched é emitida sempre que --advice está ligado (mesmo com a Fase 0/2 puramente determinística, sem modelo). Suas contagens derivam dos findings mesclados: um finding conta para remediation se tiver uma Remediation, para references se tiver qualquer References, e para recommendation se tiver um anexo de IA Advice.
  • quorum_advice_provider e quorum_advice_fix são emitidas apenas quando um provedor de IA foi de fato usado (--advice-provider=local ou remote, ou seja, Provider não é vazio nem none). Só com a Fase 0/2 há apenas quorum_advice_enriched.
  • quorum_advice_fix reporta o laço verify-the-fix: proposed é quantos patches o modelo sugeriu, verified é quantos sobreviveram ao re-scan (aplicados a uma cópia temporária, re-escaneados com o mesmo scanner, mantidos só se o finding desapareceu e o arquivo ainda faz parse). A razão verified/proposed é a taxa de sucesso do verify-the-fix. --fix nunca aplica automaticamente e é recusado com --advice-provider=remote (faria upload de código-fonte).

Exemplo de bloco de aconselhamento anexado ao arquivo .prom (só com --advice --advice-provider=local --fix=suggest):

# HELP quorum_advice_enriched Findings that gained an advisory attachment, by kind.
# TYPE quorum_advice_enriched gauge
quorum_advice_enriched{kind="remediation"} 14
quorum_advice_enriched{kind="references"} 11
quorum_advice_enriched{kind="recommendation"} 9
# HELP quorum_advice_provider The AI recommendation provider used (1 = active).
# TYPE quorum_advice_provider gauge
quorum_advice_provider{provider="local"} 1
# HELP quorum_advice_fix AI suggested fixes by verify-the-fix stage.
# TYPE quorum_advice_fix gauge
quorum_advice_fix{stage="proposed"} 3
quorum_advice_fix{stage="verified"} 2

Métricas de aconselhamento emitidas por report.WriteMetrics (todas gauge), com a fonte exata no código:

Métrica Rótulos Emitida quando Fonte no código
quorum_advice_enriched kind = remediation | references | recommendation --advice ligado contagem de res.Merged com uma Remediation / References não-vazia / um anexo Advice
quorum_advice_provider provider = local | remote provedor de IA usado constante 1 para o provedor ativo (AdviceMetrics.Provider)
quorum_advice_fix stage = proposed | verified provedor de IA usado AdviceMetrics.FixProposed / AdviceMetrics.FixVerified (a taxa do verify-the-fix)

Observações de fidelidade:

  • O bloco de aconselhamento é anexado depois de quorum_multi_detected; ele não altera nenhuma das métricas do core acima dele. Um consumidor anterior à camada de aconselhamento continua funcionando sem mudanças.
  • quorum_advice_provider é um único ponto (valor 1) para o provedor de fato usado — é um marcador de presença, não uma contagem.
  • Todo anexo de IA é rotulado como "AI-generated, advisory only" no relatório; as métricas apenas contam quantos findings carregam cada tipo de anexo. Se o modelo estiver inacessível a execução degrada graciosamente — o relatório sai sem conselho de IA, os contadores ficam em 0, e o scan nunca falha por causa da camada de aconselhamento.
  • A recomendação de IA é reproduzível (temperature=0 + um cache em disco chaveado por fingerprint+provider+model), então execuções repetidas sobre os mesmos findings produzem contagens de conselho estáveis.

2.8 Mapa dos sinais atuais

flowchart TD
    Run[quorum scan] --> Orq[orchestrator.Run]
    Orq -->|Logf text/json| Stderr["stderr: [quorum] … / NDJSON"]
    Orq --> Runs["ScannerRun{status,version,findings,duration,error}"]
    Runs --> Sum[printSummary → stderr]
    Runs --> Met["--metrics → arquivo .prom (0644)"]
    Runs --> Rep[relatório SARIF/JSON/XML]
    Adv["--advice (opt-in)"] -.->|AdviceMetrics| Met
    Adv -.-> Rep
    Run --> Exit[exit code 0/1/2]
    Met --> TF[textfile collector / Pushgateway]
    Rep --> Art[artefato / Code Scanning]
    Stderr --> CILogs[logs do runner de CI]

3. O que é N/A (e por quê)

Para um binário/contêiner efêmero que termina em segundos, os pilares abaixo não se aplicam ao runtime do Quorum. Declaramos N/A com justificativa técnica; onde já existe uma saída equivalente compatível com o modelo efêmero (ex.: --metrics textfile), isso é apontado.

Pilar Status Justificativa técnica
Endpoint de métricas raspável (/metrics HTTP scrape) N/A (runtime) — mas ver §2.4 Não há processo persistente para expor /metrics nem para ser raspado; o ciclo de vida (segundos) é mais curto que um intervalo típico de scrape. A exportação numérica existe, porém, via --metrics <arquivo> no formato textfile (coletado por node_exporter/Pushgateway).
Tracing distribuído (OpenTelemetry traces) N/A (runtime) Execução local, single-process, fan-out em goroutines dentro do mesmo processo. Não há saltos de rede entre serviços para correlacionar.
Endpoint de Health check (/healthz, readiness/liveness) N/A Não há servidor para checar saúde. O análogo de "health" é o exit code, o status por scanner e a métrica quorum_scanner_up. list-scanners serve como verificação de capacidade.
Dashboards ao vivo (Grafana) N/A (runtime) Sem série temporal emitida em tempo real. A visualização de tendência é responsabilidade da plataforma que ingere o SARIF ou o textfile de métricas.
Alertas (Alertmanager/PagerDuty) N/A (runtime) O alerta nativo é o gate por exit code (--fail-on) interpretado pelo CI. Regras de alerta podem ser montadas sobre as métricas --metrics no Prometheus, mas não são internas ao Quorum.
Log aggregation própria (ELK/Loki) N/A O Quorum escreve em stderr (text ou json); a agregação é delegada ao runner. --log-format json facilita a ingestão, mas reimplementar shippers de log seria reinventar a plataforma hospedeira.
APM / profiling contínuo N/A Sem processo vivo para perfilar continuamente; profiling pontual é tarefa de desenvolvimento (go test -bench, pprof ad hoc), não de produção.

Importante: N/A significa "não aplicável ao modelo de execução atual", não "impossível". A §2.4 já mostra como exportar métricas sem virar serviço; a §4 detalha as evoluções ainda não implementadas.


4. Propostas futuras (NÃO implementado)

Tudo nesta seção é proposta, claramente separada do estado atual. Nada abaixo existe no código hoje. São desenhos de evolução compatíveis com a natureza efêmera e CLI-first do Quorum (princípio: telemetria opt-in, sem daemon).

Graduaram para o estado atual (§2): os antigos --log-format json (agora §2.1), --metrics <arquivo> (agora §2.4) e as métricas da camada de aconselhamento (agora §2.7). Foram implementados e por isso saíram desta lista.

4.1 OpenTelemetry para o pipeline (spans opt-in via OTLP)

Mesmo sendo single-process, o pipeline scan → normalize → resolve aliases → correlate → score → report tem fases bem definidas e o fan-out de scanners é naturalmente um conjunto de spans irmãos. Uma exportação opt-in via OTLP (ativada por env vars OTel padrão e --otel) permitiria correlacionar uma execução de Quorum com o trace do pipeline de CI que a invocou (via traceparent herdado).

  • [ ] Flag --otel + honrar OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_SERVICE_NAME, traceparent.
  • [ ] Span raiz quorum.scan; spans filhos por scanner (runOne) e por fase do correlator.
  • [ ] Exportar no shutdown (flush síncrono — processo efêmero não pode confiar em batch assíncrono).
gantt
    title Trace proposto de um quorum scan (spans)
    dateFormat X
    axisFormat %S
    section scan
    quorum.scan            :a, 0, 9
    section scanners (fan-out)
    trivy.run              :0, 4
    grype.run              :0, 5
    checkov.probe (OOM)    :0, 1
    section pós
    correlate+alias        :5, 7
    consensus.score        :7, 8
    report.write           :8, 9

4.2 Dashboards de tendência via SARIF (sem servidor)

A análise de tendência (findings ao longo do tempo, MTTR, taxa de multi-detecção) deve ser delegada à plataforma que ingere o SARIF — tipicamente GitHub Code Scanning, que já oferece histórico, gráficos e alertas a partir dos partialFingerprints["quorum/v1"]. Alternativamente, as métricas do arquivo --metrics podem alimentar um Prometheus/Grafana externo. O Quorum não precisa de Grafana próprio.

  • [ ] Documentar o fluxo quorum scan -f sarifupload-sarif → Code Scanning como o caminho de tendência recomendado.
  • [ ] (Opcional) job de relatório que coleta os arquivos --metrics de várias execuções e plota tendências no Prometheus.
  • [ ] Garantir estabilidade do fingerprint entre versões (já é sha256(correlationKey)).

4.3 "Alertas" opt-in pós-execução

Manter o gate por exit code como mecanismo primário, e oferecer um hook de notificação opcional (ex.: --notify-webhook <url> enviando o summary JSON), executado apenas no fim do processo. Sem polling, sem daemon.

  • [ ] Flag --notify-webhook (off por default; respeita --offline).
  • [ ] Payload = summary estruturado (severidades, status por scanner, gate).
  • [ ] Falha de envio é não-fatal (degradação graciosa, como o alias OSV).

4.4 Resumo das propostas

Recurso Flag/mecanismo Compatível com efêmero? Situação
Logs JSON --log-format json Sim (stderr) Implementado (§2.1)
Métricas textfile --metrics <path> Sim (arquivo, sem servidor) Implementado (§2.4)
Métricas de aconselhamento --advicequorum_advice_* Sim (mesmo textfile) Implementado (§2.7)
OTel pipeline --otel + env OTLP Sim (flush no shutdown) Proposta (§4.1)
Tendência SARIF Code Scanning / Prometheus Sim (delegado) Proposta (§4.2)
Webhook --notify-webhook Sim (one-shot) Proposta (§4.3)

5. Como observar o Quorum hoje (guia prático)

Checklist para integrar a observabilidade existente num pipeline:

  • [ ] Capture stdout e stderr separadamente. Direcione o relatório com -o report.sarif e deixe os logs [quorum]/NDJSON em stderr para o runner.
  • [ ] Escolha o formato de log conforme o consumidor: --log-format text para leitura humana, --log-format json quando um agregador (Loki/ELK/CloudWatch) for parsear o stream linha a linha.
  • [ ] Use --fail-on para que o exit code seja seu sinal de alerta primário (gate).
  • [ ] Ative --metrics quorum.prom se você tem um node_exporter textfile collector ou Pushgateway — o arquivo .prom é gravado mesmo quando o gate falha, então não perca a telemetria em builds vermelhos.
  • [ ] Se usar --advice, observe as séries quorum_advice_*: quorum_advice_enriched para a cobertura de remediation/references/recommendation, e — com um provedor de IA — a razão verified/proposed de quorum_advice_fix como taxa de sucesso do verify-the-fix. Lembre que elas estão ausentes sem --advice e nunca afetam o gate.
  • [ ] Faça upload do relatório como artefato (e/ou ingira o SARIF no Code Scanning) — é sua trilha de auditoria durável.
  • [ ] Inspecione o status por scanner (quorum_scanner_up, coluna do summary, properties.scanners), não só a contagem de findings: um unavailable/timeout/error significa cobertura reduzida, não ausência de risco.
  • [ ] Não confunda quorum_scanner_findings (bruto) com quorum_findings_after_consensus/quorum_findings_total (pós-consenso) ao montar dashboards.
  • [ ] Não use --quiet em CI a menos que você já persista o relatório JSON/SARIF (e/ou o .prom) — --quiet remove o summary e o progresso em ambos os formatos.
  • [ ] Trate exit code 2 como falha de infraestrutura (uso/runtime), distinta do gate 1.
  • [ ] Em ambientes com pouca memória, observe as mensagens de probe (OOM/timeout) e ajuste memória ou --scanners.

Exemplo (GitHub Actions, conceitual):

- name: Quorum scan
  run: >
    quorum scan . -f sarif -o quorum.sarif
    --fail-on high --log-format json --metrics quorum.prom
  # exit 1 => gate; exit 2 => erro de runtime
- name: Upload SARIF
  if: always()
  uses: github/codeql-action/upload-sarif@v3
  with: { sarif_file: quorum.sarif }
- name: Upload raw report + metrics (auditoria/telemetria)
  if: always()
  uses: actions/upload-artifact@v4
  with: { name: quorum-report, path: "quorum.sarif\nquorum.prom" }

6. Referências cruzadas

  • 06-interfaces-cli-e-formatos.md — flags (--metrics, --log-format, --fail-on, --min-severity, --advice*, --fix), gating e exit codes.
  • 06-interfaces-cli-e-formatos.md — estrutura do SARIF, partialFingerprints, scanners no properties.
  • 13-ia.md — enquadramento honesto da camada de IA/aconselhamento (o core não tem IA; a camada de aconselhamento local/remota é opt-in e desligada por default).
  • DESIGN.md §14 (status de scanner) e §6 (matriz de correlação/consenso).
  • Código: cmd/quorum/scan.go (logf com --log-format, printSummary, writeMetricsFile, wiring de aconselhamento, exit codes), internal/report/metrics.go (WriteMetrics, AdviceMetrics), internal/orchestrator/orchestrator.go (ScannerRun, probe, status), internal/report/sarif.go (scannerSummary, fingerprints).

Premissas

  • Versão de referência: v0.8.3 (revisão 2026-07-04); afirmações verificadas em cmd/quorum/scan.go, internal/report/metrics.go, cmd/quorum/root.go, internal/orchestrator/orchestrator.go e internal/report/sarif.go no estado atual do repositório.
  • --log-format json produz um envelope minimalista e estável por linha (ts, level, msg), com level hoje sempre "info"; não assumi campos tipados adicionais (scanner, event, durationMs) que não existem no struct atual.
  • --metrics grava um arquivo Prometheus textfile com permissão 0644; os nomes e rótulos de métrica foram copiados literalmente de internal/report/metrics.go (incluindo severity em maiúsculas e a ausência de um quorum_gate_triggered). Assumi que UNKNOWN não é emitido porque o loop de severidade em WriteMetrics não o inclui.
  • As séries quorum_advice_* são emitidas apenas quando --advice rodou (WriteMetrics recebe um *AdviceMetrics não-nulo); quorum_advice_provider e quorum_advice_fix exigem um provedor de IA de fato (local/remote, não none). Nomes e rótulos (kind, provider, stage) foram copiados literalmente de internal/report/metrics.go. Sem --advice a saída é byte-idêntica.
  • Assumi que o consumidor de telemetria é a plataforma invocadora (runner de CI, docker run, shell) ou um coletor externo (textfile/Pushgateway), pois o Quorum não possui stack de observabilidade próprio.
  • O nome dos campos no SARIF (properties.scanners, partialFingerprints["quorum/v1"]) e no JSON (scanners, durationMs) foi tirado do código; mudanças futuras de schema podem alterá-los.
  • As flags restantes na §4 (--otel, --notify-webhook) são hipotéticas; nenhuma existe hoje e os nomes são sugestões de design, não compromissos.
  • Os exemplos de saída (summary, .prom, NDJSON, bloco de aconselhamento) são ilustrativos; valores numéricos não vêm de uma execução real.
  • O default do probe de versão é 60s (defaultProbeTime) e o --timeout por scanner é 5m, conforme o código; assumi que esses são os defaults efetivos quando Options.ProbeTime/PerScannerTime não são sobrescritos.