YAML meets IConfiguration: Declarative configuration for the OpenTelemetry .NET SDK
You can change sampling without shipping a new build, because the first declarative configuration package parses YAML into a typed model and projects it into the flat keys the OpenTelemetry .NET SDK already reads.
Elastic speaks OpenTelemetry natively. Send traces, logs, and metrics over OTLP straight into Elasticsearch, no proprietary agents required. See how it fits together, try it for free in the cloud, or run it locally.
Soon you will be able to configure the OpenTelemetry (OTel) .NET Client from a YAML file, with one call to UseDeclarativeConfiguration() and no further changes to your instrumentation code. Pull request (PR) #7413 added a new OpenTelemetry.Configuration.Declarative package that parses a declarative configuration document into a typed model and then projects it into the IConfiguration that the SDK already reads. Roughly two thirds of its 8,800 lines are tests, including fuzz tests for the parser and for environment variable substitution. The typed model is the part that matters, because a structured, language-neutral document doesn’t fit .NET’s flat and layered configuration system without one.
Why OpenTelemetry needs declarative configuration
The OTel specification defines three ways to configure an SDK, and each makes a different tradeoff. They differ in their ability to express structure and the need for source access, along with whether changing them means a new build:
Mechanism | Expresses structure | Needs code access | Needs redeploy to change |
Environment variables | No, flat key/value only | No | No |
Programmatic | Yes | Yes | Yes |
Declarative file | Yes | No* | No |
* Needs a package reference and one time registration to opt-in to declarative config. Thereafter, the config file can change and is applied at application launch.
Environment variables are the mechanism that most people encounter first. They’re portable, available everywhere and they ask nothing of your application code. Setting OTEL_SERVICE_NAME or OTEL_EXPORTER_OTLP_ENDPOINT works nicely because those values are single strings. Where they fall down is structural configuration, such as defining a sampler and its related options, or two exporters with different batching behavior. The flat key/value model has no way to express that shape, so you either go without the capability or smuggle structure into a string, and neither option ages well.
Programmatic configuration sits at the other end. It’s completely expressive, but it asks a lot in return, including access to the source and the ability to change it, in addition to a rebuild and redeploy every time somebody wants to adjust sampling. In many environments, that isn’t a reasonable ask. Production systems and legacy applications share the same constraint, as does anything instrumented automatically without touching code. The people who need to tune the telemetry are frequently not those who can ship a new build.
Declarative configuration fills that gap. It introduces a standardized, file-based representation of a configuration data model, defined in the Configuration SDK specification. This excerpt from the spec’s getting-started example shows the shape. The sampler and the exporter sit inside the tracer provider that they belong to, and a second exporter with its own batching settings would be one more entry under processors:
tracer_provider:
sampler:
parent_based:
root:
always_on:
processors:
- batch:
exporter:
otlp_http:
endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://localhost:4318}/v1/tracesA document like this offers the expressiveness of programmatic configuration without requiring code changes. And, because the format is language-agnostic, the same shape of document configures a .NET service and a Java one. The model also covers instrumentation alongside SDK components, so its reach isn’t limited to the parts that you would otherwise wire up yourself.
The specification was declared stable in March 2026, and the announcement listed .NET among the implementations still in development. The .NET auto-instrumentation project has its own implementation for zero-code setups, but the SDK itself doesn’t support declarative configuration yet. Bringing it into the SDK makes it available to any application or distribution. Issue #7658 tracks that work. In this post, we cover the early stages of those efforts.
Why YAML doesn’t fit .NET’s IConfiguration
Reading a YAML file is the easy part. The hard part is that .NET already has a configuration system, and it’s a very good one.
Microsoft.Extensions.Configuration gives every modern .NET application an IConfiguration; that is, a single view composed from an ordered list of sources, typically appsettings.json, an environment-specific overlay, environment variables, command-line arguments, and perhaps a secrets store. When two sources define the same key, the later source wins. The options pattern then binds that view onto typed classes. .NET developers rely on this layering, and they expect any new configuration mechanism to join in.
Before writing any code, we analyzed how the SDK consumes configuration today. The first finding was encouraging. No SDK component reads environment variables directly. Every OTEL_* value is read through IConfiguration, which is why you can already set OTEL_SERVICE_NAME in appsettings.json and it simply works. Most option types are built from IConfiguration through the SDK's options factory. So a declarative file could plausibly slot in as one more configuration source, and the existing SDK would read it unchanged.
Projecting a declarative file into IConfiguration would only get part of the way, for five reasons:
YAML is hierarchical, with snake_case keys and nested components. The SDK's options are flat and keyed like environment variables (
OTEL_BSP_SCHEDULE_DELAY). Every value inIConfigurationis a string, sovalue: 3andvalue: "3", an integer and a string in the configuration schema, become indistinguishable. An absent property and one explicitly set tonullmean different things in the schema, and ordinary configuration access collapses them.
Today, a sampler, exporter, or propagator exists because someone wrote
SetSampler(new TraceIdRatioBasedSampler(0.5))orAddOtlpExporter(). Where the SDK does turn a string into a component, as withOTEL_TRACES_SAMPLER, it uses a handwritten switch statement that third parties cannot extend. Some spec-defined variables, such asOTEL_PROPAGATORSand theOTEL_*_EXPORTERfamily, aren’t implemented at all. A document that says "use aparent_basedsampler wrappingtrace_id_ratio_based" needs a registry that maps names to components, and the SDK has none.
The SDK resolves options through
IOptionsMonitor<T>, which looks like latent reload support but isn't. That choice was made to fix a dependency-injection lifetime problem. Nothing subscribes to change notifications, and many values are captured into long-lived state at construction. That matters for the longer-term goal of dynamic control, and it means that reload was explicitly out of scope for the first steps.
The spec says that when
OTEL_CONFIG_FILEis set, "all other environment variables besides those referenced in the configuration file for environment variable substitution MUST be ignored," because "there is no intuitive way to merge the flat environment variable scheme with the structured file configuration scheme in all cases." A .NET developer's instinct is the opposite: the file should be another layer. Because environment variables reach the SDK throughIConfigurationin the same key space asappsettings.json, nothing when a value is read records where it came from.
We also looked at prior art. The Java SDK is the reference implementation of the specification, and the .NET auto-instrumentation project already ships its own file-based configuration. There were practical questions, too, such as whether the work belonged in the SDK repository or started in contrib, and whether to take a dependency on a YAML library at all.
So the question was: How does a structured, typed, language-neutral document fit into a platform whose configuration is flat, string-typed, and layered, without breaking either model? The question was never How do we parse YAML?
A typed model behind IConfiguration
The analysis produced a broad plan: 24 proposed phases covering foundation work, new option types, a component registry, and reload support. That was too much to land in one go and too much to ask maintainers to agree to in the abstract. The next step was a deliberately minimal proof of concept (PoC) that the maintainers could react to.
The first cut took the obvious route. A custom IConfigurationProvider walked the YAML and emitted flat OTEL_* keys for the existing SDK to read. It worked, but emitting strings straight from the YAML collapsed absent, null, and present into two states, and the spec requires Parse to distinguish missing from null.
So the PoC was refined into two stages, and that shape is what PR #7413 shipped:
Parse into a typed model. The YAML is parsed into internal records that carry no YAML dependency, with every scalar wrapped in a three-state value. Only the parser touches the YAML library (YamlDotNet), so a future source, such as an Open Agent Management Protocol (OpAMP) push, could build the same model.
Project into
IConfiguration. The model is projected into the flatOTEL_*keys that the SDK already understands, through a configuration provider.disabled: truebecomesOTEL_SDK_DISABLED=true, and every existing read site picks it up with no changes to the core SDK.
The PR added a new package, OpenTelemetry.Configuration.Declarative, so applications never take on its YAML dependency without opting in. It supported a deliberately small slice of the schema: file_format, disabled, resource.attributes, and resource.attributes_list, plus the spec's environment-variable substitution. Wiring it up takes one call using UseDeclarativeConfiguration on the OpenTelemetryBuilder.
The following code adds the OTel SDK with declarative configuration and tracing enabled:
services.AddOpenTelemetry()
.UseDeclarativeConfiguration()
.WithTracing(...);The following basic YAML configuration adds two resource attributes that apply to telemetry data exported from this service:
file_format: "1.2"
resource:
attributes:
- name: service.name
value: ${SERVICE_NAME:-my-service}
- name: service.version
value: "1.0.0"We called this overlay mode. The YAML source is appended after every source already registered, so the file takes precedence over environment variables and appsettings.json. Keys that the file doesn’t set fall through to whatever was there before, and sources added later still override the file, exactly as .NET developers expect.
Overlay mode was the right place to start. It needed no changes to the stable core SDK. The file behaved like any other configuration source, so its precedence didn’t need an explanation. Plus, the parser and the model could be reviewed on their own, without a component engine attached, as could the substitution rules.
The most important decision in that PR, though, was making the typed model, rather than IConfiguration, the source of truth. Flat keys are one projection of that model, feeding the SDK's existing options pipeline. The design also anticipated a second projection: a schemaless view of each node, the spec's ConfigProperties, for building components. This was necessary because IConfiguration cannot represent absent versus null or polymorphic component selection, nor can it represent arbitrary nesting. Only the first existed in PR #7413, but the second was designed in from the start.
The PR was also candid about what overlay mode didn’t not do, such as:
Resource attributes were string-only, because typed values cannot survive projection into the
OTEL_RESOURCE_ATTRIBUTESstring.Sections such as
tracer_providerwere logged and not applied.The file was read once at start-up.
There was no strict mode. Standard
IConfigurationordering applied, so leftoverOTEL_*variables still took effect for any setting that the file left out.
The design is now heading somewhere different with that last gap. Take a partially written file, for example, which is the normal case during a migration. It sets no sampler, but the deployment still carries OTEL_TRACES_SAMPLER=always_off from before. Under overlay mode, that leftover variable quietly applies, so two services with identical files can behave differently, which no other OTel SDK would do. The spec's answer is that anyone who needs to combine sources should customize the configuration model, not merge keys.
With the initial phases landed, the pragmatic choice is to align with the spec and make strict behavior the default: when a document is in use, it’s the source of OTel settings. That closes off the partial-file problem entirely. The .NET-style layering that developers value isn’t being ruled out. We’ll investigate later how .NET might support model customization, with explicit, opt-in merge rules. Either way, the typed model at the center of PR #7413 makes the change possible without starting again.
Getting the OTel configuration spec right
Configuration is the first thing that an SDK touches and the last thing that anyone suspects when telemetry goes missing, so even small deviations carry risk. Much of the first PR went on details that a casual reading of the spec skips over. Roughly two thirds of its 8,800 lines are tests, including fuzz tests for the parser and for substitution.
Three states, everywhere. C# nullable annotations cannot distinguish "absent" from "present and
null". Union types, which could model this directly, are coming in C# 15 with .NET 11, but they’re still in preview, and the package also has to build fornetstandard2.0and .NET Framework. So every model field is a three-state value, and every field is tested in all three states, not only the happy path.
YAML 1.2, not YAML 1.1. The spec requires the YAML 1.2 core schema. YAML 1.1 habits include merge keys (
<<: *defaults) and a looser set of Boolean spellings. The package ships its own YAML 1.2 scalar resolver rather than relying on library defaults, and it rejects merge keys outright. Quoting is significant:file_format: "1.1"is a string and is accepted, whilefile_format: 1.1is a number and is rejected.
Substitution happens at the right moment. Environment-variable substitution runs on the decoded scalar value, after YAML escapes are processed and before types are resolved. That matches the YAML processing model and the Java implementation, in addition to the official configuration validator. So
value: ${PORT}may resolve to a number, whilevalue: "${PORT}"is always a string, and substitution can never inject structure into a document.
Silence is a bug. Anything that the package couldn’t honor, such as skipped resource attributes, unapplied sections, or repeated registrations, is reported through the SDK's self-diagnostics rather than quietly dropped. Someone debugging a missing attribute should be able to find out why without reading the source.
How this work is planned and reviewed upstream
This all started with a discussion in the original issue, as well as an analysis of the current configuration landscape within the SDK and the proposed issues. It didn’t start with code. Even now, the design continues to evolve. I track it in a local repository, assisted by AI agents. The repo holds a high-level plan and a decision record in which every significant choice is dated with its context and consequences. It also holds a specification for each phase, written before implementation starts. That level of detail is too much for general consumption, so the high-level phases are published in the public meta issue.
I've planned the work in small, deliverable units to reduce the review burden and to keep code merging. This approach depends on a close working relationship with Martin Costello, one of the OTel .NET maintainers, who has reviewed each phase as it has landed. His feedback has repeatedly improved the design. After each phase lands, I reevaluate the phases that follow, fold in what review has taught us, and adjust direction where needed, recording the change in the decision record so that the plan never quietly drifts.
What’s next for declarative configuration in OpenTelemetry .NET
The package hasn’t shipped a release yet, and PR #7413 is the foundation rather than the feature. The plan that came out of it sets out the next steps, some of which are already implemented, including:
A library-neutral value model. The public
ConfigPropertiescontract for reading any node of the document, distinguishing absent,null, present, and type-mismatched values.Access to the parsed document. Parse once, and keep the result. Let distributions read it before providers are built, including sections that the SDK doesn’t yet apply.
Typed resource attributes. Integers, Booleans, and arrays reach the
Resourcewith their declared types, rather than through a flat string.Strict mode by default. Honor the spec's requirement that other
OTEL_*settings are ignored when a file is used, and investigate how model customization could bring back .NET-style layering.Components. A plugin provider abstraction and the built-in samplers, along with the wiring that lets the document itself build providers.
Coverage, generation, and
Parse/Create. Extend coverage to the rest of the schema, and generate the model from it. Publish the spec'sParseandCreateoperations.
Access to the parsed document is a large part of why Elastic is investing in this work. The Elastic Distribution of OpenTelemetry .NET (EDOT .NET) builds on the upstream SDK, and a distribution needs to read the user's configuration before the SDK builds its providers. Building that capability upstream, rather than inside EDOT, means that every distribution and application gets the same behavior.
Once the core phases are in place, the plan is to ship a public alpha, so users can start experimenting and give feedback on the path to a stable release.
If you want to follow along or share your feedback, the meta issue #7658 tracks the planned phases and current progress. The design is still open to change before the first release, and feedback from anyone who configures OTel in production, or builds a distribution on top of it, is exactly what shapes it.
How helpful was this content?




