Swift: Swift CLI 日志分析工具实战教程:综合运用 Swift 核心知识
实战是最好的学习方式——本课将用 80 行 Swift 代码构建一个完整的 CLI 日志分析工具,综合运用前 27 课的全部核心知识。
1. 你将学到
- 如何设计一个 CLI 工具的整体架构
- 综合运用泛型、枚举、Codable 等高级特性
- 使用 FileManager 和命令行参数处理
- 函数式编程链式数据转换
- 错误处理与用户交互设计
2. 项目背景
Bob 是一名后端工程师,每天需要分析 10 万行以上的服务器访问日志。他厌倦了手动用 grep 和 awk 拼凑统计,决定用 Swift 写一个专用 CLI 工具——既能跨平台运行(macOS/Linux),又能完全掌控解析逻辑。 这个项目的目标是: `ash
1. 统计日志中各种 HTTP 状态码的出现次数
swift log-analyzer.swift access.log --status
2. 筛选出响应时间超过 500ms 的慢请求
swift log-analyzer.swift access.log --slow 500
3. 生成 Top 10 最频繁 IP 的报告
swift log-analyzer.swift access.log --top-ip 10
3. 项目架构
`mermaid 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["输出统计报告"] F --> H G --> H
(1) 模块划分
| 模块 | 职责 | 涉及知识点 |
|---|---|---|
| LogEntry 模型 | 日志行解析为结构体 | Codable、正则、可选链 |
| LogParser | 批量解析日志行 | 泛型、高阶函数、错误处理 |
| Analyzer | 统计分析 | 闭包、泛型、集合操作 |
| CLI | 参数解析和输出 | 枚举、switch 模式匹配 |
4. 核心模块实现
(1) 日志模型
`mermaid 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 // ============================================ // 核心模块:日志条目模型与解析 // ============================================ 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 // ============================================ // 使用高阶函数进行统计分析 // ============================================ import Foundation func countByStatus(_ entries: [LogEntry]) -> [Int: Int] { Dictionary(grouping: entries, by: { .statusCode }) .mapValues { .count } } func filterSlowRequests(_ entries: [LogEntry], threshold: Int) -> [LogEntry] { entries.filter { .responseTime > threshold } .sorted { .responseTime > .responseTime } } func topIPAddresses(_ entries: [LogEntry], limit: Int) -> [(String, Int)] { Dictionary(grouping: entries, by: { .ip }) .mapValues { .count } .sorted { .value > .value } .prefix(limit) .map { (.key, .value) } } print("分析函数已定义")
▶ 示例:错误日志聚合与时段统计
除了 HTTP 状态码分布,还可以按小时聚合错误日志,帮助定位问题高发时段:
// ============================================
// 错误日志按小时聚合统计
// ============================================
import Foundation
struct ErrorReport {
let hour: Int
let count: Int
let endpoints: [String: Int]
}
func aggregateErrorsByHour(_ entries: [LogEntry]) -> [ErrorReport] {
// 筛选错误请求(状态码 >= 400)
let errors = entries.filter { $0.statusCode >= 400 }
// 按小时分组
let calendar = Calendar.current
let grouped = Dictionary(grouping: errors) { entry in
calendar.component(.hour, from: entry.date)
}
// 生成报告
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 }
}
// 模拟数据
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("时段 \(report.hour):00 — \(report.count) 个错误")
for (endpoint, count) in report.endpoints {
print(" \(endpoint): \(count) 次")
}
}
输出:
TEXT 📖 仅展示时段 12:00 — 3 个错误 /api/login: 2 次 /api/order: 1 次
5. 完整代码:CLI 日志分析工具
`swift // ============================================ // log-analyzer.swift - CLI 日志分析工具 // 综合运用:泛型、枚举、Codable、闭包、文件IO // 使用:swift log-analyzer.swift access.log --status // ============================================ import Foundation // MARK: - 1. 日志模型 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. 命令行参数解析 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. 分析引擎 struct Analyzer { let entries: [LogEntry] func statusBreakdown() -> [(Int, Int)] { Dictionary(grouping: entries, by: { .statusCode }) .mapValues { .count } .sorted { .key < .key } } func slowRequests(above ms: Int) -> [LogEntry] { entries.filter { .responseTime > ms } .sorted { .responseTime > .responseTime } } func topIPs(limit: Int) -> [(String, Int)] { Dictionary(grouping: entries, by: { .ip }) .mapValues { .count } .sorted { .value > .value } .prefix(limit) .map { (.key, .value) } } } // MARK: - 4. 文件读取 func readLogFile(at path: String) -> Result<String, String> { let fm = FileManager.default guard fm.fileExists(atPath: path) else { return .failure("文件不存在: (path)") } guard let content = try? String(contentsOfFile: path, encoding: .utf8) else { return .failure("无法读取文件: (path)") } return .success(content) } // MARK: - 5. 主程序 func main() { let arguments = CommandLine.arguments guard let (filePath, command) = parseCommand(from: arguments) else { print("用法: swift log-analyzer.swift <file> [选项]") print("选项: --status, --slow <ms>, --top-ip <n>") return } let fileResult = readLogFile(at: filePath) switch fileResult { case .failure(let error): print("错误: (error)") return case .success(let content): let entries = content.split(separator: "\n") .compactMap { LogEntry(from: String()) } let analyzer = Analyzer(entries: entries) print("=== 日志分析报告 ===") print("总行数: (content.split(separator: "\n").count)") print("有效条目: (entries.count)") switch command { case .status: print("\n--- 状态码分布 ---") for (code, count) in analyzer.statusBreakdown() { print(" (code): (count) 次") } case .slow(let ms): let slow = analyzer.slowRequests(above: ms) print("\n--- 慢请求 (>(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) IP ---") for (ip, count) in analyzer.topIPs(limit: n) { print(" (ip): (count) 次") } } } } // 运行:在终端取消注释后执行 swift log-analyzer.swift access.log --status // main()
输出(示例运行结果): ` ext === 日志分析报告 === 总行数: 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
--- Top 10 IP --- 192.168.1.1: 450 次 10.0.0.1: 320 次 `
❓ 常见问题
📖 小节
- 枚举与模式匹配非常适合 CLI 命令路由和状态管理
- 泛型和集合高阶函数(filter、map、reduce)让数据分析代码简洁高效
- FileManager + String 内容读取是文件 I/O 的基础操作
- Result 类型比 throw 更适合 CLI 的错误处理场景
- 架构设计时预留扩展点便于后续增加分析功能
- 80-100 行 Swift 代码即可构建一个实用的生产级 CLI 工具
📝 作业
- 基础题: 修改第 5 节的 CLI 工具,增加 --errors 参数,只显示状态码 >= 400 的请求和关键信息。
- 进阶题: 给 Analyzer 添加 andwidthReport() 方法,按 IP 统计每个 IP 的总 bytesSent,返回 Top 5 带宽消耗者。
- 挑战题: 扩展日志解析器支持 Apache Common Log Format(标准格式),提取 User-Agent 字段,并统计每种浏览器的访问次数分布。