Observabilidade em Zig: Logs Estruturados e Métricas Prometheus | Zig Brasil

Observabilidade em Zig: Logs Estruturados e Métricas Prometheus

Serviços em produção precisam de observabilidade: a capacidade de responder rapidamente “o que está acontecendo agora?” e “por que isso falhou?”. As três pernas clássicas são logs, métricas e traces. Zig não traz um framework de observabilidade pronto na biblioteca padrão, mas a combinação de std.log, um writer JSON simples e um endpoint Prometheus entrega cobertura sólida para a maioria dos serviços.

Este guia mostra como implementar, do zero, logging estruturado JSON e métricas no formato Prometheus exposition, prontos para Grafana, Loki, Datadog, CloudWatch ou qualquer stack moderna.

Os Três Pilares da Observabilidade

PilarResponde aFormato típico
LogsO que aconteceu, em detalheLinhas de texto ou JSON
MétricasComo está o sistema, agregadoSéries temporais (counter, gauge, histogram)
TracesPor que uma requisição foi lentaSpans encadeados por trace_id

Para começar de forma pragmática, foque em logs estruturados e métricas Prometheus — os dois retornam valor imediato e cobrem 90% das necessidades de monitoramento.

Logging Estruturado em JSON

Logs em texto livre são difíceis de filtrar em escala. JSON estruturado, em contraste, é parseado sem ambiguidade e permite consultas como level=error AND route=/checkout em Loki, Elasticsearch ou Datadog.

A std.log do Zig escreve para stderr com níveis (Debug, Info, Warn, Error) e suporta escopos nomeados via std.log.scoped. Para produzir JSON estruturado, vamos construir um helper simples:

const std = @import("std");

pub const LogLevel = enum {
    debug, info, warn, err,

    pub fn toString(self: LogLevel) []const u8 {
        return switch (self) {
            .debug => "debug",
            .info => "info",
            .warn => "warn",
            .err => "error",
        };
    }
};

pub const Logger = struct {
    out: std.fs.File.Writer,
    level: LogLevel,

    pub fn log(self: *Logger, level: LogLevel, comptime fmt: []const u8, args: anytype, trace_id: []const u8) void {
        if (@intFromEnum(level) < @intFromEnum(self.level)) return;
        const msg = std.fmt.allocPrint(std.heap.page_allocator, fmt, args) catch return;
        defer std.heap.page_allocator.free(msg);
        const ts = std.time.timestamp();
        self.out.print(
            \\{{"ts":{d},"level":"{s}","trace_id":"{s}","msg":"{s}"}}
            ++ "\n",
            .{ ts, level.toString(), trace_id, msg },
        ) catch return;
    }
};

Uso típico em um handler HTTP:

var logger = Logger{
    .out = std.io.getStdErr().writer(),
    .level = .info,
};

logger.log(.info, "request method={s} path={s} status={d} duration_ms={d}", .{ method, path, status, duration }, trace_id);

Saída:

{"ts":1718956800,"level":"info","trace_id":"a3f2","msg":"request method=GET path=/checkout status=200 duration_ms=12"}

Para escapar aspas dentro de strings reais, troque o msg por um objeto JSON montado com std.json.stringify, que cuida de escaping corretamente para valores dinâmicos.

Boas práticas para logs em produção

  • Sempre inclua trace_id: gerado no início de cada request e propagado para downstream.
  • Logue em stderr: deixa stdout livre para output de comando e facilita coleta por Docker/journald.
  • Use níveis com disciplina: info para eventos de negócio, warn para degradações recuperáveis, error para falhas que precisam de ação.
  • Evite dados sensíveis: nunca logue tokens, senhas, PII em texto puro.
  • Log estruturado desde o início: migrar de texto livre para JSON depois é caro.

Métricas no Formato Prometheus

Prometheus coleta métricas fazendo scrape periódico de um endpoint HTTP /metrics. Esse endpoint retorna texto simples no formato exposition, que qualquer linguagem consegue gerar. Não é necessário SDK proprietário.

Exemplo de saída válida para /metrics:

# HELP http_requests_total Total de requisições HTTP por rota.
# TYPE http_requests_total counter
http_requests_total{method="GET",route="/home",status="200"} 4523
http_requests_total{method="POST",route="/checkout",status="500"} 7

# HELP http_request_duration_seconds Latência das requisições.
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{route="/home",le="0.005"} 4300
http_request_duration_seconds_bucket{route="/home",le="0.01"} 4450
http_request_duration_seconds_bucket{route="/home",le="0.05"} 4510
http_request_duration_seconds_bucket{route="/home",le="0.1"} 4520
http_request_duration_seconds_bucket{route="/home",le="+Inf"} 4523
http_request_duration_seconds_sum 12.34
http_request_duration_seconds_count 4523

# HELP go_goroutines Conexões ativas.
# TYPE process_open_connections gauge
process_open_connections 42

Implementando um Counter thread-safe

const std = @import("std");

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

    pub fn init(name: []const u8, help: []const u8) Counter {
        return .{ .name = name, .help = help };
    }

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

    pub fn add(self: *Counter, n: u64) void {
        _ = self.value.fetchAdd(n, .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) });
    }
};

Gauge para valores que sobem e descem

pub const Gauge = struct {
    value: std.atomic.Value(u64) = .{ .raw = 0 },
    name: []const u8,
    help: []const u8,

    pub fn set(self: *Gauge, v: u64) void {
        self.value.store(v, .monotonic);
    }

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

Expondo o endpoint /metrics

Agregue todos os counters/gauges em um struct global e sirva via HTTP. Exemplo conceitual:

var requests_total = Counter.init("http_requests_total", "Total de requisições HTTP.");
var open_connections = Gauge.init("process_open_connections", "Conexões abertas.");

fn metricsHandler(writer: anytype) !void {
    try requests_total.write(writer);
    try open_connections.write(writer);
}

Aponte o Prometheus para http://seu-servico:8080/metrics com scrape_interval: 15s. Em segundos, os dashboards no Grafana começam a aparecer.

Histograma de Latência

Para responder “qual o p99 da latência do checkout?”, você precisa de um histograma. A ideia é dividir observações em buckets e expor contagem por bucket + soma + total. O Prometheus computa p50, p90, p99 via histogram_quantile().

pub const Histogram = struct {
    buckets: []const f64, // limites, ex: {0.005, 0.01, 0.05, 0.1, 0.5, 1, +Inf}
    counts: []std.atomic.Value(u64),
    sum: std.atomic.Value(u64) = .{ .raw = 0 }, // soma em microssegundos p/ inteiro
    total: std.atomic.Value(u64) = .{ .raw = 0 },
    name: []const u8,
    help: []const u8,

    pub fn observe(self: *Histogram, value_seconds: f64) void {
        const us: u64 = @intFromFloat(value_seconds * 1_000_000);
        _ = self.sum.fetchAdd(us, .monotonic);
        _ = self.total.fetchAdd(1, .monotonic);
        for (self.buckets, 0..) |bound, i| {
            if (value_seconds <= bound) {
                _ = self.counts[i].fetchAdd(1, .monotonic);
            }
        }
    }
};

No Grafana, a query típica para p99 é:

histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, route))

Composição: Logs + Métricas + Trace ID

O padrão produtivo em serviços Zig reais:

  1. Middleware HTTP gera trace_id (UUID ou timestamp+random) no início de cada request.
  2. Counter http_requests_total é incrementado a cada resposta, com labels de route e status.
  3. Histograma http_request_duration_seconds observa a duração de cada request.
  4. Logger estruturado emite uma linha JSON por request com trace_id, method, path, status, duration_ms.
  5. Gauge process_open_connections reflete conexões ativas.

Com isso, quando um alerta no Grafana dispara, você segue o trace_id do pico de latência até o log correspondente, e do log para o span (se tracing estiver habilitado).

OpenTelemetry em Zig

Ainda não há SDK oficial OTel estável para Zig em 2026. As opções práticas:

  • Métricas via Prometheus: cobre a maior parte das necessidades.
  • Traces via OTLP HTTP manual: gere spans em JSON OTLP e poste para o collector.
  • Bindings C: linkar a biblioteca C do OTel SDK via @cImport para traces completos.
  • W3C Trace Context: propague o header traceparent manualmente entre serviços.

Para a maioria dos serviços pequenos e médios, Prometheus + Loki (para logs JSON) já entrega observabilidade robusta sem precisar de OTel completo.

Erros Comuns a Evitar

  • Métricas de alta cardinalidade: nunca use user_id ou email como label — explode a memória do Prometheus.
  • Logs verbosos em hot path: logar dentro de loops apertados degrada performance.
  • Esquecer # TYPE: sem isso, o Prometheus pode inferir o tipo errado e quebrar queries.
  • Buckets mal calibrados: buckets default não servem para todo serviço; ajuste ao seu SLO.
  • stderr não coletado: configure Docker/journald/k8s para capturar stderr do processo Zig.

Conclusão

Observabilidade em Zig é direto de implementar porque os formatos (JSON para logs, texto simples para Prometheus) são agnósticos de linguagem. Com um logger JSON, alguns wrappers atômicos sobre std.atomic.Value e um endpoint /metrics, você entrega dashboards e alertas profissionais sem depender de SDKs pesados. Comece por logs estruturados com trace_id e um counter de requests; adicione histograma de latência e gauges de recursos conforme o serviço amadurece.

Para aprofundar a stack de produção, leia também nosso guia de Servidor HTTP em Zig para Produção, o tutorial de Zig com Docker (para empacotar o serviço com healthcheck) e o artigo sobre Cron Jobs em Zig para Produção, que mostra como orquestrar tarefas recorrentes observáveis.

Continue aprendendo Zig

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