Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions ddtrace/opentelemetry/metric/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ const (
//
// Priority order for each attribute:
// - service.name: DD_SERVICE → DD_TAGS[service] → OTEL_SERVICE_NAME → OTEL_RESOURCE_ATTRIBUTES[service.name]
// - deployment.environment: DD_ENV → DD_TAGS[env] → OTEL_RESOURCE_ATTRIBUTES[deployment.environment]
// - deployment.environment: DD_ENV → DD_TAGS[env] → OTEL_RESOURCE_ATTRIBUTES[deployment.environment.name]
// → OTEL_RESOURCE_ATTRIBUTES[deployment.environment]
// - service.version: DD_VERSION → DD_TAGS[version] → OTEL_RESOURCE_ATTRIBUTES[service.version]
// - host.name: OTEL_RESOURCE_ATTRIBUTES[host.name] (highest priority, always used if present)
// → If DD_TRACE_REPORT_HOSTNAME="true": DD_HOSTNAME → detected hostname (os.Hostname())
Expand All @@ -65,7 +66,8 @@ func buildDatadogResource(ctx context.Context, opts ...resource.Option) (*resour
attrs = append(attrs, semconv.ServiceName(serviceName))
}

// 2. Environment priority: DD_ENV → DD_TAGS[env] → OTEL_RESOURCE_ATTRIBUTES[deployment.environment]
// 2. Environment priority: DD_ENV → DD_TAGS[env] → OTEL_RESOURCE_ATTRIBUTES[deployment.environment.name]
// → OTEL_RESOURCE_ATTRIBUTES[deployment.environment]
envName := environmentName(ddTags, otelAttrs)
if envName != "" {
attrs = append(attrs, semconv.DeploymentEnvironmentNameKey.String(envName))
Expand Down Expand Up @@ -99,10 +101,11 @@ func buildDatadogResource(ctx context.Context, opts ...resource.Option) (*resour
// 6. Add OTEL_RESOURCE_ATTRIBUTES (excluding reserved keys AND keys already set by DD_TAGS)
// DD_TAGS has higher priority than OTEL_RESOURCE_ATTRIBUTES for custom tags
excludeKeys := map[string]bool{
"service.name": true,
"deployment.environment": true,
"service.version": true,
"host.name": true,
"service.name": true,
string(semconv.DeploymentEnvironmentNameKey): true,
"deployment.environment": true,
"service.version": true,
"host.name": true,
}
for key, val := range otelAttrs {
if excludeKeys[key] {
Expand Down Expand Up @@ -155,6 +158,9 @@ func environmentName(ddTags, otelAttrs map[string]string) string {
if v, ok := ddTags["env"]; ok && v != "" {
return v
}
if v, ok := otelAttrs[string(semconv.DeploymentEnvironmentNameKey)]; ok && v != "" {
return v
}
// OTEL_RESOURCE_ATTRIBUTES[deployment.environment]
if v, ok := otelAttrs["deployment.environment"]; ok && v != "" {
return v
Expand Down
104 changes: 104 additions & 0 deletions ddtrace/opentelemetry/metric/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,110 @@ func TestBuildDatadogResource_OtelFallback(t *testing.T) {
assert.Equal(t, "otel-version", attrMap["service.version"])
}

func TestBuildDatadogResource_DeploymentEnvironment(t *testing.T) {
tests := []struct {
name string
ddEnv string
ddTags string
resourceAttributes string
expected string
}{
{
name: "stable key only",
resourceAttributes: "deployment.environment.name=stable,custom=value",
expected: "stable",
},
{
name: "legacy key only",
resourceAttributes: "deployment.environment=legacy,custom=value",
expected: "legacy",
},
{
name: "stable key before legacy key",
resourceAttributes: "deployment.environment.name=stable,deployment.environment=legacy,custom=value",
expected: "stable",
},
{
name: "legacy key before stable key",
resourceAttributes: "deployment.environment=legacy,deployment.environment.name=stable,custom=value",
expected: "stable",
},
{
name: "last duplicate stable key wins",
resourceAttributes: "deployment.environment.name=old,deployment.environment.name=new,custom=value",
expected: "new",
},
{
name: "empty last stable key falls back to legacy key",
resourceAttributes: "deployment.environment.name=old,deployment.environment=legacy,deployment.environment.name=,custom=value",
expected: "legacy",
},
{
name: "last duplicate legacy key wins",
resourceAttributes: "deployment.environment=old,deployment.environment=new,custom=value",
expected: "new",
},
{
name: "empty last legacy key is unset",
resourceAttributes: "deployment.environment=old,deployment.environment=,custom=value",
expected: "",
},
{
name: "unrelated DD_TAGS preserves stable environment",
ddTags: "team:platform",
resourceAttributes: "deployment.environment.name=stable,custom=value",
expected: "stable",
},
{
name: "stable key overrides generic env",
resourceAttributes: "deployment.environment.name=stable,env=custom,custom=value",
expected: "stable",
},
{
name: "DD_ENV takes precedence",
ddEnv: "datadog",
resourceAttributes: "deployment.environment=legacy,deployment.environment.name=stable,custom=value",
expected: "datadog",
},
{
name: "DD_TAGS takes precedence",
ddTags: "env:datadog",
resourceAttributes: "deployment.environment=legacy,deployment.environment.name=stable,custom=value",
expected: "datadog",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv(envDDEnv, tt.ddEnv)
t.Setenv(envDDTags, tt.ddTags)
t.Setenv(envOtelResourceAttributes, tt.resourceAttributes)

res, err := buildDatadogResource(context.Background())
require.NoError(t, err)

attributes := make(map[string]string)
stableEnvironmentCount := 0
for _, attr := range res.Attributes() {
key := string(attr.Key)
attributes[key] = attr.Value.AsString()
if attr.Key == semconv.DeploymentEnvironmentNameKey {
stableEnvironmentCount++
}
}

assert.Equal(t, tt.expected, attributes["deployment.environment.name"])
expectedEnvironmentCount := 1
if tt.expected == "" {
expectedEnvironmentCount = 0
}
assert.Equal(t, expectedEnvironmentCount, stableEnvironmentCount)
assert.NotContains(t, attributes, "deployment.environment")
assert.Equal(t, "value", attributes["custom"])
})
}
}

// TestBuildDatadogResource_Hostname verifies hostname resolution priority:
// 1. OTEL_RESOURCE_ATTRIBUTES[host.name] (always wins)
// 2. DD_HOSTNAME (only if DD_TRACE_REPORT_HOSTNAME=true)
Expand Down
7 changes: 6 additions & 1 deletion ddtrace/tracer/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,10 +269,15 @@ func newConfig(opts ...StartOption) (*config, error) {
_, globalTagsOrigin := c.internalConfig.GlobalTagsConfig().Baseline()
if c.internalConfig.Env() == "" {
if v, ok := globalTags["env"]; ok {
if e, ok := v.(string); ok {
if e, ok := v.(string); ok && e != "" {
c.internalConfig.SetEnv(e, globalTagsOrigin, internalconfig.ProductTracer)
}
}
if c.internalConfig.Env() == "" {
if e := c.internalConfig.OTelResourceEnvironment(); e != "" {
c.internalConfig.SetEnv(e, telemetry.OriginEnvVar, internalconfig.ProductTracer)
}
}
}
if c.internalConfig.Version() == "" {
if v, ok := globalTags["version"]; ok {
Expand Down
38 changes: 38 additions & 0 deletions ddtrace/tracer/option_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1613,6 +1613,16 @@ func TestEnvConfig(t *testing.T) {
assert.Equal("testing", c.internalConfig.Env())
})

t.Run("DD_ENV takes precedence over stable OTEL_RESOURCE_ATTRIBUTES", func(t *testing.T) {
t.Setenv("DD_ENV", "datadog")
t.Setenv("OTEL_RESOURCE_ATTRIBUTES", "deployment.environment.name=opentelemetry")
assert := assert.New(t)
c, err := newTestConfig()
assert.NoError(err)

assert.Equal("datadog", c.internalConfig.Env())
})

t.Run("DD_TAGS", func(t *testing.T) {
t.Setenv("DD_TAGS", "env:testing")
assert := assert.New(t)
Expand All @@ -1622,6 +1632,34 @@ func TestEnvConfig(t *testing.T) {
assert.Equal("testing", c.internalConfig.Env())
})

t.Run("unrelated DD_TAGS preserves stable OTEL_RESOURCE_ATTRIBUTES", func(t *testing.T) {
t.Setenv("DD_TAGS", "team:platform")
t.Setenv("OTEL_RESOURCE_ATTRIBUTES", "deployment.environment.name=opentelemetry")
assert := assert.New(t)
c, err := newTestConfig()

assert.NoError(err)
assert.Equal("opentelemetry", c.internalConfig.Env())
})

t.Run("last duplicate legacy OTEL_RESOURCE_ATTRIBUTES key wins", func(t *testing.T) {
t.Setenv("OTEL_RESOURCE_ATTRIBUTES", "deployment.environment=old,deployment.environment=new")
assert := assert.New(t)
c, err := newTestConfig()

assert.NoError(err)
assert.Equal("new", c.internalConfig.Env())
})

t.Run("stable OTEL_RESOURCE_ATTRIBUTES key overrides generic env", func(t *testing.T) {
t.Setenv("OTEL_RESOURCE_ATTRIBUTES", "deployment.environment.name=stable,env=custom")
assert := assert.New(t)
c, err := newTestConfig()

assert.NoError(err)
assert.Equal("stable", c.internalConfig.Env())
})

t.Run("override-chain", func(t *testing.T) {
assert := assert.New(t)
c, err := newTestConfig()
Expand Down
10 changes: 9 additions & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ type Config struct {
// configured service get the version tag.
universalVersion bool
// env contains the environment that this application will run under.
env string
env string
otelResourceEnvironment string
// site specifies the Datadog site to send data to
site string
// serviceMappings holds a set of service mappings to dynamically rename services.
Expand Down Expand Up @@ -376,6 +377,7 @@ func loadConfig() *Config {
cfg.version = p.GetString("DD_VERSION", "")
cfg.universalVersion = p.GetBool("DD_TRACE_UNIVERSAL_VERSION_ENABLED", false)
cfg.env = p.GetString("DD_ENV", "")
cfg.otelResourceEnvironment = provider.OTelResourceEnvironment()
cfg.site = p.GetString("DD_SITE", "datadoghq.com")
cfg.serviceMappings = p.GetMap("DD_SERVICE_MAPPING", nil, internal.DDTagsDelimiter)
cfg.runtimeMetrics = p.GetBool("DD_RUNTIME_METRICS_ENABLED", false)
Expand Down Expand Up @@ -1357,6 +1359,12 @@ func (c *Config) Env() string {
return c.env
}

func (c *Config) OTelResourceEnvironment() string {
c.mu.RLock()
defer c.mu.RUnlock()
return c.otelResourceEnvironment
}

func (c *Config) SetEnv(env string, origin telemetry.Origin, product ...Product) {
c.mu.Lock()
defer c.mu.Unlock()
Expand Down
53 changes: 48 additions & 5 deletions internal/config/provider/otelenvconfigsource.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ import (
)

const (
ddPrefix = "config_datadog:"
otelPrefix = "config_opentelemetry:"
ddPrefix = "config_datadog:"
otelPrefix = "config_opentelemetry:"
deploymentEnvironment = "deployment.environment"
deploymentEnvironmentName = "deployment.environment.name"
)

type otelEnvConfigSource struct{}
Expand Down Expand Up @@ -92,9 +94,9 @@ var otelConfigs = map[string]*otelDDEnv{
}

var ddTagsMapping = map[string]string{
"service.name": "service",
"deployment.environment": "env",
"service.version": "version",
"service.name": "service",
deploymentEnvironment: "env",
"service.version": "version",
}

var unsupportedSamplerMapping = map[string]string{
Expand Down Expand Up @@ -200,8 +202,29 @@ func mapPropagationStyle(ot string) (string, error) {

// mapDDTags maps OTEL_RESOURCE_ATTRIBUTES to DD_TAGS
func mapDDTags(ot string) (string, error) {
stableEnvironment, legacyEnvironment := deploymentEnvironmentValues(ot)

ddTags := make([]string, 0)
environmentMapped := false
internal.ForEachStringTag(ot, internal.OtelTagsDelimeter, func(key, val string) {
if key == deploymentEnvironmentName {
if stableEnvironment != "" && !environmentMapped && val != "" {
ddTags = append([]string{"env" + internal.DDTagsDelimiter + stableEnvironment}, ddTags...)
environmentMapped = true
}
return
}
if key == deploymentEnvironment {
if stableEnvironment == "" && legacyEnvironment != "" && !environmentMapped {
ddTags = append([]string{"env" + internal.DDTagsDelimiter + legacyEnvironment}, ddTags...)
environmentMapped = true
}
return
}
if key == "env" && (stableEnvironment != "" || legacyEnvironment != "") {
return
}

// replace otel delimiter with dd delimiter and normalize tag names
if ddkey, ok := ddTagsMapping[key]; ok {
ddTags = append([]string{ddkey + internal.DDTagsDelimiter + val}, ddTags...)
Expand All @@ -217,3 +240,23 @@ func mapDDTags(ot string) (string, error) {

return strings.Join(ddTags, ","), nil
}

func deploymentEnvironmentValues(ot string) (stable, legacy string) {
internal.ForEachStringTag(ot, internal.OtelTagsDelimeter, func(key, val string) {
switch key {
case deploymentEnvironmentName:
stable = val
case deploymentEnvironment:
legacy = val
}
})
return stable, legacy
}

func OTelResourceEnvironment() string {
stable, legacy := deploymentEnvironmentValues(env.Get("OTEL_RESOURCE_ATTRIBUTES"))
if stable != "" {
return stable
}
return legacy
}
Loading
Loading