Observabilidade boa não começa quando o incidente já está aberto. Ela começa no desenho do binário: quais operações merecem um trace, quais métricas indicam degradação real, quais logs ajudam a explicar uma falha e quanto overhead você aceita pagar para enxergar o sistema. Em Zig, essa discussão fica ainda mais importante porque a linguagem não esconde custo em framework, runtime ou agente mágico. Se você quer sinal de produção, precisa escolher onde instrumentar.
OpenTelemetry virou o padrão de fato para traces, métricas e logs em aplicações modernas. Em linguagens como Go, Java, Python e Node, o ecossistema já oferece SDKs maduros, auto-instrumentação e exporters prontos para OTLP, Prometheus, Jaeger, Tempo, Honeycomb, Datadog e outros backends. Em Zig, o cenário ainda é mais manual. Isso não torna o padrão inútil. Pelo contrário: a mentalidade do OpenTelemetry ajuda a organizar a instrumentação mesmo quando parte do código ainda é própria.
Este guia mostra como pensar em OpenTelemetry com Zig sem prometer maturidade que o ecossistema ainda não tem. O foco é arquitetura prática: spans para operações importantes, métricas com cardinalidade controlada, logs correlacionados, propagação de contexto em HTTP, exporters simples e limites de overhead. Ele complementa os guias de observabilidade em Zig, servidor HTTP em produção, filas e workers em background, eBPF para observabilidade Linux e configuração segura em produção.
O que o OpenTelemetry resolve
OpenTelemetry não é apenas uma biblioteca. É um vocabulário comum para descrever o que uma aplicação fez. Esse vocabulário tem três pilares principais.
Traces mostram o caminho de uma operação entre componentes. Uma requisição HTTP pode começar no gateway, passar por um serviço Zig, chamar uma API externa, gravar em SQLite, publicar um job e responder ao usuário. Cada etapa vira um span com nome, duração, status e atributos.
Métricas mostram comportamento agregado ao longo do tempo: taxa de requisições, latência por rota, erros por tipo, tamanho de fila, jobs pendentes, bytes processados, conexões abertas, uso de memória e tempo de boot.
Logs explicam eventos discretos. Eles devem carregar contexto suficiente para responder perguntas como: qual trace_id falhou, qual rota estava em execução, qual job foi reprocessado, qual timeout ocorreu e qual dependência externa respondeu mal.
O valor do padrão aparece quando esses sinais conversam. Um alerta de latência aponta para uma métrica. A métrica leva para um trace lento. O trace mostra o span problemático. O span tem logs correlacionados. Você investiga a causa sem precisar adivinhar em qual máquina o erro apareceu.
A realidade do ecossistema Zig
Hoje, Zig ainda não tem a mesma camada pronta de OpenTelemetry que linguagens mais estabelecidas têm. Isso significa que você deve evitar duas armadilhas.
A primeira é fingir que existe auto-instrumentação completa. Se o seu serviço usa std.http.Server, sockets diretos, SQLite via C, filas próprias e um loop de workers, você provavelmente vai criar wrappers explícitos em volta desses pontos. Isso combina com Zig, mas exige disciplina.
A segunda é copiar a arquitetura de uma linguagem com garbage collector sem adaptar os custos. Um SDK de tracing pode alocar por span, criar strings, montar JSON, fazer batching e exportar em background. Em Zig, você deve saber qual allocator está sendo usado, quando o buffer cresce, como o shutdown descarrega eventos e o que acontece se o collector estiver fora.
Um caminho realista é começar com uma camada pequena de instrumentação interna, compatível com os conceitos do OpenTelemetry, e exportar em um formato simples. Quando bibliotecas Zig maduras aparecerem ou quando você decidir usar uma biblioteca C, a fronteira já estará clara.
Desenhe spans como operações de negócio
O erro comum em tracing é criar span demais. Se cada função vira um span, o trace fica barulhento, caro e difícil de ler. Em Zig, prefira instrumentar fronteiras relevantes:
- entrada de requisição HTTP;
- chamada para API externa;
- consulta ou transação de banco;
- execução de job em background;
- leitura ou escrita de arquivo grande;
- parsing de payload pesado;
- operação crítica de criptografia, compressão ou compilação;
- chamada FFI para biblioteca C importante.
Um span mínimo precisa de nome, início, fim, status e atributos. Em Zig, isso pode começar como uma struct simples:
const std = @import("std");
const SpanStatus = enum { ok, err };
const Span = struct {
trace_id: [16]u8,
span_id: [8]u8,
parent_span_id: ?[8]u8,
name: []const u8,
start_ns: i128,
end_ns: i128 = 0,
status: SpanStatus = .ok,
pub fn finish(self: *Span) void {
self.end_ns = std.time.nanoTimestamp();
}
pub fn durationNs(self: Span) i128 {
return self.end_ns - self.start_ns;
}
};
Esse exemplo não é um SDK completo. Ele mostra a fronteira: o código de negócio recebe um contexto, abre um span, executa a operação e fecha o span. Depois outro componente decide se serializa, amostra, descarta ou exporta.
Propagação de contexto em HTTP
Tracing distribuído depende de propagação. Se uma requisição entra com cabeçalhos traceparent e tracestate, o serviço Zig deve preservar esse contexto ao criar spans filhos e repassá-lo para chamadas downstream.
O formato W3C Trace Context usa um cabeçalho como:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
Ele carrega versão, trace_id, parent_id e flags. Para um serviço Zig, a primeira implementação não precisa cobrir todos os detalhes do mundo. Ela precisa fazer o básico com segurança:
- validar tamanho e separadores;
- rejeitar IDs inválidos ou zerados;
- preservar o
trace_idquando válido; - criar novo
span_idpara a operação atual; - incluir
traceparentnas chamadas HTTP de saída.
Se o cabeçalho não existir, gere um novo trace. Se existir mas for inválido, comece outro trace e registre um contador de propagação inválida. Não deixe uma string externa malformada quebrar a requisição.
Métricas: controle cardinalidade antes de exportar
Métricas são mais perigosas do que parecem. Um contador simples como http_requests_total é barato. O problema aparece quando você adiciona labels livres demais: user_id, email, path completo, job_id, tenant, error_message. Cardinalidade alta explode o backend de métricas e transforma observabilidade em incidente.
Para Zig, a regra deve ser explícita: label só entra se tiver conjunto pequeno e previsível. Bons labels:
- rota normalizada, como
/api/jobs/:id, não o caminho real; - método HTTP;
- status class, como
2xx,4xx,5xx; - tipo de job;
- nome da dependência externa;
- resultado, como
ok,timeout,erro_validacao.
Evite IDs, tokens, endereços de e-mail, mensagens brutas e payloads. Essa disciplina também protege privacidade e reduz risco de vazar segredo, como discutido no guia de configuração segura.
Um conjunto inicial útil para serviço HTTP em Zig:
http_server_requests_totalpor método, rota e status;http_server_request_duration_secondspor método e rota;process_start_time_seconds;zig_allocator_bytes_in_use, se você tiver wrapper de allocator;worker_jobs_totalpor tipo e resultado;worker_queue_depthpor fila;external_client_requests_totalpor dependência e resultado.
Comece pequeno. Métrica boa é aquela que alguém usa em alerta, dashboard ou investigação.
Logs correlacionados não são despejo de texto
Logs continuam úteis, mas precisam carregar contexto. Em vez de imprimir apenas erro ao processar job, registre campos previsíveis:
{"level":"error","msg":"job falhou","trace_id":"...","span_id":"...","job_type":"webhook","attempt":3,"error":"timeout"}
Em Zig, isso costuma significar escrever um logger estruturado próprio ou um wrapper em torno de std.log. O ponto principal é não depender de concatenação solta de strings. Defina campos comuns: service.name, service.version, environment, trace_id, span_id, route, job_type e dependency.
Também defina uma política de redaction. Logs de produção não devem carregar token, senha, cookie, header Authorization, DSN com credencial, payload completo de webhook ou dados pessoais desnecessários. Como Zig incentiva tipos explícitos, use wrappers como Secret e funções de serialização que se recusam a imprimir valores sensíveis.
Exporters: simples primeiro, OTLP depois
OpenTelemetry normalmente exporta dados via OTLP, por HTTP ou gRPC. Para Zig, você tem algumas opções.
A opção mais simples é expor métricas em formato Prometheus e enviar logs JSON para stdout. Em containers, isso já integra bem com Prometheus, Grafana Agent, Vector, Fluent Bit ou outro coletor. Para muitos serviços pequenos, esse primeiro passo entrega 80% do valor.
A segunda opção é enviar spans e métricas para um OpenTelemetry Collector local. Seu processo Zig fala com o collector por HTTP em formato OTLP/JSON ou por um formato intermediário que você controla. O collector faz batching, retry, autenticação e exportação para o backend final. Essa separação é saudável: o binário Zig continua pequeno e previsível, enquanto o collector absorve complexidade operacional.
A terceira opção é usar bibliotecas C ou bindings quando o projeto exigir compatibilidade mais profunda. Zig consegue linkar com C, mas isso muda o perfil de distribuição, build e segurança. Vale a pena quando você precisa de OTLP completo, compressão, TLS avançado, batching robusto e semântica compatível com uma plataforma corporativa.
Para aplicações Zig pequenas, a recomendação prática é: métricas Prometheus, logs JSON correlacionados e spans exportados para collector apenas nos fluxos críticos.
Amostragem e overhead
Nem todo trace precisa ser exportado. Em tráfego alto, grave todos os erros, todos os fluxos raros e uma amostra das requisições bem-sucedidas. A decisão pode ser probabilística, por rota ou por header de debug interno.
O cuidado é manter a decisão no início do trace. Se uma requisição não foi amostrada, evite alocar listas de eventos e atributos que serão descartados no final. Em Zig, essa diferença importa. Você pode representar isso no contexto:
const TraceContext = struct {
trace_id: [16]u8,
current_span_id: [8]u8,
sampled: bool,
};
Quando sampled for falso, spans podem virar no-ops baratos, enquanto métricas e logs de erro continuam ativos. Assim você mantém visibilidade sem transformar observabilidade em gargalo.
Checklist para produção
Antes de chamar a instrumentação de pronta, passe por esta lista:
- cada requisição HTTP recebe ou cria
trace_id; - chamadas externas propagam
traceparent; - erros incluem
trace_idnos logs; - métricas não usam labels de alta cardinalidade;
- segredos são redigidos antes de logar;
- exporter não bloqueia o caminho crítico indefinidamente;
- shutdown tenta descarregar spans pendentes com timeout;
- dashboards mostram latência, erro e saturação;
- alertas apontam para sintomas de usuário, não ruído interno;
- overhead foi medido com carga realista.
Essa última linha é essencial. Não basta afirmar que Zig é rápido. Instrumentação muda alocação, CPU, lock contention, tamanho de resposta e tempo de shutdown. Meça antes e depois, usando os mesmos princípios do guia de benchmarking em Zig.
Quando usar Go, Rust ou outro componente junto
Zig pode ser excelente para o serviço ou agente que precisa ser pequeno, previsível e próximo do sistema. Mas o ecossistema de OpenTelemetry em outras linguagens é mais maduro. Em uma plataforma grande, faz sentido deixar o collector, gateways de telemetria e integrações complexas em ferramentas já consolidadas, enquanto o binário Zig emite sinal simples e correto.
No cluster de programação, Go continua sendo uma referência natural para serviços de observabilidade e integrações OpenTelemetry, principalmente por causa de SDKs, exporters e projetos como Prometheus, Grafana Tempo e muitos agentes cloud-native. A decisão saudável não é “Zig ou Go para tudo”. É usar Zig onde controle de memória, distribuição e custo operacional importam, e integrar com ferramentas maduras onde o padrão já está resolvido.
Conclusão
OpenTelemetry em Zig ainda exige trabalho manual, mas a direção é clara: traces para operações importantes, métricas com cardinalidade controlada, logs estruturados com correlação e exportação que não derruba o caminho crítico. Zig combina bem com essa disciplina porque força escolhas explícitas sobre alocação, I/O, erros e limites.
O melhor começo não é criar um SDK perfeito. É instrumentar um serviço real com poucos sinais bons: latência por rota, taxa de erro, profundidade de fila, chamadas externas, logs com trace_id e um trace amostrado para fluxos críticos. Depois disso, evolua para collector, OTLP e bibliotecas mais completas conforme a necessidade aparecer.
Observabilidade não deve transformar o binário Zig em uma aplicação pesada. Ela deve fazer o contrário: dar visibilidade suficiente para manter serviços pequenos, rápidos e confiáveis em produção.