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



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:

BASH
# 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

100%
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

100%
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

SWIFT
// ============================================
// 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

SWIFT
// ============================================
// 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:

SWIFT
// ============================================
// 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 leitura
Hour 12:00 -- 3 errors
   /api/login: 2 times
   /api/order: 1 time


5. Código Completo: Ferramenta CLI de Análise de Log

SWIFT
// ============================================
// 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: Execute swift analisador-log.swift teste.log --slow 500 no 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


📝 Exercícios

  1. Básico: Modifique a ferramenta CLI da Seção 5 para adicionar um parâmetro --errors que exibe apenas requisições com código de status >= 400 e suas informações principais.
  2. 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.
  3. 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.
Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%