Pular para o conteúdo

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.kmd define 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 regra C* 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):

LinguagemHome do bindingConsumidores típicos
Goengines/sdk/gobackends, daemons, gateways, CLIs
Dartengines/sdk/koder_kitapps Flutter (mobile/desktop/web/tv)
JS/TSengines/sdk/jsweb (templ+HTMX islands), TV
Pythonengines/sdk/pythontooling, 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):

CampoTipoOrigem
tsRFC3339 UTC msclock (fakeable via koder_test_clock)
levelenum error|warn|info|debugcall site (semântica C1.1)
msgstring curta, estáticacall site (sem interpolação de PII)
servicestringconfig do componente
versionsemver stringbuild
trace_id128-bit hex (W3C)context (C3) — vazio se fora de request
span_id64-bit hexcontext (C3)
tenant_idstring opacacontext (C3) — vazio se não-tenant
fieldsobjeto k→vcall 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 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 WithSpan carrega o trace_id do 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 fields não aparece no sink (C5).
  • CT5debug desligado 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_id daqui).
  • Audit estático (CT3/alert-runbook) — é KTOOLS-033.
  • Determinismo de repro de UIheadless-first R5.

Open questions

  1. 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.
  2. 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 via component-names.md se virar componente nomeado.

Referências