diff --git a/ddtrace/tracer/option.go b/ddtrace/tracer/option.go index 53d57477cee..721aba14e8b 100644 --- a/ddtrace/tracer/option.go +++ b/ddtrace/tracer/option.go @@ -474,7 +474,7 @@ func newConfig(opts ...StartOption) (*config, error) { } if c.statsdClient == nil { // configure statsd client - addr := resolveDogstatsdAddr(c.dogstatsdAddr, af) + addr := resolveDogstatsdAddr(c.dogstatsdAddr, af, defaultSocketDSD) globalconfig.SetDogstatsdAddr(addr) c.dogstatsdAddr = addr } @@ -543,41 +543,59 @@ func resolveTraceTransport(cfg *internalconfig.Config) (traceURL string, headers return traceURL, datadogHeaders() } -// resolveDogstatsdAddr resolves the Dogstatsd address to use, based on the user-defined -// address and the agent-reported port. If the agent reports a port, it will be used -// instead of the user-defined address' port. UDS paths are honored regardless of the -// agent-reported port. -func resolveDogstatsdAddr(addr string, af agentFeatures) string { - if addr == "" { - // no config defined address; use host and port from env vars - // or default to localhost:8125 if not set - addr = defaultDogstatsdAddr() - } - agentport := af.StatsdPort - if agentport == 0 { - // the agent didn't report a port; use the already resolved address as - // features are loaded from the trace-agent, which might be not running - return addr - } - // the agent reported a port - host, _, err := net.SplitHostPort(addr) - if err != nil { - // parsing the address failed; use the already resolved address as is - return addr +// resolveDogstatsdAddr resolves the Dogstatsd address to use with the following +// priority order: +// 1. Explicitly configured address via WithDogstatsdAddr. +// 2. Environment variables DD_DOGSTATSD_HOST (or DD_AGENT_HOST) and DD_DOGSTATSD_PORT. +// 3. Auto-discovery: UDS socket at /var/run/datadog/dsd.socket, agent-reported port, +// or the default localhost:8125. +func resolveDogstatsdAddr(configAddr string, af agentFeatures, socketDSDPath string) string { + // 1. User explicitly set address via WithDogstatsdAddr; honor it as-is. + if configAddr != "" { + return configAddr + } + + // 2. Build address from dogstatsd-specific environment variables. + // DD_AGENT_HOST is only used as a host fallback when DD_DOGSTATSD_HOST or + // DD_DOGSTATSD_PORT is set — on its own it does not trigger this path. + envHost := env.Get("DD_DOGSTATSD_HOST") + envPort := env.Get("DD_DOGSTATSD_PORT") + if envHost != "" || envPort != "" { + host := envHost + if host == "" { + host = env.Get("DD_AGENT_HOST") + } + if host == "" { + host = defaultHostname + } + // For the port, prefer the env var, then the agent-reported port + // (loaded from the trace-agent /info endpoint), then the default. + port := envPort + if port == "" && af.StatsdPort != 0 { + port = strconv.Itoa(af.StatsdPort) + } else if port == "" { + port = defaultStatsdPort + } + return net.JoinHostPort(host, port) } - if host == "unix" { - // no need to change the address because it's a UDS connection - // and these don't have ports - return addr + + // 3. No user configuration at all — auto-discover. + // Check for the UDS socket first; this is the preferred transport when available. + if _, err := os.Stat(socketDSDPath); err == nil { + return "unix://" + socketDSDPath } + // For TCP, use DD_AGENT_HOST as the hostname if available, otherwise localhost. + host := env.Get("DD_AGENT_HOST") if host == "" { - // no host was provided; use the default hostname host = defaultHostname } - // use agent-reported address if it differs from the user-defined TCP-based protocol URI - // we have a valid host:port address; replace the port because the agent knows better - addr = net.JoinHostPort(host, strconv.Itoa(agentport)) - return addr + // Use the agent-reported port if available. Agent features are loaded from + // the trace-agent, which may not be running — in that case StatsdPort is 0. + if af.StatsdPort != 0 { + return net.JoinHostPort(host, strconv.Itoa(af.StatsdPort)) + } + // Fall back to default port 8125. + return net.JoinHostPort(host, defaultStatsdPort) } func newStatsdClient(c *config) (internal.StatsdClient, error) { @@ -587,26 +605,6 @@ func newStatsdClient(c *config) (internal.StatsdClient, error) { return internal.NewStatsdClient(c.dogstatsdAddr, statsTags(c)) } -// defaultDogstatsdAddr returns the default connection address for Dogstatsd. -func defaultDogstatsdAddr() string { - envHost, envPort := env.Get("DD_DOGSTATSD_HOST"), env.Get("DD_DOGSTATSD_PORT") - if envHost == "" { - envHost = env.Get("DD_AGENT_HOST") - } - if _, err := os.Stat(defaultSocketDSD); err == nil && envHost == "" && envPort == "" { - // socket exists and user didn't specify otherwise via env vars - return "unix://" + defaultSocketDSD - } - host, port := defaultHostname, defaultStatsdPort - if envHost != "" { - host = envHost - } - if envPort != "" { - port = envPort - } - return net.JoinHostPort(host, port) -} - type integrationConfig struct { Instrumented bool `json:"instrumented"` // indicates if the user has imported and used the integration Available bool `json:"available"` // indicates if the user is using a library that can be used with DataDog integrations diff --git a/ddtrace/tracer/option_test.go b/ddtrace/tracer/option_test.go index 9e236ab926f..1706f646718 100644 --- a/ddtrace/tracer/option_test.go +++ b/ddtrace/tracer/option_test.go @@ -561,8 +561,8 @@ func TestTracerOptionsDefaults(t *testing.T) { defer tracer.Stop() assert.NoError(t, err) c := tracer.config - assert.Equal(t, "localhost:8125", c.dogstatsdAddr) - assert.Equal(t, "localhost:8125", globalconfig.DogstatsdAddr()) + assert.Equal(t, "localhost:123", c.dogstatsdAddr) + assert.Equal(t, "localhost:123", globalconfig.DogstatsdAddr()) }) t.Run("env-port: agent not available", func(t *testing.T) { @@ -585,8 +585,8 @@ func TestTracerOptionsDefaults(t *testing.T) { assert.NoError(t, err) defer tracer.Stop() c := tracer.config - assert.Equal(t, "localhost:8125", c.dogstatsdAddr) - assert.Equal(t, "localhost:8125", globalconfig.DogstatsdAddr()) + assert.Equal(t, "localhost:123", c.dogstatsdAddr) + assert.Equal(t, "localhost:123", globalconfig.DogstatsdAddr()) }) t.Run("env-all: agent not available", func(t *testing.T) { @@ -611,8 +611,8 @@ func TestTracerOptionsDefaults(t *testing.T) { assert.NoError(t, err) defer tracer.Stop() c := tracer.config - assert.Equal(t, "10.1.0.12:8125", c.dogstatsdAddr) - assert.Equal(t, "10.1.0.12:8125", globalconfig.DogstatsdAddr()) + assert.Equal(t, "10.1.0.12:4002", c.dogstatsdAddr) + assert.Equal(t, "10.1.0.12:4002", globalconfig.DogstatsdAddr()) }) t.Run("option: agent not available", func(t *testing.T) { @@ -1022,80 +1022,147 @@ func TestDefaultHTTPClient(t *testing.T) { }) } -func TestDefaultDogstatsdAddr(t *testing.T) { - t.Run("no-socket", func(t *testing.T) { - assert.Equal(t, defaultDogstatsdAddr(), "localhost:8125") - }) - - t.Run("host-env", func(t *testing.T) { - t.Setenv("DD_DOGSTATSD_HOST", "111.111.1.1") - t.Setenv("DD_AGENT_HOST", "222.222.2.2") - assert.Equal(t, "111.111.1.1:8125", defaultDogstatsdAddr()) - }) - - t.Run("port-env", func(t *testing.T) { - t.Setenv("DD_DOGSTATSD_PORT", "8111") - assert.Equal(t, defaultDogstatsdAddr(), "localhost:8111") - t.Setenv("DD_AGENT_HOST", "222.222.2.2") - assert.Equal(t, defaultDogstatsdAddr(), "222.222.2.2:8111") - }) - - t.Run("host-env+port-env", func(t *testing.T) { - t.Setenv("DD_DOGSTATSD_HOST", "111.111.1.1") - t.Setenv("DD_DOGSTATSD_PORT", "8888") - t.Setenv("DD_AGENT_HOST", "222.222.2.2") - assert.Equal(t, "111.111.1.1:8888", defaultDogstatsdAddr()) - }) - - t.Run("host-env+socket", func(t *testing.T) { - t.Setenv("DD_DOGSTATSD_HOST", "111.111.1.1") - assert.Equal(t, "111.111.1.1:8125", defaultDogstatsdAddr()) - f, err := os.CreateTemp("", "dsd.socket") - if err != nil { - t.Fatal(err) - } - if err := f.Close(); err != nil { - t.Fatal(err) - } - defer os.RemoveAll(f.Name()) - defer func(old string) { defaultSocketDSD = old }(defaultSocketDSD) - defaultSocketDSD = f.Name() - assert.Equal(t, "111.111.1.1:8125", defaultDogstatsdAddr()) - }) - - t.Run("port-env+socket", func(t *testing.T) { - t.Setenv("DD_DOGSTATSD_PORT", "8111") - assert.Equal(t, defaultDogstatsdAddr(), "localhost:8111") - f, err := os.CreateTemp("", "dsd.socket") - if err != nil { - t.Fatal(err) - } - if err := f.Close(); err != nil { - t.Fatal(err) - } - defer os.RemoveAll(f.Name()) - defer func(old string) { defaultSocketDSD = old }(defaultSocketDSD) - defaultSocketDSD = f.Name() - assert.Equal(t, defaultDogstatsdAddr(), "localhost:8111") - }) +func TestResolveDogstatsdAddr(t *testing.T) { + socketFile, err := os.CreateTemp("", "dsd.socket") + require.NoError(t, err) + require.NoError(t, socketFile.Close()) + t.Cleanup(func() { os.RemoveAll(socketFile.Name()) }) + socketPath := socketFile.Name() + + tests := []struct { + name string + configAddr string + af agentFeatures + env map[string]string + socketPath string + expected string + }{ + { + name: "defaults", + expected: "localhost:8125", + }, + { + name: "host-env", + env: map[string]string{"DD_DOGSTATSD_HOST": "111.111.1.1", "DD_AGENT_HOST": "222.222.2.2"}, + expected: "111.111.1.1:8125", + }, + { + name: "port-env", + env: map[string]string{"DD_DOGSTATSD_PORT": "8111"}, + expected: "localhost:8111", + }, + { + name: "port-env+agent-host-env", + env: map[string]string{"DD_DOGSTATSD_PORT": "8111", "DD_AGENT_HOST": "222.222.2.2"}, + expected: "222.222.2.2:8111", + }, + { + name: "host-env+port-env", + env: map[string]string{"DD_DOGSTATSD_HOST": "111.111.1.1", "DD_DOGSTATSD_PORT": "8888", "DD_AGENT_HOST": "222.222.2.2"}, + expected: "111.111.1.1:8888", + }, + { + name: "host-env+socket", + env: map[string]string{"DD_DOGSTATSD_HOST": "111.111.1.1"}, + socketPath: socketPath, + expected: "111.111.1.1:8125", + }, + { + name: "port-env+socket", + env: map[string]string{"DD_DOGSTATSD_PORT": "8111"}, + socketPath: socketPath, + expected: "localhost:8111", + }, + { + name: "socket", + socketPath: socketPath, + expected: "unix://" + socketPath, + }, + // DD_AGENT_HOST alone should not trigger the env var path; + // it falls through to auto-discovery. + { + name: "agent-host-env-only+socket", + env: map[string]string{"DD_AGENT_HOST": "222.222.2.2"}, + socketPath: socketPath, + expected: "unix://" + socketPath, + }, + { + name: "agent-host-env-only", + env: map[string]string{"DD_AGENT_HOST": "222.222.2.2"}, + expected: "222.222.2.2:8125", + }, + { + name: "agent-host-env-only+agent-port", + env: map[string]string{"DD_AGENT_HOST": "222.222.2.2"}, + af: agentFeatures{StatsdPort: 9876}, + expected: "222.222.2.2:9876", + }, + // configAddr (priority 1) wins over everything. + { + name: "config-addr", + configAddr: "custom:9999", + expected: "custom:9999", + }, + { + name: "config-addr+env", + configAddr: "custom:9999", + env: map[string]string{"DD_DOGSTATSD_HOST": "111.111.1.1", "DD_DOGSTATSD_PORT": "8111"}, + expected: "custom:9999", + }, + { + name: "config-addr+socket", + configAddr: "custom:9999", + socketPath: socketPath, + expected: "custom:9999", + }, + { + name: "config-addr+agent-port", + configAddr: "custom:9999", + af: agentFeatures{StatsdPort: 9876}, + expected: "custom:9999", + }, + // Agent-reported port used as fallback when env host is set but no env port. + { + name: "host-env+agent-port", + env: map[string]string{"DD_DOGSTATSD_HOST": "111.111.1.1"}, + af: agentFeatures{StatsdPort: 9876}, + expected: "111.111.1.1:9876", + }, + // Env port wins over agent-reported port. + { + name: "host-env+port-env+agent-port", + env: map[string]string{"DD_DOGSTATSD_HOST": "111.111.1.1", "DD_DOGSTATSD_PORT": "8111"}, + af: agentFeatures{StatsdPort: 9876}, + expected: "111.111.1.1:8111", + }, + // Auto-discovery: agent-reported port when no env and no socket. + { + name: "agent-port", + af: agentFeatures{StatsdPort: 9876}, + expected: "localhost:9876", + }, + // Auto-discovery: socket wins over agent-reported port. + { + name: "socket+agent-port", + af: agentFeatures{StatsdPort: 9876}, + socketPath: socketPath, + expected: "unix://" + socketPath, + }, + } - t.Run("socket", func(t *testing.T) { - defer func(old string) { os.Setenv("DD_AGENT_HOST", old) }(os.Getenv("DD_AGENT_HOST")) - defer func(old string) { os.Setenv("DD_DOGSTATSD_PORT", old) }(os.Getenv("DD_DOGSTATSD_PORT")) - os.Unsetenv("DD_AGENT_HOST") - os.Unsetenv("DD_DOGSTATSD_PORT") - f, err := os.CreateTemp("", "dsd.socket") - if err != nil { - t.Fatal(err) - } - if err := f.Close(); err != nil { - t.Fatal(err) - } - defer os.RemoveAll(f.Name()) - defer func(old string) { defaultSocketDSD = old }(defaultSocketDSD) - defaultSocketDSD = f.Name() - assert.Equal(t, defaultDogstatsdAddr(), "unix://"+f.Name()) - }) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Clear all relevant env vars so that each subtest is isolated + // from the host environment and other subtests. + for _, key := range []string{"DD_DOGSTATSD_HOST", "DD_DOGSTATSD_PORT", "DD_AGENT_HOST"} { + t.Setenv(key, "") + } + for k, v := range tt.env { + t.Setenv(k, v) + } + assert.Equal(t, tt.expected, resolveDogstatsdAddr(tt.configAddr, tt.af, tt.socketPath)) + }) + } } func TestServiceName(t *testing.T) {