Swift: Swift CLI Log Analysis Tool Practical Tutorial

Hands-on practice is the best way to learn -- this lesson builds a complete CLI log analysis tool in ~80 lines of Swift code, integrating all the core knowledge from the previous 27 lessons.


1. What You'll Learn



2. Project Background

Bob is a backend engineer who needs to analyze over 100,000 lines of server access logs daily. Tired of manually piecing together statistics with grep and awk, he decided to write a dedicated CLI tool in Swift -- one that runs cross-platform (macOS/Linux) and gives him full control over the parsing logic. The goals of this project are:

BASH
# 1. Count occurrences of each HTTP status code in logs
swift log-analyzer.swift access.log --status

# 2. Filter slow requests with response time over 500ms
swift log-analyzer.swift access.log --slow 500

# 3. Generate a Top 10 most frequent IP report
swift log-analyzer.swift access.log --top-ip 10

3. Project Architecture

100%
graph TB
    A["main.swift"] --> B["parseArguments()"]
    A --> C["readLogFile()"]
    C --> D["LogEntry models"]
    D --> E["analyzeByStatus()"]
    D --> F["filterSlowRequests()"]
    D --> G["topIPAddresses()"]
    E --> H["Output statistics report"]
    F --> H
    G --> H

(1) Module Breakdown

Module Responsibility Knowledge Areas
LogEntry model Parsing log lines into structs Codable, regex, optional chaining
LogParser Batch parsing log lines Generics, higher-order functions, error handling
Analyzer Statistical analysis Closures, generics, collection operations
CLI Argument parsing and output Enums, switch pattern matching


4. Core Module Implementation

(1) Log Model

100%
classDiagram
    class LogEntry {
        +String ip
        +Date date
        +String method
        +String path
        +Int statusCode
        +Int responseTime
        +Int bytesSent
    }
    class LogParser {
        +parseLine(String) LogEntry?
        +parseLines([String]) [LogEntry]
    }
    class Analyzer {
        +statusCount([LogEntry]) [Int: Int]
        +slowRequests([LogEntry], Int) [LogEntry]
        +topIPs([LogEntry], Int) [(String, Int)]
    }

▶ Example: Log Parsing Functions

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
    )
}

▶ Example: Core Analysis Functions

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")

▶ Example: Error Log Aggregation and Time-Slot Statistics

Beyond HTTP status code distribution, you can also aggregate error logs by hour to help identify peak problem periods:

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")
    }
}

Output:

TEXT 📖 Display only
Hour 12:00 -- 3 errors
   /api/login: 2 times
   /api/order: 1 time


5. Complete Code: CLI Log Analysis Tool

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()

Output (sample run):

TEXT 📖 Display only
=== Log Analysis Report ===
Total lines: 1500
Valid entries: 1485

--- Status Code Breakdown ---
  200: 1200 times
  301: 50 times
  404: 30 times
  500: 15 times

--- Slow Requests (>500ms) ---
  192.168.1.5 GET /api/report 2300ms
  10.0.0.3 POST /upload 1800ms

--- Top 10 IPs ---
  192.168.1.1: 450 times
  10.0.0.1: 320 times

❓ FAQ

Q Can this CLI tool run on Linux?
A Yes. The open-source Swift supports Linux. After installing Swift on Ubuntu, run the script directly with swift log-analyzer.swift access.log.
Q Why not use Foundation's JSONSerialization and parse manually instead?
A Log formats are typically fixed-delimiter plain text, not JSON. Custom parsing is more flexible, and you can also use regular expressions directly.
Q What is the first element of CommandLine.arguments?
A It's the script's file path. When running via the swift command, argv[0] is the script path, and argv[1] onwards are the passed arguments.
Q How do I test this CLI tool?
A Run swift log-analyzer.swift test.log --slow 500 in the terminal. The examples in this lesson demonstrate the logic; actual testing requires first preparing a test.log file with log data.
Q What's the difference between compactMap and flatMap?
A compactMap filters out nil and unwraps (Swift 4.1+); flatMap flattens nested collections. The former is for cleaning optional values in arrays; the latter is for flattening multi-dimensional arrays.
Q Which is better for CLI -- Result type or throw?
A Result is recommended for CLI tools because the caller can decide how to handle errors -- print a friendly message or continue execution. throw forces callers to use try syntax.

📖 Summary


📝 Exercises

  1. Basic: Modify the CLI tool from Section 5 to add a --errors parameter that only displays requests with status code >= 400 and their key information.
  2. Intermediate: Add a bandwidthReport() method to Analyzer that calculates the total bytesSent per IP and returns the Top 5 bandwidth consumers.
  3. Challenge: Extend the log parser to support Apache Common Log Format (standard format), extract the User-Agent field, and calculate the visit count distribution for each browser type.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏