Observabilidade em Zig: Logs, Métricas, Traces e Health Checks

Observabilidade em Zig não deve ser tratada como um pacote que você instala no fim do projeto. Ela é uma decisão de arquitetura: quais sinais o binário vai emitir, quanto overhead você aceita, como investigar falhas às três da manhã e quais perguntas uma pessoa consegue responder sem recompilar o serviço.

A boa notícia é que Zig combina muito bem com observabilidade enxuta. A linguagem deixa claro quando você aloca, copia, bloqueia ou formata uma string. Isso ajuda a criar logs, métricas e traces previsíveis, sem arrastar um runtime pesado. A notícia menos confortável é que o ecossistema ainda não oferece a mesma camada pronta que Go, Java ou Python. Em muitos projetos, você vai escrever uma instrumentação pequena e explícita.

Este guia mostra um caminho prático para observabilidade em Zig: logs estruturados, métricas compatíveis com Prometheus, health checks, tracing leve, correlação de contexto, alertas úteis e limites de cardinalidade. Ele complementa os artigos de OpenTelemetry em Zig, servidor HTTP em produção, eBPF para observabilidade no Linux, debugging e profiling com Tracy, Valgrind e perf e configuração segura com variáveis de ambiente.

O que observar em uma aplicação Zig

Antes de escolher formato ou ferramenta, defina as perguntas que a produção precisa responder. Em um serviço HTTP, por exemplo:

  • a aplicação está viva e pronta para receber tráfego?
  • qual rota está lenta?
  • qual dependência externa falha com mais frequência?
  • quantas requisições terminam em erro por minuto?
  • a fila de jobs está crescendo?
  • o processo está vazando memória ou descritores de arquivo?
  • qual trace_id conecta log, métrica e requisição lenta?

Observabilidade útil nasce desses casos de uso. O erro comum é registrar tudo e descobrir depois que nada está conectado. Em Zig, prefira uma camada mínima, mas consistente: cada requisição recebe um identificador; logs carregam esse identificador; métricas usam nomes estáveis; health checks dizem se o processo está pronto; traces aparecem nas operações que realmente explicam latência.

Logs estruturados sem esconder custo

std.log é suficiente para começar, mas logs de produção precisam ser fáceis de filtrar. JSON por linha costuma funcionar bem porque qualquer stack de coleta consegue ler. O ponto importante é evitar formatação acidentalmente cara em caminho quente.

Um logger mínimo pode aceitar nível, mensagem, trace_id opcional e pares de campos. Em serviços pequenos, escrever para stdout já é o contrato correto: systemd, Docker, Kubernetes ou um agente coletor cuidam do transporte.

const std = @import("std");

const LogLevel = enum { debug, info, warn, err };

const Logger = struct {
    writer: std.fs.File.Writer,
    min_level: LogLevel,

    pub fn log(
        self: Logger,
        level: LogLevel,
        trace_id: ?[]const u8,
        comptime msg: []const u8,
    ) void {
        if (@intFromEnum(level) < @intFromEnum(self.min_level)) return;

        const ts = std.time.timestamp();
        const trace = trace_id orelse "";
        self.writer.print(
            "{{\"ts\":{d},\"level\":\"{s}\",\"trace_id\":\"{s}\",\"msg\":\"{s}\"}}\n",
            .{ ts, @tagName(level), trace, msg },
        ) catch {};
    }
};

Esse exemplo é simples de propósito. Em código real, cuide de escaping de strings, campos dinâmicos, limites de tamanho e queda segura quando o destino de log bloquear. O contrato principal é que o log seja legível por máquina e contenha contexto suficiente.

Evite três armadilhas comuns:

  1. logar payload completo com dados sensíveis;
  2. montar strings enormes antes de checar nível de log;
  3. transformar erro esperado em log de erro em cada requisição.

Para segurança operacional, combine este padrão com o guia de configuração segura e segredos. Logs nunca devem imprimir tokens, senhas, chaves privadas ou cabeçalhos de autenticação.

Métricas Prometheus em formato texto

Métricas respondem perguntas agregadas. Elas não explicam uma requisição específica, mas mostram tendência: latência subiu, erro aumentou, fila travou, worker parou.

O formato texto do Prometheus é simples o bastante para um exporter Zig pequeno. Um contador atômico já cobre muitos casos:

const std = @import("std");

const Counter = struct {
    name: []const u8,
    help: []const u8,
    value: std.atomic.Value(u64) = std.atomic.Value(u64).init(0),

    pub fn inc(self: *Counter) void {
        _ = self.value.fetchAdd(1, .monotonic);
    }

    pub fn write(self: *Counter, writer: anytype) !void {
        try writer.print("# HELP {s} {s}\n", .{ self.name, self.help });
        try writer.print("# TYPE {s} counter\n", .{ self.name });
        try writer.print("{s} {d}\n", .{ self.name, self.value.load(.monotonic) });
    }
};

Algumas métricas boas para começar:

MétricaTipoPor que existe
http_requests_totalcountervolume por rota/status
http_request_duration_secondshistogramlatência percebida pelo usuário
http_errors_totalcountererros por classe
jobs_processed_totalcounterthroughput de worker
job_queue_depthgaugebacklog atual
process_start_time_secondsgaugereinícios inesperados
allocator_failures_totalcounterpressão ou bug de memória

Em Zig, histogramas merecem atenção. Uma implementação ingênua pode alocar em cada observação. Para produção, prefira buckets fixos e arrays estáticos. A meta é medir latência sem adicionar latência relevante.

Cardinalidade: o detalhe que quebra monitoramento

O maior erro em métricas não é escolher contador ou histograma. É criar labels demais. Nunca use valores sem limite como label: user_id, email, path completo com ID, payload, query string ou mensagem de erro dinâmica.

Prefira labels controlados:

http_requests_total{method="GET",route="/api/items/:id",status="200"} 18241
http_requests_total{method="GET",route="/api/items/:id",status="500"} 12

Em vez de:

http_requests_total{path="/api/items/9f1a2b...",user="[email protected]"} 1

Cardinalidade alta encarece armazenamento, degrada dashboards e torna alertas instáveis. Esse cuidado é especialmente importante em serviços Zig pequenos: não adianta criar binário leve e derrubar o backend de métricas com séries infinitas.

Health check e readiness check

Health check não é só retornar 200 OK. Separe pelo menos dois conceitos:

  • liveness: o processo está vivo e o event loop responde?
  • readiness: o processo está pronto para receber tráfego real?

Um serviço pode estar vivo, mas não pronto porque ainda carrega configuração, espera migração, perdeu conexão com banco ou não conseguiu abrir arquivo essencial.

const Health = struct {
    ready: bool,
    db_ok: bool,
    queue_ok: bool,
};

fn statusCode(h: Health) u16 {
    if (h.ready and h.db_ok and h.queue_ok) return 200;
    return 503;
}

Em Kubernetes, use liveness para reiniciar processo travado e readiness para tirar o pod do balanceador. Em systemd ou Docker simples, o mesmo endpoint ainda ajuda o deploy e a automação de rollback.

Não coloque checks caros em cada health check. Um endpoint chamado a cada poucos segundos não deve fazer consulta pesada, varrer disco ou chamar API externa lenta. Mantenha estado de saúde atualizado por background checks e exponha o último resultado.

Tracing leve: spans onde há fronteira

Tracing não precisa começar com um SDK completo. Você pode criar spans simples para operações importantes: requisição HTTP, chamada externa, consulta de banco, job em background, leitura de arquivo grande ou chamada FFI.

Um span mínimo registra nome, início, fim, status e identificadores:

const Span = struct {
    trace_id: [16]u8,
    span_id: [8]u8,
    name: []const u8,
    start_ns: i128,
    end_ns: i128 = 0,
    ok: bool = true,

    pub fn finish(self: *Span) void {
        self.end_ns = std.time.nanoTimestamp();
    }
};

Não transforme cada função em span. Isso cria ruído e overhead. Instrumente fronteiras que explicam tempo ou falha. Se uma requisição lenta passa 80% do tempo em uma API externa, um único span dessa chamada vale mais que cem spans de helpers internos.

Quando o projeto crescer, use a mentalidade do OpenTelemetry em Zig para mapear esses spans em OTLP, Jaeger, Tempo ou outro backend.

Propagação de contexto em HTTP

Se o serviço recebe HTTP, aceite um identificador de correlação do upstream ou gere um novo. O padrão W3C traceparent é o caminho mais interoperável. Mesmo que você ainda não exporte traces completos, preservar o trace_id ajuda a conectar logs entre serviços.

Fluxo prático:

  1. ler traceparent se existir;
  2. validar tamanho/formato mínimo;
  3. gerar trace_id novo quando ausente;
  4. colocar o identificador nos logs;
  5. devolver ou encaminhar o contexto em chamadas externas.

Essa disciplina evita um problema comum: o frontend, o gateway e o serviço Zig têm logs, mas ninguém consegue provar que pertencem à mesma requisição.

Alertas bons são poucos e acionáveis

Dashboard bonito não substitui alerta útil. Comece por sintomas, não por causas internas obscuras.

Bons alertas:

  • taxa de erro 5xx acima do normal por alguns minutos;
  • p95 ou p99 de latência acima do SLO;
  • fila crescendo continuamente;
  • worker sem processar job há tempo demais;
  • reinícios frequentes;
  • readiness falhando em pods demais;
  • disco ou memória perto do limite.

Alertas ruins:

  • qualquer log warn;
  • CPU momentânea acima de 80% por poucos segundos;
  • métrica experimental sem dono;
  • erro esperado em retry que se recupera sozinho.

Em Zig, inclua falhas de allocator, erros de FFI e perda de evento de observabilidade como métricas próprias quando fizer sentido. O objetivo é descobrir degradação antes que o usuário perceba, mas sem acordar a equipe por ruído.

Integração com Grafana, Prometheus e coletores

Uma arquitetura simples para um serviço Zig em produção pode ser:

  1. logs JSON em stdout coletados por Vector, Fluent Bit ou Loki;
  2. endpoint /metrics em formato Prometheus;
  3. endpoint /healthz e /readyz;
  4. spans exportados por OTLP quando o projeto justificar;
  5. dashboards com latência, taxa de erro, throughput e saturação.

Se você ainda não tem collector, não bloqueie o projeto. Comece emitindo os sinais corretos. Um binário Zig que escreve logs JSON, expõe /metrics e responde readiness já está muito à frente de um serviço que só imprime texto solto.

Para Linux profundo, combine aplicação e sistema: eBPF ajuda a enxergar syscalls, rede e kernel; profiling com perf e Tracy ajuda a explicar CPU; OpenTelemetry ajuda a conectar serviços.

Checklist de produção

Antes de chamar um serviço Zig de observável, confira:

  • logs estruturados em formato estável;
  • trace_id ou correlação por requisição;
  • métricas de requisição, erro, latência e fila;
  • labels com cardinalidade controlada;
  • health check barato;
  • readiness check separado quando houver dependências;
  • alertas baseados em sintomas;
  • logs sem segredos;
  • shutdown descarrega buffers importantes;
  • documentação de onde investigar incidente.

Perguntas frequentes

Zig já tem OpenTelemetry oficial?

O ecossistema Zig ainda é mais manual que Go, Java ou Python. Use os conceitos do OpenTelemetry desde cedo, mas evite prometer auto-instrumentação completa. Para detalhes de spans, métricas e logs no padrão OTel, leia o guia de OpenTelemetry em Zig.

Preciso de Prometheus para ter métricas?

Não obrigatoriamente. Você pode começar com contadores internos, logs agregáveis e uma página /metrics simples. Prometheus fica natural quando você precisa de coleta periódica, alertas e dashboards.

Logs JSON são sempre melhores?

Para produção, geralmente sim, porque facilitam busca e parsing. Para CLI local, logs humanos podem ser melhores. Muitos projetos oferecem modo humano em desenvolvimento e JSON em produção.

Quanto overhead observabilidade deve ter?

Depende do serviço, mas o orçamento precisa ser explícito. Evite alocar por métrica, limitar cardinalidade, usar sampling quando traces ficarem caros e medir o próprio custo da instrumentação.

Próximos passos

Se você está preparando um serviço Zig para produção, siga esta ordem:

  1. adicione logs estruturados e IDs de correlação;
  2. exponha /healthz, /readyz e /metrics;
  3. crie métricas para erro, latência e saturação;
  4. adicione spans em fronteiras importantes;
  5. conecte com OpenTelemetry ou eBPF quando a necessidade aparecer.

Esse caminho mantém a filosofia da linguagem: controle explícito, binário enxuto e operação previsível. Observabilidade em Zig não precisa ser grande. Precisa responder rápido quando algo quebra.

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.