Last9 named a Gartner Cool Vendor in AI for SRE Observability for 2025! Read more →
Last9

OTel Updates: Declarative Config — A Steadier Way to Configure OpenTelemetry SDKs

Declarative Config brings structure to OTel SDKs with clean, rule-based settings that stay consistent across every environment.

Nov 3rd, ‘25
OTel Updates: Declarative Config — A Steadier Way to Configure OpenTelemetry SDKs
See How Last9 Works

Unified observability for all your telemetry. Open standards. Simple pricing.

Talk to an Expert

TL;DR:

  • This update adds a clear, typed configuration model for OTel SDKs with support for rule-based sampling, processor chains, and resource configuration.
  • It provides a single source of truth that stays predictable across dev, staging, and production.

Introduction

Application configs change over time, often in small ways that are easy to miss. They may start simple — a few environment variables, one exporter, nothing unexpected. As your instrumentation grows, you add rules for filtering health check spans, adjust sampling based on attributes, or introduce environment-specific resource settings. Each change makes sense on its own.

But months later, the picture can look different across dev, staging, and production. Behaviors don’t always match, and understanding why takes a bit of digging. You’re not debugging code as much as tracing how the configuration evolved:

“Why is sampling different here?”
“Was this processor meant to behave this way?”
“Did this batch setting change recently, or was it always like this?”

Declarative Config is OpenTelemetry’s way of bringing more clarity to situations like these. It doesn’t add new processors or exporters. Instead, it offers a steadier, more structured way to describe SDK behavior so configuration stays predictable even as your telemetry needs grow.

A Different Way of Thinking About Configuration

For years, OpenTelemetry relied on environment variables for configuration. Simple, universal, easy to parse. But environment variables are flat strings — they can't express structure, conditionals, or complex rules.

Back in August 2020, someone opened what seemed like a straightforward feature request: Can we drop health check spans from traces? Those /actuator and /health endpoints were filling traces with noise and driving up costs.

The answer back then? There wasn't a clean way to do it. You can't express "if url.path matches this pattern, drop the span" as an environment variable. You'd need structured conditionals, not flat key-value pairs.

Five years later, that request is finally solved. Not through a workaround, but by rethinking how OpenTelemetry configuration works entirely.

Declarative Config takes a step back and asks: What if we separated the description of what the SDK should do from the mechanics of how environment variables wire everything together?

In the Declarative model, you define what you want — samplers, processors, exporters, resource detection — as clean, structured components in a YAML file. Here's the style of structure Declarative Config encourages:

file_format: '1.0-rc.1'
resource:
  attributes_list: ${OTEL_RESOURCE_ATTRIBUTES}

tracer_provider:
  processors:
    - batch:
  exporter:
    otlp_http:
      endpoint: ${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:-http://localhost:4318}/v1/traces

There's a noticeable shift here. You're no longer inventing string formats to represent structured data. You're describing intent in a format that can actually express complexity. As your instrumentation needs grow, this format stays stable — where environment-variable-based config tends to drift.

💡
Also, read our OTel update on consistent probability sampling, which explains how OTel keeps trace decisions aligned across services.

Why This Matters for Day-to-Day Work

If you've ever compared configurations across environments to spot what differs, you'll appreciate what this format solves. Declarative Config removes a lot of the "where did this setting come from?" mystery by keeping everything explicit and in one place.

Instead of figuring out whether a missing processor was intentionally removed or accidentally lost during a copy-paste, you can see the source of truth. If someone updates the batch processor settings or adds a sampling rule, that change is visible in the config file. No silent inconsistencies between environments. No version where staging quietly diverges from production.

It's especially helpful when multiple teams touch instrumentation. The YAML structure creates guardrails: you can see which part changed without reading through dozens of environment variable definitions scattered across deployment scripts.

Solving the Health Check Problem

Here's what solving that five-year-old filtering problem looks like in practice:

file_format: '1.0-rc.1'
tracer_provider:
  sampler:
    rule_based_routing:
      fallback_sampler:
        always_on:
      span_kind: SERVER
      rules:
        - action: DROP
          attribute: url.path
          pattern: /actuator.*
        - action: DROP
          attribute: url.path
          pattern: /health.*

The sampler checks each span's url.path attribute against your patterns. If it matches, the span gets dropped before it leaves your service. No backend costs, no trace noise.

You can stack as many rules as you need. Match on any span attribute — http.method, http.status_code, custom attributes. The structure supports it without inventing new syntax.

This is the type of configuration that environment variables fundamentally couldn't express. Not because it's complex, but because it requires structure: conditions, patterns, actions.

How This Fits Across Languages

The configuration file is language-agnostic. Write it once, use it across Java, Go, JavaScript, PHP — any SDK that supports declarative configuration.

The only exceptions are language-specific instrumentation settings. For example, if you're converting a Java system property like otel.instrumentation.spring-batch.experimental.chunk.new-trace, you'd write:

file_format: '1.0-rc.1'
instrumentation/development:
  java:
    spring_batch:
      experimental:
        chunk:
          new_trace: true

Pattern: remove otel.instrumentation, split on ., convert - to _. Everything else — traces, metrics, logs, exporters — stays in the shared config that works across languages.

The important thing is that nothing breaks. Declarative Config doesn't replace environment variables; it builds on top of them. You can start adopting it gradually, service by service, without rewriting everything.

💡
If you’re shaping clearer configurations, this guide on practical naming patterns for spans, attributes, and metrics helps you keep things consistent!

Move from Environment Variables (without breaking everything)

If you're already using environment variables, there's a migration config template that references them using ${VAR_NAME} syntax.

Your existing setup keeps working. The file just gives you a place to add the complex configurations that environment variables couldn't handle:

file_format: '1.0-rc.1'
resource:
  attributes_list: ${OTEL_RESOURCE_ATTRIBUTES}

detection/development:
  detectors:
    - service:

tracer_provider:
  processors:
    - batch:
  exporter:
    otlp_http:
      endpoint: ${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:-http://localhost:4318}/v1/traces

meter_provider:
  readers:
    - periodic:
      exporter:
        otlp_http:
          endpoint: ${OTEL_EXPORTER_OTLP_METRICS_ENDPOINT:-http://localhost:4318}/v1/metrics

Notice the ${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:-http://localhost:4318} syntax? That's environment variable substitution with defaults. If the env var isn't set, it falls back to the default value. Took the community months to agree on this format, but it works consistently now.

The OpenTelemetry project provides three starting points:

  1. Basic config — minimal setup for fresh starts
  2. Migration config — references existing environment variables
  3. Kitchen sink — every available option with documentation as comments

Pick migration config if you're already running OTel with environment variables. Pick basic config if you're setting this up fresh.

Where This Works

Java

Fully supported starting with agent 2.21+. Most instrumentations work. Some edge cases are still being sorted through 2026.

Enable it with:

-Dotel.experimental.config.file=/path/to/otel-config.yaml

If you have questions or feedback, reach out #otel-java on CNCF Slack.

JavaScript

The implementation is under active development. A new opentelemetry-configuration package handles both environment variables and declarative config. With this approach, you don't need to change instrumentation code when switching between configuration methods — the package returns the same configuration model for both.

Currently, this configuration package is being added to other instrumentation packages. If you have questions, reach out in #otel-js on CNCF Slack.

PHP

Partially compliant. You can initialize from a config file. Set OTEL_EXPERIMENTAL_CONFIG_FILE=/path/to/config.yaml and you're running.

For help or feedback, reach out in #otel-php on CNCF Slack.

Go

Partial implementation. Each schema version has its own package directory. Import go.opentelemetry.io/contrib/otelconf/v0.3.0 for version 0.3.0 support. You can find all available versions in the package index.

If you have questions, reach out #otel-go on CNCF Slack.

What's Still Experimental

Declarative configuration is marked experimental for a reason. It works, but expect changes before it hits stable.

Some things aren't fully implemented yet across all SDKs:

  • Certain processors in some languages
  • All exporter types everywhere
  • Advanced sampling strategies

Check the Java unsupported features list to see what's still in progress. Track broader implementation status on the language tracking issue.

The OpenTelemetry team is keenly interested in user feedback as they continue to develop and refine these features. Found something broken or missing? Share your feedback in #otel-config-file on CNCF Slack.

💡
Also, check out our update on how Prometheus 3.0 improves resource-attribute handling for OTel metrics.

How to Start

You don't have to migrate your entire telemetry stack to try Declarative Config. Start with one service. Enable the config file. Watch how it behaves. The benefits show up early — clearer intentions, cleaner diffs, fewer surprises.

  1. Confirm your language has support (Java works, others check the compliance matrix)
  2. Grab the migration config if you're on environment variables, basic config if starting fresh
  3. Add one simple rule to test (like dropping a span pattern)
  4. Validate the syntax (no official validation tool for SDK config yet, but the YAML structure itself will catch basic errors)
  5. Deploy to a non-critical service first
  6. Watch for errors, adjust, and roll out wider

And since environment variables remain supported, you're not taking on risk. You're adding structure on top.

If You're Sending Telemetry to Last9

A light note:

Declarative Config makes it easier to keep your instrumentation behavior steady across environments. When everything is defined through structured YAML and validated before deployment, you avoid the usual "why is this sampler different only in staging?" questions that come up during debugging.

Your SDKs stay aligned, and telemetry behaves predictably no matter where it runs. If you need a backend that handles high-cardinality attributes from your filtered traces without choking, Last9 is built for that. No attribute dropping, no query timeouts — just full searchability. Get started in 5 minutes.

Resources

To learn more about declarative configuration:

Authors
Anjali Udasi

Anjali Udasi

Helping to make the tech a little less intimidating. I

Contents

Do More with Less

Unlock unified observability and faster triaging for your team.