Instrumentation Contract
observability specs/observability/instrumentation-contract.kmd
Contrato único que todo binding de instrumentação Koder (Go, Dart, JS, Python, …) DEVE satisfazer. Define o schema de log, a convenção de métrica + deny-list de cardinalidade, o context object propagado implicitamente, a propagação W3C, a redação de PII e o export OTLP. É o "como" implementável da policy `observability-first.kmd` (o "o quê" normativo). Um único contrato → N bindings idênticos em comportamento.
Corpo da especificação
Spec — Instrumentation Contract
Esta spec é o contrato que os bindings implementam. A policy
observability-first.kmddefine o que é exigido (regras R*); esta spec define a forma exata (schemas, assinaturas, deny-lists) pra que Go, Dart, JS e Python produzam telemetria idêntica em comportamento. Toda regraC*abaixo é testável (conformance tests no fim).
Escopo
Aplica a todo binding de instrumentação consumido por componentes
Koder. Os bindings vivem nos SDKs de cada linguagem (não se reinventa por
componente — reuse-first):
| Linguagem | Home do binding | Consumidores típicos |
|---|---|---|
| Go | engines/sdk/go | backends, daemons, gateways, CLIs |
| Dart | engines/sdk/koder_kit | apps Flutter (mobile/desktop/web/tv) |
| JS/TS | engines/sdk/js | web (templ+HTMX islands), TV |
| Python | engines/sdk/python | tooling, ML, scripts de produção |
Rollout: Go primeiro (maior superfície backend, onde SLO/RED mais importam). Dart em seguida (bloqueado hoje por lock de sessão em
koder_kit). JS/Python depois.
C1 — Schema do evento de log
Todo log é um objeto com campos obrigatórios (tipos fixos):
| Campo | Tipo | Origem |
|---|---|---|
ts | RFC3339 UTC ms | clock (fakeable via koder_test_clock) |
level | enum error|warn|info|debug | call site (semântica C1.1) |
msg | string curta, estática | call site (sem interpolação de PII) |
service | string | config do componente |
version | semver string | build |
trace_id | 128-bit hex (W3C) | context (C3) — vazio se fora de request |
span_id | 64-bit hex | context (C3) |
tenant_id | string opaca | context (C3) — vazio se não-tenant |
fields | objeto k→v | call site (passa por redação C5) |
C1.1 — Semântica de level (idêntica a observability-first R1.2):
error = "alguém precisa olhar eventualmente"; erro esperado/tratado é
info/warn. debug off em prod por default, ligável em runtime sem
redeploy (R1.3).
C2 — Métricas: naming + cardinalidade
C2.1 — Naming: koder_<service>_<unit>_<suffix> snake_case, suffix
Prometheus (_total, _seconds, _bytes). Latência sempre
histograma.
C2.2 — Deny-list de label (cardinalidade ilimitada — hard fail): o binding rejeita em build/lint qualquer label em:
user_id, tenant_id, trace_id, span_id, request_id, session_id,
email, path-com-id, error_message, ip, url-com-query
Atribuição por tenant/usuário vai em exemplar de trace ou em log
(C1), nunca em série temporal. (Exceção allow-listed: tenant_tier
bounded.) Mapeia observability-first R2.3.
C3 — Context object & propagação implícita
C3.1 — O binding carrega um context object da linguagem (context. Context em Go, zona/Zone ou equivalente em Dart, contextvars em
Python, AsyncLocalStorage em JS) contendo {trace_id, span_id, tenant_id, request_id}.
C3.2 — Log e span herdam esses campos automaticamente do context
— o call site não passa trace_id na mão (R4.2). Passar manualmente
é violação de contrato.
C3.3 — Assinatura mínima que cada binding expõe:
WithSpan(ctx, name) -> (ctx, span) // abre span filho
Log(ctx, level, msg, fields) // log herda ctx
Metric counters/histograms via registry com guarda C2.2
Inject(ctx, carrier) / Extract(carrier) -> ctx // C4
C4 — Propagação W3C
Toda borda de saída injeta traceparent/tracestate (W3C Trace
Context); toda entrada extrai. Um request cross-service é um trace
(R3.1). Clientes de surface (mobile/desktop/web) iniciam o trace e
propagam pro backend (R3.4).
C5 — Redação de PII (allow-list, não deny-list)
C5.1 — fields (C1) e atributos de span passam por um redator que
só deixa passar chaves explicitamente allow-listed; o resto é
elidido ("[redacted]"). Default seguro: desconhecido → redigido.
C5.2 — Proibido em qualquer sinal: senha, token, chave, email, CPF,
conteúdo de mensagem/documento de usuário (R8.1 + security.kmd).
Request body inteiro nunca é logado.
C6 — Export OTLP
O binding exporta os 3 sinais em OTLP (vendor-neutral) pro collector
self-hosted de infra/observe/ (OBS-061). Sem SDK proprietário de vendor
(R7.2 / reversibilidade D9). Sampling: head default + tail 100% em
erro/latência>p99 (R3.3), decisão coerente entre log e trace.
Requisitos por binding (paridade)
Cada binding é conforme quando expõe C3.3, aplica C2.2 em build-time,
redige por C5.1, propaga C4, e exporta C6 — e passa os conformance tests.
Divergência de comportamento entre bindings = bug de paridade (registry
chat-channels-parity-style a criar se necessário).
Conformance tests
Mapeiam os T* da observability-first.kmd:
- CT1 (=T2) — log emitido dentro de
WithSpancarrega otrace_iddo context; recuperável por ele. - CT2 (=T1) — request cross-binding produz um trace, parent-child correto.
- CT3 (=T3) — registrar métrica com label da deny-list C2.2 falha em build/lint.
- CT4 (=T4) — campo sensível em
fieldsnão aparece no sink (C5). - CT5 —
debugdesligado por default; ligável em runtime sem redeploy (C1.1 / R1.3).
Non-goals
- Backend de storage/dashboards — é OBS-061, não esta spec.
- Symbolication de crash — é OBS-063 (consome o
trace_iddaqui). - Audit estático (CT3/alert-runbook) — é KTOOLS-033.
- Determinismo de repro de UI —
headless-firstR5.
Open questions
- Reusar OpenTelemetry SDK por linguagem como base (vendor-neutral, já OTLP) vs binding fino próprio? Default proposto: usar o OTel SDK como substrato e encapsular num thin Koder layer que impõe C1/C2.2/C5 (não reinventar wire/propagação) — decidir no início do binding Go.
- Home canônico do thin layer compartilhado (cross-language) — provável
engines/sdk/<lang>por linguagem, com o contrato (esta spec) como fonte única. Confirmar naming viacomponent-names.mdse virar componente nomeado.
Referências
policies/observability-first.kmdpolicies/multi-tenant-by-default.kmdpolicies/security.kmdpolicies/reuse-first.kmdpolicies/identity-data-retention.kmd