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
- Zig 0.13+ instalado (guia de instalação)
- Familiaridade com I/O de arquivos em Zig
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
- Explore leitura de arquivos para processamento avançado
- Combine com o File Watcher para monitoramento em tempo real
- Construa o próximo projeto: Mini Grep