Swift: Swift CLIログ分析ツール実践チュートリアル:Swiftコア知識の総合応用
実践的なハンズオンが学習の最善の方法です — このレッスンでは約80行のSwiftコードで完全なCLIログ分析ツールを構築し、これまでの27レッスンで学んだすべての中核知識を統合します。
1. 学習目標
- CLIツールの全体的なアーキテクチャ設計方法
- ジェネリック、列挙型、Codableなどの高度な機能の総合的な使用
- FileManagerとコマンドライン引数の処理
- チェーンデータ変換による関数型プログラミング
- エラー処理とユーザーインタラクション設計
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. プロジェクトアーキテクチャ
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) ログモデル
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構文の使用を強制します。
📖 まとめ
- パターンマッチング付き列挙型はCLIコマンドルーティングと状態管理に最適
- ジェネリックとコレクション高階関数(filter、map、reduce)によりデータ分析コードが簡潔で効率的になる
- FileManager + Stringコンテンツ読み取りがファイルI/Oの基本操作
- Result型はCLIのエラー処理シナリオにおいてthrowよりも適している
- アーキテクチャ設計時には、後で分析機能を追加できる拡張ポイントを残しておく
- 80〜100行のSwiftコードで実用的な本番対応CLIツールを構築できる
📝 練習問題
- 基本: 第5節のCLIツールを修正し、ステータスコードが400以上のリクエストとそのキー情報のみを表示する
--errorsパラメータを追加してください。 - 中級: Analyzerに
bandwidthReport()メソッドを追加し、IPごとの合計bytesSentを計算して帯域幅消費のTop 5を返すようにしてください。 - 発展: ログパーサーを拡張してApache Common Log Format(標準形式)をサポートし、User-Agentフィールドを抽出してブラウザタイプごとの訪問数分布を計算してください。