From af6bd4e505f0930f55972c6f8a2f326fdea14e95 Mon Sep 17 00:00:00 2001 From: Sven van Ginkel Date: Fri, 31 Oct 2025 00:02:09 +0100 Subject: [PATCH] [Feature] Add env var to exclude containers from being monitored (#1352) --- agent/docker.go | 46 +++++++++++++++++++ agent/docker_test.go | 104 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+) diff --git a/agent/docker.go b/agent/docker.go index 27803f19..1840b9db 100644 --- a/agent/docker.go +++ b/agent/docker.go @@ -13,6 +13,7 @@ import ( "net/http" "net/url" "os" + "path" "strings" "sync" "time" @@ -53,6 +54,7 @@ type dockerManager struct { buf *bytes.Buffer // Buffer to store and read response bodies decoder *json.Decoder // Reusable JSON decoder that reads from buf apiStats *container.ApiStats // Reusable API stats object + containerExclude []string // Patterns to exclude containers by name (supports wildcards) // Cache-time-aware tracking for CPU stats (similar to cpu.go) // Maps cache time intervals to container-specific CPU usage tracking @@ -94,6 +96,20 @@ func (d *dockerManager) dequeue() { } } +// shouldExcludeContainer checks if a container name matches any exclusion pattern using path.Match +func (dm *dockerManager) shouldExcludeContainer(name string) bool { + if len(dm.containerExclude) == 0 { + return false + } + for _, pattern := range dm.containerExclude { + // Use path.Match for wildcard support + if match, _ := path.Match(pattern, name); match { + return true + } + } + return false +} + // Returns stats for all running containers with cache-time-aware delta tracking func (dm *dockerManager) getDockerStats(cacheTimeMs uint16) ([]*container.Stats, error) { resp, err := dm.client.Get("http://localhost/containers/json") @@ -121,6 +137,19 @@ func (dm *dockerManager) getDockerStats(cacheTimeMs uint16) ([]*container.Stats, for _, ctr := range dm.apiContainerList { ctr.IdShort = ctr.Id[:12] + + // Extract container name and check if it should be excluded + name := ctr.Names[0] + if len(name) > 0 && name[0] == '/' { + name = name[1:] + } + + // Skip this container if it matches the exclusion pattern + if dm.shouldExcludeContainer(name) { + slog.Debug("Excluding container", "name", name, "patterns", dm.containerExclude) + continue + } + dm.validIds[ctr.IdShort] = struct{}{} // check if container is less than 1 minute old (possible restart) // note: can't use Created field because it's not updated on restart @@ -503,6 +532,22 @@ func newDockerManager(a *Agent) *dockerManager { userAgent: "Docker-Client/", } + // Read container exclusion patterns from environment variable (comma-separated, supports wildcards) + var containerExclude []string + if excludeStr, set := GetEnv("CONTAINER_EXCLUDE"); set && excludeStr != "" { + // Split by comma and trim whitespace + parts := strings.Split(excludeStr, ",") + for _, part := range parts { + trimmed := strings.TrimSpace(part) + if trimmed != "" { + containerExclude = append(containerExclude, trimmed) + } + } + if len(containerExclude) > 0 { + slog.Info("Container exclusion patterns set", "patterns", containerExclude) + } + } + manager := &dockerManager{ client: &http.Client{ Timeout: timeout, @@ -512,6 +557,7 @@ func newDockerManager(a *Agent) *dockerManager { sem: make(chan struct{}, 5), apiContainerList: []*container.ApiInfo{}, apiStats: &container.ApiStats{}, + containerExclude: containerExclude, // Initialize cache-time-aware tracking structures lastCpuContainer: make(map[uint16]map[string]uint64), diff --git a/agent/docker_test.go b/agent/docker_test.go index 599a684a..b79f3d10 100644 --- a/agent/docker_test.go +++ b/agent/docker_test.go @@ -1099,3 +1099,107 @@ func TestAllocateBuffer(t *testing.T) { }) } } + +func TestShouldExcludeContainer(t *testing.T) { + tests := []struct { + name string + containerName string + patterns []string + expected bool + }{ + { + name: "empty patterns excludes nothing", + containerName: "any-container", + patterns: []string{}, + expected: false, + }, + { + name: "exact match - excluded", + containerName: "test-web", + patterns: []string{"test-web", "test-api"}, + expected: true, + }, + { + name: "exact match - not excluded", + containerName: "prod-web", + patterns: []string{"test-web", "test-api"}, + expected: false, + }, + { + name: "wildcard prefix match - excluded", + containerName: "test-web", + patterns: []string{"test-*"}, + expected: true, + }, + { + name: "wildcard prefix match - not excluded", + containerName: "prod-web", + patterns: []string{"test-*"}, + expected: false, + }, + { + name: "wildcard suffix match - excluded", + containerName: "myapp-staging", + patterns: []string{"*-staging"}, + expected: true, + }, + { + name: "wildcard suffix match - not excluded", + containerName: "myapp-prod", + patterns: []string{"*-staging"}, + expected: false, + }, + { + name: "wildcard both sides match - excluded", + containerName: "test-myapp-staging", + patterns: []string{"*-myapp-*"}, + expected: true, + }, + { + name: "wildcard both sides match - not excluded", + containerName: "prod-yourapp-live", + patterns: []string{"*-myapp-*"}, + expected: false, + }, + { + name: "multiple patterns - matches first", + containerName: "test-container", + patterns: []string{"test-*", "*-staging"}, + expected: true, + }, + { + name: "multiple patterns - matches second", + containerName: "myapp-staging", + patterns: []string{"test-*", "*-staging"}, + expected: true, + }, + { + name: "multiple patterns - no match", + containerName: "prod-web", + patterns: []string{"test-*", "*-staging"}, + expected: false, + }, + { + name: "mixed exact and wildcard - exact match", + containerName: "temp-container", + patterns: []string{"temp-container", "test-*"}, + expected: true, + }, + { + name: "mixed exact and wildcard - wildcard match", + containerName: "test-web", + patterns: []string{"temp-container", "test-*"}, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dm := &dockerManager{ + containerExclude: tt.patterns, + } + result := dm.shouldExcludeContainer(tt.containerName) + assert.Equal(t, tt.expected, result) + }) + } +}