WWDC Quick Look 💓 By SwiftGGTeam
Explore logging in Swift

Explore logging in Swift

观看原视频

Highlight

Apple 在 Xcode 12 为统一日志系统提供 Swift 原生 Logger API,让 App 用类型安全的字符串插值记录事件、默认遮盖非数字敏感数据,并通过 Console 与 log collect 追踪难复现问题。

核心内容

Fruta 的礼品卡页面有一个典型线上 bug:列表滚到底部会继续从服务器加载卡片,用户点选某张卡片时又会停止当前加载。大多数时候一切正常,偶尔会在加载过程中点卡片后失败。问题很少发生,也不会稳定出现在开发机旁边,所以断点调试抓不到现场。(01:19

Apple 在这场 session 里给出的解法,是把关键事件写进统一日志系统(Unified Logging)。Xcode 12 新增的 Swift API 以 Logger 类型为入口,开发者可以在任务开始、失败、停止、重试等位置写入结构化日志。系统会把这些日志压缩归档在设备上,之后再用 log collect 拉回 Mac,并在 Console 里按 subsystem、category 和任务标识过滤。(02:24

这套 API 的重点不只是把 print() 换个名字。日志消息不会在调用点完整转成字符串;编译器和日志库会保留类型信息,只有日志真的展示时才付出格式化成本。运行时数据默认也会被保护:字符串和对象等非数字类型会在日志里遮盖,只有明确标记为 .public 的值才会显示。(03:44

Fruta 的例子最后靠任务 UUID 串起了时间线。日志显示:任务先因网络错误停止并等待重试,用户随后选择礼品卡,代码又试图停止一个已经不存在的任务,于是进入不一致状态。这个结论来自真实设备上的日志,不需要在本地重新撞出同样的网络时序。(07:35

详细内容

1. 创建 Logger,并把它放在关键路径上

02:44)接入分三步:导入 os 模块,创建 Logger,在重要事件发生处调用日志方法。subsystem 通常使用 App 的 bundle identifier,category 用来区分程序里的不同功能区。

// Add logging to your app in three simple steps

import os

let logger = Logger(subsystem: "com.example.Fruta", category: "giftcards")

func beginTask(url: URL, handler: (Data) -> Void) {
    launchTask(with: url) {
       handler($0)
    }

    logger.log("Started a task")
}

关键点:

  • import os 引入统一日志 API。
  • Logger(subsystem:category:) 给后续每条日志附上来源信息,Console 可以用它过滤。
  • logger.log 使用默认的 notice 级别,适合记录排查问题时必须保留的关键事件。
  • 日志写在 launchTask 之后,能确认下载任务已经被启动。

2. 记录运行时数据,同时控制隐私

03:32Logger 支持 Swift 字符串插值。你可以把任务 ID、服务器编号、耗时等运行时数据放进日志,后续用这些值缩小 Console 搜索范围。

// Add runtime data to the log messsage using string interpolation

import os

let logger = Logger(subsystem: "com.example.Fruta", category: "giftcards")

func beginTask(url: URL, handler: (Data) -> Void) {
    launchTask(with: url) {
       handler($0)
    }
    logger.log("Started a task \(taskId)")
}

关键点:

  • \(taskId) 会被日志系统作为类型化数据处理,不会先变成普通字符串。
  • 编译器和日志库会保留消息结构,展示日志时再按需要格式化。
  • 适合作为过滤条件的 ID 要稳定,例如本场 Fruta 示例里的任务 UUID。

04:28)隐私默认值很保守。非数字类型,例如字符串或对象,会被当作 private 数据遮盖。

logger.log("Paid with bank account \(accountNumber)")

关键点:

  • accountNumber 是字符串,日志输出里会显示为 private。
  • 这个默认行为面向已经发布到用户设备上的 App,避免日志泄露个人信息。
  • 不要为了方便搜索,把账户、邮箱、姓名等个人数据标成公开。

05:01)如果某个值本身不敏感,可以显式标记为公开。

logger.log("Ordered smoothie \(smoothieName, privacy: .public)")

关键点:

  • privacy: .public 让 Console 和日志归档显示真实值。
  • 标记公开前要先判断数据是否可能识别用户。
  • Fruta 示例把任务 UUID 标成公开,因为它只用于串起一次下载任务。

3. 用 log collect 和 Console 还原现场

05:20)日志写进系统后,操作系统会以压缩形式保存在设备上。演讲没有展示完整命令行参数,只明确了排查流程:连接设备,在 Mac 上运行 log collect,指定 device option、开始时间和输出文件名,然后用 Console 打开 .logarchive

log collect
device option
start time a few minutes before the bug
output log archive file

关键点:

  • 开始时间要选在第一次看到 bug 前几分钟,避免收集无关日志。
  • 输出结果是 log archive,双击后可以在 Console app 里查看。
  • 归档里包含系统上所有进程的日志,第一步要按 subsystem 过滤到自己的 App。

06:42)Console 的搜索和过滤才是定位问题的核心。Fruta 先按 bundle identifier 过滤出自己的日志,再追加失败任务的 UUID,只保留与这次失败有关的几条记录。

logger.log("Starting a new task for loading cards \(currentTaskID, privacy: .public)")
logger.fault("Task \(currentTaskID, privacy: .public) is not runinng, cannot be stopped!")
logger.log("Task \(currentTaskID, privacy: .public) interrupted")

关键点:

  • 所有日志都带同一个 currentTaskID,Console 能用它把一次下载任务串起来。
  • logger.fault 用在程序假设被破坏的场景,本例是代码试图停止一个没有运行的任务。
  • privacy: .public 让 UUID 出现在日志里,开发者才能把同一任务的多条记录筛出来。

4. 选择合适的日志级别

09:27Logger 提供五个级别:debug、info、notice、error、fault。级别影响 Console 中的视觉提示、是否持久化、保留时间和性能成本。error 与 fault 会在 Console 里高亮,debug 速度最快但不会持久化。

logger.debug("\(slowFunction(data))")

关键点:

  • debug 级别的消息在没有被 stream 时会被丢弃。
  • Swift 编译器会避免执行构造 debug 消息所需的代码,所以 slowFunction(data) 不会让用户付出成本。
  • 需要事后用 log collect 找回的关键事件,不应放在 debug 级别。

10:20)选择级别时可以按用途分层。debug 留给开发期细节,info 记录有帮助但非必要的信息,notice 记录排查问题时必须知道的事件,error 记录执行期间的错误,fault 记录潜在程序 bug 导致的严重状态。

logger.log("Task \(currentTaskID, privacy: .public) interrupted")
logger.fault("Task \(currentTaskID, privacy: .public) is not runinng, cannot be stopped!")

关键点:

  • logger.log 对应默认 notice 级别,适合记录任务被中断这种关键流程事件。
  • logger.fault 对应最严重级别,适合记录违反程序预期的状态。
  • error 和 fault 的持久化时间通常更长,但设备存储空间仍会影响保留期。

5. 格式化日志,让数据可以被读和复制

12:58)日志不只服务于错误排查,也能收集轻量统计数据。Fruta 会记录任务 ID、礼品卡 ID、服务器 ID 和耗时。原始输出不对齐时很难阅读,alignformat 可以在不增加日志调用成本的前提下改善可读性。

import SwiftUI
import os

let statisticsLogger = Logger(subsystem: "com.example.Fruta", category: "statistics")

// Log statistics about communication with a server.
func logStatistics(taskID: UUID, giftCardID: String, serverID: Int, seconds: Double) {
    statisticsLogger.log("\(taskID) \(giftCardID, align: .left(columns: GiftCard.maxIDLength)) \(serverID) \(seconds, format: .fixed(precision: 2))")
}

关键点:

  • align: .left(columns:) 把礼品卡 ID 固定为相同宽度,便于按列阅读。
  • format: .fixed(precision: 2) 把耗时保留两位小数,减少无用精度。
  • 对齐后的日志可以用 Option 键做 column select,再粘贴到 Numbers 里分析。
  • 本场示例通过图表发现慢任务都来自 server three,从而把这台服务器下线维护。

15:00)格式化选项不止固定小数。Xcode code completion 可以看到十六进制、八进制、指数格式等选项,也可以配合列宽做右对齐。

logger.log("\(data, format: .hex, align: .right(columns: width))")

关键点:

  • format: .hex 适合把二进制或数字数据按十六进制显示。
  • align: .right(columns:) 让不同长度的数据在同一列右对齐。
  • 格式化由日志系统处理,演讲明确说明它不会增加日志调用成本。

6. 用 hash 遮盖敏感值,但保留匹配能力

15:25)日志会在 App 发布后持续发生,拥有设备和密码的人可以收集日志。个人信息不应该被标成 public。遇到需要判断两条日志是否指向同一个值的场景,可以使用 equality-preserving hash。

logger.log("Paid with bank account: \(accountNumber, privacy: .private(mask: .hash))")

关键点:

  • .private(mask: .hash) 不显示银行账号原文。
  • 相同账号会得到相同 hash,开发者仍然可以判断两条日志是否关联。
  • 这个方式适合过滤和聚合同一敏感值,不适合把敏感内容还原给人看。

核心启发

给异步任务建立日志时间线

做什么:为网络请求、后台下载、同步任务生成任务 ID,并在开始、重试、取消、完成和失败处写日志。

为什么值得做:Fruta 的 bug 依赖网络错误和用户点击的时序,任务 UUID 让 Console 能还原同一次任务里的事件顺序。

怎么开始:创建 Logger(subsystem:category:),在每个任务入口生成 UUID,用 privacy: .public 记录这个无敏感含义的 ID。

把日志级别写进工程约定

做什么:给团队定义 debug、info、notice、error、fault 的使用边界。

为什么值得做:级别决定是否能事后收集,也决定 Console 高亮和保留时间。把关键错误写成 debug,问题发生后可能已经没有日志。

怎么开始:把用户可感知错误写成 logger.error,把违反程序假设的状态写成 logger.fault,把高频开发期细节放进 logger.debug

做一个隐私安全的日志审计

做什么:检查日志里的姓名、账号、订单、邮箱、地理位置和设备标识,确认哪些值应该默认遮盖、公开或 hash。

为什么值得做:session 明确指出字符串和对象默认 private,日志可以被有设备访问权的人收集,错误的 .public 会暴露个人信息。

怎么开始:先移除敏感值上的 .public;需要关联同一用户或同一账号时,改用 .private(mask: .hash)

让性能排查日志直接可分析

做什么:记录服务器编号、耗时、资源 ID 等字段,并用 alignformat 把输出整理成列。

为什么值得做:Fruta 示例把对齐日志复制到 Numbers 后发现慢任务集中在 server three,日志直接变成了诊断数据。

怎么开始:对固定宽度字段使用 align: .left(columns:),对耗时使用 format: .fixed(precision: 2),在 Console 中用 column select 拷贝到表格工具。

关联 Session

评论

GitHub Issues · utterances