可以。下面给你一套 AIChats 可直接落地的完整版 Logger。
目标是:
统一入口
分类清晰
支持 OSLog
Debug 下自动 print
支持日志级别
支持 metadata
支持错误打印
支持简单埋点入口
后续方便接入 Crashlytics / Sentry / 文件日志
我会尽量按你现在项目的风格来写,结构清晰、可扩展。
一、推荐目录结构
Infra/
└── Log/
├── AppLogger.swift
├── LogCategory.swift
├── LogLevel.swift
├── LogMetadata.swift
├── LogDestination.swift
└── AnalyticsLogger.swift如果你现在不想拆这么细,也可以先放在一个文件里,后面再拆。
二、完整代码
1)LogLevel.swift
import OSLog
enum LogLevel: String, Sendable, CaseIterable {
case debug
case info
case notice
case warning
case error
case critical
var osLogType: OSLogType {
switch self {
case .debug:
return .debug
case .info:
return .info
case .notice:
return .default
case .warning:
return .error
case .error:
return .fault
case .critical:
return .fault
}
}
var emoji: String {
switch self {
case .debug: return "🟣"
case .info: return "🔵"
case .notice: return "🟢"
case .warning: return "🟠"
case .error: return "🔴"
case .critical: return "🚨"
}
}
var label: String {
rawValue.uppercased()
}
}2)LogCategory.swift
enum LogCategory: String, Sendable, CaseIterable {
case app
case ui
case network
case auth
case ai
case avatar
case chat
case storage
case analytics
case crash
case firebase
}3)LogMetadata.swift
typealias LogMetadata = [String: AnySendable]这里用一个 AnySendable 来承接各种值。
4)AnySendable.swift
struct AnySendable: @unchecked Sendable, CustomStringConvertible {
let value: Any
init(_ value: Any) {
self.value = value
}
var description: String {
String(describing: value)
}
}5)LogDestination.swift
这层是为了以后扩展:
Console
File
Crashlytics
Remote Log
protocol LogDestination: Sendable {
func write(
subsystem: String,
category: LogCategory,
level: LogLevel,
message: String,
metadata: LogMetadata?,
file: String,
function: String,
line: Int
)
}6)OSLogDestination.swift
import Foundation
import OSLog
struct OSLogDestination: LogDestination {
func write(
subsystem: String,
category: LogCategory,
level: LogLevel,
message: String,
metadata: LogMetadata?,
file: String,
function: String,
line: Int
) {
let logger = Logger(
subsystem: subsystem,
category: category.rawValue
)
let finalMessage = Self.buildMessage(
level: level,
category: category,
message: message,
metadata: metadata,
file: file,
function: function,
line: line
)
switch level {
case .debug:
logger.debug("\(finalMessage, privacy: .public)")
case .info:
logger.info("\(finalMessage, privacy: .public)")
case .notice:
logger.notice("\(finalMessage, privacy: .public)")
case .warning:
logger.warning("\(finalMessage, privacy: .public)")
case .error, .critical:
logger.error("\(finalMessage, privacy: .public)")
}
}
private static func buildMessage(
level: LogLevel,
category: LogCategory,
message: String,
metadata: LogMetadata?,
file: String,
function: String,
line: Int
) -> String {
let fileName = URL(fileURLWithPath: file).lastPathComponent
let metadataText: String
if let metadata, !metadata.isEmpty {
let pairs = metadata
.map { "\($0.key)=\($0.value)" }
.sorted()
.joined(separator: ", ")
metadataText = " | \(pairs)"
} else {
metadataText = ""
}
return "\(level.emoji) [\(level.label)] [\(category.rawValue)] \(message)\(metadataText) | \(fileName):\(line) \(function)"
}
}7)ConsolePrintDestination.swift
Debug 下很好用。
import Foundation
struct ConsolePrintDestination: LogDestination {
func write(
subsystem: String,
category: LogCategory,
level: LogLevel,
message: String,
metadata: LogMetadata?,
file: String,
function: String,
line: Int
) {
#if DEBUG
let fileName = URL(fileURLWithPath: file).lastPathComponent
let metadataText: String
if let metadata, !metadata.isEmpty {
let pairs = metadata
.map { "\($0.key)=\($0.value)" }
.sorted()
.joined(separator: ", ")
metadataText = " | \(pairs)"
} else {
metadataText = ""
}
print("\(level.emoji) [\(level.label)] [\(category.rawValue)] \(message)\(metadataText) | \(fileName):\(line) \(function)")
#endif
}
}8)CrashDestination.swift
先留骨架,后续你可以接 Crashlytics / Sentry。
struct CrashDestination: LogDestination {
func write(
subsystem: String,
category: LogCategory,
level: LogLevel,
message: String,
metadata: LogMetadata?,
file: String,
function: String,
line: Int
) {
guard level == .error || level == .critical else { return }
// 这里后续接 Crashlytics / Sentry
// 例如:
// Crashlytics.crashlytics().log(message)
// SentrySDK.capture(message: message)
#if DEBUG
print("☁️ [CrashDestination] \(message)")
#endif
}
}9)AppLogger.swift
这是核心。
import Foundation
struct AppLogger: Sendable {
static let shared = AppLogger()
let subsystem: String
private let destinations: [any LogDestination]
init(
subsystem: String = "com.sinduke.AIChats",
destinations: [any LogDestination] = [
OSLogDestination(),
ConsolePrintDestination(),
CrashDestination()
]
) {
self.subsystem = subsystem
self.destinations = destinations
}
func log(
_ level: LogLevel,
_ message: @autoclosure () -> String,
category: LogCategory = .app,
metadata: LogMetadata? = nil,
file: String = #fileID,
function: String = #function,
line: Int = #line
) {
let messageValue = message()
for destination in destinations {
destination.write(
subsystem: subsystem,
category: category,
level: level,
message: messageValue,
metadata: metadata,
file: file,
function: function,
line: line
)
}
}
func debug(
_ message: @autoclosure () -> String,
category: LogCategory = .app,
metadata: LogMetadata? = nil,
file: String = #fileID,
function: String = #function,
line: Int = #line
) {
log(.debug, message(), category: category, metadata: metadata, file: file, function: function, line: line)
}
func info(
_ message: @autoclosure () -> String,
category: LogCategory = .app,
metadata: LogMetadata? = nil,
file: String = #fileID,
function: String = #function,
line: Int = #line
) {
log(.info, message(), category: category, metadata: metadata, file: file, function: function, line: line)
}
func notice(
_ message: @autoclosure () -> String,
category: LogCategory = .app,
metadata: LogMetadata? = nil,
file: String = #fileID,
function: String = #function,
line: Int = #line
) {
log(.notice, message(), category: category, metadata: metadata, file: file, function: function, line: line)
}
func warning(
_ message: @autoclosure () -> String,
category: LogCategory = .app,
metadata: LogMetadata? = nil,
file: String = #fileID,
function: String = #function,
line: Int = #line
) {
log(.warning, message(), category: category, metadata: metadata, file: file, function: function, line: line)
}
func error(
_ message: @autoclosure () -> String,
category: LogCategory = .app,
metadata: LogMetadata? = nil,
file: String = #fileID,
function: String = #function,
line: Int = #line
) {
log(.error, message(), category: category, metadata: metadata, file: file, function: function, line: line)
}
func critical(
_ message: @autoclosure () -> String,
category: LogCategory = .app,
metadata: LogMetadata? = nil,
file: String = #fileID,
function: String = #function,
line: Int = #line
) {
log(.critical, message(), category: category, metadata: metadata, file: file, function: function, line: line)
}
func error(
_ error: Error,
category: LogCategory = .app,
metadata: LogMetadata? = nil,
file: String = #fileID,
function: String = #function,
line: Int = #line
) {
var mergedMetadata = metadata ?? [:]
mergedMetadata["error_type"] = AnySendable(String(describing: type(of: error)))
mergedMetadata["error_description"] = AnySendable(error.localizedDescription)
log(
.error,
error.localizedDescription,
category: category,
metadata: mergedMetadata,
file: file,
function: function,
line: line
)
}
}10)Log.swift
这个是最顺手的入口。
enum Log {
private static let logger = AppLogger.shared
static func debug(
_ message: @autoclosure () -> String,
category: LogCategory = .app,
metadata: LogMetadata? = nil,
file: String = #fileID,
function: String = #function,
line: Int = #line
) {
logger.debug(message(), category: category, metadata: metadata, file: file, function: function, line: line)
}
static func info(
_ message: @autoclosure () -> String,
category: LogCategory = .app,
metadata: LogMetadata? = nil,
file: String = #fileID,
function: String = #function,
line: Int = #line
) {
logger.info(message(), category: category, metadata: metadata, file: file, function: function, line: line)
}
static func notice(
_ message: @autoclosure () -> String,
category: LogCategory = .app,
metadata: LogMetadata? = nil,
file: String = #fileID,
function: String = #function,
line: Int = #line
) {
logger.notice(message(), category: category, metadata: metadata, file: file, function: function, line: line)
}
static func warning(
_ message: @autoclosure () -> String,
category: LogCategory = .app,
metadata: LogMetadata? = nil,
file: String = #fileID,
function: String = #function,
line: Int = #line
) {
logger.warning(message(), category: category, metadata: metadata, file: file, function: function, line: line)
}
static func error(
_ message: @autoclosure () -> String,
category: LogCategory = .app,
metadata: LogMetadata? = nil,
file: String = #fileID,
function: String = #function,
line: Int = #line
) {
logger.error(message(), category: category, metadata: metadata, file: file, function: function, line: line)
}
static func critical(
_ message: @autoclosure () -> String,
category: LogCategory = .app,
metadata: LogMetadata? = nil,
file: String = #fileID,
function: String = #function,
line: Int = #line
) {
logger.critical(message(), category: category, metadata: metadata, file: file, function: function, line: line)
}
static func error(
_ error: Error,
category: LogCategory = .app,
metadata: LogMetadata? = nil,
file: String = #fileID,
function: String = #function,
line: Int = #line
) {
logger.error(error, category: category, metadata: metadata, file: file, function: function, line: line)
}
}11)AnalyticsLogger.swift
简单埋点入口。
enum AnalyticsLog {
static func event(
_ name: String,
parameters: [String: AnySendable]? = nil,
file: String = #fileID,
function: String = #function,
line: Int = #line
) {
Log.notice(
"Analytics Event: \(name)",
category: .analytics,
metadata: parameters,
file: file,
function: function,
line: line
)
// 这里后续可以接 FirebaseAnalytics / 自建埋点
// 例如:
// Analytics.logEvent(name, parameters: ...)
}
}三、怎么使用
普通日志
Log.info("App launched")指定分类
Log.info("User authenticated", category: .auth)
Log.warning("Token will expire soon", category: .auth)
Log.error("Request failed", category: .network)带 metadata
Log.info(
"Send message",
category: .chat,
metadata: [
"chat_id": AnySendable(chatId),
"user_id": AnySendable(userId),
"message_length": AnySendable(text.count)
]
)打印 Error
do {
try await fetchData()
} catch {
Log.error(
error,
category: .network,
metadata: [
"endpoint": AnySendable("/chat/send")
]
)
}埋点
AnalyticsLog.event(
"chat_send_clicked",
parameters: [
"chat_id": AnySendable(chatId),
"avatar_id": AnySendable(avatarId)
]
)四、给你几个 AIChats 场景示例
1. AI 请求开始
Log.info(
"AI request started",
category: .ai,
metadata: [
"model": AnySendable("gpt-4.1-mini"),
"message_count": AnySendable(messages.count)
]
)2. 流式返回中
Log.debug(
"Streaming delta received",
category: .ai,
metadata: [
"delta_length": AnySendable(delta.count)
]
)3. Firebase 用户登录
Log.notice(
"User signed in",
category: .auth,
metadata: [
"uid": AnySendable(user.uid)
]
)4. 聊天保存失败
Log.error(
"Failed to save chat message",
category: .chat,
metadata: [
"chat_id": AnySendable(chatId),
"message_id": AnySendable(messageId)
]
)五、为什么这版算“工业化”
因为它已经有这些特征:
统一入口 Log
分类 LogCategory
级别 LogLevel
元数据 metadata
多目标输出 destinations
兼容 OSLog
Debug 打印
Error 专门入口
埋点入口
后续可接:
File Logger
Crashlytics
Sentry
Remote Log Server
六、后续你最值得继续加的两个模块
1)FileLogDestination
把日志写入本地文件,方便导出和排查。
2)CrashlyticsDestination
把 .error / .critical 自动同步到 Firebase Crashlytics。
七、一个小优化建议
你现在项目里如果经常写:
AnySendable(...)会有点烦。可以加一个辅助方法:
extension Dictionary where Key == String, Value == AnySendable {
static func build(_ pairs: (String, Any)...) -> Self {
Dictionary(uniqueKeysWithValues: pairs.map { ($0.0, AnySendable($0.1)) })
}
}这样就能写成:
Log.info(
"Chat loaded",
category: .chat,
metadata: .build(
("chat_id", chatId),
("count", messages.count)
)
)这个会顺手很多。
如果你愿意,我下一条可以继续直接帮你补上 FileLogDestination 完整版,这样这套 Logger 就更像真正线上项目的基础设施了。