首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >YAML 与 IConfiguration 的融合:OpenTelemetry .NET SDK 的声明式配置

YAML 与 IConfiguration 的融合:OpenTelemetry .NET SDK 的声明式配置

原创
作者头像
点火三周
发布于 2026-10-20 15:33:36
发布于 2026-10-20 15:33:36
360
举报
文章被收录于专栏:Elastic Stack专栏Elastic Stack专栏

Elastic 原生支持 OpenTelemetry。您可以通过 OTLP 将 trace、日志和指标直接发送到 Elasticsearch,无需专有代理。查看集成方式,在云服务中免费试用,或在本地运行。

很快,您就可以通过一次 UseDeclarativeConfiguration() 调用,从 YAML 文件配置 OpenTelemetry (OTel) .NET 客户端,而无需对检测代码做任何进一步修改。Pull request (PR) #7413 新增了一个 OpenTelemetry.Configuration.Declarative 包,该包将声明式配置文档解析为类型化模型,然后将其投射为 SDK 已经读取的 IConfiguration。其 8,800 行代码中约有三分之二是测试,包括针对解析器和环境变量替换的模糊测试。类型化模型才是关键所在,因为一份结构化的、与语言无关的文档,若没有这个模型,就无法适配 .NET 扁平且分层的配置系统。

为什么 OpenTelemetry 需要声明式配置

OTel 规范定义了三种配置 SDK 的方式,每种方式在权衡上各有取舍。它们之间的差异体现在表达结构的能力、是否需要源码访问权限,以及变更时是否需要重新构建:

机制

表达结构

需要代码访问

变更时需要重新部署

环境变量

否,仅扁平键/值

否

否

编程式配置

是

是

是

声明式文件

是

否*

否

* 需要引入包引用并进行一次性注册以启用声明式配置。此后,配置文件可随时更改,并在应用程序启动时生效。

大多数人最先接触的是环境变量这一机制。它们可移植、随处可用,且不要求应用代码做任何事。设置 OTEL_SERVICE_NAME 或 OTEL_EXPORTER_OTLP_ENDPOINT 很有效,因为这些值都是简单的字符串。但在处理结构性配置时,它们就显得力不从心,例如定义采样器及其相关选项,或定义两个具有不同批处理行为的导出器。扁平的键/值模型无法表达这种结构,因此您要么放弃该能力,要么将结构塞进字符串中,而这两种方式都难以长期维护。

编程式配置则处于另一个极端。它表达力完备,但要求也高,包括源码访问权限和修改能力,此外每次有人想调整采样时,都需要重新构建和重新部署。在许多环境中,这种要求并不合理。生产系统和遗留应用都面临同样的限制,任何无需改动代码即可自动完成检测的场景也是如此。需要调整遥测配置的人往往是那些无法发布新构建的人。

声明式配置正好填补了这一空白。它引入了一种基于文件的、标准化的配置数据模型表示,定义在 Configuration SDK 规范中。以下是该规范入门示例的节选,展示了其结构。采样器和导出器位于它们所属的 tracer provider 内部;如果还有第二个导出器并带有自己的批处理设置,那它会是 processors 下的另一个条目:

代码语言:yaml
复制
tracer_provider:  sampler:    parent_based:      root:        always_on:  processors:    - batch:        exporter:          otlp_http:            endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://localhost:4318}/v1/traces

这样的文档提供了编程式配置的表达力,却无需修改代码。而且,由于格式是语言无关的,同一份文档既能配置 .NET 服务,也能配置 Java 服务。该模型还涵盖检测(instrumentation)以及 SDK 组件,因此其适用范围不仅仅限于您原本需要手动接入的部分。

该规范已于 2026 年 3 月宣布稳定,公告中列出的仍在开发中的实现包括 .NET。.NET 自动检测项目已拥有针对零代码场景的自己的实现,但 SDK 本身尚不支持声明式配置。将其引入 SDK,可以让任何应用程序或发行版都能使用。Issue #7658 跟踪了这项工作。在本文中,我们将介绍这些工作的早期阶段。

为什么 YAML 不适合 .NET 的 IConfiguration

读取 YAML 文件是容易的部分。困难在于 .NET 已经有一套配置系统,而且是一套非常优秀的配置系统。

Microsoft.Extensions.Configuration 为每个现代 .NET 应用程序提供了一个 IConfiguration;也就是说,一个由有序来源列表组成的统一视图,通常包含 appsettings.json、特定环境的覆盖文件、环境变量、命令行参数,也许还有一个密钥存储。当两个来源定义了同一个键时,后一个来源会覆盖前一个。随后,options 模式将该视图绑定到类型化的类上。.NET 开发者依赖这种分层机制,并且期望任何新的配置机制都能融入其中。

在编写任何代码之前,我们分析了 SDK 目前如何消费配置。第一个发现令人鼓舞。没有任何 SDK 组件直接读取环境变量。每个 OTEL_* 值都是通过 IConfiguration 读取的,这就是为什么您已经可以在 appsettings.json 中设置 OTEL_SERVICE_NAME 并且它开箱即用的原因。大多数选项类型都是通过 SDK 的 options 工厂从 IConfiguration 构建的。因此,声明式文件有可能作为又一个配置来源插槽,而现有 SDK 无需更改即可读取它。

将声明式文件直接投射到 IConfiguration 只能走通部分路径,原因有五点:

  • YAML 是分层的,使用 snake_case 键和嵌套组件。SDK 的选项是扁平的,键名风格与环境变量类似(如 OTEL_BSP_SCHEDULE_DELAY)。IConfiguration 中的每个值都是字符串,因此配置模式中的整数和字符串(如 value: 3 与 value: "3")会变得无法区分。在配置模式中,属性缺失与显式设置为 null 具有不同的含义,而普通的配置访问会将两者混为一谈。
  • 目前,一个采样器、导出器或传播器之所以存在,是因为有人写了 SetSampler(new TraceIdRatioBasedSampler(0.5)) 或 AddOtlpExporter()。当 SDK 确实需要将字符串转换为组件时(例如 OTEL_TRACES_SAMPLER),它使用的是手写的 switch 语句,第三方无法扩展。一些规范定义的变量,如 OTEL_PROPAGATORS 和 OTEL_*_EXPORTER 系列,尚未实现。一份声明“使用一个包装了 trace_id_ratio_based 的 parent_based 采样器”的文档,需要一个将名称映射到组件的注册表,而 SDK 目前没有。
  • SDK 通过 IOptionsMonitor<T> 解析选项,这看起来像是潜在的重新加载支持,但实际上并非如此。选择这种设计是为了解决依赖注入生命周期问题。没有任何组件订阅变更通知,许多值在构造时就被捕获到长期存活的状态中。这对长期目标——动态控制——很重要,也意味着重新加载在第一阶段被明确排除在外。
  • 规范规定,当设置了 OTEL_CONFIG_FILE 时,“除配置文件中引用用于环境变量替换的环境变量之外的所有其他环境变量都必须被忽略”,因为“在所有情况下,都没有直观的方法将扁平的环境变量方案与结构化的文件配置方案合并”。.NET 开发者的直觉恰恰相反:文件应该是另一层。由于环境变量通过 IConfiguration 以与 appsettings.json 相同的键空间到达 SDK,因此在读取某个值时,没有任何记录表明它来自哪里。
  • 我们还研究了已有的实现。Java SDK 是规范的标准实现,.NET 自动检测项目也已经拥有自己的基于文件的配置。还有一些实际问题,比如这项工作应该放在 SDK 仓库中还是从 contrib 开始,以及是否应该依赖某个 YAML 库。

因此,问题变成了:一份结构化、类型化、与语言无关的文档,如何融入一个配置层扁平、字符串类型、分层的平台,而不破坏任何一种模型? 问题从来不是如何解析 YAML。

IConfiguration 背后的类型化模型

分析产生了一个粗略的规划:24 个拟议阶段,涵盖基础工作、新的选项类型、组件注册表和重新加载支持。这不可能一次全部落地,也不能要求维护者在抽象层面表示同意。下一步是一个刻意保持最小化的概念验证(PoC),以便维护者能够做出回应。

最初的版本走了显而易见的路。一个自定义的 IConfigurationProvider 遍历 YAML,并为现有 SDK 发出扁平的 OTEL_* 键。它能工作,但直接从 YAML 发出字符串会把“缺失”“null”和“存在”折叠成两种状态,而规范要求 Parse 区分缺失与 null。

于是 PoC 被细化为两个阶段,最终形态就是 PR #7413 所交付的:

  1. 解析为类型化模型。 YAML 被解析为内部记录(record),这些记录不携带任何 YAML 依赖,每个标量都包装在一个三状态值中。只有解析器接触 YAML 库(YamlDotNet),因此未来其他来源(例如通过 Open Agent Management Protocol (OpAMP) 推送)可以构建相同的模型。
  2. 投射到 IConfiguration。 模型通过一个配置提供程序,被投射为 SDK 已经理解的扁平 OTEL_* 键。disabled: true 变为 OTEL_SDK_DISABLED=true,每个现有的读取点都能拾取到它,核心 SDK 无需任何改动。

PR 新增了一个包 OpenTelemetry.Configuration.Declarative,因此应用程序只有在主动选择时才会引入 YAML 依赖。它支持刻意缩小的规范子集:file_format、disabled、resource.attributes 和 resource.attributes_list,以及规范中的环境变量替换。接入只需在 OpenTelemetryBuilder 上调用一次 UseDeclarativeConfiguration 即可。

以下代码启用了带有声明式配置和追踪(tracing)的 OTel SDK:

代码语言:csharp
复制
services.AddOpenTelemetry()
    .UseDeclarativeConfiguration()
    .WithTracing(...);

以下基本的 YAML 配置添加了两个资源属性,它们将应用于该服务导出的遥测数据:

代码语言:yaml
复制
file_format: "1.2"
resource:
  attributes:
    - name: service.name
      value: ${SERVICE_NAME:-my-service}
    - name: service.version
      value: "1.0.0"

我们将其称为重叠模式(overlay mode)。YAML 来源被追加在已注册的所有来源之后,因此文件的优先级高于环境变量和 appsettings.json。文件中未设置的键会落到之前已有的值上,而后添加的来源仍然覆盖文件,这与 .NET 开发者的预期完全一致。

重叠模式是正确的起点。它不需要更改稳定的核心 SDK。文件的行为与其他任何配置来源一样,因此其优先级无需解释。此外,解析器和模型可以独立于组件引擎进行审查,替换规则也可以单独审查。

不过,该 PR 中最重要的决定,是让类型化模型而非 IConfiguration 成为事实来源。扁平键是该模型的一种投射,服务于 SDK 现有的 options 管道。该设计还预见了第二种投射:每个节点的无模式视图,即规范中的 ConfigProperties,用于构建组件。这是必要的,因为 IConfiguration 无法表示“缺失”与“null”的区别,也无法表示多态组件选择,更无法表示任意嵌套。PR #7413 中只有第一种投射,但第二种从一开始就纳入了设计。

PR 也坦诚地指出了重叠模式尚未做到的事情,例如:

  • 资源属性仅限字符串,因为类型化值无法在投射进 OTEL_RESOURCE_ATTRIBUTES 字符串后存活。
  • 诸如 tracer_provider 之类的部分会被记录日志但不会应用。
  • 文件在启动时只读取一次。
  • 没有严格模式。标准的 IConfiguration 排序规则生效,因此对于文件未覆盖的任何设置,残留的 OTEL_* 变量仍然会起作用。

设计现在针对最后一个缺口走向了不同的方向。例如,考虑一份部分编写的文件,这在迁移过程中是常见情况。它没有设置采样器,但部署环境中仍然带着之前的 OTEL_TRACES_SAMPLER=always_off。在重叠模式下,这个残留变量会悄然生效,于是两个文件完全相同的服务可能表现出不同的行为,而其他任何 OTel SDK 都不会这样。规范的答案是:任何需要合并来源的人都应该定制配置模型,而不是合并键。

在初始阶段落地之后,务实的选择是与规范保持一致,并将严格行为设为默认:当一份文档被使用时,它就是 OTel 设置的来源。这彻底解决了部分文件问题。开发者珍视的 .NET 式分层并未被排除。我们稍后会研究 .NET 如何支持模型定制,通过显式、选择性的合并规则。无论如何,位于 PR #7413 核心的类型化模型使得这一变更成为可能,无需从零开始。

把 OTel 配置规范做对

配置是 SDK 接触的第一件事,也是遥测丢失时最容易被怀疑的最后一件事,因此即使很小的偏差也伴随着风险。第一个 PR 中的大量工作都花在了粗读规范时容易忽略的细节上。其 8,800 行代码中约有三分之二是测试,包括针对解析器和替换逻辑的模糊测试。

  • 处处三状态。 C# 的可空标注无法区分“缺失”与“存在但为 null”。能够直接建模这一点的联合类型(union types)将在 C# 15 / .NET 11 中到来,但它们仍处于预览阶段,而且该包还需要针对 netstandard2.0 和 .NET Framework 构建。因此每个模型字段都是三状态值,每个字段都要在三种状态下进行测试,而不仅仅是快乐路径。
  • YAML 1.2,不是 YAML 1.1。 规范要求使用 YAML 1.2 核心模式(core schema)。YAML 1.1 的习惯包括合并键(<<: *defaults)和更宽泛的布尔拼写集合。该包自带 YAML 1.2 标量解析器(scalar resolver),而不是依赖库的默认行为,并且直接拒绝合并键。引号至关重要:file_format: "1.1" 是字符串,会被接受;而 file_format: 1.1 是数字,会被拒绝。
  • 替换在正确的时机发生。 环境变量替换是在解码后的标量值上执行的——在 YAML 转义处理之后、类型解析之前。这与 YAML 处理模型以及 Java 实现、官方配置验证器都一致。因此 value: ${PORT} 可能解析为一个数字,而 value: "${PORT}" 则永远是字符串,并且替换永远不会向文档注入结构。
  • 沉默即是缺陷。 任何该包无法兑现的内容,例如跳过的资源属性、未应用的部分或重复注册,都会通过 SDK 的自诊断(self-diagnostics)机制报告,而不是悄悄丢弃。某个调试缺失属性的人,应该能够找到原因,而无需阅读源码。

这项工作如何在上游进行规划和评审

这一切始于原始 issue 中的一次讨论,以及对 SDK 当前配置格局的分析和拟议的 issue。最初并不是从代码开始的。即便是现在,设计仍在持续演进。我在一个本地仓库中跟踪它,借助 AI 代理协助。该仓库保存着一份高层规划和一份决策记录,其中每个重要选择都标注了日期及其背景和后果。它还保存着每个阶段的规范,这些规范是在实现开始之前编写的。这样的详细程度不适合大众阅读,因此高层阶段已发布在公共元 issue 中。

我将工作规划为小型的、可交付的单元,以减轻评审负担并保持代码持续合并。这种方法依赖于与 OTel .NET 维护者之一 Martin Costello 的密切合作,他评审了每个已落地的阶段。他的反馈反复改善了设计。在每个阶段落地后,我会重新评估后续阶段,将评审所获得的经验融入其中,并在需要时调整方向,同时将变更记录在决策记录中,从而确保计划永远不会悄然漂移。

OpenTelemetry .NET 中声明式配置的下一步

该包尚未发布正式版本,PR #7413 只是基础,而非完整功能。由此产生的计划列出了后续步骤,其中一些已经实现,包括:

  • 与库无关的值模型。 公开的 ConfigProperties 契约,用于读取文档的任何节点,能够区分缺失、null、存在和类型不匹配的值。
  • 对解析后文档的访问。 只解析一次并保留结果。允许发行版在构建提供程序之前读取文档,包括 SDK 尚未应用的部分。
  • 类型化资源属性。 整数、布尔值和数组以其声明的类型到达 Resource,而不是通过扁平字符串。
  • 默认严格模式。 遵循规范要求,在使用文件时忽略其他 OTEL_* 设置,并研究模型定制如何带回 .NET 风格的分层。
  • 组件。 插件提供程序抽象和内置采样器,以及让文档自身构建提供程序的接线逻辑。
  • 覆盖范围、代码生成与 Parse/Create。 将覆盖范围扩展到规范的其他部分,并从中生成模型。发布规范中的 Parse 和 Create 操作。

对解析后文档的访问能力,是 Elastic 投资这项工作的重要原因。Elastic Distribution of OpenTelemetry .NET (EDOT .NET) 构建在上游 SDK 之上,而发行版需要在 SDK 构建其提供程序之前读取用户的配置。在上游构建这一能力,而不是在 EDOT 内部,意味着每个发行版和应用程序都能获得一致的行为。

一旦核心阶段就位,计划将发布公开 alpha 版本,以便用户开始试验,并在这条通往稳定版本的道路上提供反馈。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 为什么 OpenTelemetry 需要声明式配置
  • 为什么 YAML 不适合 .NET 的 IConfiguration
  • IConfiguration 背后的类型化模型
    • 把 OTel 配置规范做对
  • 这项工作如何在上游进行规划和评审
  • OpenTelemetry .NET 中声明式配置的下一步
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档