diff --git a/ddtrace/tracer/option.go b/ddtrace/tracer/option.go index 2413a6a624..5d41d8943e 100644 --- a/ddtrace/tracer/option.go +++ b/ddtrace/tracer/option.go @@ -33,6 +33,7 @@ import ( "github.com/DataDog/dd-trace-go/v2/internal" appsecconfig "github.com/DataDog/dd-trace-go/v2/internal/appsec/config" "github.com/DataDog/dd-trace-go/v2/internal/civisibility/constants" + internalconfig "github.com/DataDog/dd-trace-go/v2/internal/config" "github.com/DataDog/dd-trace-go/v2/internal/env" "github.com/DataDog/dd-trace-go/v2/internal/globalconfig" llmobsconfig "github.com/DataDog/dd-trace-go/v2/internal/llmobs/config" @@ -606,10 +607,9 @@ func newConfig(opts ...StartOption) (*config, error) { if c.logger != nil { log.UseLogger(c.logger) } - if c.debug { + if internalconfig.GlobalConfig().IsDebugEnabled() { log.SetLevel(log.LevelDebug) } - // Check if CI Visibility mode is enabled if internal.BoolEnv(constants.CIVisibilityEnabledEnvironmentVariable, false) { c.ciVisibilityEnabled = true // Enable CI Visibility mode diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000000..14fa98b560 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,121 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2025 Datadog, Inc. + +package config + +import ( + "net/url" + "time" +) + +var globalConfig *Config + +// Config represents global configuration properties. +type Config struct { + // AgentURL is the URL of the Datadog agent. + AgentURL *url.URL `json:"DD_AGENT_URL"` + + // Debug enables debug logging. + Debug bool `json:"DD_TRACE_DEBUG"` // has trace in the name, but impacts all products? + + LogStartup bool `json:"DD_TRACE_STARTUP_LOGS"` + + ServiceName string `json:"DD_SERVICE"` + + Version string `json:"DD_VERSION"` + + Env string `json:"DD_ENV"` + + ServiceMappings map[string]string `json:"DD_SERVICE_MAPPING"` + + Hostname string `json:"DD_TRACE_SOURCE_HOSTNAME"` + + RuntimeMetrics bool `json:"DD_RUNTIME_METRICS_ENABLED"` + + RuntimeMetricsV2 bool `json:"DD_RUNTIME_METRICS_V2_ENABLED"` + + ProfilerHotspots bool `json:"DD_PROFILING_CODE_HOTSPOTS_COLLECTION_ENABLED"` + + ProfilerEndpoints bool `json:"DD_PROFILING_ENDPOINT_COLLECTION_ENABLED"` + + SpanAttributeSchemaVersion int `json:"DD_TRACE_SPAN_ATTRIBUTE_SCHEMA"` + + PeerServiceDefaultsEnabled bool `json:"DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED"` + + PeerServiceMappings map[string]string `json:"DD_TRACE_PEER_SERVICE_MAPPING"` + + DebugAbandonedSpans bool `json:"DD_TRACE_DEBUG_ABANDONED_SPANS"` + + SpanTimeout time.Duration `json:"DD_TRACE_SPAN_TIMEOUT"` + + PartialFlushMinSpans int `json:"DD_TRACE_PARTIAL_FLUSH_MIN_SPANS"` + + PartialFlushEnabled bool `json:"DD_TRACE_PARTIAL_FLUSH_ENABLED"` + + StatsComputationEnabled bool `json:"DD_TRACE_STATS_COMPUTATION_ENABLED"` + + DataStreamsMonitoringEnabled bool `json:"DD_DATA_STREAMS_ENABLED"` + + DynamicInstrumentationEnabled bool `json:"DD_DYNAMIC_INSTRUMENTATION_ENABLED"` + + GlobalSampleRate float64 `json:"DD_TRACE_SAMPLE_RATE"` + + CIVisibilityEnabled bool `json:"DD_CIVISIBILITY_ENABLED"` + + CIVisibilityAgentless bool `json:"DD_CIVISIBILITY_AGENTLESS_ENABLED"` + + LogDirectory string `json:"DD_TRACE_LOG_DIRECTORY"` + + TraceRateLimitPerSecond float64 `json:"DD_TRACE_RATE_LIMIT"` + + TraceProtocol float64 `json:"DD_TRACE_AGENT_PROTOCOL_VERSION"` +} + +func loadConfig() *Config { + cfg := new(Config) + + // TODO: Use defaults from config json instead of hardcoding them here + cfg.AgentURL = provider.getURL("DD_TRACE_AGENT_URL", &url.URL{Scheme: "http", Host: "localhost:8126"}) + cfg.Debug = provider.getBool("DD_TRACE_DEBUG", false) + cfg.LogStartup = provider.getBool("DD_TRACE_STARTUP_LOGS", false) + cfg.ServiceName = provider.getString("DD_SERVICE", "") + cfg.Version = provider.getString("DD_VERSION", "") + cfg.Env = provider.getString("DD_ENV", "") + cfg.ServiceMappings = provider.getMap("DD_SERVICE_MAPPING", nil) + cfg.Hostname = provider.getString("DD_TRACE_SOURCE_HOSTNAME", "") + cfg.RuntimeMetrics = provider.getBool("DD_RUNTIME_METRICS_ENABLED", false) + cfg.RuntimeMetricsV2 = provider.getBool("DD_RUNTIME_METRICS_V2_ENABLED", false) + cfg.ProfilerHotspots = provider.getBool("DD_PROFILING_CODE_HOTSPOTS_COLLECTION_ENABLED", false) + cfg.ProfilerEndpoints = provider.getBool("DD_PROFILING_ENDPOINT_COLLECTION_ENABLED", false) + cfg.SpanAttributeSchemaVersion = provider.getInt("DD_TRACE_SPAN_ATTRIBUTE_SCHEMA", 0) + cfg.PeerServiceDefaultsEnabled = provider.getBool("DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED", false) + cfg.PeerServiceMappings = provider.getMap("DD_TRACE_PEER_SERVICE_MAPPING", nil) + cfg.DebugAbandonedSpans = provider.getBool("DD_TRACE_DEBUG_ABANDONED_SPANS", false) + cfg.SpanTimeout = provider.getDuration("DD_TRACE_ABANDONED_SPAN_TIMEOUT", 0) + cfg.PartialFlushMinSpans = provider.getInt("DD_TRACE_PARTIAL_FLUSH_MIN_SPANS", 0) + cfg.PartialFlushEnabled = provider.getBool("DD_TRACE_PARTIAL_FLUSH_ENABLED", false) + cfg.StatsComputationEnabled = provider.getBool("DD_TRACE_STATS_COMPUTATION_ENABLED", false) + cfg.DataStreamsMonitoringEnabled = provider.getBool("DD_DATA_STREAMS_ENABLED", false) + cfg.DynamicInstrumentationEnabled = provider.getBool("DD_DYNAMIC_INSTRUMENTATION_ENABLED", false) + cfg.GlobalSampleRate = provider.getFloat("DD_TRACE_SAMPLE_RATE", 0.0) + cfg.CIVisibilityEnabled = provider.getBool("DD_CIVISIBILITY_ENABLED", false) + cfg.CIVisibilityAgentless = provider.getBool("DD_CIVISIBILITY_AGENTLESS_ENABLED", false) + cfg.LogDirectory = provider.getString("DD_TRACE_LOG_DIRECTORY", "") + cfg.TraceRateLimitPerSecond = provider.getFloat("DD_TRACE_RATE_LIMIT", 0.0) + cfg.TraceProtocol = provider.getFloat("DD_TRACE_AGENT_PROTOCOL_VERSION", 0.0) + + return cfg +} + +func GlobalConfig() *Config { + if globalConfig == nil { + globalConfig = loadConfig() + } + return globalConfig +} + +func (c *Config) IsDebugEnabled() bool { + return c.Debug +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000000..91e0cdf140 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,88 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2025 Datadog, Inc. + +package config + +// func TestConfigHasFields(t *testing.T) { +// // TODO: Use supported configurations JSON as expectedFields instead +// expectedFields := map[string]reflect.Type{ +// "AgentURL": reflect.TypeOf((*url.URL)(nil)), +// "Debug": reflect.TypeOf(false), +// "LogToStdout": reflect.TypeOf(false), +// "LogStartup": reflect.TypeOf(false), +// "ServiceName": reflect.TypeOf(""), +// "Version": reflect.TypeOf(""), +// "Env": reflect.TypeOf(""), +// //"Sampler": reflect.TypeOf((*RateSampler)(nil)), +// // "OriginalAgentURL": reflect.TypeOf((*url.URL)(nil)), // We probably don't need this anymore +// "ServiceMappings": reflect.TypeOf((map[string]string)(nil)), +// // "GlobalTags": reflect.TypeOf((*dynamicConfig[map[string]interface{}])(nil)), +// // "Transport": reflect.TypeOf((*transport)(nil)), +// "HTTPClientTimeout": reflect.TypeOf(int64(0)), +// // "Propagator": reflect.TypeOf((*Propagator)(nil)), +// "Hostname": reflect.TypeOf(""), +// // "Logger": reflect.TypeOf((*Logger)(nil)), +// "RuntimeMetrics": reflect.TypeOf(false), +// "RuntimeMetricsV2": reflect.TypeOf(false), +// // "StatsdClient": reflect.TypeOf((*internal.StatsdClient)(nil)), +// // "SpanRules": reflect.TypeOf((*[]SamplingRule)(nil)), +// // "TraceRules": reflect.TypeOf((*[]SamplingRule)(nil)), +// "ProfilerHotspots": reflect.TypeOf(false), +// "ProfilerEndpoints": reflect.TypeOf(false), +// // "TracingEnabled": reflect.TypeOf((*dynamicConfig[bool])(nil)), +// "EnableHostnameDetection": reflect.TypeOf(false), +// "SpanAttributeSchemaVersion": reflect.TypeOf(0), +// "PeerServiceDefaultsEnabled": reflect.TypeOf(false), +// "PeerServiceMappings": reflect.TypeOf((map[string]string)(nil)), +// "DebugAbandonedSpans": reflect.TypeOf(false), +// "SpanTimeout": reflect.TypeOf(time.Duration(int64(0))), +// "PartialFlushMinSpans": reflect.TypeOf(0), +// "PartialFlushEnabled": reflect.TypeOf(false), +// "StatsComputationEnabled": reflect.TypeOf(false), +// "DataStreamsMonitoringEnabled": reflect.TypeOf(false), +// // "OrchestrionCfg": reflect.TypeOf((*orchestrionConfig)(nil)), +// // "TraceSampleRate": reflect.TypeOf((*dynamicConfig[float64])(nil)), +// // "TraceSampleRules": reflect.TypeOf((*dynamicConfig[[]SamplingRule])(nil)), +// // "HeaderAsTags": reflect.TypeOf((*dynamicConfig[[]string])(nil)), +// "DynamicInstrumentationEnabled": reflect.TypeOf(false), +// "GlobalSampleRate": reflect.TypeOf(float64(0)), +// "CIVisibilityEnabled": reflect.TypeOf(false), +// "CIVisibilityAgentless": reflect.TypeOf(false), +// "LogDirectory": reflect.TypeOf(""), +// "TracingAsTransport": reflect.TypeOf(false), +// "TraceRateLimitPerSecond": reflect.TypeOf(float64(0)), +// "TraceProtocol": reflect.TypeOf(float64(0)), +// // "LLMObsEnabled": reflect.TypeOf(false), +// // "LLMObsMLApp": reflect.TypeOf(""), +// // "LLMObsAgentlessEnabled": reflect.TypeOf(false), +// // "LLMObsProjectName": reflect.TypeOf(""), +// } + +// // Get the Config struct type +// configType := reflect.TypeOf(Config{}) + +// // Verify the number of expected fields matches the actual number of fields +// actualFieldCount := configType.NumField() +// expectedFieldCount := len(expectedFields) +// assert.Equal(t, expectedFieldCount, actualFieldCount, +// "Expected %d fields in Config struct, but found %d. Update the test when adding/removing fields.", +// expectedFieldCount, actualFieldCount) + +// // Verify each expected field exists with the correct type +// for fieldName, expectedType := range expectedFields { +// field, found := configType.FieldByName(fieldName) +// assert.True(t, found, "Field %s should exist on Config struct", fieldName) + +// if found { +// assert.Equal(t, expectedType, field.Type, +// "Field %s should have type %s, but has type %s", +// fieldName, expectedType, field.Type) +// } +// } + +// // Verify we can instantiate the config +// cfg := new(Config) +// assert.NotNil(t, cfg, "Should be able to create new Config instance") +// } diff --git a/internal/config/configprovider.go b/internal/config/configprovider.go new file mode 100644 index 0000000000..eb359dc03d --- /dev/null +++ b/internal/config/configprovider.go @@ -0,0 +1,172 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2025 Datadog, Inc. + +package config + +import ( + "net/url" + "strconv" + "strings" + "time" +) + +var provider = DefaultConfigProvider() + +type ConfigProvider struct { + sources []ConfigSource // In order of priority +} + +func (p *ConfigProvider) getString(key string, def string) string { + // TODO: Eventually, iterate over all sources and report telemetry + for _, source := range p.sources { + if v := source.Get(key); v != "" { + return v + } + } + return def +} + +func (p *ConfigProvider) getBool(key string, def bool) bool { + for _, source := range p.sources { + if v := source.Get(key); v != "" { + if v == "true" { + return true + } else if v == "false" { + return false + } + } + } + return def +} + +func (p *ConfigProvider) getInt(key string, def int) int { + for _, source := range p.sources { + if v := source.Get(key); v != "" { + v, err := strconv.Atoi(v) + if err == nil { + return v + } + } + } + return def +} + +func (p *ConfigProvider) getInt64(key string, def int64) int64 { + for _, source := range p.sources { + if v := source.Get(key); v != "" { + v, err := strconv.ParseInt(v, 10, 64) + if err == nil { + return v + } + } + } + return def +} + +func (p *ConfigProvider) getMap(key string, def map[string]string) map[string]string { + for _, source := range p.sources { + if v := source.Get(key); v != "" { + m := parseMapString(v) + if len(m) > 0 { + return m + } + } + } + return def +} + +func (p *ConfigProvider) getDuration(key string, def time.Duration) time.Duration { + for _, source := range p.sources { + if v := source.Get(key); v != "" { + d, err := time.ParseDuration(v) + if err == nil { + return d + } + } + } + return def +} + +func (p *ConfigProvider) getFloat(key string, def float64) float64 { + for _, source := range p.sources { + if v := source.Get(key); v != "" { + v, err := strconv.ParseFloat(v, 64) + if err == nil { + return v + } + } + } + return def +} + +func (p *ConfigProvider) getURL(key string, def *url.URL) *url.URL { + for _, source := range p.sources { + if v := source.Get(key); v != "" { + u, err := url.Parse(v) + if err == nil { + return u + } + } + } + return def +} + +func DefaultConfigProvider() *ConfigProvider { + return &ConfigProvider{ + sources: []ConfigSource{ + ManagedDeclarativeConfig, + new(envConfigSource), + LocalDeclarativeConfig, + }, + } +} + +type ConfigSource interface { + Get(key string) string +} + +// normalizeKey is a helper function for ConfigSource implementations to normalize the key to a valid environment variable name. +func normalizeKey(key string) string { + // Try to convert key to a valid environment variable name + if strings.HasPrefix(key, "DD_") || strings.HasPrefix(key, "OTEL_") { + return key + } + return "DD_" + strings.ToUpper(key) +} + +// parseMapString parses a string containing key:value pairs separated by comma or space. +// Format: "key1:value1,key2:value2" or "key1:value1 key2:value2" +func parseMapString(str string) map[string]string { + result := make(map[string]string) + + // Determine separator (comma or space) + sep := " " + if strings.Contains(str, ",") { + sep = "," + } + + // Parse each key:value pair + for _, pair := range strings.Split(str, sep) { + pair = strings.TrimSpace(pair) + if pair == "" { + continue + } + + // Split on colon delimiter + kv := strings.SplitN(pair, ":", 2) + key := strings.TrimSpace(kv[0]) + if key == "" { + continue + } + + var val string + if len(kv) == 2 { + val = strings.TrimSpace(kv[1]) + } + result[key] = val + } + + return result +} diff --git a/internal/config/configprovider_test.go b/internal/config/configprovider_test.go new file mode 100644 index 0000000000..6318fea963 --- /dev/null +++ b/internal/config/configprovider_test.go @@ -0,0 +1,205 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2025 Datadog, Inc. + +package config + +import ( + "os" + "testing" + + "net/url" + + "github.com/DataDog/dd-trace-go/v2/internal/telemetry" + "github.com/stretchr/testify/assert" +) + +func newTestConfigProvider(sources ...ConfigSource) *ConfigProvider { + return &ConfigProvider{ + sources: sources, + } +} + +type testConfigSource struct { + entries map[string]string +} + +func newTestConfigSource(entries map[string]string) *testConfigSource { + if entries == nil { + entries = make(map[string]string) + } + return &testConfigSource{ + entries: entries, + } +} + +func (s *testConfigSource) Get(key string) string { + return s.entries[key] +} + +func TestGetMethods(t *testing.T) { + t.Run("defaults", func(t *testing.T) { + // Test that defaults are used when the queried key does not exist + provider := newTestConfigProvider(newTestConfigSource(nil)) + assert.Equal(t, "value", provider.getString("DD_SERVICE", "value")) + assert.Equal(t, true, provider.getBool("DD_TRACE_DEBUG", true)) + assert.Equal(t, 1, provider.getInt("DD_TRACE_PARTIAL_FLUSH_MIN_SPANS", 1)) + assert.Equal(t, 1.0, provider.getFloat("DD_TRACE_SAMPLE_RATE", 1.0)) + assert.Equal(t, &url.URL{Scheme: "http", Host: "localhost:8126"}, provider.getURL("DD_TRACE_AGENT_URL", &url.URL{Scheme: "http", Host: "localhost:8126"})) + }) + t.Run("non-defaults", func(t *testing.T) { + // Test that non-defaults are used when the queried key exists + entries := map[string]string{ + "DD_SERVICE": "string", + "DD_TRACE_DEBUG": "true", + "DD_TRACE_PARTIAL_FLUSH_MIN_SPANS": "1", + "DD_TRACE_SAMPLE_RATE": "1.0", + "DD_TRACE_AGENT_URL": "https://localhost:8126", + } + provider := newTestConfigProvider(newTestConfigSource(entries)) + assert.Equal(t, "string", provider.getString("DD_SERVICE", "value")) + assert.Equal(t, true, provider.getBool("DD_TRACE_DEBUG", false)) + assert.Equal(t, 1, provider.getInt("DD_TRACE_PARTIAL_FLUSH_MIN_SPANS", 0)) + assert.Equal(t, 1.0, provider.getFloat("DD_TRACE_SAMPLE_RATE", 0.0)) + assert.Equal(t, &url.URL{Scheme: "https", Host: "localhost:8126"}, provider.getURL("DD_TRACE_AGENT_URL", &url.URL{Scheme: "https", Host: "localhost:8126"})) + }) +} + +func TestDefaultConfigProvider(t *testing.T) { + t.Run("Settings only exist in EnvConfigSource", func(t *testing.T) { + // Setup: environment variables of each type + t.Setenv("DD_SERVICE", "string") + t.Setenv("DD_TRACE_DEBUG", "true") + t.Setenv("DD_TRACE_PARTIAL_FLUSH_MIN_SPANS", "1") + t.Setenv("DD_TRACE_SAMPLE_RATE", "1.0") + t.Setenv("DD_TRACE_AGENT_URL", "https://localhost:8126") + // TODO: Add more types as we go along + + provider := DefaultConfigProvider() + + // Configured values are returned correctly + assert.Equal(t, "string", provider.getString("DD_SERVICE", "value")) + assert.Equal(t, true, provider.getBool("DD_TRACE_DEBUG", false)) + assert.Equal(t, 1, provider.getInt("DD_TRACE_PARTIAL_FLUSH_MIN_SPANS", 0)) + assert.Equal(t, 1.0, provider.getFloat("DD_TRACE_SAMPLE_RATE", 0.0)) + assert.Equal(t, &url.URL{Scheme: "https", Host: "localhost:8126"}, provider.getURL("DD_TRACE_AGENT_URL", &url.URL{Scheme: "https", Host: "localhost:8126"})) + + // Defaults are returned for settings that are not configured + assert.Equal(t, "value", provider.getString("DD_ENV", "value")) + }) + t.Run("Settings only exist in LocalDeclarativeConfigSource", func(t *testing.T) { + const localYaml = ` +apm_configuration_default: + DD_SERVICE: local + DD_TRACE_DEBUG: true + DD_TRACE_PARTIAL_FLUSH_MIN_SPANS: "1" + DD_TRACE_SAMPLE_RATE: 1.0 + DD_TRACE_AGENT_URL: https://localhost:8126 +` + + tempLocalPath := "local.yml" + err := os.WriteFile(tempLocalPath, []byte(localYaml), 0644) + assert.NoError(t, err) + defer os.Remove(tempLocalPath) + + LocalDeclarativeConfig = newDeclarativeConfigSource(tempLocalPath, telemetry.OriginLocalStableConfig) + defer func() { + LocalDeclarativeConfig = newDeclarativeConfigSource(localFilePath, telemetry.OriginLocalStableConfig) + }() + + provider := DefaultConfigProvider() + + assert.Equal(t, "local", provider.getString("DD_SERVICE", "value")) + assert.Equal(t, true, provider.getBool("DD_TRACE_DEBUG", false)) + assert.Equal(t, 1, provider.getInt("DD_TRACE_PARTIAL_FLUSH_MIN_SPANS", 0)) + assert.Equal(t, 1.0, provider.getFloat("DD_TRACE_SAMPLE_RATE", 0.0)) + assert.Equal(t, &url.URL{Scheme: "https", Host: "localhost:8126"}, provider.getURL("DD_TRACE_AGENT_URL", &url.URL{Scheme: "https", Host: "localhost:8126"})) + + // Defaults are returned for settings that are not configured + assert.Equal(t, "value", provider.getString("DD_ENV", "value")) + }) + + t.Run("Settings only exist in ManagedDeclarativeConfigSource", func(t *testing.T) { + const managedYaml = ` +apm_configuration_default: + DD_SERVICE: managed + DD_TRACE_DEBUG: true + DD_TRACE_PARTIAL_FLUSH_MIN_SPANS: "1" + DD_TRACE_SAMPLE_RATE: 1.0 + DD_TRACE_AGENT_URL: https://localhost:8126` + + tempManagedPath := "managed.yml" + err := os.WriteFile(tempManagedPath, []byte(managedYaml), 0644) + assert.NoError(t, err) + defer os.Remove(tempManagedPath) + + ManagedDeclarativeConfig = newDeclarativeConfigSource(tempManagedPath, telemetry.OriginManagedStableConfig) + defer func() { + ManagedDeclarativeConfig = newDeclarativeConfigSource(managedFilePath, telemetry.OriginManagedStableConfig) + }() + + provider := DefaultConfigProvider() + + assert.Equal(t, "managed", provider.getString("DD_SERVICE", "value")) + assert.Equal(t, true, provider.getBool("DD_TRACE_DEBUG", false)) + assert.Equal(t, 1, provider.getInt("DD_TRACE_PARTIAL_FLUSH_MIN_SPANS", 0)) + assert.Equal(t, 1.0, provider.getFloat("DD_TRACE_SAMPLE_RATE", 0.0)) + assert.Equal(t, &url.URL{Scheme: "https", Host: "localhost:8126"}, provider.getURL("DD_TRACE_AGENT_URL", &url.URL{Scheme: "https", Host: "localhost:8126"})) + + // Defaults are returned for settings that are not configured + assert.Equal(t, "value", provider.getString("DD_ENV", "value")) + }) + t.Run("Settings exist in all ConfigSources", func(t *testing.T) { + localYaml := ` +apm_configuration_default: + DD_SERVICE: local + DD_TRACE_DEBUG: false + DD_TRACE_SOURCE_HOSTNAME: otherhost + DD_TRACE_PARTIAL_FLUSH_MIN_SPANS: "1"` + + managedYaml := ` +apm_configuration_default: + DD_SERVICE: managed + DD_TRACE_DEBUG: true + DD_TRACE_PARTIAL_FLUSH_ENABLED: true + DD_VERSION: 1.0.0` + + t.Setenv("DD_SERVICE", "env") + t.Setenv("DD_TRACE_PARTIAL_FLUSH_ENABLED", "false") + t.Setenv("DD_ENV", "dev") + t.Setenv("DD_TRACE_SOURCE_HOSTNAME", "otherhost") + + tempLocalPath := "local.yml" + err := os.WriteFile(tempLocalPath, []byte(localYaml), 0644) + assert.NoError(t, err) + defer os.Remove(tempLocalPath) + + LocalDeclarativeConfig = newDeclarativeConfigSource(tempLocalPath, telemetry.OriginLocalStableConfig) + defer func() { + LocalDeclarativeConfig = newDeclarativeConfigSource(localFilePath, telemetry.OriginLocalStableConfig) + }() + + tempManagedPath := "managed.yml" + err = os.WriteFile(tempManagedPath, []byte(managedYaml), 0644) + assert.NoError(t, err) + defer os.Remove(tempManagedPath) + + ManagedDeclarativeConfig = newDeclarativeConfigSource(tempManagedPath, telemetry.OriginManagedStableConfig) + defer func() { + ManagedDeclarativeConfig = newDeclarativeConfigSource(managedFilePath, telemetry.OriginManagedStableConfig) + }() + + provider := DefaultConfigProvider() + assert.Equal(t, "managed", provider.getString("DD_SERVICE", "value")) + assert.Equal(t, true, provider.getBool("DD_TRACE_DEBUG", false)) + assert.Equal(t, "otherhost", provider.getString("DD_TRACE_SOURCE_HOSTNAME", "value")) + assert.Equal(t, 1, provider.getInt("DD_TRACE_PARTIAL_FLUSH_MIN_SPANS", 0)) + assert.Equal(t, "dev", provider.getString("DD_ENV", "value")) + assert.Equal(t, "1.0.0", provider.getString("DD_VERSION", "0")) + assert.Equal(t, true, provider.getBool("DD_TRACE_PARTIAL_FLUSH_ENABLED", false)) + + // Defaults are returned for settings that are not configured + assert.Equal(t, false, provider.getBool("DD_TRACE_STARTUP_LOGS", false)) + }) +} diff --git a/internal/config/declarativeconfig.go b/internal/config/declarativeconfig.go new file mode 100644 index 0000000000..67efd4e9b2 --- /dev/null +++ b/internal/config/declarativeconfig.go @@ -0,0 +1,35 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2025 Datadog, Inc. + +package config + +import "github.com/DataDog/dd-trace-go/v2/internal/telemetry" + +// declarativeConfig represents a configuration loaded from a YAML source file. +type declarativeConfig struct { + Config map[string]string `yaml:"apm_configuration_default,omitempty"` // Configuration key-value pairs. + ID string `yaml:"config_id,omitempty"` // Identifier for the config set. +} + +func (d *declarativeConfig) get(key string) string { + return d.Config[key] +} + +func (d *declarativeConfig) getID() string { + return d.ID +} + +// To be used by tests +// func (d *declarativeConfig) isEmpty() bool { +// return d.ID == telemetry.EmptyID && len(d.Config) == 0 +// } + +// emptyDeclarativeConfig creates and returns a new, empty declarativeConfig instance. +func emptyDeclarativeConfig() *declarativeConfig { + return &declarativeConfig{ + Config: make(map[string]string), + ID: telemetry.EmptyID, + } +} diff --git a/internal/config/declarativeconfigsource.go b/internal/config/declarativeconfigsource.go new file mode 100644 index 0000000000..42623a33da --- /dev/null +++ b/internal/config/declarativeconfigsource.go @@ -0,0 +1,99 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2025 Datadog, Inc. + +package config + +import ( + "os" + + "go.yaml.in/yaml/v3" + + "github.com/DataDog/dd-trace-go/v2/internal/log" + "github.com/DataDog/dd-trace-go/v2/internal/telemetry" +) + +const ( + // File paths are supported on linux only + localFilePath = "/etc/datadog-agent/application_monitoring.yaml" + managedFilePath = "/etc/datadog-agent/managed/datadog-agent/stable/application_monitoring.yaml" + + // maxFileSize defines the maximum size in bytes for declarative config files (4KB). This limit ensures predictable memory use and guards against malformed large files. + maxFileSize = 4 * 1024 +) + +// LocalDeclarativeConfig holds the configuration loaded from the user-managed file. +var LocalDeclarativeConfig = newDeclarativeConfigSource(localFilePath, telemetry.OriginLocalStableConfig) + +// ManagedDeclarativeConfig holds the configuration loaded from the fleet-managed file. +var ManagedDeclarativeConfig = newDeclarativeConfigSource(managedFilePath, telemetry.OriginManagedStableConfig) + +// declarativeConfigSource represents a source of declarative configuration loaded from a file. +type declarativeConfigSource struct { + filePath string // Path to the configuration file. + origin telemetry.Origin // Origin identifier for telemetry. + config *declarativeConfig // Parsed declarative configuration. +} + +func (d *declarativeConfigSource) Get(key string) string { + return d.config.get(normalizeKey(key)) +} + +func (d *declarativeConfigSource) GetID() string { + return d.config.getID() +} + +// newDeclarativeConfigSource initializes a new declarativeConfigSource from the given file. +func newDeclarativeConfigSource(filePath string, origin telemetry.Origin) *declarativeConfigSource { + return &declarativeConfigSource{ + filePath: filePath, + origin: origin, + config: parseFile(filePath), + } +} + +// ParseFile reads and parses the config file at the given path. +// Returns an empty config if the file doesn't exist or is invalid. +func parseFile(filePath string) *declarativeConfig { + info, err := os.Stat(filePath) + if err != nil { + // It's expected that the declarative config file may not exist; its absence is not an error. + if !os.IsNotExist(err) { + log.Warn("Failed to stat declarative config file %q, dropping: %v", filePath, err.Error()) + } + return emptyDeclarativeConfig() + } + + if info.Size() > maxFileSize { + log.Warn("Declarative config file %s exceeds size limit (%d bytes > %d bytes), dropping", + filePath, info.Size(), maxFileSize) + return emptyDeclarativeConfig() + } + + data, err := os.ReadFile(filePath) + if err != nil { + // It's expected that the declarative config file may not exist; its absence is not an error. + if !os.IsNotExist(err) { + log.Warn("Failed to read declarative config file %q, dropping: %v", filePath, err.Error()) + } + return emptyDeclarativeConfig() + } + + return fileContentsToConfig(data, filePath) +} + +// fileContentsToConfig parses YAML data into a declarativeConfig struct. +// Returns an empty config if parsing fails or the data is malformed. +func fileContentsToConfig(data []byte, fileName string) *declarativeConfig { + dc := &declarativeConfig{} + err := yaml.Unmarshal(data, dc) + if err != nil { + log.Warn("Parsing declarative config file %s failed due to error, dropping: %v", fileName, err.Error()) + return emptyDeclarativeConfig() + } + if dc.Config == nil { + dc.Config = make(map[string]string) + } + return dc +} diff --git a/internal/config/envconfigsource.go b/internal/config/envconfigsource.go new file mode 100644 index 0000000000..874e72e66f --- /dev/null +++ b/internal/config/envconfigsource.go @@ -0,0 +1,14 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2025 Datadog, Inc. + +package config + +import "github.com/DataDog/dd-trace-go/v2/internal/env" + +type envConfigSource struct{} + +func (e *envConfigSource) Get(key string) string { + return env.Get(normalizeKey(key)) +} diff --git a/internal/config/otelenvconfigsource.go b/internal/config/otelenvconfigsource.go new file mode 100644 index 0000000000..23e224a4a5 --- /dev/null +++ b/internal/config/otelenvconfigsource.go @@ -0,0 +1,16 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2025 Datadog, Inc. + +package config + +// import "github.com/DataDog/dd-trace-go/v2/internal/env" + +// type otelEnvConfigSource struct{ +// keys map[string]string +// } + +// func (o *otelEnvConfigSource) Get(key string) string { +// return o.keys[key] +// }