Swift: Tutorial Prático de Ferramenta CLI de Análise de Log…
A prática hands-on é a melhor maneira de aprender -- esta lição constrói uma ferramenta CLI completa de análise de log em ~80 linhas de código Swift, integrando todos os conhecimentos centrais das 27 lições anteriores.
1. O Que Você Vai Aprender
- Como projetar a arquitetura geral de uma ferramenta CLI
- Uso abrangente de recursos avançados como genéricos, enums e Codable
- FileManager e manipulação de argumentos de linha de comando
- Programação funcional com transformações de dados encadeadas
- Tratamento de erros e design de interação com o usuário
2. Contexto do Projeto
Bob é um engenheiro back-end que precisa analisar mais de 100.000 linhas de logs de acesso ao servidor diariamente. Cansado de montar estatísticas manualmente com grep e awk, ele decidiu escrever uma ferramenta CLI dedicada em Swift -- uma que execute em múltiplas plataformas (macOS/Linux) e lhe dê controle total sobre a lógica de análise. Os objetivos deste projeto são:
# 1. Contar ocorrências de cada código de status HTTP nos logs
swift analisador-log.swift access.log --status
# 2. Filtrar requisições lentas com tempo de resposta acima de 500ms
swift analisador-log.swift access.log --slow 500
# 3. Gerar um relatório Top 10 dos IPs mais frequentes
swift analisador-log.swift access.log --top-ip 10
3. Arquitetura do Projeto
graph TB
A["main.swift"] --> B["analisarArgumentos()"]
A --> C["lerArquivoLog()"]
C --> D["modelos EntradaLog"]
D --> E["analisarPorStatus()"]
D --> F["filtrarRequisicoesLentas()"]
D --> G["topEnderecosIP()"]
E --> H["Exibir relatório de estatísticas"]
F --> H
G --> H
(1) Divisão de Módulos
| Módulo | Responsabilidade | Áreas de Conhecimento |
|---|---|---|
| Modelo EntradaLog | Analisar linhas de log em structs | Codable, regex, encadeamento opcional |
| AnalisadorLog | Análise em lote de linhas de log | Genéricos, funções de ordem superior, tratamento de erros |
| Analisador | Análise estatística | Closures, genéricos, operações de coleção |
| CLI | Análise de argumentos e saída | Enums, correspondência de padrões switch |
4. Implementação do Módulo Central
(1) Modelo de Log
classDiagram
class EntradaLog {
+String ip
+Date data
+String metodo
+String caminho
+Int codigoStatus
+Int tempoResposta
+Int bytesEnviados
}
class AnalisadorLog {
+analisarLinha(String) EntradaLog?
+analisarLinhas([String]) [EntradaLog]
}
class Analisador {
+contagemStatus([EntradaLog]) [Int: Int]
+requisicoesLentas([EntradaLog], Int) [EntradaLog]
+topIPs([EntradaLog], Int) [(String, Int)]
}
▶ Exemplo: Funções de Análise de Log
// ============================================
// Core module: log entry model and parsing
// ============================================
import Foundation
struct LogEntry {
let ip: String
let date: Date
let method: String
let path: String
let statusCode: Int
let responseTime: Int
let bytesSent: Int
}
func parseLogLine(_ line: String) -> LogEntry? {
let parts = line.split(separator: " ").map(String.init)
guard parts.count >= 7 else { return nil }
let ip = parts[0]
let method = parts[2]
let path = parts[3]
guard let statusCode = Int(parts[4]),
let responseTime = Int(parts[5]),
let bytesSent = Int(parts[6]) else { return nil }
let date = Date()
return LogEntry(
ip: ip, date: date, method: method,
path: path, statusCode: statusCode,
responseTime: responseTime, bytesSent: bytesSent
)
}
▶ Exemplo: Funções de Análise Central
// ============================================
// Statistical analysis using higher-order functions
// ============================================
import Foundation
func countByStatus(_ entries: [LogEntry]) -> [Int: Int] {
Dictionary(grouping: entries, by: { $0.statusCode })
.mapValues { $0.count }
}
func filterSlowRequests(_ entries: [LogEntry], threshold: Int) -> [LogEntry] {
entries.filter { $0.responseTime > threshold }
.sorted { $0.responseTime > $1.responseTime }
}
func topIPAddresses(_ entries: [LogEntry], limit: Int) -> [(String, Int)] {
Dictionary(grouping: entries, by: { $0.ip })
.mapValues { $0.count }
.sorted { $0.value > $1.value }
.prefix(limit)
.map { ($0.key, $0.value) }
}
print("Analysis functions defined")
▶ Exemplo: Agregação de Logs de Erro e Estatísticas por Faixa Horária
Além da distribuição de códigos de status HTTP, você também pode agregar logs de erro por hora para ajudar a identificar períodos de pico de problemas:
// ============================================
// Aggregating error logs by hour
// ============================================
import Foundation
struct ErrorReport {
let hour: Int
let count: Int
let endpoints: [String: Int]
}
func aggregateErrorsByHour(_ entries: [LogEntry]) -> [ErrorReport] {
// Filter for error requests (status code >= 400)
let errors = entries.filter { $0.statusCode >= 400 }
// Group by hour
let calendar = Calendar.current
let grouped = Dictionary(grouping: errors) { entry in
calendar.component(.hour, from: entry.date)
}
// Generate the report
return grouped.map { hour, items in
let endpoints = Dictionary(grouping: items, by: { $0.path })
.mapValues { $0.count }
return ErrorReport(hour: hour, count: items.count, endpoints: endpoints)
}
.sorted { $0.hour < $1.hour }
}
// Simulated data
let sampleEntries = [
LogEntry(ip: "192.168.1.1", date: Date(), method: "GET", path: "/api/login", statusCode: 500, responseTime: 1200, bytesSent: 200),
LogEntry(ip: "10.0.0.1", date: Date(), method: "POST", path: "/api/order", statusCode: 404, responseTime: 300, bytesSent: 50),
LogEntry(ip: "192.168.1.2", date: Date(), method: "GET", path: "/api/login", statusCode: 500, responseTime: 800, bytesSent: 180)
]
let reports = aggregateErrorsByHour(sampleEntries)
for report in reports {
print("Hour \(report.hour):00 -- \(report.count) errors")
for (endpoint, count) in report.endpoints {
print(" \(endpoint): \(count) times")
}
}
Saída:
TEXT 📖 Somente leituraHour 12:00 -- 3 errors /api/login: 2 times /api/order: 1 time
5. Código Completo: Ferramenta CLI de Análise de Log
// ============================================
// log-analyzer.swift - CLI log analysis tool
// Integrates: generics, enums, Codable, closures, file I/O
// Usage: swift log-analyzer.swift access.log --status
// ============================================
import Foundation
// MARK: - 1. Log Model
struct LogEntry {
let ip: String
let method: String
let path: String
let statusCode: Int
let responseTime: Int
let bytesSent: Int
init?(from line: String) {
let parts = line.split(separator: " ").map(String.init)
guard parts.count >= 7,
let code = Int(parts[4]),
let time = Int(parts[5]),
let bytes = Int(parts[6]) else { return nil }
ip = parts[0]
method = parts[2]
path = parts[3]
statusCode = code
responseTime = time
bytesSent = bytes
}
}
// MARK: - 2. Command-Line Argument Parsing
enum Command {
case status
case slow(Int)
case topIP(Int)
}
func parseCommand(from args: [String]) -> (String, Command)? {
guard args.count >= 2 else { return nil }
let filePath = args[1]
guard args.count >= 3 else { return (filePath, .status) }
switch args[2] {
case "--status":
return (filePath, .status)
case "--slow":
let threshold = args.count > 3 ? Int(args[3]) ?? 500 : 500
return (filePath, .slow(threshold))
case "--top-ip":
let limit = args.count > 3 ? Int(args[3]) ?? 10 : 10
return (filePath, .topIP(limit))
default:
return (filePath, .status)
}
}
// MARK: - 3. Analysis Engine
struct Analyzer {
let entries: [LogEntry]
func statusBreakdown() -> [(Int, Int)] {
Dictionary(grouping: entries, by: { $0.statusCode })
.mapValues { $0.count }
.sorted { $0.key < $1.key }
}
func slowRequests(above ms: Int) -> [LogEntry] {
entries.filter { $0.responseTime > ms }
.sorted { $0.responseTime > $1.responseTime }
}
func topIPs(limit: Int) -> [(String, Int)] {
Dictionary(grouping: entries, by: { $0.ip })
.mapValues { $0.count }
.sorted { $0.value > $1.value }
.prefix(limit)
.map { ($0.key, $0.value) }
}
}
// MARK: - 4. File Reading
func readLogFile(at path: String) -> Result<String, String> {
let fm = FileManager.default
guard fm.fileExists(atPath: path) else {
return .failure("File not found: \(path)")
}
guard let content = try? String(contentsOfFile: path, encoding: .utf8) else {
return .failure("Unable to read file: \(path)")
}
return .success(content)
}
// MARK: - 5. Main Program
func main() {
let arguments = CommandLine.arguments
guard let (filePath, command) = parseCommand(from: arguments) else {
print("Usage: swift log-analyzer.swift <file> [options]")
print("Options: --status, --slow <ms>, --top-ip <n>")
return
}
let fileResult = readLogFile(at: filePath)
switch fileResult {
case .failure(let error):
print("Error: \(error)")
return
case .success(let content):
let entries = content.split(separator: "\n")
.compactMap { LogEntry(from: String($0)) }
let analyzer = Analyzer(entries: entries)
print("=== Log Analysis Report ===")
print("Total lines: \(content.split(separator: "\n").count)")
print("Valid entries: \(entries.count)")
switch command {
case .status:
print("\n--- Status Code Breakdown ---")
for (code, count) in analyzer.statusBreakdown() {
print(" \(code): \(count) times")
}
case .slow(let ms):
let slow = analyzer.slowRequests(above: ms)
print("\n--- Slow Requests (\(ms)ms) ---")
for entry in slow.prefix(20) {
print(" \(entry.ip) \(entry.method) \(entry.path) \(entry.responseTime)ms")
}
case .topIP(let n):
print("\n--- Top \(n) IPs ---")
for (ip, count) in analyzer.topIPs(limit: n) {
print(" \(ip): \(count) times")
}
}
}
}
// To run: uncomment in terminal and execute swift log-analyzer.swift access.log --status
// main()
Saída (execução de exemplo):
TEXT 📖 Somente leitura=== Relatório de Análise de Log === Total de linhas: 1500 Entradas válidas: 1485 --- Detalhamento de Códigos de Status --- 200: 1200 vezes 301: 50 vezes 404: 30 vezes 500: 15 vezes --- Requisições Lentas (>500ms) --- 192.168.1.5 GET /api/relatorio 2300ms 10.0.0.3 POST /upload 1800ms --- Top 10 IPs --- 192.168.1.1: 450 vezes 10.0.0.1: 320 vezes
❓ Perguntas Frequentes
P: Esta ferramenta CLI pode rodar no Linux? R: Sim. O Swift de código aberto suporta Linux. Após instalar o Swift no Ubuntu, execute o script diretamente com
swift analisador-log.swift access.log. P: Por que não usar JSONSerialization do Foundation e analisar manualmente? R: Os formatos de log são tipicamente texto simples com delimitadores fixos, não JSON. A análise personalizada é mais flexível, e você também pode usar expressões regulares diretamente. P: Qual é o primeiro elemento de CommandLine.arguments? R: É o caminho do arquivo de script. Ao executar via comando swift, argv[0] é o caminho do script, e argv[1] em diante são os argumentos passados. P: Como testar esta ferramenta CLI? R: Executeswift analisador-log.swift teste.log --slow 500no terminal. Os exemplos nesta lição demonstram a lógica; o teste real requer primeiro preparar um arquivo teste.log com dados de log. P: Qual é a diferença entre compactMap e flatMap? R: compactMap filtra nil e desempacota (Swift 4.1+); flatMap achata coleções aninhadas. O primeiro é para limpar valores opcionais em arrays; o último é para achatar arrays multidimensionais. P: O que é melhor para CLI -- tipo Result ou throw? R: Result é recomendado para ferramentas CLI porque o chamador pode decidir como tratar os erros -- imprimir uma mensagem amigável ou continuar a execução. throw força os chamadores a usar a sintaxe try.
📖 Resumo
- Enums com correspondência de padrões são perfeitos para roteamento de comandos CLI e gerenciamento de estado
- Genéricos e funções de ordem superior de coleção (filter, map, reduce) tornam o código de análise de dados conciso e eficiente
- FileManager + leitura de conteúdo String são as operações básicas para E/S de arquivos
- O tipo Result é mais adequado que throw para cenários de tratamento de erros em CLI
- Ao projetar a arquitetura, deixe pontos de extensão para adicionar recursos de análise posteriormente
- 80--100 linhas de código Swift podem construir uma ferramenta CLI prática e pronta para produção
📝 Exercícios
- Básico: Modifique a ferramenta CLI da Seção 5 para adicionar um parâmetro
--errorsque exibe apenas requisições com código de status >= 400 e suas informações principais. - Intermediário: Adicione um método
relatorioLarguraBanda()ao Analisador que calcula o total de bytesEnviados por IP e retorna os Top 5 consumidores de largura de banda. - Desafio: Estenda o analisador de log para suportar o Formato Comum de Log do Apache (formato padrão), extraia o campo User-Agent e calcule a distribuição de contagem de visitas para cada tipo de navegador.