Files
e-party-iOS/yana/APIs/APILogger.swift
edwinQQQ 90a840c5f3 feat: 更新日志级别管理及底部导航栏组件化
- 在ContentView中根据编译模式初始化日志级别,确保调试信息的灵活性。
- 在APILogger中使用actor封装日志级别,增强并发安全性。
- 新增通用底部Tab栏组件,优化MainPage中的底部导航逻辑,提升代码可维护性。
- 移除冗余的AppRootView和MainView,简化视图结构,提升代码整洁性。
2025-09-18 16:12:18 +08:00

313 lines
13 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<String> = [
"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..<prefixEnd]
let suffix = value[suffixStart..<value.endIndex]
return String(prefix) + String(repeating: "*", count: max(0, value.distance(from: prefixEnd, to: suffixStart))) + String(suffix)
}
/// headers
private static func maskHeaders(_ headers: [String: String]) -> [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] = "<redacted>" }
} 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 "<non-json body> (\(body.count) bytes)"
}
// MARK: - Request Logging
static func logRequest<T: APIRequestProtocol>(
_ 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("<non-json text> (\(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<T>(_ 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
}
}