关于 observability
面向 Swift 开发者的可观测性技能,指导为应用选择日志、signpost、指标和诊断信号,并检查隐私、生产数据采集与符号化流程。
主要功能
- 01按场景选择日志、signpost 或指标
- 02检查 Logger 级别与隐私标注
- 03指导设备日志检索与导出
- 04覆盖 MetricKit 和崩溃诊断
- 05检查生产遥测与符号化流程
适用场景
- 01替换应用中的 print 日志
- 02追踪操作耗时与用户流程
- 03分析生产环境崩溃或卡顿
- 04构建指标导出与监控流程
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ n0an/ observability-agent-skill --skill observability安装后在 Claude Code 中输入/observability确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
agents/openai.yamlreferences/activity-tracing.mdanti-patterns.mdcrash-reporting.mdfundamentals.mdlog-cli-and-console.mdlogger-api.mdmetrickit.mdnetwork-telemetry.mdoslogstore.mdproduction-monitoring.mdsignposts.mdsymbolication.mdthird-party-sdks.md详细介绍
编写并审查 Swift 代码,让应用具备可观测性:在恰当的级别发出正确的信号(日志、signpost、指标、诊断),确保日志中不包含用户数据,并以可读的形式从生产环境取回数据——完成符号化、聚合,并关联到正确的版本。
审查流程:
- 使用
references/fundamentals.md确立信号模型(三大支柱、统一日志架构、级别与持久化、开发环境与生产环境中的可见性)。 - 根据下方决策树确认每次发射都使用了正确的信号;如果不正确,则改用合适的信号(该用 Logger 时却用了 print,该用 signpost 时却用了日志,MetricKit 已经测量时却手动计时)。
- 使用
references/logger-api.md验证Logger的用法(子系统/类别结构、级别选择、隐私注解、插值性能、封装设计)。 - 使用
references/log-cli-and-console.md验证日志检索工作流(Console.app、logCLI 谓词、日志配置文件、sysdiagnose)。 - 使用
references/oslogstore.md验证应用内日志访问和支持日志导出。 - 使用
references/signposts.md验证 signpost 埋点(OSSignposter、ID、区间、兴趣点、Instruments 接入)。 - 使用
references/activity-tracing.md验证跨流程关联(活动、关联 ID)。 - 使用
references/metrickit.md验证 MetricKit 的采用方式(订阅时机、负载处理、直方图、诊断信息、iOS 27 的MetricManagerAPI)。 - 使用
references/crash-reporting.md验证崩溃/卡顿/OOM 的可观测性(报告会包含和不会包含什么、退出原因、Organizer)。 - 使用
references/symbolication.md验证符号化(dSYM 处理、UUID 匹配、atos、MetricKit 调用栈)。 - 使用
references/production-monitoring.md验证生产流水线(导出格式、单位、聚合策略、启动指标偏差、告警)。 - 使用
references/network-telemetry.md验证网络请求的可观测性(错误分类与噪声过滤、非致命报告设计、URLSessionTaskMetrics、聚合监控)。 - 使用
references/third-party-sdks.md验证第三方 SDK 集成(崩溃处理器的唯一性、dSYM 上传、桥接、隐私)。 - 使用
references/anti-patterns.md找出常见错误。
如果只处理部分工作,只加载相关参考文件。
按任务路由
根据用户目标匹配阅读顺序。只加载所需内容。
“为我的应用添加(规范的)日志记录/替换 print 语句”
references/logger-api.md- Logger 设置、类别、级别、隐私references/fundamentals.md- 哪些级别会持久化,以及谁能看到什么
“插值的值显示为 /我需要隐藏日志中的用户数据”
references/logger-api.md- 隐私注解、.private(mask: .hash)、相等性跟踪
“从设备读取日志/在 Console 中过滤噪声/调试时捕获日志”
references/log-cli-and-console.md- Console.app 工作流、log stream/show/collect、谓词、脚本化的模拟器/设备捕获级别
“让用户把日志发给我/在应用内显示日志”
references/oslogstore.md- OSLogStore、范围限制、导出设计references/logger-api.md- 持久化规则决定哪些日志可以检索
“测量操作耗时/找出耗时较长的部分”
references/signposts.md- OSSignposter 区间、Instruments os_signpostreferences/production-monitoring.md- 如果测量数据必须来自生产环境
“跨子系统跟踪用户流程/关联相关日志”
references/activity-tracing.md- 活动、关联 ID、适合改用 signpost 的情况
“从生产环境收集性能/电池/启动指标”
references/metrickit.md- MXMetricManager、负载、直方图、自定义 mxSignpost 指标references/production-monitoring.md- 导出、聚合、告警
“为什么我的应用在生产环境中崩溃 / 卡住 / 被终止?”
references/crash-reporting.md- 崩溃处理流程、退出原因、OOM、看门狗、卡顿references/metrickit.md- 诊断载荷包含调用栈references/symbolication.md- 让调用栈变得可读
“对这份崩溃日志 / MetricKit 调用栈进行符号化”
references/symbolication.md- dSYM、UUID 匹配、atos 用法
“配置 Sentry / Crashlytics / Datadog,或判断我是否需要它们”
references/third-party-sdks.md- 它们在 MetricKit 基础上增加了什么、集成规则references/crash-reporting.md- 它们所对标的原生基础方案
“构建指标仪表板 / 将遥测数据发送到我的后端”
references/production-monitoring.md- ASC Power and Performance API、管道设计、单位、分桶、索引指标references/metrickit.md- 你要导出的载荷形状
“监控网络错误 / 诊断生产环境中的慢请求”
references/network-telemetry.md- 错误分类、非致命报告设计、URLSessionTaskMetrics、聚合监控references/third-party-sdks.md- 如果使用现成的 RUM / 性能 SDK
“我遇到了 bug / 日志缺失 / 载荷始终未送达”
references/anti-patterns.md- 常见原因references/fundamentals.md- 持久化和传递规则
决策树
我应该发送哪种信号?
Recording a discrete event to reconstruct behavior later?
-> Logger (unified logging), with the level matching persistence needs
Measuring the duration of an operation for local profiling?
-> OSSignposter interval + Instruments os_signpost / Points of Interest
Measuring durations or counts across the production fleet?
-> mxSignpost (MetricKit histograms) or your own telemetry event
Daily launch / hang / battery / disk / memory numbers from production?
-> MetricKit metric payloads (MXMetricPayload)
Crash, hang, CPU-spin, disk-write, or OOM call stacks from production?
-> MetricKit diagnostic payloads + Xcode Organizer (+ third-party if you
need real-time alerting and grouping)
Correlating logs that belong to one user flow across subsystems?
-> os_activity scopes or an explicit correlation ID in the message
Users need to send you what happened on their device?
-> OSLogStore export in-app, or a sysdiagnose for deep system issues
Real-time dashboards, alerting, release health, session context?
-> third-party SDK (Sentry/Crashlytics/Datadog) or your own pipeline
(OpenTelemetry + OTLP backend is the vendor-neutral build-it route)
应该使用哪种日志级别?
Only useful while actively developing, high volume? -> debug / trace
Helpful context, fine if it evaporates after the fact? -> info
Essential for troubleshooting, must survive on device? -> notice (default)
Something failed but the app copes? -> error
A bug: broken invariant, data loss risk, must stand out? -> fault / critical
持久化方式取决于级别:debug 永远不会写入磁盘;info 仅在
log collect 期间写入;notice/error/fault 会持久化到存储中(直至
轮换)。如果支持人员需要从用户设备上查看日志,级别必须为
notice 或更高。
核心说明
- 绝不要将
print或NSLog作为应用的日志机制。调试器之外看不到print,而且它不会持久化;NSLog是同步的,速度很慢。请使用Logger(iOS 14+)或os_log(iOS 10+)。 - 为每组子系统/类别创建一个
Logger,并将其设为static let;使用包标识符作为子系统,并按功能区域设置类别(networking、persistence、ui)。不要让整个应用共用一个类别。 - 绝不要通过接收普通
String的函数封装Logger。只有在消息实际发出时,OSLogMessage中的插值才会延迟求值;String参数会强制每次调用都求值。如果必须抽象,请使用@autoclosure () -> String,或直接公开各类别的Logger。 - 默认将插值字符串和对象视为已隐藏(在没有连接调试器的设备上,它们会记录为
<private>)。确认不含用户数据后,才将值标记为privacy: .public;如果需要关联相同的值,但不泄露其内容,请使用privacy: .private(mask: .hash)。 - 绝不要将 PII(姓名、电子邮件、令牌、包含凭据的 URL、账号)作为
.public记录,也绝不要将其写入文件或第三方 SDK,因为操作系统的隐藏机制无法保护这些内容。任何能接触设备的人都可以在 Console 中读取应用持久化的日志消息。 - 选择日志级别时应考虑其持久化行为和成本,而非感觉:
debug(仅驻留内存,未采集时成本几乎为零)、info(驻留内存,仅在log collect中持久化)、notice(默认持久化)、error(持久化,用于应用会处理的失败)、fault/critical(持久化并捕获额外状态,用于真正的程序错误)。fault应仅用于程序错误,而非预期中的失败。 - 仅使用静态格式字符串:插入值,不要先通过字符串拼接来构建消息——拼接会同时破坏隐私标注和延迟求值。
- 测量时长时,请使用
OSSignposter区间,而不是带时间戳的日志行。为每个操作实例生成一个OSSignpostID(makeSignpostID()),保存返回的OSSignpostIntervalState,并使用相同的名称结束区间。对于要显示在专用 Instruments 跟踪中的事件,请使用.pointsOfInterest类别。 - 在发布版本中保留 signpost——它们的设计开销很低,而且对发布版本进行 Instruments 性能分析时依赖它们。如果需要,可通过
OSSignposter.disabled或环境检查来限制高频 signpost。 - 在启动路径中尽早订阅 MetricKit(
MXMetricManager.shared.add(_:)),且只订阅一次;应选择始终会执行的启动路径(App init / didFinishLaunching)。只有在应用已订阅的情况下,pastPayloads才会返回数据;延迟或有条件的订阅会悄无声息地丢失历史数据。 - MetricKit 只会在真机上提供数据——绝不要在模拟器中调试它。连接设备后,使用 Xcode 的 Debug > Simulate MetricKit Payloads,并注意指标 payload 最多每 24 小时才会到达一次(iOS 15+ 上诊断信息会立即到达,崩溃诊断信息则会在下次启动时到达)。
- MetricKit 订阅者回调会在后台队列中触发:不要操作 UI,并应快速持久化或转发 payload——payload 对象提供了
dictionaryRepresentation()/jsonRepresentation(),可用于导出。 - 导出 MetricKit 直方图时,请保留 Apple 的单位(
Measurement值以及 payload 自身单位的区间),并且只在数据处理边界进行一次转换。写入转换逻辑前,请核实每个指标的单位——秒与毫秒不匹配却未报错时,可能会在任何人发现之前污染一周的仪表板数据。 - 绝不要在未考虑预热的情况下,将“进程启动到 didFinishLaunching”作为冷启动耗时来测量(请检查环境中的
ActivePrewarm):从 iOS 15 起,系统可能会在用户点击之前很久就启动进程,导致测出长达数分钟的虚假启动耗时。优先使用 MetricKit 的histogrammedTimeToFirstDraw。 - 不要只关注均值和低百分位,也要关注它们:预热造成的离群值和封顶处理会掩盖分布中间部分的回归。要查看全体设备的情况,请先将指标转换为 0-100 范围内的有界分数,再计算平均值。
- OOM 终止不会留下崩溃报告。请通过
MXAppExitMetric中与内存相关的退出原因(以及新 API 中的内存异常诊断信息)来观察 OOM,并通过降低后台内存占用来减少此类问题——不要等崩溃报告工具显示出来。 - 收到内存警告时,应调整缓存策略(停止缓存、限制大小、优先使用会自动清理的
NSCache),而不是急着遍历并释放压缩集合——访问压缩页面会触发解压,恰恰在最难承受时增加内存占用。 - 绝不要在同一个应用中安装两个崩溃报告 SDK(或同时安装崩溃 SDK 和自定义 Mach/信号处理程序)——处理程序之间的链式调用不可靠,还会破坏彼此的报告。只选择一个;MetricKit 不会冲突,因为它在进程外收集数据。
- 为每个已发布的构建归档 dSYM,并在符号化前验证 UUID 匹配(
dwarfdump --uuid)。MetricKit 调用栈以binaryUUID和偏移量的形式到达,尚未符号化——请在设备外使用atos -i符号化(-i会还原内联栈帧;加载地址 = 栈帧地址减去offsetIntoBinaryTextSegment)。向 App Store 提交构建时请附带符号,这样 Organizer 报告才会显示名称并支持跳转到源代码。 - 根据主线程 CPU 占用排查卡顿:接近 100% 表示任务本身太慢(应使用 Time Profiler 分析);接近空闲表示线程被 I/O、锁、IPC 或优先级反转阻塞(应使用 System Trace 分析)——加大 CPU 性能分析力度无法找到被阻塞的线程。卡顿从 250 ms 起会被报告;请将 500 ms 及以上视为严重问题。
- 绝不要在模拟器或调试器下验证 watchdog 行为——这两种环境都会禁用启动 watchdog。请在支持设备中最旧的一台上独立测试启动,并启用 Organizer 回归通知,这样卡顿或终止率上升时通知会发给你,而不是你的用户。
- 在主线程之外读取
OSLogStore;在 iOS 上,它仅限于自身进程(.currentProcessIdentifier),且只会返回符合持久化规则的内容——生产环境中不会包含debug/info消息。 - 使用 activities(
os_activity)或明确的关联 ID 来关联某个流程中的日志;使用 signpost 测量该流程。activities 用于关联,signpost 用于计时——不要让一种机制承担另一种机制的职责。 - 有选择地报告网络失败:解码和解码后验证失败属于契约违约,应针对每位用户生成非致命错误报告(请封装为带有易读描述的自定义错误——绝不要使用原始
URLError,其共享域会将不同问题合并到仪表板的同一行);离线、连接断开和超时等噪声应汇总统计;由你发起的取消则应忽略。使用URLSessionTaskMetrics的阶段信息(DNS、连接、TLS、首字节时间、isReusedConnection)诊断慢请求——请参阅references/network-telemetry.md。 - 先做检测,再优化:追查仅在生产环境出现的问题时,先添加能验证假设的指标/诊断信息,发布并查看数据——不要根据无法复现问题的本地复现结果猜测。
- 记录关键决策点,而不是噪声:状态转换、分支结果、边界输入以及所有错误路径。每帧或每次滚动都会触发的日志应改用 signpost,或直接移除。
输出格式
如果用户要求进行审查,请按文件整理发现的问题。对于每个问题:
- 指出文件和相关行号。
- 说明要替换的反模式。
- 简要展示修改前后的代码。
跳过没有问题的文件。最后按优先级总结最值得优先处理的改动。
如果用户要求编写或修复可观测性代码,请直接进行修改,而不是返回问题报告。
示例输出:
CheckoutService.swift
第 31 行:在生产环境的失败路径中使用 print——不可见且不会持久化。
// Before
print("Payment failed for \(user.email): \(error)")
// After - persisted, categorized, PII kept private
private static let logger = Logger(
subsystem: Bundle.main.bundleIdentifier!, category: "checkout")
Self.logger.error("Payment failed for \(user.email, privacy: .private(mask: .hash)): \(error.localizedDescription, privacy: .public)")
第 58 行:手动编写的计时日志——应使用 signpost 区间。
// Before
let start = Date()
let receipt = try await submit(order)
logger.info("submit took \(Date().timeIntervalSince(start))s")
// After
private static let signposter = OSSignposter(
subsystem: Bundle.main.bundleIdentifier!, category: "checkout")
let id = Self.signposter.makeSignpostID()
let state = Self.signposter.beginInterval("SubmitOrder", id: id)
let receipt = try await submit(order)
Self.signposter.endInterval("SubmitOrder", state)
总结
- **隐私(高):**对电子邮件进行哈希脱敏;当前原始值公开在持久化的错误消息中。
- **信号选择(中):**将所有时长日志改为 signpost,以便 Instruments 和 MetricKit 汇总。
示例结束。
参考资料
references/fundamentals.md- 可观测性模型:Apple 平台上的三大支柱、统一日志架构(二进制存储、内存与磁盘)、级别与持久性、开发环境与生产环境中不同人员可查看的内容、信号映射。references/logger-api.md-Logger和旧版os_log:设置模式、级别、隐私注解与屏蔽、格式化与对齐、延迟插值、包装器与多目标门面设计。references/log-cli-and-console.md- Console.app 工作流、log stream/show/collect/config、谓词语法、日志配置描述文件、sysdiagnose、用于 Agent 辅助调试的脚本化采集工作流(模拟器流式输出到文件、通过带环境变量门控的 print 镜像启动devicectl控制台、带时间戳的sudo log collect设备归档)。references/oslogstore.md- 在运行时读取自己的日志、范围与持久性限制、构建支持日志导出功能。references/signposts.md-OSSignposter区间/事件/动画区间、signpost ID、旧版os_signpost、Instruments os_signpost 和 Points of Interest、xctrace、XCTest 指标、用于无法插桩进程的 DTrace 探针。references/activity-tracing.md- Swift 中的os_activity、作用域、labelUserAction、关联 ID、activity 与 signpost 的区别。references/metrickit.md-MXMetricManager/MXMetricPayload/MXDiagnosticPayload(iOS 13-25)、iOS 27 的MetricManager异步 API、载荷频率、直方图、自定义mxSignpost指标、扩展启动跟踪、测试。references/crash-reporting.md- 报告会包含和不会包含哪些内容:崩溃处理流程、异常类型、看门狗代码、退出原因、OOM、卡顿工具矩阵(Thread Performance Checker、设备端检测、Organizer 卡顿报告)、Xcode Organizer 窗格与回归。references/symbolication.md- dSYM、UUID 匹配、atos、调试信息层级与符号剥离行为、Swift 符号反混淆、dyld 共享缓存、对 .ips 文件和 MetricKit 调用栈树进行符号化、SDK 符号上传。references/production-monitoring.md- 发布遥测:App Store Connect Power and Performance API(指标、智能洞察、诊断签名/日志)、iOS 上的 OpenTelemetry(opentelemetry-swift、OTLP 导出器、仪表/计数器/直方图语义、TaskLocal span 传播、withSpan 门面设计、详细级别门控)、导出流水线设计、单位与分桶、实验室与实地、启动指标偏差(预热)、评分索引、百分位数与均值、告警。references/network-telemetry.md- 请求级网络可观测性:为何实验室条件会低估实际情况、报告/计数/丢弃错误分类、非致命报告设计(可读的自定义错误、userInfo 上下文、内嵌面包屑)、URLSessionTaskMetrics阶段与诊断(持久连接、服务器处理时间、延迟的物理限制)、聚合监控(Firebase Performance / OTel)、针对性完整转储采集。references/third-party-sdks.md- Sentry/Crashlytics/Datadog/Firebase 与原生技术栈的比较、崩溃处理程序的独占性、统一日志桥接、dSYM 上传自动化、隐私清单与成本。references/anti-patterns.md- LLM 在生成日志和遥测代码时常犯的错误。
评论
支持 Markdown