From 07592829bb471d9fff13ee6ac9bb8840aee9b2e1 Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Thu, 27 Aug 2026 17:27:59 -0400 Subject: [PATCH 1/6] Add stable OTel environment attribute mapping --- lib/datadog/core/configuration/settings.rb | 5 ++ .../core/configuration/settings_spec.rb | 54 +++++++++++++++++-- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/lib/datadog/core/configuration/settings.rb b/lib/datadog/core/configuration/settings.rb index 2935fa0a5f..55fc3d47dd 100644 --- a/lib/datadog/core/configuration/settings.rb +++ b/lib/datadog/core/configuration/settings.rb @@ -744,6 +744,7 @@ def initialize(*_) # Parses a string containing key-value pairs and returns a hash. # Key-value pairs are delimited by ':' OR `=`, and pairs are separated by whitespace, comma, OR BOTH. result = {} + named_environment = nil unless env_value.nil? || env_value.empty? # falling back to comma as separator sep = env_value.include?(",") ? "," : " " @@ -757,6 +758,9 @@ def initialize(*_) val ||= "" # maps OpenTelemetry semantic attributes to Datadog tags key = case key.downcase + when "deployment.environment.name" + named_environment = val + next when "deployment.environment" then "env" when "service.version" then "version" when "service.name" then "service" @@ -765,6 +769,7 @@ def initialize(*_) result[key] = val unless key.empty? end end + result["env"] = named_environment unless named_environment.nil? result end o.setter do |new_value, old_value| diff --git a/spec/datadog/core/configuration/settings_spec.rb b/spec/datadog/core/configuration/settings_spec.rb index bee4282f8c..0057123e6e 100644 --- a/spec/datadog/core/configuration/settings_spec.rb +++ b/spec/datadog/core/configuration/settings_spec.rb @@ -1382,12 +1382,15 @@ around do |example| ClimateControl.modify( "OTEL_RESOURCE_ATTRIBUTES" => otel_tags, - Datadog::Core::Environment::Ext::ENV_TAGS => dd_tags + Datadog::Core::Environment::Ext::ENV_TAGS => dd_tags, + Datadog::Core::Environment::Ext::ENV_ENVIRONMENT => dd_env ) do example.run end end + let(:dd_env) { nil } + context "is defined and DD_TAGS is set" do let(:otel_tags) { "deployment.environment=prod,service.name=bleh,service.version=1.0,mkey=val1" } let(:dd_tags) { "service:moon-test,version:v42069,env:prod,token:gg" } @@ -1395,9 +1398,54 @@ end context "is defined and DD_TAGS is not set" do - let(:otel_tags) { "deployment.environment=prod,service.name=bleh,service.version=1.0,mkey=val1" } let(:dd_tags) { nil } - it { is_expected.to include("env" => "prod", "service" => "bleh", "version" => "1.0", "mkey" => "val1") } + + context "with only deployment.environment.name" do + let(:otel_tags) { "deployment.environment.name=stable" } + + it { is_expected.to eq("env" => "stable") } + end + + context "with only deployment.environment" do + let(:otel_tags) { "deployment.environment=legacy" } + + it { is_expected.to eq("env" => "legacy") } + end + + context "with deployment.environment.name before deployment.environment" do + let(:otel_tags) { "deployment.environment.name=stable,deployment.environment=legacy" } + + it { is_expected.to eq("env" => "stable") } + end + + context "with deployment.environment before deployment.environment.name" do + let(:otel_tags) { "deployment.environment=legacy,deployment.environment.name=stable" } + + it { is_expected.to eq("env" => "stable") } + end + + context "with both deployment environment attributes and an unrelated attribute" do + let(:otel_tags) do + "deployment.environment.name=stable,deployment.environment=legacy,custom.attribute=value" + end + + it "consumes the deployment environment attributes and preserves the unrelated attribute" do + is_expected.to eq("env" => "stable", "custom.attribute" => "value") + end + end + + context "with service, version, and an unrelated attribute" do + let(:otel_tags) { "deployment.environment=prod,service.name=bleh,service.version=1.0,mkey=val1" } + + it { is_expected.to include("env" => "prod", "service" => "bleh", "version" => "1.0", "mkey" => "val1") } + end + + context "and DD_ENV is set" do + let(:otel_tags) { "deployment.environment.name=stable,deployment.environment=legacy" } + let(:dd_env) { "datadog" } + + it { is_expected.to eq("env" => "datadog") } + end end context "is not defined and DD_TAGS is not set" do From b9a3a6aefc5704d503868e575ee87e218d157214 Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Thu, 10 Sep 2026 11:25:20 -0400 Subject: [PATCH 2/6] Emit canonical OTel environment resource attributes --- .../opentelemetry/signal_configuration.rb | 16 +++++- .../core/configuration/settings_spec.rb | 2 +- spec/datadog/opentelemetry/logs_spec.rb | 49 ++++++++++++++++++- spec/datadog/opentelemetry/metrics_spec.rb | 38 +++++++++++++- 4 files changed, 98 insertions(+), 7 deletions(-) diff --git a/lib/datadog/opentelemetry/signal_configuration.rb b/lib/datadog/opentelemetry/signal_configuration.rb index 109ce0141e..4d4bfac57f 100644 --- a/lib/datadog/opentelemetry/signal_configuration.rb +++ b/lib/datadog/opentelemetry/signal_configuration.rb @@ -11,11 +11,22 @@ module SignalConfiguration def create_resource resource_attributes = {} + tagged_environment = nil + named_environment = nil + legacy_environment = nil @settings.tags&.each do |key, value| # steep:ignore otel_key = case key when "service" then "service.name" - when "env" then "deployment.environment" + when "env" + tagged_environment = value + next + when "deployment.environment.name" + named_environment = value + next + when "deployment.environment" + legacy_environment = value + next when "version" then "service.version" else key end @@ -23,7 +34,8 @@ def create_resource end resource_attributes["service.name"] = @settings.service_without_fallback || resource_attributes["service.name"] || Datadog::Core::Environment::Ext::FALLBACK_SERVICE_NAME # steep:ignore - resource_attributes["deployment.environment"] = @settings.env if @settings.env # steep:ignore + environment = @settings.env || tagged_environment || named_environment || legacy_environment # steep:ignore + resource_attributes["deployment.environment.name"] = environment if environment resource_attributes["service.version"] = @settings.version if @settings.version # steep:ignore hostname = Datadog::Core::Environment::Socket.resolved_hostname(@settings) # steep:ignore diff --git a/spec/datadog/core/configuration/settings_spec.rb b/spec/datadog/core/configuration/settings_spec.rb index 0057123e6e..fee797fd54 100644 --- a/spec/datadog/core/configuration/settings_spec.rb +++ b/spec/datadog/core/configuration/settings_spec.rb @@ -1383,7 +1383,7 @@ ClimateControl.modify( "OTEL_RESOURCE_ATTRIBUTES" => otel_tags, Datadog::Core::Environment::Ext::ENV_TAGS => dd_tags, - Datadog::Core::Environment::Ext::ENV_ENVIRONMENT => dd_env + Datadog::Core::Environment::Ext::ENV_ENVIRONMENT => dd_env, ) do example.run end diff --git a/spec/datadog/opentelemetry/logs_spec.rb b/spec/datadog/opentelemetry/logs_spec.rb index 335cd690e8..935f7ff903 100644 --- a/spec/datadog/opentelemetry/logs_spec.rb +++ b/spec/datadog/opentelemetry/logs_spec.rb @@ -91,6 +91,49 @@ describe "Resource Attributes" do subject(:resource_attributes) { attributes } + + context "with deployment.environment.name provided by OpenTelemetry resource attributes" do + let(:env_overrides) { {"OTEL_RESOURCE_ATTRIBUTES" => "deployment.environment.name=production"} } + + it "emits the canonical deployment environment attribute" do + expect(resource_attributes["deployment.environment.name"]).to eq("production") + expect(resource_attributes).not_to have_key("deployment.environment") + end + end + + context "with programmatic deployment environment tags" do + let(:configuration) do + proc do |c| + c.tags = { + "deployment.environment" => "legacy", + "deployment.environment.name" => "stable", + } + end + end + + it "prioritizes the stable attribute when it follows the legacy attribute" do + expect(resource_attributes["deployment.environment.name"]).to eq("stable") + expect(resource_attributes).not_to have_key("deployment.environment") + end + + context "when DD_ENV is set and the legacy attribute follows the stable attribute" do + let(:env_overrides) { {"DD_ENV" => "datadog"} } + let(:configuration) do + proc do |c| + c.tags = { + "deployment.environment.name" => "stable", + "deployment.environment" => "legacy", + } + end + end + + it "prioritizes DD_ENV" do + expect(resource_attributes["deployment.environment.name"]).to eq("datadog") + expect(resource_attributes).not_to have_key("deployment.environment") + end + end + end + context "with service metadata provided by environment variables" do let(:env_overrides) do { @@ -105,9 +148,10 @@ expect(resource_attributes).to include( "service.name" => "custom-service", "service.version" => "2.0.0", - "deployment.environment" => "production", + "deployment.environment.name" => "production", "host.name" => Datadog::Core::Environment::Socket.hostname, ) + expect(resource_attributes).not_to have_key("deployment.environment") end end @@ -178,11 +222,12 @@ expect(resource_attributes).to include( "service.name" => "test-service", "service.version" => "1.0.0", - "deployment.environment" => "test", + "deployment.environment.name" => "test", "host.name" => "myhost", "team" => "backend", "region" => "us-east-1", ) + expect(resource_attributes).not_to have_key("deployment.environment") end end diff --git a/spec/datadog/opentelemetry/metrics_spec.rb b/spec/datadog/opentelemetry/metrics_spec.rb index 137faae2c4..0314a9cd2b 100644 --- a/spec/datadog/opentelemetry/metrics_spec.rb +++ b/spec/datadog/opentelemetry/metrics_spec.rb @@ -154,6 +154,38 @@ def setup_metrics(env_overrides = {}, &config_block) end describe "Resource Attributes" do + it "emits the canonical deployment environment attribute from OpenTelemetry resource attributes" do + setup_metrics("OTEL_RESOURCE_ATTRIBUTES" => "deployment.environment.name=production") + + expect(attributes["deployment.environment.name"]).to eq("production") + expect(attributes).not_to have_key("deployment.environment") + end + + it "prioritizes a programmatic stable attribute when the legacy attribute follows it" do + setup_metrics do |c| + c.tags = { + "deployment.environment.name" => "stable", + "deployment.environment" => "legacy", + } + end + + expect(attributes["deployment.environment.name"]).to eq("stable") + expect(attributes).not_to have_key("deployment.environment") + end + + it "prioritizes the configured environment when the stable attribute follows the legacy attribute" do + setup_metrics do |c| + c.env = "datadog" + c.tags = { + "deployment.environment" => "legacy", + "deployment.environment.name" => "stable", + } + end + + expect(attributes["deployment.environment.name"]).to eq("datadog") + expect(attributes).not_to have_key("deployment.environment") + end + it "includes service name, version, and environment from Datadog config" do setup_metrics( "DD_SERVICE" => "custom-service", @@ -164,7 +196,8 @@ def setup_metrics(env_overrides = {}, &config_block) expect(attributes["service.name"]).to eq("custom-service") expect(attributes["service.version"]).to eq("2.0.0") - expect(attributes["deployment.environment"]).to eq("production") + expect(attributes["deployment.environment.name"]).to eq("production") + expect(attributes).not_to have_key("deployment.environment") expect(attributes["host.name"]).to eq(Datadog::Core::Environment::Socket.hostname) end @@ -203,7 +236,8 @@ def setup_metrics(env_overrides = {}, &config_block) expect(attributes["service.name"]).to eq("test-service") expect(attributes["service.version"]).to eq("1.0.0") - expect(attributes["deployment.environment"]).to eq("test") + expect(attributes["deployment.environment.name"]).to eq("test") + expect(attributes).not_to have_key("deployment.environment") expect(attributes["host.name"]).to eq("myhost") expect(attributes["team"]).to eq("backend") expect(attributes["region"]).to eq("us-east-1") From a3c66ce7d2f12b3ff3ad4217a9ed674ee2811a81 Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Thu, 10 Sep 2026 12:21:34 -0400 Subject: [PATCH 3/6] Type OpenTelemetry signal configuration --- .../opentelemetry/signal_configuration.rb | 32 +++++++------------ .../opentelemetry/configuration/settings.rbs | 2 ++ .../opentelemetry/signal_configuration.rbs | 4 ++- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/lib/datadog/opentelemetry/signal_configuration.rb b/lib/datadog/opentelemetry/signal_configuration.rb index 4d4bfac57f..35b4d1316f 100644 --- a/lib/datadog/opentelemetry/signal_configuration.rb +++ b/lib/datadog/opentelemetry/signal_configuration.rb @@ -11,36 +11,28 @@ module SignalConfiguration def create_resource resource_attributes = {} - tagged_environment = nil - named_environment = nil - legacy_environment = nil + tagged_environment = @settings.tags["env"] + named_environment = @settings.tags["deployment.environment.name"] + legacy_environment = @settings.tags["deployment.environment"] - @settings.tags&.each do |key, value| # steep:ignore + @settings.tags&.each do |key, value| otel_key = case key when "service" then "service.name" - when "env" - tagged_environment = value - next - when "deployment.environment.name" - named_environment = value - next - when "deployment.environment" - legacy_environment = value - next + when "env", "deployment.environment.name", "deployment.environment" then next when "version" then "service.version" else key end resource_attributes[otel_key] = value end - resource_attributes["service.name"] = @settings.service_without_fallback || resource_attributes["service.name"] || Datadog::Core::Environment::Ext::FALLBACK_SERVICE_NAME # steep:ignore - environment = @settings.env || tagged_environment || named_environment || legacy_environment # steep:ignore + resource_attributes["service.name"] = @settings.service_without_fallback || resource_attributes["service.name"] || Datadog::Core::Environment::Ext::FALLBACK_SERVICE_NAME + environment = @settings.env || tagged_environment || named_environment || legacy_environment resource_attributes["deployment.environment.name"] = environment if environment - resource_attributes["service.version"] = @settings.version if @settings.version # steep:ignore + resource_attributes["service.version"] = @settings.version if @settings.version - hostname = Datadog::Core::Environment::Socket.resolved_hostname(@settings) # steep:ignore + hostname = Datadog::Core::Environment::Socket.resolved_hostname(@settings) if hostname - if hostname == @settings.hostname # steep:ignore + if hostname == @settings.hostname resource_attributes["host.name"] = hostname elsif !resource_attributes.key?("host.name") resource_attributes["host.name"] = hostname @@ -53,9 +45,9 @@ def create_resource # Returns the signal-specific option value when explicitly set, # otherwise falls back to the general OTLP exporter config or computed_default. def config_or_exporter_fallback(signal:, option_name:, computed_default: nil) - signal_settings = @settings.opentelemetry.public_send(signal) # steep:ignore + signal_settings = @settings.opentelemetry.public_send(signal) if signal_settings.using_default?(option_name) - @settings.opentelemetry.exporter.public_send(option_name) || computed_default # steep:ignore + @settings.opentelemetry.exporter.public_send(option_name) || computed_default else signal_settings.public_send(option_name) end diff --git a/sig/datadog/opentelemetry/configuration/settings.rbs b/sig/datadog/opentelemetry/configuration/settings.rbs index 82e9f334af..dc42f57bbc 100644 --- a/sig/datadog/opentelemetry/configuration/settings.rbs +++ b/sig/datadog/opentelemetry/configuration/settings.rbs @@ -23,6 +23,8 @@ module Datadog def metrics: () -> _Metrics def logs: () -> _Logs + + def public_send: (:logs | :metrics signal) -> (_Logs | _Metrics) end interface _Exporter diff --git a/sig/datadog/opentelemetry/signal_configuration.rbs b/sig/datadog/opentelemetry/signal_configuration.rbs index e059ed5a8c..a0d744dabc 100644 --- a/sig/datadog/opentelemetry/signal_configuration.rbs +++ b/sig/datadog/opentelemetry/signal_configuration.rbs @@ -1,11 +1,13 @@ module Datadog module OpenTelemetry module SignalConfiguration + @settings: Core::Configuration::Settings + private def create_resource: () -> ::OpenTelemetry::SDK::Resources::Resource - def config_or_exporter_fallback: (signal: ::Symbol, option_name: ::Symbol, ?computed_default: any) -> any + def config_or_exporter_fallback: (signal: (:logs | :metrics), option_name: ::Symbol, ?computed_default: any) -> any end end end From d81c9ce9c89e5f66d10989fed88aba9090ead674 Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Wed, 30 Sep 2026 09:58:14 -0400 Subject: [PATCH 4/6] Address OpenTelemetry review feedback --- lib/datadog/core/configuration/settings.rb | 11 ++++------- lib/datadog/opentelemetry/signal_configuration.rb | 10 +++++----- sig/datadog/opentelemetry/configuration/settings.rbs | 2 -- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/lib/datadog/core/configuration/settings.rb b/lib/datadog/core/configuration/settings.rb index 55fc3d47dd..1b17c88409 100644 --- a/lib/datadog/core/configuration/settings.rb +++ b/lib/datadog/core/configuration/settings.rb @@ -744,7 +744,6 @@ def initialize(*_) # Parses a string containing key-value pairs and returns a hash. # Key-value pairs are delimited by ':' OR `=`, and pairs are separated by whitespace, comma, OR BOTH. result = {} - named_environment = nil unless env_value.nil? || env_value.empty? # falling back to comma as separator sep = env_value.include?(",") ? "," : " " @@ -757,19 +756,17 @@ def initialize(*_) key, val = tag.split(/[:=]/, 2).map(&:strip) val ||= "" # maps OpenTelemetry semantic attributes to Datadog tags + result["env"] = val if key.downcase == "deployment.environment.name" key = case key.downcase - when "deployment.environment.name" - named_environment = val - next - when "deployment.environment" then "env" + when "deployment.environment.name" then nil + when "deployment.environment" then "env" unless result.key?("env") when "service.version" then "version" when "service.name" then "service" else key end - result[key] = val unless key.empty? + result[key] = val unless key.nil? || key.empty? end end - result["env"] = named_environment unless named_environment.nil? result end o.setter do |new_value, old_value| diff --git a/lib/datadog/opentelemetry/signal_configuration.rb b/lib/datadog/opentelemetry/signal_configuration.rb index 35b4d1316f..71df783089 100644 --- a/lib/datadog/opentelemetry/signal_configuration.rb +++ b/lib/datadog/opentelemetry/signal_configuration.rb @@ -11,9 +11,6 @@ module SignalConfiguration def create_resource resource_attributes = {} - tagged_environment = @settings.tags["env"] - named_environment = @settings.tags["deployment.environment.name"] - legacy_environment = @settings.tags["deployment.environment"] @settings.tags&.each do |key, value| otel_key = case key @@ -26,7 +23,7 @@ def create_resource end resource_attributes["service.name"] = @settings.service_without_fallback || resource_attributes["service.name"] || Datadog::Core::Environment::Ext::FALLBACK_SERVICE_NAME - environment = @settings.env || tagged_environment || named_environment || legacy_environment + environment = @settings.env || @settings.tags["env"] || @settings.tags["deployment.environment.name"] || @settings.tags["deployment.environment"] resource_attributes["deployment.environment.name"] = environment if environment resource_attributes["service.version"] = @settings.version if @settings.version @@ -45,7 +42,10 @@ def create_resource # Returns the signal-specific option value when explicitly set, # otherwise falls back to the general OTLP exporter config or computed_default. def config_or_exporter_fallback(signal:, option_name:, computed_default: nil) - signal_settings = @settings.opentelemetry.public_send(signal) + signal_settings = case signal + when :logs then @settings.opentelemetry.logs + when :metrics then @settings.opentelemetry.metrics + end if signal_settings.using_default?(option_name) @settings.opentelemetry.exporter.public_send(option_name) || computed_default else diff --git a/sig/datadog/opentelemetry/configuration/settings.rbs b/sig/datadog/opentelemetry/configuration/settings.rbs index dc42f57bbc..82e9f334af 100644 --- a/sig/datadog/opentelemetry/configuration/settings.rbs +++ b/sig/datadog/opentelemetry/configuration/settings.rbs @@ -23,8 +23,6 @@ module Datadog def metrics: () -> _Metrics def logs: () -> _Logs - - def public_send: (:logs | :metrics signal) -> (_Logs | _Metrics) end interface _Exporter From 7fd3a2c3d4f86e157d56284f6a530514ee828536 Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Wed, 30 Sep 2026 10:08:59 -0400 Subject: [PATCH 5/6] Add OpenTelemetry changelog fragment --- unreleased/20260930140422.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 unreleased/20260930140422.json diff --git a/unreleased/20260930140422.json b/unreleased/20260930140422.json new file mode 100644 index 0000000000..fc72d09add --- /dev/null +++ b/unreleased/20260930140422.json @@ -0,0 +1,6 @@ +{ + "type": "Added", + "product": "OpenTelemetry", + "pull_request": "https://github.com/DataDog/dd-trace-rb/pull/6261", + "message": "Support the stable `deployment.environment.name` resource attribute as the Datadog environment, including Logs and Metrics, while retaining `deployment.environment` as a fallback." +} From 82a7190f76906617cfc47cc27d8e064a8273f538 Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Wed, 30 Sep 2026 10:30:58 -0400 Subject: [PATCH 6/6] Refine OpenTelemetry review changes --- lib/datadog/core/configuration/settings.rb | 5 +++-- unreleased/20260930140422.json | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/datadog/core/configuration/settings.rb b/lib/datadog/core/configuration/settings.rb index 1b17c88409..6f98c0f45d 100644 --- a/lib/datadog/core/configuration/settings.rb +++ b/lib/datadog/core/configuration/settings.rb @@ -756,9 +756,10 @@ def initialize(*_) key, val = tag.split(/[:=]/, 2).map(&:strip) val ||= "" # maps OpenTelemetry semantic attributes to Datadog tags - result["env"] = val if key.downcase == "deployment.environment.name" key = case key.downcase - when "deployment.environment.name" then nil + when "deployment.environment.name" + result["env"] = val + nil when "deployment.environment" then "env" unless result.key?("env") when "service.version" then "version" when "service.name" then "service" diff --git a/unreleased/20260930140422.json b/unreleased/20260930140422.json index fc72d09add..8a54e1b101 100644 --- a/unreleased/20260930140422.json +++ b/unreleased/20260930140422.json @@ -1,6 +1,6 @@ { - "type": "Added", + "type": "Changed", "product": "OpenTelemetry", "pull_request": "https://github.com/DataDog/dd-trace-rb/pull/6261", - "message": "Support the stable `deployment.environment.name` resource attribute as the Datadog environment, including Logs and Metrics, while retaining `deployment.environment` as a fallback." + "message": "Emit `deployment.environment.name` instead of `deployment.environment` for Logs and Metrics resources, and accept it as the Datadog environment while retaining `deployment.environment` as a fallback." }