用OpenTelemetry (OTel)
搞定iOS用户行为!通过metrics
、logs
、traces
标准化移动端observability
。告别单体应用,拥抱模块化,用Span
在模块间传递遥测数据,无痛集成Context
,实时监控UI
、网络、用户认证,让你的App性能一览无余!
译自:How To Make Sense of iOS User Activity With OpenTelemetry
作者:David Rifkin
软件工程师构建软件系统并将其发布到世界。在构建期间和之后,工程师希望获得关于系统如何运行、执行和崩溃的实时反馈。
这种实践被称为可观测性,它是指从应用程序运行时收集信号或遥测数据,并使用这些信号来提出关于应用程序的问题。可观测性的价值来自于所有开发人员最终都会学到的一个真理:当你发布应用程序时,总会发生一些事情,而处理这种情况的最佳方法是收集尽可能多的关于应用程序的信息,以便对原因进行假设。
OpenTelemetry 是一套非常流行的工具,用于提出这些问题。OpenTelemetry,或简称 OTel,为开发人员提供了一种标准化的方式来传输应用程序信息,目前包括指标 (metrics)、日志 (logs) 和追踪 (traces),几乎支持任何流行的编程语言。任何环境中的应用程序都可以将相同类型的数据发送到其可观测性后端,从而创建一个标准,该标准对于任何了解 OTel 的人都具有可识别性、可理解性和可用性。
OpenTelemetry 和可观测性总体上仍在构建共识的一个领域是观察移动应用程序。移动可观测性必须考虑到许多因素,这些因素对于 Web 服务或数据库来说并不存在,例如电池寿命或用户体验。让我们探讨一下 OTel 如何使用现有工具解决移动可观测性特有的问题。
OpenTelemetry 允许开发人员使用十几个强大支持的语言 API 和 SDK 将遥测数据添加到他们的应用程序中。这些工具包中的每一个都是根据 OTel 生态系统中存在的规范创建的,并且每一个都为在应用程序中收集 OTel 信号提供了简单的检测。
例如,以下是使用 OTel-Swift instrumentation 的追踪示例:
// Set up the tracer
let tracer = OpenTelemetry.instance.tracerProvider
.get(
instrumentationName: "instrumentation-library-name",
instrumentationVersion: "1.0.0"
)
// Start a span to trace an activity in the app
let span = tracer.spanBuilder(spanName: "start-activity")
.startSpan()
// End the span after all the activity is completed
span.end()
可观测性工具的一个关键方面是代码应该在编写时进行检测。让应用程序代码直接告知其遥测数据比从外部猜测发生了什么更容易:
// Situation 1: Telemetry isn’t tied directly to functionality
func myFunction() {
action.start()
while.action.state != .ended {
if action.result == .interrupted {
action.start()
} else {
continue
}
}
}
// When calling myFunction, we can only guess at when the functionality began and ended
// We also know nothing about what happened in the function
let span = tracer.spanBuilder(spanName: "myFunction")
.startSpan()
myFunction()
span.end()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Situation 2: Telemetry is tied to functionality
func myFunction() {
action.start()
// We can begin tracing the action right when it starts
let span = tracer.spanBuilder(spanName: "myFunction")
.startSpan()
while.action.state != .ended {
if action.result == .interrupted {
// We can note information about the execution of our function
span.addEvent(
name: “action interrupted”,
timestamp: Date.now
)
action.start()
} else {
continue
}
}
// We can end the span right when the action completes
span.end()
}
// Calling the function will include the instrumentation
myFunction()
以这种方式添加检测允许开发人员将操作的整个上下文封装在操作生成的遥测数据中,而无需任何解释或遗漏。
这种工具能够胜任移动开发人员想要做的大部分事情。然而,OTel规范起源于Kubernetes集群和后端系统的可观测性用例,这有时会导致移动端上出现未考虑到的情况。OTel收集的信息主要集中在资源等项目上,这些项目在很大程度上不影响移动端,而像应用程序崩溃这样的关键移动活动目前在OTel中没有概念模型。更根本的是,移动应用程序是生产中单个编译的软件,而不是一组微服务,因此它们的工具方法必须不同。
对于OTel工具来说,移动应用程序的结构提出了一个复杂的情况。iOS和Android应用程序必须通过传输单个可下载文件发布到各自的应用商店。这意味着所有应用程序活动,以及关于该应用程序的信息,都是在编译时从单个代码库生成的。
想象一下一个代码库,其中包含UI元素、网络、用户身份验证详细信息和设备上的SQLite存储的具体信息,仅举几例。这些功能都必须存在,才能使实时移动应用程序正常运行,但对于开发人员来说,这是一个需要维护的相互关联的文件夹的混乱集合:
图1
更复杂的是,这些功能中的每一个都可能依赖于其他功能才能使应用程序运行。
例如,用户令牌管理器可能需要从本地存储检索其令牌,然后使用网络库与身份验证服务再次检查令牌是否仍然有效,然后触发已验证用户的导航更新,以将其进一步带入应用程序体验。在一个项目中编写所有这些内容,然后使用OTel跟踪该单个过程,可能会造成重叠的责任和黑盒子的混乱:
// Workflow root
let authRootSpan = tracer.spanBuilder(spanName: "auth-root")
.startSpan()
// Retrieve the auth token
let token = TokenManager.retrieveToken()
// Add to the Auth root a span for retrieving the auth token
// Issue: we don’t know any of the internals of the .retrieveToken() call, or even when it will complete
let retrieveAuthTokenSpan = tracer.spanBuilder(spanName: "retrieve-auth-token")
.setParent(authRootSpan)
.startSpan()
// Send auth token to web service for verification
let response = NetworkingManager.verify(authToken: token)
// Add to the Auth root a span for this request
// Issue: we don’t know any of the internals of the .verify call, or when it will complete.
// This is especially egregious for networking, as we’d like to know the full details of ~what happened~ during a request
let verifyAuthTokenRequestSpan = tracer.spanBuilder(spanName: "verify-auth-token")
.setParent(authRootSpan)
.startSpan()
// ...There will continue to be a similar pattern of issues for this approach
这项工作不应该在一个地方完成!
为了管理这种复杂性,移动开发人员通常将他们的单体应用程序组织成具有有限依赖关系的独立模块。这些模块各自管理其自身功能的职责,并尝试仅公开其他模块可以使用的接口:
*图2
*
将应用程序功能拆分为其自己的模块具有访问控制的内置优势:模块仅公开开发人员允许其公开的功能。例如,在以下代码中,只有makeRequest
函数可以在其他地方访问:
// in RunningApp-Networking
public struct RequestHelper() {
// Inaccessible to others
private setupRequest() {}
private teardownRequest() {}
// Accessible to others
public makeRequest() -> Result {
setupRequest()
defer {
teardownRequest()
}
// do request logic
return Result.success
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// In other modules, we can get the Result object
import RunningApp-Networking
let networkingResult = RequestHelper.makeRequest()
// in RunningApp-Networking
public struct RequestHelper() {
// Inaccessible to others
private setupRequest() {}
private teardownRequest() {}
// Accessible to others
public makeRequest() -> Result {
setupRequest()
defer {
teardownRequest()
}
// do request logic
return Result.success
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// In other modules, we can get the Result object
import RunningApp-Networking
let networkingResult = RequestHelper.makeRequest()
这里重要的步骤是:使用 RequestHelper.makeRequest
将会返回一个 Result
对象到应用程序中的其他模块。我们可以使用类似的模式为模块之间的 OTel instrumentation 返回对象,并且只返回另一个模块可能需要的信息:
// in RunningApp-Networking
public struct RequestHelper() {
private setupRequest() {}
private teardownRequest() {}
public makeRequest() -> (result: Result, processStartTime: Date) {
let startTime = Date.now
setupRequest()
defer {
teardownRequest()
}
// do request logic
return (result: Result.success, processStartTime: startTime)
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// In other modules
import RunningApp-Networking
let (networkingResult, networkingStartTime) = RequestHelper.makeRequest()
let networkingRequestSpan = tracer.spanBuilder(spanName: "networking")
.setStartTime(time: networkingStartTime)
.startSpan()
我们可以进一步迭代共享的特定信息,但为什么不使用共享的数据格式和语言呢?如果有一套工具可以让我们标准化在不同应用程序边界之间收集和传输的遥测数据,而无需每次都自定义传输信息的格式,那该多好。
我们可以让我们的模块使用 OpenTelemetry 在它们之间进行通信!通过在接口中传递 span 作为返回类型,模块可以传递它们自己的遥测数据,并将它们与来自应用程序其他部分的遥测数据组合起来,然后再将它们传输到设备之外。我们还可以使用我们想要附加的任何其他属性和事件来装饰遥测数据,而无需更改共享数据模型。将这种丰富的上下文添加到遥测数据是理解和重现影响用户的问题的关键:
// in RunningApp-Networking
public struct RequestHelper() {
private setupRequest() {}
private teardownRequest() {}
public makeRequest() -> (result: Result, span: Span) {
var span = tracer.spanBuilder(spanName: "networking")
.setStartTime(time:Date.now)
setupRequest()
defer {
teardownRequest()
}
// do request logic
// we can decorate the span with any pertinent information
// maybe being in low power mode is affecting the outcome
span.setAttribute(key: ”is-low-power-mode”, value: true)
// maybe certain operating systems experience worse networking outcomes
span.setAttribute(
key: “operating-system”,
value: UIDevice.current.systemVersion
)
return (result: Result.success, span: span)
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// In other modules
import RunningApp-Networking
let largerProcessSpan = tracer.spanBuilder(spanName: "larger-process")
.startSpan()
let (networkingResult, networkingSpan) = RequestHelper.makeRequest()
networkingSpan.setParent(largerProcessSpan)
.startSpan()
这种方法允许我们在模块之间共享完全形成的遥测数据,而无需让其他模块负责该遥测数据的细节或目的。毕竟,这就是遥测数据的目标:报告影响我们系统所有部分的因素,同时系统还在运行。 开发人员可以将应用程序的不同部分建模为单独的服务,并在之后创建一个完整的画面。
在总结之前,我们应该提到 context 的概念,它直接构建到所有 OTel SDK 中。Context “包含发送和接收服务或执行单元的信息,以便将一个信号与另一个信号相关联。” 换句话说,它与它通信的其他服务共享有关给定服务的信息,以便它们的遥测数据可以以任何数量的方式组合在一起。
对于移动可观测性而言,这是一个令人兴奋的机会,因为应用程序开发人员可以受益于了解在给定时间点收集的遥测数据,而不管它在应用程序中的位置如何。在 iOS 应用程序中,您可以检查 OpenTelemetry SDK 中 span 在该时间处于活动状态:
let currentSpan = OpenTelemetry.instance.contextProvider.activeSpan
但是,以这种方式使用 OTel 接口会给许多场景带来偶然性。移动设备是多核计算机,可以同时运行多个进程。如果网络请求在后台运行并且屏幕正在滚动,那么正在测量的“活动 span”将是什么?“活动 span”如何解释异步操作,这些操作尤其关系到网络和本地存储中的数据完整性?这些是移动开发中 单例 的标准问题,但在处理应用程序遥测的上下文时仍然值得考虑。
OpenTelemetry 的标准化概念和工具包允许开发人员以可预测的方式跨系统边界共享信息。在移动开发中,使用 OTel 进行强大的检测可以告诉开发人员代码的每一层发生了什么。此外,将 OTel 视为信息共享抽象允许 移动开发人员标准化其应用程序发送信息的结构。
在 Embrace,我们希望增强在移动设备上捕获的信息的功能,以便轻松了解哪些因素会影响用户体验。加入我们的 community Slack 提出问题,并详细了解我们对 OTel 的方法和我们的旅程。