Observability-first

mandatory

Todo componente Koder nasce instrumentado: structured logs, métricas (RED/USE), e traces distribuídos, correlacionados por um único `trace_id` que atravessa surfaces e serviços. Não se enxerga produção por acaso — instrumentação é parte do "done", não retrofit pós-incidente. Alerta em sintoma (SLO burn), nunca em causa ruidosa. Cardinalidade tem orçamento. Telemetria nunca vaza PII nem cruza tenant. Backend é o stack self-hosted em `infra/observe/`, não SaaS externo. É a dimensão D8 da rubrica de Solidez Arquitetural (`architecture-quality.kmd`) graduada a disciplina própria.

Princípio mandatory: a Koder Stack é observável desde o primeiro commit. Todo componente emite os três sinais — logs estruturados, métricas, traces — correlacionados por um identificador único que atravessa surfaces e serviços. Se você não consegue responder *"o que está acontecendo em produção agora?"* e *"por que aquele request do tenant X foi lento ontem às 14h?"* sem adicionar instrumentação nova, o componente está incompleto. Observabilidade não é dashboard — é a capacidade de fazer perguntas que você não previu.

Por que

always-on.kmd garante que o produto degrada com graça quando algo falha. Esta policy garante que você enxerga o que falhou, onde, pra quem, e por quê — sem fazer deploy de "agora com log". Em escala planetária (alvo da Stack: 100M+ contas, multi-região, multi-surface), três fatos tornam observabilidade load-bearing:

  1. O incidente já está acontecendo quando você descobre. Sem SLO +

    alerta de sintoma, você descobre por ticket de usuário — tarde demais.

  2. A causa raiz raramente está onde o sintoma aparece. Um p99 alto no

    gateway pode ser um lock no kdb três saltos abaixo. Sem trace correlacionado, é arqueologia de log.

  3. "Funciona na minha máquina" não escala pra 100M usuários. O que

    importa é o comportamento agregado e a long tail (p99/p999) sob carga real, multi-tenant, multi-região.

Observabilidade ≠ resiliência (always-on) ≠ throughput (hyperscale). Os três eixos são ortogonais e mandatórios. Um sistema pode ser resiliente e rápido e ainda assim ser uma caixa-preta — e aí cada incidente é uma escavação.

Os três sinais — visão geral

Sinal Pergunta que responde Custo Cardinalidade
Logs "O que exatamente aconteceu neste evento?" Alto por evento Ilimitada (texto)
Métricas "Qual a saúde agregada agora? Está piorando?" Barato, sempre-on Limitada (budget)
Traces "Por onde passou este request e onde gastou tempo?" Médio, amostrado Alta (por trace)

Regra de ouro: métrica pra detectar, trace pra localizar, log pra entender. Os três compartilham o mesmo trace_id (§4) — é o que transforma três silos em uma narrativa.


§1. Logs estruturados

R1.1 — JSON estruturado, nunca texto livre em produção

Todo log de produção é um objeto estruturado (JSON ou equivalente self-describing), uma linha por evento. Campos mínimos obrigatórios:

{
  "ts": "2026-05-29T14:03:21.441Z",
  "level": "info",
  "msg": "message delivered",
  "service": "koder-talk-gateway",
  "version": "3.2.1",
  "trace_id": "01HX...",
  "span_id": "a3f...",
  "tenant_id": "ws_8821",
  "fields": { "thread_id": "...", "latency_ms": 42 }
}

Anti-padrão: printf("user %s did %s\n", ...) — não-parseável, não- correlacionável, não-agregável.

R1.2 — Níveis com semântica fixa

Level Quando Aciona alerta?
error Falha que precisa de atenção humana eventual Agrega pra SLO
warn Degradação recuperável, comportamento inesperado tolerado Não direto
info Marco de negócio (request servido, job concluído) Não
debug Detalhe de diagnóstico; off em prod por default Não

error NÃO é "qualquer coisa que deu ruim" — é "alguém eventualmente precisa olhar". Erro esperado e tratado (input inválido do usuário) é info/warn, não error. Poluir error mata o sinal.

R1.3 — Log level dinâmico, sem redeploy

Mudar o nível de log de um componente em produção é runtime (endpoint admin / signal / config hot-reload), nunca requer redeploy. Diagnosticar incidente não pode depender de release.

R1.4 — Amostragem de logs de alto volume

Logs em hot path (≥ 1k/s) são amostrados (ex.: 1%) exceto error e warn, que são sempre 100%. Sampling decision carrega no trace_id (coerente com §3) pra que log e trace amostrem o mesmo request.

R1.5 — Panic/crash é evento estruturado com stacktrace simbolicado

Todo panic / exceção não-tratada / crash vira um log error estruturado, nunca um dump cru no stderr. Obrigatório:

  • Stacktrace simbolicado: nomes de função + arquivo:linha, não

    offsets crus (0x7f3a...) nem endereços. Artifact de produção nunca é full-strip sem o debug-info correspondente arquivado (símbolos uploadados pro stack de infra/observe/ ou guardados por build-id pra resolução offline).

  • Contexto correlacionado: trace_id + tenant_id do request que

    causou o crash (§4) — o panic é rastreável até a causa, não um evento órfão.

  • Core dump opcional pra falha nativa (RustCGo), atrás de flag, com

    símbolos resolvíveis.

Anti-padrão: panic logado como goroutine 42 [running]: 0x7f3a... sem símbolos → triagem impossível às 3h da manhã. Esta regra fecha a fatia de backend da dimensão D11 (debuggability) de architecture-quality.kmd; a fatia de repro determinístico vive em headless-first.kmd R5 (UI) e regression-tests.kmd (repro-as-test).


§2. Métricas

R2.1 — RED pra serviços, USE pra recursos

Todo serviço que atende request expõe RED:

  • Rate — requests/s
  • Errors — fração que falha
  • Duration — histograma de latência (p50p90p99/p999)

Todo recurso (pool, fila, cache, disco) expõe USE:

  • Utilization, Saturation, Errors

R2.2 — Convenção de nomes

koder_<service>_<unit>_<suffix> em snake_case, com suffix padrão Prometheus (_total, _seconds, _bytes). Ex.: koder_talk_requests_total, koder_talk_request_duration_seconds. Latência sempre em histograma (não gauge de média — média esconde a long tail que hyperscale-first se importa).

R2.3 — Orçamento de cardinalidade (regra dura)

Nunca usar como label de métrica um valor de cardinalidade ilimitada: user_id, trace_id, request_id, email, path com IDs embutidos, mensagem de erro livre. Isso explode a série temporal e derruba o backend.

Label OK (bounded) Label PROIBIDO (unbounded)
service, version, region user_id, tenant_id¹
endpoint (template /users/:id) path (/users/8821)
status_code, method error_message
tenant_tier (freeproent) email, session_id

¹ tenant_id como label é proibido por cardinalidade (milhões de tenants) e por isolamento — atribuição por tenant vai em exemplars no trace ou em log, não em label de métrica. Para análise por tenant, usar trace/log (§4) ou agregação offline, nunca série temporal per-tenant. (Exceção: top-N tenants enterprise explicitamente allow-listed, com cap rígido.)

R2.4 — Métrica é barata e sempre-on; não amostrar

Diferente de log e trace, métrica nunca é amostrada — é agregada na borda (counter/histogram em memória, scrape periódico). O custo é O(séries), não O(eventos); por isso R2.3 é a regra que protege o custo.


§3. Traces distribuídos

R3.1 — Toda borda propaga contexto W3C

Todo request cross-service propaga traceparent / tracestate (W3C Trace Context). Toda entrada (HTTP handler, consumer de fila, gRPC) extrai o contexto; toda saída (client HTTP, publish, RPC) injeta. Um request que atravessa 5 serviços é um trace, não 5.

R3.2 — Span por unidade de trabalho significativa

Abre-se span pra: handler de request, query de DB, chamada RPC, publish/ consume de fila, operação de I/O externa. Cada span carrega atributos úteis (db.statement sanitizado, tenant_id como atributo, rows, cache.hit). Span sem atributo é quase inútil.

R3.3 — Amostragem head-based + tail-based pra erros

  • Head sampling default (ex.: 1–10%) pra volume normal.
  • Tail sampling: 100% dos traces com erro ou latência > p99 são

    mantidos, mesmo que a head sampling os descartaria. Erro nunca é amostrado out.

  • Decisão de sampling é coerente com §1.4 (log e trace do mesmo request

    decidem junto).

R3.4 — Trace cruza surfaces

Mobiledesktopweb iniciam o trace no client e propagam o trace_id pro backend (header). Um tap no app é rastreável até a query no kdb. Compõe com always-on §8 (cross-surface) — o handshake carrega o contexto.


§4. Correlação & propagação de contexto

R4.1 — trace_id é a chave-mestra dos três sinais

Todo log (§1.1), todo exemplar de métrica (§2.3), todo span (§3) carrega o mesmo trace_id. Dado um incidente, o fluxo é: alerta de SLO → métrica mostra qual endpoint/região → exemplar leva ao trace → trace mostra o span lento → log do span explica. Sem trace_id comum, isso vira três buscas desconexas.

R4.2 — Contexto propaga implicitamente, não na mão

O contexto (trace_id, tenant_id, request_id) viaja no context-object da linguagem (Go context.Context, etc.) e é injetado automaticamente em todo log/span pelo SDK de observabilidade — o desenvolvedor não passa trace_id a cada chamada. Passar na mão é anti-padrão (alguém esquece, a correlação quebra).

R4.3 — SDK único, não instrumentação ad-hoc

A instrumentação vem de um SDK Koder compartilhado (koder_kit / binding equivalente — compõe com reuse-first/sdk-first), não de cada componente reinventando logger + metrics client + tracer. O SDK garante: formato de log (§1.1), nomes de métrica (§2.2), propagação (§3.1, §4.2), e redação de PII (§8.1) — uma vez, pra todos.

O contrato exato que cada binding implementa (schema de log, deny-list de cardinalidade, assinaturas do context object, propagação W3C, redação) está em specs/observability/instrumentation-contract.kmd — fonte única pros bindings GoDartJS/Python (OBS-062).


§5. SLOs & error budgets

R5.1 — Todo serviço user-facing declara SLO

Em koder.toml:

[slo]
availability = 99.9      # % de requests não-erro / janela
latency_p99_ms = 300     # p99 abaixo disso
window_days = 30

SLI é medido das métricas RED (§2.1), não inventado. Sem SLO declarado, o serviço não tem definição de "saudável" — e aí alerta é chute.

R5.2 — Alerta é sobre error budget burn, não threshold cru

Alertar quando o error budget está queimando rápido (multi-window burn rate), não quando "p99 passou de 300ms uma vez". Isso mata flapping: um spike de 5s não acorda o oncall; uma degradação sustentada que vai estourar o budget em horas, sim.

R5.3 — Error budget governa cadência de release

hyperscale/always-on empurram features; o error budget é o freio. Budget esgotado no período → congela feature work do serviço, foca em confiabilidade até recuperar. Compõe com always-on (rollout) — budget é o sinal objetivo de "pare de adicionar risco".


§6. Alerting

R6.1 — Alerta em sintoma, não em causa

Alertar no que o usuário sente (SLO burn: erro/latência), não em causas isoladas (CPU 80%, fila com 100 itens). Causa vira alerta só se preditiva e acionável. Alerta de causa que não afeta SLO é ruído que treina o oncall a ignorar.

R6.2 — Todo alerta tem runbook acionável

Regra de alerta carrega link pra runbook (meta/context/ ou infra/observe/runbooks/) com: o que significa, como confirmar, como mitigar, como escalar. Alerta sem runbook = pager às 3h sem mapa.

R6.3 — Zero alerta crônico

Alerta que dispara e ninguém age (ou age sempre do mesmo jeito mecânico) é deletado ou automatizado. Alert fatigue é falha de observabilidade: backlog de alertas flapping mascara o incidente real. Revisão periódica mata alertas que viraram ruído.


§7. Backend self-hosted (infra/observe/)

R7.1 — Telemetria fica no stack Koder, não em SaaS externo

Logs, métricas, traces, dashboards e alertas vão pro stack de observabilidade self-hosted em infra/observe/. Datadog, New Relic, Grafana Cloud, Sentry SaaS et al. não são default — caem na régua de 5 gates de self-hosted-first.kmd. Dado de telemetria contém padrão de uso de usuário; mandar pra terceiro é exfiltração de dado de tenant (compõe com multi-tenant-by-default + identity-data-retention).

R7.2 — Protocolo de wire é OpenTelemetry (OTLP)

Componentes emitem em OTLP (vendor-neutral). Isso desacopla a instrumentação do backend — trocar o backend não toca o código instrumentado (compõe com D9 reversibilidade da rubrica). Anti-padrão: acoplar ao SDK proprietário de um vendor.

R7.3 — Retenção segue tier de dado

Retenção de telemetria respeita identity-data-retention.kmd e error-reporting-retention.kmd: raw com janela curta, agregados longos. Telemetria não é desculpa pra reter PII além do permitido.


§8. Multi-tenant & privacidade na telemetria

R8.1 — Nunca PII em logmétricatrace

Proibido em qualquer sinal: senha, token, chave, email, CPF, conteúdo de mensagem do usuário, payload de documento. O SDK (§4.3) redige campos sensíveis por default (allow-list, não deny-list). Compõe com security.kmd. Anti-padrão clássico: logar o request body inteiro "pra debugar".

R8.2 — tenant_id é atributo de trace/log, label bounded de métrica

Atribuição por tenant existe (pra responder "o tenant X está sendo afetado?") mas via trace/log (§2.3 nota ¹), não série temporal per-tenant. Queries de telemetria por tenant respeitam isolamento — um operador vê agregado; ver dado de um tenant específico é ação auditada.

R8.3 — Telemetria de client respeita opt-out

Telemetria coletada de apps (mobiledesktopweb) respeita o toggle de auto-report (specs/errors/reporting.kmd). Crash/erro só sobem com consentimento; métrica de produto é anônima/agregada.


Templates de teste mandatórios

T1 — Trace end-to-end cross-service

Request sintético atravessa ≥ 2 serviços; assertir que existe um trace com spans de ambos, mesmo trace_id, parent-child correto.

T2 — Correlação log↔trace

Disparar request com trace_id conhecido; assertir que os logs emitidos carregam o mesmo trace_id e são recuperáveis por ele.

T3 — Guarda de cardinalidade

Lint/teste estático que rejeita label de métrica em deny-list (§2.3: user_id, path com ID, error_message, etc.). Roda no CI.

T4 — Redação de PII

Alimentar campos sensíveis (email, token, body) num evento; assertir que o sink final não os contém (redação do SDK funcionou).

T5 — SLO declarado e medível

Assertir que todo serviço user-facing tem [slo] em koder.toml e que o SLI correspondente é computável das métricas RED expostas.

T6 — Alerta tem runbook

Lint: toda regra de alerta referencia um runbook existente (link resolve). Alerta órfão = falha.


Gate de release

Componente NÃO entra em release pipeline se:

  • Não expõe RED (serviço) ou USE (recurso) — T1/T5
  • Métrica usa label de cardinalidade ilimitada — T3 (block)
  • Log não-estruturado em path de produção — audit estático
  • PII detectável em algum sinal — T4 (block)
  • Serviço user-facing sem [slo] declarado — T5
  • Trace não propaga contexto W3C cross-service — T1
  • Regra de alerta sem runbook — T6 (warning)

Severity:

  • Error (block): cardinalidade ilimitada (R2.3), PII em telemetria

    (R8.1), log não-estruturado em prod (R1.1), SLO ausente em user-facing (R5.1)

  • Warning (advisory): alerta sem runbook (R6.2), trace não cruza

    surface (R3.4), telemetria em SaaS externo sem gate (R7.1)


Anti-padrões explícitos

Anti-pattern Por que dói Correto
printf/texto livre em prod Não-parseável, não-correlacionável R1.1 JSON estruturado
user_id como label de métrica Explode cardinalidade, derruba backend R2.3 (trace/log, não label)
Logar request body "pra debugar" Vaza PII/conteúdo de tenant R8.1 redação por allow-list
error pra input inválido do usuário Polui o sinal, mata o alerta R1.2 (é info/warn)
Média de latência em gauge Esconde a long tail (p99/p999) R2.2 histograma
Alerta em CPU 80% Ruído não-acionável → alert fatigue R6.1 alerta em SLO burn
Passar trace_id na mão Alguém esquece, correlação quebra R4.2 context implícito
Adicionar log e fazer deploy pra diagnosticar Instrumentação como retrofit observabilidade é parte do "done"
Mandar telemetria pro Datadog Exfiltra padrão de uso de tenant R7.1 infra/observe/ self-hosted
Tracer proprietário de vendor Acopla, irreversível (D9) R7.2 OTLP
Panic logado com offsets crus (0x7f3a...) Triagem impossível, sem causa R1.5 stacktrace simbolicado + trace_id

Composes with

  • policies/architecture-quality.kmd: esta policy É a dimensão *8

    (observabilidade) graduada a disciplina própria. A rubrica delega o detalhe normativo aqui. Também sustenta a fatia de prod/backend da dimensão D11 (debuggability)*via R1.5 (symbolication) + R4.1 (correlação trace_id).

  • policies/always-on.kmd: always-on é resiliência (o que fazer

    quando falha); observability é enxergar (o que falhou e por quê). Healthcheck (always-on §4.2) é a fronteira que ambos tocam.

  • policies/hyperscale-first.kmd: o histograma de latência (R2.2) e

    o SLO p99 (R5.1) são como hyperscale mede a long tail sob carga.

  • policies/multi-tenant-by-default.kmd + identity-data-retention.kmd:

    §8 garante que telemetria não vaza PII nem cruza tenant.

  • policies/self-hosted-first.kmd: §7 mantém telemetria no stack

    Koder, não em SaaS externo.

  • policies/reuse-first.kmd / sdk-first.kmd: a instrumentação

    vem de um SDK único (§4.3), não reinventada por componente.

  • specs/errors/reporting.kmd: error reporting é um consumidor desta

    policy (respeita opt-out, §8.3).

Exceptions

  1. Dev / staging — sampling pode ser 100%, retenção curta, SLO

    relaxado. O gate de release roda contra prod-likeness.

  2. Componentes pré-policy (≤ 2026-05-29) — não exige refactor

    imediato; próximo refactor substancial instrumenta. Débito catalogado em meta/docs/stack/registries/observability-debt.md (criar quando a primeira violação for descoberta).

  3. Tooling interno efêmero (scripts one-off, scratch) — fora do gate;

    produção tem o gate.

Migration path

  • Novos componentes/endpoints: instrumentados desde o primeiro commit via

    SDK (§4.3).

  • Componentes existentes: instrumentam no próximo refactor major; novos

    paths dentro deles seguem a policy já.

  • Auditoria: /k-audit policies (advisory) sinaliza componentes sem RED,

    com label de cardinalidade suspeita, ou com error poluído.

Significance

always-on mantém o produto no ar; hyperscale mantém rápido; observability-first é o que permite saber que ambos continuam verdadeiros em produção — e diagnosticar em minutos, não em escavação de log, quando deixam de ser. Sem ela, a Stack opera no escuro: o primeiro a saber de um incidente é o usuário, e a causa raiz é sempre uma sexta-feira à noite. É a diferença entre "temos dashboards" e "conseguimos responder perguntas que não prevíamos".