Swift: Swift CLIログ分析ツール実践チュートリアル:Swiftコア知識の総合応用

実践的なハンズオンが学習の最善の方法です — このレッスンでは約80行のSwiftコードで完全なCLIログ分析ツールを構築し、これまでの27レッスンで学んだすべての中核知識を統合します。


1. 学習目標



2. プロジェクト背景

Bobはバックエンドエンジニアで、毎日10万行以上のサーバーアク��スログを分析する必要があります。grepやawkで手動で統計を組み合わせるのに疲れ、Swiftで専用のCLIツールを作成することにしました — クロスプラットフォーム(macOS/Linux)で動作し、解析ロジックを完全に制御できます。 このプロジェクトの目標は以下の通りです:

BASH
# 1. ログ内の各HTTPステータスコードの出現回数をカウント
swift log-analyzer.swift access.log --status

# 2. レスポンス時間が500msを超える遅いリクエストをフィルタ
swift log-analyzer.swift access.log --slow 500

# 3. 最も頻度の高いIPアドレスTop 10レポートを生成
swift log-analyzer.swift access.log --top-ip 10

3. プロジェクトアーキテクチャ

100%
graph TB
    A["main.swift"] --> B["parseArguments()"]
    A --> C["readLogFile()"]
    C --> D["LogEntry モデル"]
    D --> E["analyzeByStatus()"]
    D --> F["filterSlowRequests()"]
    D --> G["topIPAddresses()"]
    E --> H["統計レポートを出力"]
    F --> H
    G --> H

(1) モジュール分割

モジュール 責務 知識領域
LogEntryモデル ログ行を構造体に解析 Codable、正規表現、オプショナルチェーン
LogParser ログ行のバッチ解析 ジェネリック、高階関数、エラー処理
Analyzer 統計分析 クロージャ、ジェネリック、コレクション操作
CLI 引数解析と出力 列挙型、switchパターンマッチング


4. コアモジュール実装

(1) ログモデル

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

▶ サンプル: ログ解析関数

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

▶ サンプル: コア分析関数

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

▶ サンプル: エラーログ集計と時間帯統計

HTTPステータスコードの分布に加えて、エラーログを時間帯別に集計して問題のピーク時間帯を特定することもできます:

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

出力:

TEXT 📖 参照専用
Hour 12:00 -- 3 errors
   /api/login: 2 times
   /api/order: 1 time


5. 完全なコード:CLIログ分析ツール

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

出力(サンプル実行):

TEXT 📖 参照専用
=== ログ分析レポート ===
総行数: 1500
有効エントリ: 1485

--- ステータスコード内訳 ---
  200: 1200回
  301: 50回
  404: 30回
  500: 15回

--- 遅延リクエスト (>500ms) ---
  192.168.1.5 GET /api/report 2300ms
  10.0.0.3 POST /upload 1800ms

--- トップ10 IP ---
  192.168.1.1: 450回
  10.0.0.1: 320回

❓ よくある質問

Q このCLIツールはLinuxで動作します��?
A はい。オープンソースのSwiftはLinuxをサポートしています。UbuntuにSwiftをインストールした後、swift log-analyzer.swift access.log でスクリプトを直接実行できます。
Q FoundationのJSONSerializationを使わずに手動で解析するのはなぜですか?
A ログ形式は通常、固定区切りのプレーンテキストであり、JSONではありません。カスタム解析の方が柔軟で、正規表現を直接使用することもできます。
Q CommandLine.argumentsの最初の要素は何ですか?
A スクリプトのファイルパスです。swiftコマンドで実行する場合、argv[0]がスクリプトパスで、argv[1]以降が渡された引数です。
Q このCLIツールをどうテストしますか?
A ターミナルで swift log-analyzer.swift test.log --slow 500 を実行します。このレッスンの例はロジックを示しており、実際のテストには事前にログデータを含むtest.logファイルを準備する必要があります。
Q compactMapとflatMapの違いは何ですか?
A compactMapはnilをフィルタしてアンラップします(Swift 4.1以降)。flatMapはネストコレクションを平坦化します。前者は配列内のオプショナル値のクリーンアップ用で、後者は多次元配列の平坦化用です。
Q CLIにはResult型とthrowのどちらが適していますか?
A CLIツールにはResultが推奨されます。呼び出し側がエラーの処理方法(親切なメッセージの出力か実行継続か)を決定できるためです。throwは呼び出し側にtry構文の使用を強制します。

📖 まとめ


📝 練習問題

  1. 基本: 第5節のCLIツールを修正し、ステータスコードが400以上のリクエストとそのキー情報のみを表示する --errors パラメータを追加してください。
  2. 中級: Analyzerに bandwidthReport() メソッドを追加し、IPごとの合計bytesSentを計算して帯域幅消費のTop 5を返すようにしてください。
  3. 発展: ログパーサーを拡張してApache Common Log Format(標準形式)をサポートし、User-Agentフィールドを抽出してブラウザタイプごとの訪問数分布を計算してください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%