Observability-first
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:
- O incidente já está acontecendo quando você descobre. Sem SLO +
alerta de sintoma, você descobre por ticket de usuário — tarde demais.
- 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.
- "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ãooffsets crus (
0x7f3a...) nem endereços. Artifact de produção nunca é full-strip sem o debug-info correspondente arquivado (símbolos uploadados pro stack deinfra/observe/ou guardados por build-id pra resolução offline). - Contexto correlacionado:
trace_id+tenant_iddo request quecausou 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 = 30SLI é 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 fazerquando 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) eo 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 stackKoder, não em SaaS externo.
policies/reuse-first.kmd/sdk-first.kmd: a instrumentaçãovem de um SDK único (§4.3), não reinventada por componente.
specs/errors/reporting.kmd: error reporting é um consumidor destapolicy (respeita opt-out, §8.3).
Exceptions
- Dev / staging — sampling pode ser 100%, retenção curta, SLO
relaxado. O gate de release roda contra prod-likeness.
- 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). - 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
errorpoluí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".