哥们,我的错误在哪里?OpenTelemetry 如何记录错误

博客文章在发布后不会更新。这篇文章已经发布一年多了,其内容可能已过时,部分链接可能无效。在依赖任何信息之前,请务必核实。

A confused penguin trying to learn about errors and exceptions. Image generated with AI using Dalle3 via Bing Copilot

根据您习惯的开发语言,您可能对什么是错误,以及什么构成异常以及如何处理它有特定的想法。例如,Go 没有异常,部分原因是为了阻止程序员将过多的普通错误标记为异常。另一方面,Java 和 Python 等语言提供了内置支持来抛出和捕获异常。

当不同语言在错误或异常的定义和处理方式上存在分歧时,当您需要对用这些语言编写的微服务进行标准化遥测和错误报告时,您会使用什么?OpenTelemetry 就是我们将要解决并更多问题的工具。

  • 错误在后端中的可视化方式可能与您认为的并不一致,或者看起来并不像您期望的那样。
  • Span 类型如何影响错误报告。
  • Span 与日志报告的错误。

错误与异常

在深入探讨 OTel 如何处理错误和异常之前,让我们先定义它们是什么,以及它们之间的区别。虽然对这些术语的定义存在差异,但我们已经确定了以下定义,并将在本文中使用。请注意,这不是官方的 OTel 语言;它们是通用的行业定义。

错误是程序中妨碍其执行的意外问题。例如,语法错误,如缺少分号或缩进不正确,以及运行时错误,由逻辑错误引起。

异常是一种运行时错误,会中断程序的正常流程。例如,除以零或访问无效内存地址。

Python 和 JavaScript 等某些语言将错误和异常视为同义词;而 PHP 和 Java 等其他语言则不然。理解错误和异常之间的区别对于有效的错误处理至关重要,因为它使您能够采取更细致的策略来处理和从应用程序的故障中恢复。

OTel 中的错误处理

那么 OTel 如何处理不同语言之间的所有这些概念差异呢?这就是 规范(简称“spec”)发挥作用的地方。该规范为从事项目不同部分的开发人员提供了蓝图,并使所有语言的实现标准化。

由于语言 API 和 SDK 是规范的实现,因此一般规则是禁止实现规范未涵盖的任何内容。这提供了一个指导原则,有助于组织对项目的贡献。实践中存在一些例外情况;例如,一种语言可能会在将新功能添加到规范的过程中对其进行原型化,但该功能可能会在相应的语言添加之前发布(通常是 alpha 或实验性的)。

另一种例外情况是,某种语言可能会决定偏离规范。尽管通常不建议这样做,但有时存在强大的特定语言原因来做不同的事情。这样,规范允许每种语言在实现功能时尽可能地符合其习惯用法。例如,大多数语言都实现了 RecordException(例如,Python),而 Go 则实现了 RecordError,它在功能上做了同样的事情。

您可以查看所有语言的 规范合规性矩阵,但通过查看各个语言的存储库,您将获得最新信息。现在我们可以开始弄清楚如何在 OTel 中处理错误,首先是如何报告它们。

  • Spans
  • 日志

Span 中的错误

在 OTel 中,Span 是分布式跟踪的基本构建块,代表分布式系统中的单个工作单元。Span 通过上下文相互关联,并与跟踪关联。简而言之,上下文是将数据包变成统一跟踪的粘合剂。上下文传播允许我们在多个系统之间传递信息,从而将它们联系起来。通过元数据和 Span 事件,跟踪可以告诉我们有关应用程序的各种信息。

Graphic that shows the spans within a trace

使用元数据增强 Span

OTel 使您能够以键值对的形式使用元数据(属性)来增强 Span。通过将相关信息附加到 Span,例如用户 ID、请求参数或环境变量,您可以更深入地了解错误发生的环境,并快速确定其根本原因。这种丰富的元数据错误处理方法可以显着减少诊断和解决问题的耗时和精力,最终提高应用程序的可靠性和可维护性。

Span 还有一个 Span 类型字段,它为我们提供了一些额外的元数据,可以帮助开发人员排除错误。OTel 定义了几种 Span 类型,每种类型对错误报告都有独特的含义。

  • client:用于传出的同步远程调用(例如,传出的 HTTP 请求或数据库调用)
  • server:用于传入的同步远程调用(例如,传入的 HTTP 请求或远程过程调用)
  • internal:用于不跨越进程边界的操作(例如,检测函数调用)
  • producer:用于创建可能稍后异步处理的工作(例如,插入到作业队列中的作业)
  • consumer:用于处理由生产者创建的作业,这可能在生产者 Span 结束很长时间后才开始

Span 类型由使用的检测库自动确定。

Span 还可以通过 Span 状态进行增强。默认情况下,Span 状态标记为 Unset,除非另有指定。如果生成的 Span 显示错误,您可以将 Span 状态标记为 Error;如果生成的 Span 没有错误,则标记为 Ok

使用 Span 事件增强 Span

Span 事件是嵌入在 Span 中的结构化日志消息。Span 事件通过提供 Span 的描述性信息来帮助增强 Span。Span 事件也可以拥有自己的属性

前面,我们提到了一个名为 RecordException 的方法。根据 规范(重点是我们自己),“为了方便记录异常,语言应该提供一个 RecordException 方法,如果该语言使用异常……该方法的签名由每种语言确定,并且可以根据需要进行重载。”

由于 Go 不支持“传统”的异常概念,因此它支持 RecordError,它在功能上以惯用的方式做了同样的事情。如果它应该被标记为错误,您需要额外调用一次将其状态设置为 Error,因为它不会自动设置为该状态。同样,RecordException 可用于在不将 Span 状态设置为 Error 的情况下记录 Span 事件,这意味着您可以使用它来记录 Span 的任何其他数据。

通过将 Span 状态与在 Span 异常发生时自动设置为 Error 分开,您可以支持使用状态为 OkUnset 的异常事件的用例。这为检测库作者提供了最大的灵活性。

日志中的错误

在 OTel 中,日志是服务或其他组件发出的结构化、带时间戳的消息。最近将日志添加到 OTel 使我们有了另一种报告错误的方式。日志传统上具有不同的严重性级别来表示发出的消息类型,例如 DEBUGINFOWARNINGERRORCRITICAL

OTel 允许日志与跟踪相关联,其中日志消息可以通过跟踪上下文相关性与跟踪中的 Span 相关联。因此,查找严重性级别为 ERRORCRITICAL 的日志消息,可以通过调出相关跟踪来提供有关导致该错误的更多信息。

要在日志中记录错误,需要 exception.typeexception.message,同时建议使用 exception.stacktrace。有关更多信息,请参阅 日志中异常的语义约定

捕获错误的日志还是 Span?

在这一切之后,您可能想知道使用哪个信号来捕获错误:Span 还是日志?答案是:“取决于!”也许您的团队主要使用跟踪;也许它主要使用日志。

Span 在捕获错误方面可能非常有用,因为如果操作出错,将 Span 标记为错误会使其脱颖而出,从而更容易发现。另一方面,如果您没有过滤或尾部采样您的跟踪,并且您的系统每分钟产生数千个 Span,您可能会错过不经常发生但仍需要处理的错误。

使用 Span 事件而不是日志怎么样?同样,这取决于。使用 Span 事件可能很方便,因为当 Span 状态设置为 Error 时,会自动创建具有异常消息(以及您可能想捕获的其他元数据)的 Span 事件。

另一个考虑因素是您的可观察性后端。您的后端是否同时渲染日志和跟踪?您的日志、Span 和 Span 事件的查询或发现能力如何?是否支持日志和跟踪关联?

在不同后端中可视化错误

虽然 OTel 为我们提供了由系统发出的原始遥测数据,但它并没有提供数据可视化或解释。这是由可观察性后端完成的。由于 OTel 是供应商中立的,这意味着发出的相同信息可以由不同的后端进行可视化和解释,而无需重新检测您的应用程序。

Jaeger

让我们来看看 OTel 错误在 Jaeger 中是什么样的。错误数据由 此存储库中的代码生成。这是服务 py-otel-server 的跟踪视图。如下所示,错误 Span 显示为红点。

List of traces in the Jaeger UI

如果我们深入研究并聚焦于错误 Span,我们可以点击 Logs(这是 Span 事件在 Jaeger 中的表达方式),并查看上面捕获的信息。

Attributes and other metadata for an error span in Jaeger

Span 清楚地标记为错误,并且包含一个带有捕获的异常的 Span 事件。Jaeger 将 Span 事件表示为日志,但不可视化 Span 之外的日志。

专有后端

如果您一直在使用专有代理来监控您的应用程序,并且最近迁移到 OTel,您可能会注意到 OTel 错误在您的可观察性后端中的表达方式可能与您期望的有所不同,而与专有代理捕获的相同错误相比。这很可能是因为 OTel 对错误的建模方式与供应商以前的建模方式不同。

作为一个广泛的例子,供应商可能有自己关于应用程序中什么构成逻辑工作单元的概念。您可能熟悉 transaction 这个术语,它的含义因供应商而异。在 OTel 中,这由跟踪表示。您可能已经注意到数据可视化体验上的差异,因为供应商对他们的平台进行了自己的调整,以适应 OTLP 作为一流的数据类型。

作为一个更具体的例子,OTel 的 Span 类型概念可能会影响您的 OTel 错误在后端中的表达方式。例如,如果您有一个具有一个异常的跟踪,并且该异常位于类型为 Error 的内部 Span 上,您应该会看到该跟踪被标记为错误,但它可能不会被计入您的总体应用程序错误率。这是因为供应商可能认为只有入口点 Span(服务器 Span)和使用者 Span 上的错误才应计入您的错误率。

如果您的后端支持跟踪和日志关联,您应该能够从日志导航到关联的跟踪,反之亦然。此外,虽然 Jaeger 将 Span 事件可视化为日志,但一些供应商可能会将 Span 事件综合为自己的数据类型,而不是作为日志数据类型,这将影响您查询该数据的方式。

结论

我们刚刚探讨了在微服务架构中处理不同编程语言的错误和异常的挑战,并介绍了 OTel 作为标准化遥测和错误报告的解决方案。OTel 规范为跨不同语言的错误处理标准化提供了蓝图,为实现提供了指导方针,但允许一定程度的灵活性。

您可以通过使用语言 SDK 的 RecordException 或其等效项来在 Span 上记录错误,并通过添加自定义属性来进一步丰富 Span 事件。您还可以通过添加 exception.typeexception.message 来在日志中记录错误,并通过添加 exception.stacktrace 来捕获堆栈跟踪,以提供有关发生的事件的更多信息。

一旦这些数据进入您的可观察性后端,如果您以前使用过其专有监控代理,您可能会注意到 OTel 检测的错误与代理检测的错误可视化方式存在差异。这主要是因为 OTel 对错误的建模方式可能与供应商以前的做法不同。

通过利用 OTel 的能力通过日志和 Span 记录错误并用元数据增强它们,您可以更深入地了解应用程序的行为并更有效地排除故障。您将能更好地构建和维护在当今动态且要求苛刻的环境中具有弹性、可靠性和高性能的软件应用程序。要了解更多信息,请参阅 OpenTelemetry 中的错误处理

本文的一个版本 最初发布在 New Relic 博客上。