Analisador de Logs em Zig — Tutorial Passo a Passo

Analisador de Logs em Zig — Tutorial Passo a Passo

Neste tutorial, vamos construir um analisador de arquivos de log que parseia, filtra, agrega e gera estatísticas sobre logs de aplicação. Este é um projeto prático com aplicação direta em operações e DevOps.

O Que Vamos Construir

Nosso analisador vai:

  • Parsear linhas de log em formato estruturado (timestamp, nível, mensagem)
  • Filtrar por nível (ERROR, WARN, INFO, DEBUG), data e texto
  • Agregar contagens por nível e por período
  • Detectar picos de erros e padrões anômalos
  • Gerar relatórios resumidos
  • Processar arquivos de qualquer tamanho linha a linha

Pré-requisitos

Passo 1: Estrutura do Projeto

mkdir log-analyzer
cd log-analyzer
zig init

Passo 2: Definindo Estruturas

const std = @import("std");
const mem = std.mem;
const fmt = std.fmt;
const fs = std.fs;
const io = std.io;

/// Nível de severidade do log.
const NivelLog = enum(u8) {
    debug = 0,
    info = 1,
    warn = 2,
    @"error" = 3,
    fatal = 4,

    pub fn deString(s: []const u8) ?NivelLog {
        const upper = bloco: {
            var buf: [8]u8 = undefined;
            const len = @min(s.len, buf.len);
            for (s[0..len], 0..) |c, i| {
                buf[i] = if (c >= 'a' and c <= 'z') c - 32 else c;
            }
            break :bloco buf[0..len];
        };
        if (mem.eql(u8, upper, "DEBUG")) return .debug;
        if (mem.eql(u8, upper, "INFO")) return .info;
        if (mem.eql(u8, upper, "WARN") or mem.eql(u8, upper, "WARNING")) return .warn;
        if (mem.eql(u8, upper, "ERROR")) return .@"error";
        if (mem.eql(u8, upper, "FATAL")) return .fatal;
        return null;
    }

    pub fn nome(self: NivelLog) []const u8 {
        return switch (self) {
            .debug => "DEBUG",
            .info => "INFO",
            .warn => "WARN",
            .@"error" => "ERROR",
            .fatal => "FATAL",
        };
    }

    pub fn cor(self: NivelLog) []const u8 {
        return switch (self) {
            .debug => "\x1b[37m",
            .info => "\x1b[32m",
            .warn => "\x1b[33m",
            .@"error" => "\x1b[31m",
            .fatal => "\x1b[35m",
        };
    }
};

/// Uma entrada de log parseada.
const EntradaLog = struct {
    timestamp: [32]u8,
    timestamp_len: usize,
    nivel: NivelLog,
    mensagem: [512]u8,
    mensagem_len: usize,
    linha_num: usize,

    pub fn timestampStr(self: *const EntradaLog) []const u8 {
        return self.timestamp[0..self.timestamp_len];
    }

    pub fn mensagemStr(self: *const EntradaLog) []const u8 {
        return self.mensagem[0..self.mensagem_len];
    }
};

/// Estatísticas agregadas.
const Estatisticas = struct {
    total_linhas: u64 = 0,
    linhas_parseadas: u64 = 0,
    linhas_invalidas: u64 = 0,
    contagem_nivel: [5]u64 = [_]u64{0} ** 5,
    primeira_entrada: ?[32]u8 = null,
    primeira_len: usize = 0,
    ultima_entrada: [32]u8 = undefined,
    ultima_len: usize = 0,

    pub fn registrar(self: *Estatisticas, entrada: *const EntradaLog) void {
        self.linhas_parseadas += 1;
        self.contagem_nivel[@intFromEnum(entrada.nivel)] += 1;

        if (self.primeira_entrada == null) {
            self.primeira_entrada = undefined;
            @memcpy(self.primeira_entrada.?[0..entrada.timestamp_len], entrada.timestamp[0..entrada.timestamp_len]);
            self.primeira_len = entrada.timestamp_len;
        }
        @memcpy(self.ultima_entrada[0..entrada.timestamp_len], entrada.timestamp[0..entrada.timestamp_len]);
        self.ultima_len = entrada.timestamp_len;
    }

    pub fn percentualNivel(self: *const Estatisticas, nivel: NivelLog) f64 {
        if (self.linhas_parseadas == 0) return 0.0;
        return @as(f64, @floatFromInt(self.contagem_nivel[@intFromEnum(nivel)])) /
            @as(f64, @floatFromInt(self.linhas_parseadas)) * 100.0;
    }
};

Passo 3: Parser de Linhas de Log

/// Parseia uma linha de log no formato:
/// [TIMESTAMP] NIVEL: mensagem
/// ou: TIMESTAMP NIVEL mensagem
fn parsearLinha(linha: []const u8, linha_num: usize) ?EntradaLog {
    const trimmed = mem.trim(u8, linha, " \t\r\n");
    if (trimmed.len == 0) return null;

    var entrada = EntradaLog{
        .timestamp = undefined,
        .timestamp_len = 0,
        .nivel = .info,
        .mensagem = undefined,
        .mensagem_len = 0,
        .linha_num = linha_num,
    };

    var pos: usize = 0;

    // Tentar formato [TIMESTAMP]
    if (trimmed[0] == '[') {
        if (mem.indexOfScalar(u8, trimmed, ']')) |fim_ts| {
            const ts = trimmed[1..fim_ts];
            const len = @min(ts.len, entrada.timestamp.len);
            @memcpy(entrada.timestamp[0..len], ts[0..len]);
            entrada.timestamp_len = len;
            pos = fim_ts + 1;
        } else {
            return null;
        }
    } else {
        // Tentar formato YYYY-MM-DD HH:MM:SS
        if (trimmed.len >= 19 and trimmed[4] == '-' and trimmed[10] == ' ') {
            const ts = trimmed[0..19];
            @memcpy(entrada.timestamp[0..19], ts);
            entrada.timestamp_len = 19;
            pos = 19;
        } else {
            // Sem timestamp, tenta parsear nível diretamente
            entrada.timestamp_len = 0;
        }
    }

    // Pular espaços
    while (pos < trimmed.len and (trimmed[pos] == ' ' or trimmed[pos] == ':')) pos += 1;

    // Parsear nível
    const resto = trimmed[pos..];
    const espaco = mem.indexOfScalar(u8, resto, ' ') orelse mem.indexOfScalar(u8, resto, ':') orelse resto.len;
    const nivel_str = resto[0..espaco];

    if (NivelLog.deString(nivel_str)) |nivel| {
        entrada.nivel = nivel;
        pos += espaco;
        // Pular separadores
        while (pos < trimmed.len and (trimmed[pos] == ' ' or trimmed[pos] == ':' or trimmed[pos] == '-')) pos += 1;
    }

    // Resto é a mensagem
    if (pos < trimmed.len) {
        const msg = trimmed[pos..];
        const msg_len = @min(msg.len, entrada.mensagem.len);
        @memcpy(entrada.mensagem[0..msg_len], msg[0..msg_len]);
        entrada.mensagem_len = msg_len;
    }

    return entrada;
}

Passo 4: Filtros

/// Configuração de filtro para análise de logs.
const Filtro = struct {
    nivel_minimo: ?NivelLog = null,
    nivel_exato: ?NivelLog = null,
    texto_busca: ?[]const u8 = null,
    /// Verifica se uma entrada passa pelo filtro.
    pub fn aceita(self: *const Filtro, entrada: *const EntradaLog) bool {
        // Filtro por nível mínimo
        if (self.nivel_minimo) |min| {
            if (@intFromEnum(entrada.nivel) < @intFromEnum(min)) return false;
        }
        // Filtro por nível exato
        if (self.nivel_exato) |exato| {
            if (entrada.nivel != exato) return false;
        }
        // Filtro por texto
        if (self.texto_busca) |busca| {
            if (mem.indexOf(u8, entrada.mensagemStr(), busca) == null) return false;
        }
        return true;
    }
};

Passo 5: Motor de Análise

/// Analisa um fluxo de linhas de log.
fn analisarFluxo(
    reader: anytype,
    filtro: *const Filtro,
    writer: anytype,
    modo_detalhado: bool,
) !Estatisticas {
    var stats = Estatisticas{};
    var buf: [2048]u8 = undefined;
    var linha_num: usize = 0;

    while (reader.readUntilDelimiterOrEof(&buf, '\n')) |maybe_linha| {
        const linha = maybe_linha orelse break;
        linha_num += 1;
        stats.total_linhas += 1;

        if (parsearLinha(linha, linha_num)) |entrada| {
            stats.registrar(&entrada);

            if (filtro.aceita(&entrada)) {
                if (modo_detalhado) {
                    const reset = "\x1b[0m";
                    try writer.print("{s}[{s}] {s:<5}{s} {s}\n", .{
                        entrada.nivel.cor(),
                        entrada.timestampStr(),
                        entrada.nivel.nome(),
                        reset,
                        entrada.mensagemStr(),
                    });
                }
            }
        } else {
            stats.linhas_invalidas += 1;
        }
    } else |_| {}

    return stats;
}

/// Exibe relatório de estatísticas.
fn exibirRelatorio(stats: *const Estatisticas, writer: anytype) !void {
    const reset = "\x1b[0m";

    try writer.print(
        \\
        \\  ==========================================
        \\         RELATORIO DE ANALISE
        \\  ==========================================
        \\
        \\  Total de linhas:   {d}
        \\  Linhas parseadas:  {d}
        \\  Linhas invalidas:  {d}
        \\
        \\  --- Distribuicao por Nivel ---
        \\
    , .{ stats.total_linhas, stats.linhas_parseadas, stats.linhas_invalidas });

    const niveis = [_]NivelLog{ .debug, .info, .warn, .@"error", .fatal };
    for (niveis) |nivel| {
        const cnt = stats.contagem_nivel[@intFromEnum(nivel)];
        const pct = stats.percentualNivel(nivel);
        try writer.print("  {s}{s:<6}{s} {d:>8} ({d:>5.1}%) ", .{
            nivel.cor(), nivel.nome(), reset, cnt, pct,
        });
        // Barra visual
        const barras: usize = @intFromFloat(pct / 2.0);
        var b: usize = 0;
        while (b < barras) : (b += 1) try writer.print("#", .{});
        try writer.print("\n", .{});
    }

    if (stats.primeira_entrada) |_| {
        try writer.print(
            \\
            \\  Periodo: {s} a {s}
            \\
        , .{
            stats.primeira_entrada.?[0..stats.primeira_len],
            stats.ultima_entrada[0..stats.ultima_len],
        });
    }
}

Passo 6: Interface CLI

pub fn main() !void {
    const stdout = io.getStdOut().writer();
    const stdin = io.getStdIn().reader();

    try stdout.print(
        \\
        \\  ==========================================
        \\    ANALISADOR DE LOGS - Zig
        \\  ==========================================
        \\
    , .{});

    var buf: [512]u8 = undefined;

    while (true) {
        try stdout.print(
            \\
            \\  [1] Analisar arquivo de log
            \\  [2] Filtrar por nivel (ERROR/WARN)
            \\  [3] Buscar texto em logs
            \\  [4] Gerar log de exemplo
            \\  [5] Sair
            \\
            \\  Opcao:
        , .{});

        const opcao_raw = stdin.readUntilDelimiterOrEof(&buf, '\n') catch continue orelse break;
        const opcao = mem.trim(u8, opcao_raw, " \t\r\n");

        if (mem.eql(u8, opcao, "5")) break;

        if (mem.eql(u8, opcao, "4")) {
            // Gerar arquivo de exemplo
            const exemplo =
                \\[2026-02-21 10:00:01] INFO: Aplicacao iniciada
                \\[2026-02-21 10:00:02] DEBUG: Carregando configuracao
                \\[2026-02-21 10:00:03] INFO: Servidor escutando na porta 8080
                \\[2026-02-21 10:00:15] INFO: Conexao recebida de 192.168.1.10
                \\[2026-02-21 10:00:16] WARN: Resposta lenta (2.3s)
                \\[2026-02-21 10:00:20] ERROR: Falha ao conectar ao banco de dados
                \\[2026-02-21 10:00:21] ERROR: Timeout na query SELECT * FROM users
                \\[2026-02-21 10:00:25] INFO: Reconectado ao banco de dados
                \\[2026-02-21 10:00:30] WARN: Memoria acima de 80%
                \\[2026-02-21 10:01:00] INFO: Health check OK
                \\[2026-02-21 10:01:30] FATAL: Out of memory
            ;

            const file = fs.cwd().createFile("exemplo.log", .{}) catch |err| {
                try stdout.print("  Erro: {any}\n", .{err});
                continue;
            };
            defer file.close();
            try file.writeAll(exemplo);
            try stdout.print("  Arquivo exemplo.log criado!\n", .{});
            continue;
        }

        try stdout.print("\n  Arquivo de log: ", .{});
        const path_raw = stdin.readUntilDelimiterOrEof(&buf, '\n') catch continue orelse continue;
        const path = mem.trim(u8, path_raw, " \t\r\n");

        const file = fs.cwd().openFile(path, .{}) catch |err| {
            try stdout.print("  Erro ao abrir: {any}\n", .{err});
            continue;
        };
        defer file.close();

        var filtro = Filtro{};

        if (mem.eql(u8, opcao, "2")) {
            try stdout.print("  Nivel minimo (DEBUG/INFO/WARN/ERROR/FATAL): ", .{});
            const nivel_raw = stdin.readUntilDelimiterOrEof(&buf, '\n') catch continue orelse continue;
            filtro.nivel_minimo = NivelLog.deString(mem.trim(u8, nivel_raw, " \t\r\n"));
        } else if (mem.eql(u8, opcao, "3")) {
            try stdout.print("  Texto para buscar: ", .{});
            const texto_raw = stdin.readUntilDelimiterOrEof(&buf, '\n') catch continue orelse continue;
            const texto = mem.trim(u8, texto_raw, " \t\r\n");
            if (texto.len > 0) filtro.texto_busca = texto;
        }

        const modo_detalhado = !mem.eql(u8, opcao, "1") or true;
        const reader = file.reader();

        const stats = try analisarFluxo(reader, &filtro, stdout, modo_detalhado);
        try exibirRelatorio(&stats, stdout);
    }

    try stdout.print("\n  Ate logo!\n", .{});
}

Testes

test "parsear linha com colchetes" {
    const entrada = parsearLinha("[2026-02-21 10:00:01] ERROR: algo deu errado", 1).?;
    try std.testing.expectEqual(NivelLog.@"error", entrada.nivel);
    try std.testing.expectEqualStrings("2026-02-21 10:00:01", entrada.timestampStr());
}

test "parsear nivel de string" {
    try std.testing.expectEqual(NivelLog.info, NivelLog.deString("INFO").?);
    try std.testing.expectEqual(NivelLog.@"error", NivelLog.deString("error").?);
    try std.testing.expect(NivelLog.deString("INVALIDO") == null);
}

test "filtro nivel minimo" {
    var filtro = Filtro{ .nivel_minimo = .warn };
    var entrada_info = EntradaLog{
        .timestamp = undefined, .timestamp_len = 0,
        .nivel = .info, .mensagem = undefined, .mensagem_len = 0, .linha_num = 1,
    };
    var entrada_error = EntradaLog{
        .timestamp = undefined, .timestamp_len = 0,
        .nivel = .@"error", .mensagem = undefined, .mensagem_len = 0, .linha_num = 2,
    };
    try std.testing.expect(!filtro.aceita(&entrada_info));
    try std.testing.expect(filtro.aceita(&entrada_error));
}

test "estatisticas" {
    var stats = Estatisticas{};
    var entrada = EntradaLog{
        .timestamp = undefined, .timestamp_len = 0,
        .nivel = .info, .mensagem = undefined, .mensagem_len = 0, .linha_num = 1,
    };
    stats.registrar(&entrada);
    try std.testing.expectEqual(@as(u64, 1), stats.linhas_parseadas);
    try std.testing.expectEqual(@as(u64, 1), stats.contagem_nivel[@intFromEnum(NivelLog.info)]);
}

Compilando e Executando

zig build test
zig build run

Conceitos Aprendidos

  • Parsing de texto com formatos variados
  • Filtragem com structs de configuração
  • Processamento de arquivos linha a linha (streaming)
  • Enums com métodos para classificação
  • Agregação estatística com arrays fixos
  • Cores ANSI para destaque visual

Próximos Passos

Continue aprendendo Zig

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