Swift: برنامج Swift التعليمي العملي لأداة تحليل سجلات CLI
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
- How to design the overall architecture of a CLI tool
- Comprehensive use of advanced features like generics, enums, and Codable
- FileManager and command-line argument handling
- Functional programming with chained data transformations
- Error handling and user interaction design
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
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
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 📖 للعرض فقط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 📖 للعرض فقط=== 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
س Can this CLI tool run on Linux?
ج Yes. The open-source Swift supports Linux. After installing Swift on Ubuntu, run the script directly with
swift log-analyzer.swift access.log.س Why not use Foundation's JSONSerialization and parse manually instead?
ج Log formats are typically fixed-delimiter plain text, not JSON. Custom parsing is more flexible, and you can also use regular expressions directly.
س What is the first element of CommandLine.arguments?
ج 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.
س How do I test this CLI tool?
ج 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.س What's the difference between compactMap and flatMap?
ج 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.
س Which is better for CLI -- Result type or throw?
ج 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
- Enums with pattern matching are perfect for CLI command routing and state management
- Generics and collection higher-order functions (filter, map, reduce) make data analysis code concise and efficient
- FileManager + String content reading are the basic operations for file I/O
- The Result type is more suitable than throw for CLI error handling scenarios
- When designing architecture, leave extension points for adding analysis features later
- 80--100 lines of Swift code can build a practical, production-ready CLI tool
📝 Exercises
- Basic: Modify the CLI tool from Section 5 to add a
--errorsparameter that only displays requests with status code >= 400 and their key information. - Intermediate: Add a
bandwidthReport()method to Analyzer that calculates the total bytesSent per IP and returns the Top 5 bandwidth consumers. - 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.