
作者来自 Elastic Steve Gordon你可以调整采样配置无须发布新的构建版本因为首个声明式配置包会将 YAML 解析为强类型模型再将其映射为 OpenTelemetry .NET SDK 已支持读取的扁平化键。Elastic 原生支持 OpenTelemetry。通过 OTLP可将追踪、日志和指标直接发送到 Elasticsearch无须使用专有 Agent。了解其工作原理在云端免费试用或在本地运行。不久之后你就能通过 YAML 文件配置 OpenTelemetryOTel.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下再添加一个条目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 服务。该模型不仅涵盖 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 通过IOptionsMonitorT解析选项这看起来像是具备潜在的重新加载支持实际上却不是。采用它是为了解决依赖注入中的生命周期问题。没有组件订阅更改通知而且许多值在构造时就被保存到长期存在的状态中。这会影响实现动态控制的长期目标也意味着早期工作明确不包含重新加载功能。规范规定设置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 最终交付的结构解析为强类型模型。将 YAML 解析为不依赖任何 YAML 库的内部记录类型每个标量都封装为三态值。只有解析器会接触 YAML 库YamlDotNet因此未来的其他配置来源例如开放 Agent 管理协议OpAMP推送也可以构建相同的模型。映射到IConfiguration。通过配置提供程序将模型映射为 SDK 已理解的扁平OTEL_*键。例如disabled: true会转换为OTEL_SDK_DISABLEDtrue现有的所有配置读取位置都能直接获取该值无须修改 SDK 核心。该 PR 新增了OpenTelemetry.Configuration.Declarative包因此应用只有主动选择使用它时才会引入其 YAML 依赖。它有意只支持配置模式中的一小部分file_format、disabled、resource.attributes和resource.attributes_list以及规范定义的环境变量替换功能。只需在OpenTelemetryBuilder上调用一次UseDeclarativeConfiguration即可接入。以下代码添加 OTel SDK并启用声明式配置和追踪功能services.AddOpenTelemetry() .UseDeclarativeConfiguration() .WithTracing(...);以下基础 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 现有的选项处理管道。设计还预留了第二种映射以规范中的ConfigProperties提供每个节点的无模式视图用于构建组件。这是必要的因为IConfiguration无法区分缺失与null无法表示多态组件选择也无法表示任意嵌套结构。PR #7413 仅实现了第一种映射但第二种从一开始就已纳入设计。该 PR 也明确说明了覆盖模式的局限例如资源属性仅支持字符串因为带类型的值无法在映射为OTEL_RESOURCE_ATTRIBUTES字符串后保留其类型。tracer_provider等配置节只会被记录到日志中不会实际应用。文件仅在启动时读取一次。没有严格模式。系统遵循标准的IConfiguration顺序因此对于文件中未指定的设置残留的OTEL_*变量仍会生效。针对最后这一缺口设计方向正在调整。以迁移过程中常见的未完成配置文件为例文件没有设置采样器但部署环境中仍保留着之前的OTEL_TRACES_SAMPLERalways_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