import Foundation // MARK: - API Logger class APILogger { enum LogLevel { case none case basic case detailed } // 使用 actor 封装可变全局状态以保证并发安全 actor Config { static let shared = Config() #if DEBUG private var level: LogLevel = .detailed #else private var level: LogLevel = .none #endif func get() -> LogLevel { level } func set(_ newLevel: LogLevel) { level = newLevel } } private static let logQueue = DispatchQueue(label: "com.yana.api.logger", qos: .utility) private static let dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "HH:mm:ss.SSS" return formatter }() // MARK: - Redaction /// 需要脱敏的敏感字段(统一小写匹配) private static let sensitiveKeys: Set = [ "authorization", "token", "access-token", "access_token", "refresh-token", "refresh_token", "password", "passwd", "secret", "pub_ticket", "ticket", "set-cookie", "cookie" ] /// 对字符串做中间遮罩,保留前后若干字符 private static func maskString(_ value: String, keepPrefix: Int = 3, keepSuffix: Int = 2) -> String { guard !value.isEmpty else { return value } if value.count <= keepPrefix + keepSuffix { return String(repeating: "*", count: value.count) } let start = value.startIndex let prefixEnd = value.index(start, offsetBy: keepPrefix) let suffixStart = value.index(value.endIndex, offsetBy: -keepSuffix) let prefix = value[start.. [String: String] { var masked: [String: String] = [:] for (key, value) in headers { if sensitiveKeys.contains(key.lowercased()) { masked[key] = maskString(value) } else { masked[key] = value } } return masked } /// 递归地对 JSON 对象进行脱敏 private static func redactJSONObject(_ obj: Any) -> Any { if let dict = obj as? [String: Any] { var newDict: [String: Any] = [:] for (k, v) in dict { if sensitiveKeys.contains(k.lowercased()) { if let str = v as? String { newDict[k] = maskString(str) } else { newDict[k] = "" } } else { newDict[k] = redactJSONObject(v) } } return newDict } else if let arr = obj as? [Any] { return arr.map { redactJSONObject($0) } } else { return obj } } /// 将请求体 Data 以 Pretty JSON(脱敏后)或摘要形式输出 private static func maskedBodyString(from body: Data?) -> String { guard let body = body, !body.isEmpty else { return "No body" } if let json = try? JSONSerialization.jsonObject(with: body, options: []) { let redacted = redactJSONObject(json) if let pretty = try? JSONSerialization.data(withJSONObject: redacted, options: [.prettyPrinted]), let prettyString = String(data: pretty, encoding: .utf8) { return prettyString } } return " (\(body.count) bytes)" } // MARK: - Request Logging static func logRequest( _ request: T, url: URL, body: Data?, finalHeaders: [String: String]? = nil ) { #if !DEBUG return #else Task { let level = await Config.shared.get() guard level != .none else { return } logQueue.async { let timestamp = dateFormatter.string(from: Date()) debugInfoSync("\n🚀 [API Request] [\(timestamp)] ==================") debugInfoSync("📍 Endpoint: \(request.endpoint)") debugInfoSync("🔗 Full URL: \(url.absoluteString)") debugInfoSync("📝 Method: \(request.method.rawValue)") debugInfoSync("⏰ Timeout: \(request.timeout)s") // 显示最终的完整 headers(包括默认 headers 和自定义 headers) if let headers = finalHeaders, !headers.isEmpty { if level == .detailed { debugInfoSync("📋 Final Headers (包括默认 + 自定义):") let masked = maskHeaders(headers) for (key, value) in masked.sorted(by: { $0.key < $1.key }) { debugInfoSync(" \(key): \(value)") } } else if level == .basic { debugInfoSync("📋 Headers: \(headers.count) 个 headers") // 只显示重要的 headers let importantHeaders = ["Content-Type", "Accept", "User-Agent", "Authorization"] let masked = maskHeaders(headers) for key in importantHeaders { if let value = masked[key] { debugInfoSync(" \(key): \(value)") } } } } else if let customHeaders = request.headers, !customHeaders.isEmpty { debugInfoSync("📋 Custom Headers:") let masked = maskHeaders(customHeaders) for (key, value) in masked.sorted(by: { $0.key < $1.key }) { debugInfoSync(" \(key): \(value)") } } else { debugInfoSync("📋 Headers: 使用默认 headers") } if let queryParams = request.queryParameters, !queryParams.isEmpty { debugInfoSync("🔍 Query Parameters:") for (key, value) in queryParams.sorted(by: { $0.key < $1.key }) { let masked = sensitiveKeys.contains(key.lowercased()) ? maskString(value) : value debugInfoSync(" \(key): \(masked)") } } if level == .detailed { let pretty = maskedBodyString(from: body) debugInfoSync("📦 Request Body: \n\(pretty)") // 仅提示包含基础参数,避免跨 actor 读取 UIKit 信息 if request.includeBaseParameters { debugInfoSync("📱 Base Parameters: 已自动注入") } } else if level == .basic { let size = body?.count ?? 0 debugInfoSync("📦 Request Body: \(formatBytes(size))") // 基础模式也显示是否包含基础参数 if request.includeBaseParameters { debugInfoSync("📱 Base Parameters: 已自动注入") } } debugInfoSync("=====================================") } } #endif } // MARK: - Response Logging static func logResponse(data: Data, response: HTTPURLResponse, duration: TimeInterval) { #if !DEBUG return #else Task { let level = await Config.shared.get() guard level != .none else { return } logQueue.async { let timestamp = dateFormatter.string(from: Date()) let statusEmoji = response.statusCode < 400 ? "✅" : "❌" debugInfoSync("\n\(statusEmoji) [API Response] [\(timestamp)] ===================") debugInfoSync("⏱️ Duration: \(String(format: "%.3f", duration))s") debugInfoSync("📊 Status Code: \(response.statusCode)") debugInfoSync("🔗 URL: \(response.url?.absoluteString ?? "Unknown")") debugInfoSync("📏 Data Size: \(formatBytes(data.count))") if level == .detailed { debugInfoSync("📋 Response Headers:") // 将 headers 转为 [String:String] 后脱敏 var headers: [String: String] = [:] for (k, v) in response.allHeaderFields { headers["\(k)"] = "\(v)" } let masked = maskHeaders(headers) for (key, value) in masked.sorted(by: { $0.key < $1.key }) { debugInfoSync(" \(key): \(value)") } debugInfoSync("📦 Response Data:") if data.isEmpty { debugInfoSync(" Empty response") } else if let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []), let prettyData = try? JSONSerialization.data(withJSONObject: redactJSONObject(jsonObject), options: .prettyPrinted), let prettyString = String(data: prettyData, encoding: .utf8) { debugInfoSync(prettyString) } else if let _ = String(data: data, encoding: .utf8) { // 对非 JSON 文本响应不做内容回显,避免泄漏 debugInfoSync(" (\(data.count) bytes)") } else { debugInfoSync(" Binary data (\(data.count) bytes)") } } debugInfoSync("=====================================") } } #endif } // MARK: - Error Logging static func logError(_ error: Error, url: URL?, duration: TimeInterval) { #if !DEBUG return #else Task { let level = await Config.shared.get() guard level != .none else { return } logQueue.async { let timestamp = dateFormatter.string(from: Date()) debugErrorSync("\n❌ [API Error] [\(timestamp)] ======================") debugErrorSync("⏱️ Duration: \(String(format: "%.3f", duration))s") if let url = url { debugErrorSync("🔗 URL: \(url.absoluteString)") } if let apiError = error as? APIError { debugErrorSync("🚨 API Error: \(apiError.localizedDescription)") } else { debugErrorSync("🚨 System Error: \(error.localizedDescription)") } if level == .detailed { if let urlError = error as? URLError { debugInfoSync("🔍 URLError Code: \(urlError.code.rawValue)") debugInfoSync("🔍 URLError Localized: \(urlError.localizedDescription)") // 详细的网络错误分析 switch urlError.code { case .timedOut: debugWarnSync("💡 建议:检查网络连接或增加超时时间") case .notConnectedToInternet: debugWarnSync("💡 建议:检查网络连接") case .cannotConnectToHost: debugWarnSync("💡 建议:检查服务器地址和端口") case .resourceUnavailable: debugWarnSync("💡 建议:检查 API 端点是否正确") default: break } } debugInfoSync("🔍 Full Error: \(error)") } debugErrorSync("=====================================\n") } } #endif } // MARK: - Decoded Response Logging static func logDecodedResponse(_ response: T, type: T.Type) { #if !DEBUG return #else Task { let level = await Config.shared.get() guard level == .detailed else { return } logQueue.async { let timestamp = dateFormatter.string(from: Date()) debugInfoSync("🎯 [Decoded Response] [\(timestamp)] Type: \(type)") debugInfoSync("=====================================\n") } } #endif } // MARK: - Helper Methods private static func formatBytes(_ bytes: Int) -> String { let formatter = ByteCountFormatter() formatter.allowedUnits = [.useKB, .useMB] formatter.countStyle = .file return formatter.string(fromByteCount: Int64(bytes)) } // MARK: - Performance Logging static func logPerformanceWarning(duration: TimeInterval, threshold: TimeInterval = 5.0) { #if !DEBUG return #else Task { let level = await Config.shared.get() guard level != .none && duration > threshold else { return } logQueue.async { let timestamp = dateFormatter.string(from: Date()) debugWarnSync("\n⚠️ [Performance Warning] [\(timestamp)] ============") debugWarnSync("🐌 Request took \(String(format: "%.3f", duration))s (threshold: \(threshold)s)") debugWarnSync("💡 建议:检查网络条件或优化 API 响应") debugWarnSync("================================================\n") } } #endif } }