Instrumentation Contract
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.
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 (mobiledesktopweb/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-queryAtribuiçã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 // C4C4 — 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 (mobiledesktopweb) 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 semredeploy (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 C1C2.2C5 (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.