Observability:当 YAML 遇上 IConfiguration:OpenTelemetry .NET SDK 的声明式配置

作者:来自 Elastic Steve Gordon

你可以调整采样配置,无须发布新的构建版本,因为首个声明式配置包会将 YAML 解析为强类型模型,再将其映射为 OpenTelemetry .NET SDK 已支持读取的扁平化键。

Elastic 原生支持 OpenTelemetry。通过 OTLP,可将追踪、日志和指标直接发送到 Elasticsearch,无须使用专有 Agent 。了解其工作原理,在云端免费试用,或在本地运行。


不久之后,你就能通过 YAML 文件配置 OpenTelemetry(OTel).NET 客户端,只需调用一次 UseDeclarativeConfiguration(),无须再修改插桩代码。拉取请求(PR)#7413 新增了 OpenTelemetry.Configuration.Declarative 包,它会将声明式配置文档解析为强类型模型,再将其映射到 SDK 已支持读取的 IConfiguration 中。该 PR 的 8,800 行代码中,约三分之二是测试,包括针对解析器和环境变量替换的模糊测试。其中,强类型模型是关键,因为缺少这一层,结构化、与语言无关的文档就难以适配 .NET 扁平化、分层的配置系统。

为什么 OpenTelemetry 需要声明式配置

OTel 规范定义了三种 SDK 配置方式,各有不同的权衡。它们在表达结构的能力、是否需要访问源代码,以及修改后是否需要重新构建等方面存在差异:

配置方式 能否表达结构 是否需要访问代码 修改后是否需要重新部署
环境变量 否,仅支持扁平的键值对 否 否
编程式配置 是 是 是
声明式文件 是 否* 否

*需要添加包引用,并进行一次性注册以启用声明式配置。此后即可修改配置文件,新配置会在应用启动时生效。

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

编程式配置则处于另一个极端。它拥有完整的表达能力,但代价也很高:不仅需要访问和修改源代码,每次有人想调整采样配置时,还必须重新构建和部署。在许多环境中,这样的要求并不现实。生产系统、遗留应用,以及任何无需修改代码即可自动插桩的应用,都面临同样的限制。需要调整遥测配置的人,往往并不是能够发布新构建版本的人。

声明式配置填补了这一空白。它引入了一种基于文件的标准化表示方式,用于表达 Configuration SDK 规范定义的配置数据模型。以下摘自规范的入门示例,展示了这种结构。采样器和导出器位于其所属的追踪提供程序(tracer provider)内部。如果需要添加另一个具有独立批处理设置的导出器,只需在 processors 下再添加一个条目:

yaml 复制代码
`

1.  tracer_provider:
2.    sampler:
3.      parent_based:
4.        root:
5.          always_on:
6.    processors:
7.      - batch:
8.          exporter:
9.            otlp_http:
10.              endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://localhost:4318}/v1/traces

`AI代码解释![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

这样的文档具备编程式配置的表达能力,却无须修改代码。而且,由于格式与编程语言无关,相同结构的文档既能配置 .NET 服务,也能配置 Java 服务。该模型不仅涵盖 SDK 组件,也涵盖插桩,因此其适用范围并不限于原本需要你手动配置的部分。

该规范于 2026 年 3 月宣布进入稳定状态,公告将 .NET 列为仍在开发中的实现之一。.NET 自动插桩项目已有面向零代码场景的独立实现,但 SDK 本身尚不支持声明式配置。将其引入 SDK,就能让所有应用或发行版使用这项能力。Issue #7658 跟踪了相关工作。本文将介绍这些工作的早期进展。

为什么 YAML 难以适配 .NET 的 IConfiguration

读取 YAML 文件是容易的部分。真正的难点在于,.NET 已经拥有一套非常出色的配置系统。

Microsoft.Extensions.Configuration 为每个现代 .NET 应用提供 IConfiguration,即由一组有序配置源组合而成的统一视图。这些配置源通常包括 appsettings.json、特定环境的覆盖配置、环境变量、命令行参数,以及可能存在的机密存储。当两个配置源定义了相同的键时,后面的配置源优先。随后,选项模式会将该视图绑定到强类型类上。.NET 开发人员依赖这种分层机制,也期望任何新的配置机制都能融入其中。

在编写代码之前,我们分析了 SDK 当前使用配置的方式。第一个发现令人鼓舞:没有任何 SDK 组件直接读取环境变量。所有 OTEL_* 值都通过 IConfiguration 读取,这也是为什么你已经可以在 appsettings.json 中设置 OTEL_SERVICE_NAME,而且它能直接生效。大多数选项类型都由 SDK 的选项工厂通过 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 进入 SDK,与 appsettings.json 使用相同的键空间,因此读取某个值时,并没有信息记录它来自哪里。

  • 我们还研究了已有实现。Java SDK 是该规范的参考实现,.NET 自动插桩项目也已经提供了自己的文件式配置。此外,还有一些实际问题,例如这项工作应该放在 SDK 仓库中,还是先从 contrib 仓库开始,以及是否应该引入 YAML 库依赖。

因此,真正的问题是:如何让一份结构化、强类型且与语言无关的文档,融入一个配置方式扁平化、值为字符串且具有分层机制的平台,同时不破坏任何一方的模型? 问题从来都不是_如何解析 YAML_。

IConfiguration 背后的强类型模型

分析形成了一项整体计划:提出 24 个阶段,涵盖基础工作、新的选项类型、组件注册表和重新加载支持。这些内容太多,无法一次性落地,也很难要求维护者仅凭抽象方案就全部认可。所以下一步是有意构建一个最小化的概念验证(PoC),让维护者能够针对具体实现给出反馈。

第一版采用了最直接的方式:自定义 IConfigurationProvider 遍历 YAML,并输出扁平的 OTEL_* 键,供现有 SDK 读取。这种方式能够工作,但直接从 YAML 输出字符串,会将"缺失""null"和"有值"三种状态压缩成两种,而规范要求 Parse 能够区分缺失与 null。

因此,PoC 被改进为两个阶段,这也是 PR #7413 最终交付的结构:

  1. 解析为强类型模型。 将 YAML 解析为不依赖任何 YAML 库的内部记录类型,每个标量都封装为三态值。只有解析器会接触 YAML 库(YamlDotNet),因此未来的其他配置来源,例如开放 Agent 管理协议(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,即可接入。

以下代码添加 OTel SDK,并启用声明式配置和追踪功能:

markdown 复制代码
`

1.  services.AddOpenTelemetry()
2.      .UseDeclarativeConfiguration()
3.      .WithTracing(...);

`AI代码解释

以下基础 YAML 配置添加了两个资源属性,适用于此服务导出的遥测数据:

yaml 复制代码
`

1.  file_format: "1.2"
2.  resource:
3.    attributes:
4.      - name: service.name
5.        value: ${SERVICE_NAME:-my-service}
6.      - name: service.version
7.        value: "1.0.0"

`AI代码解释

我们将其称为_覆盖模式(overlay mode) _。YAML 配置源会追加在所有已注册的配置源之后,因此该文件的优先级高于环境变量和 appsettings.json。文件未设置的键会沿用此前配置源中的值,而后续添加的配置源仍可覆盖该文件,完全符合 .NET 开发人员的预期。

覆盖模式是合适的起点。它无须修改稳定的 SDK 核心。文件的行为与其他配置源一致,因此其优先级不需要额外解释。此外,解析器、模型和替换规则都可以独立接受审查,无须同时引入组件引擎。

不过,该 PR 中最重要的决定,是将强类型模型而非 IConfiguration 作为唯一事实来源。扁平化键只是该模型的一种映射形式,用于接入 SDK 现有的选项处理管道。设计还预留了第二种映射:以规范中的 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"。能够直接表达这种情况的联合类型将随 .NET 11 在 C# 15 中引入,但仍处于预览阶段,而且该包还必须支持面向 netstandard2.0 和 .NET Framework 构建。因此,每个模型字段都使用三态值,并对全部三种状态进行测试,而不仅仅测试正常路径。

  • 采用 YAML 1.2,而非 YAML 1.1。 规范要求使用 YAML 1.2 核心模式。YAML 1.1 的惯用特性包括合并键(<<: *defaults)和更宽松的布尔值写法。该包提供了自己的 YAML 1.2 标量解析器,而不是依赖库的默认行为,并且直接拒绝合并键。引号也具有实际意义:file_format: "1.1" 是字符串,会被接受;而 file_format: 1.1 是数字,会被拒绝。

  • 在正确的阶段执行替换。 环境变量替换作用于解码后的标量值,在处理 YAML 转义之后、解析类型之前执行。这与 YAML 处理模型、Java 实现以及官方配置验证器保持一致。因此,value: ${PORT} 可能解析为数字,而 value: "${PORT}" 始终是字符串,而且替换绝不会向文档中注入结构。

  • 静默忽略就是缺陷。 对于该包无法落实的任何内容,例如被跳过的资源属性、未应用的配置节或重复注册,都会通过 SDK 的自诊断机制报告,而不会悄然丢弃。排查属性缺失问题的人应该能够查明原因,无须阅读源代码。

这项工作如何在上游规划和审查

这一切始于原始 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 的 OpenTelemetry .NET 发行版(EDOT .NET)基于上游 SDK 构建,而发行版需要在 SDK 构建其提供程序之前读取用户配置。将这项能力构建在上游,而不是 EDOT 内部,意味着所有发行版和应用都能获得相同的行为。

核心阶段完成后,计划发布一个公开的 Alpha 版本,让用户开始试用,并为后续稳定版提供反馈。

如果希望跟进进展或分享反馈,总览 Issue #7658 跟踪了计划中的各个阶段及当前进度。首次发布之前,设计仍可调整。无论你是在生产环境中配置 OTel,还是基于它构建发行版,你的反馈都将直接影响设计。

原文:Declarative configuration for the OpenTelemetry .NET SDK | Elastic Observability Labs

相关推荐
Elasticsearch16 小时前
Elasticsearch:为自行管理的 Elastic Stack 自定义 TLS 证书
elasticsearch
Elasticsearch21 小时前
斯坦福《2026 年 AI 指数报告》与金融服务业的安全现实
elasticsearch
Elasticsearch1 天前
Elasticsearch:安装自行管理的 Elastic Stack 集群
elasticsearch
程序员差不多先生1 天前
Anthropic 用 5 个月重写了 Git:当 AI Agent 把版本控制系统逼到墙角
人工智能·git·elasticsearch·deepseek v4 pro·kimi k3·glm 5.3·qwen3.7 max
Elastic 中国社区官方博客1 天前
Jina AI:Elasticsearch 集成
大数据·运维·人工智能·elasticsearch·搜索引擎·全文检索·jina
草莓熊Lotso1 天前
【C++ 基于微服务的即时通讯系统】Elasticsearch 从安装到 C++ 客户端二次封装全落地
linux·网络·数据库·c++·elasticsearch·微服务
.冰块.1 天前
ELK 日志分析系统实验:部署 Elasticsearch 双节点集群实现 Apache 日志采集与 Kibana 可视化
elk·elasticsearch·apache·日志
顧棟2 天前
Elasticsearch Scroll :分页进度到底记在哪?
大数据·elasticsearch·搜索引擎
一枕槐安4872 天前
线上检索延迟 2 秒,我把向量库从 ES 换成了 Chroma
大数据·elasticsearch·搜索引擎