Repository: open-telemetry/opentelemetry-collector-contrib
Pull request #39614: [receiver/dockerstatsreceiver] Updated dockerstatsreceiver to respect DOCKER_HOST env variable
Author: login: sonalgaud12
Labels: name: receiver/dockerstats
Created: 2025-04-24T10:25:35Z | Merged: 2025-04-29T17:16:42Z

Description
The dockerstatsreceiver previously defaulted to using unix:///var/run/docker.sock for the Docker endpoint. While the configuration allows overriding this, it did not automatically respect the DOCKER_HOST environment variable. This update ensures that the receiver resolves the endpoint in the following order:


Returns Docker client options, using the DOCKER_HOST environment variable if no endpoint is configured
Link to tracking issue
Fixes https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/35779





Diff:
diff --git a/.chloggen/dockerstatsfix.yaml b/.chloggen/dockerstatsfix.yaml
new file mode 100644
index 0000000000000..903c9fa02d7c8
--- /dev/null
+++ b/.chloggen/dockerstatsfix.yaml
@@ -0,0 +1,11 @@
+change_type: enhancement  
+
+component: dockerstatsreceiver  
+
+note: "Returns Docker client options, using the DOCKER_HOST environment variable if no endpoint is configured."  
+
+issues: [35779]  
+
+subtext: |  
+  - Previously, the receiver required explicit configuration of the endpoint.  
+  - This change allows automatic detection of DOCKER_HOST, improving flexibility in different environments. 
\ No newline at end of file
diff --git a/receiver/dockerstatsreceiver/receiver.go b/receiver/dockerstatsreceiver/receiver.go
index 748fa2619cd1b..997679da08c5e 100644
--- a/receiver/dockerstatsreceiver/receiver.go
+++ b/receiver/dockerstatsreceiver/receiver.go
@@ -12,6 +12,7 @@ import (
 	"time"
 
 	ctypes "github.com/docker/docker/api/types/container"
+	"github.com/docker/docker/client"
 	"go.opentelemetry.io/collector/component"
 	"go.opentelemetry.io/collector/pdata/pcommon"
 	"go.opentelemetry.io/collector/pdata/pmetric"
@@ -50,9 +51,17 @@ func newMetricsReceiver(set receiver.Settings, config *Config) *metricsReceiver
 	}
 }
 
+func (r *metricsReceiver) clientOptions() []client.Opt {
+	var opts []client.Opt
+	if r.config.Endpoint == "" {
+		opts = append(opts, client.WithHostFromEnv())
+	}
+	return opts
+}
+
 func (r *metricsReceiver) start(ctx context.Context, _ component.Host) error {
 	var err error
-	r.client, err = docker.NewDockerClient(&r.config.Config, r.settings.Logger)
+	r.client, err = docker.NewDockerClient(&r.config.Config, r.settings.Logger, r.clientOptions()...)
 	if err != nil {
 		return err
 	}
diff --git a/receiver/dockerstatsreceiver/receiver_test.go b/receiver/dockerstatsreceiver/receiver_test.go
index a37231dcf47c5..bab7d572f6f3d 100644
--- a/receiver/dockerstatsreceiver/receiver_test.go
+++ b/receiver/dockerstatsreceiver/receiver_test.go
@@ -120,6 +120,44 @@ var (
 	}
 )
 
+func TestClientOptions(t *testing.T) {
+	tests := []struct {
+		name        string
+		endpoint    string
+		expectEnv   bool
+		description string
+	}{
+		{
+			name:        "Empty endpoint, DOCKER_HOST set",
+			endpoint:    "",
+			expectEnv:   true,
+			description: "Should append WithHostFromEnv() when Endpoint is empty.",
+		},
+		{
+			name:        "Config endpoint set, DOCKER_HOST ignored",
+			endpoint:    "tcp://config:1234",
+			expectEnv:   false,
+			description: "Should not append WithHostFromEnv() when Endpoint is set.",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			config := &Config{
+				Config: docker.Config{
+					Endpoint: tt.endpoint,
+				},
+			}
+
+			receiver := &metricsReceiver{config: config}
+			opts := receiver.clientOptions()
+
+			// If expectEnv is true, opts should not be empty
+			assert.Equal(t, tt.expectEnv, len(opts) > 0, tt.description)
+		})
+	}
+}
+
 func TestNewReceiver(t *testing.T) {
 	cfg := &Config{
 		ControllerConfig: scraperhelper.ControllerConfig{


Reviews:
user: login: dehaansa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T14:43:36Z
user: login: sonalgaud12
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T15:33:31Z
user: login: MovieStoreGuy
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T23:11:28Z
user: login: jamesmoessis
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T01:46:26Z
user: login: jamesmoessis
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T01:46:34Z
user: login: atoulme
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T17:16:37Z

Review comments:
diff_hunk: @@ -52,9 +53,18 @@ func newMetricsReceiver(set receiver.Settings, config *Config) *metricsReceiver
 	}
 }
 
+func (r *metricsReceiver) clientOptions() []client.Opt {
+	var opts []client.Opt
+	if r.config.Config.Endpoint == "" {
path: receiver/dockerstatsreceiver/receiver.go
user: login: dehaansa
body: As the factory defaults to the `"unix:///var/run/docker.sock"` value, this is unlikely to be encountered unless a user explicitly provides it as "", which seems counterintuitive (and at that point, they may as well configure it as `${env:DOCKER_HOST}` as it would be more explicit.

It would likely be seen as a breaking change, but perhaps the right optimization is to check if the environment variable is populated before looking at the configuration. I see another user tried this in an older [PR](https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/34404/files) that didn't get any review.
created_at: 2025-04-24T14:43:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,9 +53,18 @@ func newMetricsReceiver(set receiver.Settings, config *Config) *metricsReceiver
 	}
 }
 
+func (r *metricsReceiver) clientOptions() []client.Opt {
+	var opts []client.Opt
+	if r.config.Config.Endpoint == "" {
path: receiver/dockerstatsreceiver/receiver.go
user: login: sonalgaud12
body: In my opinion, the alternative approach is technically better from a user experience standpoint. Most Docker tools respect DOCKER_HOST by default, and users expect this behavior. Having to explicitly set an empty endpoint to make the receiver use DOCKER_HOST is somewhat counterintuitive. I'm open to adopting the alternative approach and will add appropriate tests to validate the behavior. I'd appreciate any suggestions on which implementation would be best to move forward. Also thanks for sharing this
created_at: 2025-04-24T15:33:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,9 +53,18 @@ func newMetricsReceiver(set receiver.Settings, config *Config) *metricsReceiver
 	}
 }
 
+func (r *metricsReceiver) clientOptions() []client.Opt {
+	var opts []client.Opt
+	if r.config.Config.Endpoint == "" {
path: receiver/dockerstatsreceiver/receiver.go
user: login: jamesmoessis
body: Hmm, this is a good point. The default is to use `unix:///var/run/docker.sock` if the user leaves it unconfigured. I must have missed this earlier. It does seem counter intuitive. But then, changing this would be a less-than-ideal breaking change. 

I think we can move forward with this for now and re evaluate whether we should change the default in a future PR.
created_at: 2025-04-29T01:46:25Z
author_association: CONTRIBUTOR

PR comments:
user: login: atoulme
created_at: 2025-04-27T00:08:23Z
body: Please address CI failures and mark ready for review again.
author_association: CONTRIBUTOR
user: login: sonalgaud12
created_at: 2025-04-27T04:56:32Z
body: I encountered a CODEOWNERS generation error due to duplicate entries in the allow list. How can I address the failure?  @atoulme 
author_association: CONTRIBUTOR
user: login: atoulme
created_at: 2025-04-27T22:32:40Z
body: Updating to latest main should help.
author_association: CONTRIBUTOR
user: login: sonalgaud12
created_at: 2025-04-28T06:12:50Z
body: Worked, requesting review from @jamesmoessis , @MovieStoreGuy 
author_association: CONTRIBUTOR