探索 Swift 中的日志系统
Explore logging in Swift
2020年6月25日
一句话判断
还在用 print() 调试?os.log 才是 Apple 平台上正确的日志姿势——这场 Session 全面介绍了 os 模块在 Swift 中的使用,包括 iOS 14 新增的 Logger API。
这场 Session 讲了什么
统一日志系统(Unified Logging)是 Apple 从 macOS 10.12 / iOS 10 开始引入的高性能日志框架,通过 os 模块在 Swift 中使用。Session 系统介绍了这套日志系统的设计理念、API 使用和最佳实践,重点介绍了 iOS 14 中新增的 Logger 结构体。
Logger 是 iOS 14 新引入的 Swift-native 日志 API,取代了之前的 os_log 函数。它提供了类型安全的字符串插值日志、结构化的日志级别(debug/info/error/fault)和灵活的子系统/分类配置。与 print() 不同,Logger 的日志会被系统自动收集,可以通过 Console.app 或 log 命令行工具查看,并且有智能的持久化策略——debug 级别的日志只保存在内存中,error 和 fault 级别会写入磁盘。
Session 还讨论了日志的性能影响。统一日志系统在底层做了大量优化:日志消息的格式化是惰性的(只在日志真的被记录时才发生),字符串插值在编译期就部分完成了。这意味着即使你的代码里写了很多 logger.debug() 调用,在 release 构建中这些 debug 日志几乎不产生任何开销。
值得深挖的点
日志级别的正确选择
统一日志系统定义了五个级别:debug、info、notice(默认)、error、fault。Session 给出了清晰的选择指南。Debug 用于开发期调试信息,release 中不持久化。Info 用于偶尔需要查看的运行信息。Notice 是默认级别,用于重要的流程节点。Error 用于操作失败。Fault 用于严重的系统级错误。大多数开发者犯的错误是用 print() 或 NSLog() 而不是分级别的日志——这导致生产环境中要么看不到有用信息,要么被海量无用日志淹没。
Private 日志与隐私保护
iOS 15 引入了一个重要的隐私特性:日志中的动态数据默认是私有的(private),不会在日志系统中显示具体值,只显示 <private> 占位符。Session 提到了 OSLogPrivacy 的使用——当你确实需要在日志中看到具体值时,需要显式标记为 .public。这个设计确保了日志系统不会意外泄露用户隐私数据。
代码片段
import os
// iOS 14 新的 Logger API
class OrderService {
// 创建 Logger 实例,指定子系统和分类
let logger = Logger(
subsystem: "com.example.coffeeapp",
category: "OrderService"
)
func placeOrder(_ order: Order) async throws -> OrderResult {
logger.info("开始处理订单: \(order.id, privacy: .private)")
do {
let result = try await networkClient.submit(order)
logger.info("订单处理成功: \(order.id, privacy: .private)")
return result
} catch {
// 使用 error 级别记录失败
logger.error("订单处理失败: \(error.localizedDescription)")
throw error
}
}
func debugOrderFlow() {
// debug 级别只在开发期有效,release 中几乎无开销
logger.debug("进入订单流程调试")
logger.debug("当前状态: \(debugDescription())")
}
}
// 不同日志级别的使用场景
import os
let logger = Logger(subsystem: "com.example.app", category: "Network")
func demonstrateLogLevels() {
// Debug:仅开发期可见,release 不持久化
logger.debug("网络请求开始构建")
// Info:偶发性有用信息,短期持久化
logger.info("连接到服务器 \(serverURL)")
// Notice(默认级别):重要流程节点
logger.log("用户登录成功") // .log 等同于 notice
// Error:操作失败
logger.error("网络请求超时: \(timeoutInterval)秒")
// Fault:严重错误,系统级问题
logger.fault("数据一致性检查失败: 预期 \(expected) 条记录,实际 \(actual) 条")
}
// 结构化日志与 Console.app 过滤
import os
class PaymentProcessor {
let logger = Logger(
subsystem: "com.example.payment",
category: "PaymentProcessor"
)
func processPayment(amount: Decimal, currency: String) {
// 使用结构化的日志消息,方便在 Console.app 中过滤
logger.log("""
支付处理开始:
- 金额: \(amount, privacy: .private)
- 货币: \(currency)
- 时间: \(Date())
""")
// 在 Console.app 中可以用以下方式过滤:
// subsystem:com.example.payment AND category:PaymentProcessor
// messageType:error
}
}
// 在终端查看日志
// log stream --predicate 'subsystem == "com.example.payment"' --level debug
// log show --predicate 'category == "PaymentProcessor"' --last 1h
最佳实践
- 为每个模块/服务创建独立的
Logger实例,使用不同的 category 便于过滤 - 在 release 构建中避免使用 debug 级别记录关键信息——它不会被持久化
- 动态数据(用户名、订单号等)默认是私有的,需要显式标记
.public才会在日志中显示 - 用 Console.app 或
log stream命令查看日志,不要依赖 Xcode 的控制台输出 - 日志消息要写得有上下文——“操作失败”是无效日志,“支付处理失败: 超时 30秒, 订单号 xxx”才是好日志
还有什么值得关注
Logger的字符串插值在编译期就被部分处理,性能远优于字符串拼接后再传给日志函数- 可以通过
log config命令动态调整日志级别,不需要重新编译 - Session 提到
os_signpostAPI 可以配合 Instruments 做性能分析,在时间线上标记关键事件