Explore logging in Swift
Developer Tools 进阶 22m

探索 Swift 中的日志系统

Explore logging in Swift

2020年6月25日

在 Apple 官方观看视频

一句话判断

还在用 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_signpost API 可以配合 Instruments 做性能分析,在时间线上标记关键事件
WWDC 2020