From 4b3b16ef1a4fd9f90a8d5e6858579b38c7eaccd8 Mon Sep 17 00:00:00 2001
From: Matteo Cerutti <matteo.cerutti@hotmail.co.uk>
Date: Thu, 8 Jun 2017 21:17:31 +0100
Subject: [PATCH] Add wildcard support for container inclusion/exclusion
 (#2793)

---
 plugins/inputs/docker/README.md      | 10 +++-
 plugins/inputs/docker/docker.go      | 74 +++++++++++++++++++++++-----
 plugins/inputs/docker/docker_test.go | 68 +++++++++++++++++++++++++
 3 files changed, 138 insertions(+), 14 deletions(-)

diff --git a/plugins/inputs/docker/README.md b/plugins/inputs/docker/README.md
index 12741d94..19102631 100644
--- a/plugins/inputs/docker/README.md
+++ b/plugins/inputs/docker/README.md
@@ -20,14 +20,22 @@ for the stat structure can be found
   ##   To use TCP, set endpoint = "tcp://[ip]:[port]"
   ##   To use environment variables (ie, docker-machine), set endpoint = "ENV"
   endpoint = "unix:///var/run/docker.sock"
-  ## Only collect metrics for these containers, collect all if empty
+
+  ## Only collect metrics for these containers. Values will be appended to container_name_include.
+  ## Deprecated (1.4.0), use container_name_include
   container_names = []
+
+  ## Containers to include and exclude. Collect all if empty. Globs accepted.
+  container_name_include = []
+  container_name_exclude = []
+
   ## Timeout for docker list, info, and stats commands
   timeout = "5s"
 
   ## Whether to report for each container per-device blkio (8:0, 8:1...) and
   ## network (eth0, eth1, ...) stats or not
   perdevice = true
+
   ## Whether to report for each container total blkio and network stats or not
   total = false
   
diff --git a/plugins/inputs/docker/docker.go b/plugins/inputs/docker/docker.go
index 13ade394..ca7b70f9 100644
--- a/plugins/inputs/docker/docker.go
+++ b/plugins/inputs/docker/docker.go
@@ -24,24 +24,33 @@ type DockerLabelFilter struct {
 	labelExclude filter.Filter
 }
 
+type DockerContainerFilter struct {
+	containerInclude filter.Filter
+	containerExclude filter.Filter
+}
+
 // Docker object
 type Docker struct {
 	Endpoint       string
 	ContainerNames []string
+
 	Timeout        internal.Duration
 	PerDevice      bool     `toml:"perdevice"`
 	Total          bool     `toml:"total"`
 	TagEnvironment []string `toml:"tag_env"`
 	LabelInclude   []string `toml:"docker_label_include"`
 	LabelExclude   []string `toml:"docker_label_exclude"`
+	LabelFilter    DockerLabelFilter
 
-	LabelFilter DockerLabelFilter
+	ContainerInclude []string `toml:"container_name_include"`
+	ContainerExclude []string `toml:"container_name_exclude"`
+	ContainerFilter  DockerContainerFilter
 
 	client      *client.Client
 	engine_host string
 
-	testing             bool
-	labelFiltersCreated bool
+	testing        bool
+	filtersCreated bool
 }
 
 // infoWrapper wraps client.Client.List for testing.
@@ -110,8 +119,15 @@ var sampleConfig = `
   ##   To use TCP, set endpoint = "tcp://[ip]:[port]"
   ##   To use environment variables (ie, docker-machine), set endpoint = "ENV"
   endpoint = "unix:///var/run/docker.sock"
+
   ## Only collect metrics for these containers, collect all if empty
   container_names = []
+
+  ## Containers to include and exclude. Globs accepted.
+  ## Note that an empty array for both will include all containers
+  container_name_include = []
+  container_name_exclude = []
+
   ## Timeout for docker list, info, and stats commands
   timeout = "5s"
 
@@ -161,13 +177,18 @@ func (d *Docker) Gather(acc telegraf.Accumulator) error {
 		}
 		d.client = c
 	}
+
 	// Create label filters if not already created
-	if !d.labelFiltersCreated {
+	if !d.filtersCreated {
 		err := d.createLabelFilters()
 		if err != nil {
 			return err
 		}
-		d.labelFiltersCreated = true
+		err = d.createContainerFilters()
+		if err != nil {
+			return err
+		}
+		d.filtersCreated = true
 	}
 
 	// Get daemon info
@@ -306,9 +327,12 @@ func (d *Docker) gatherContainer(
 		"container_image":   imageName,
 		"container_version": imageVersion,
 	}
-	if len(d.ContainerNames) > 0 {
-		if !sliceContains(cname, d.ContainerNames) {
-			return nil
+
+	if len(d.ContainerInclude) > 0 || len(d.ContainerExclude) > 0 {
+		if len(d.ContainerInclude) == 0 || !d.ContainerFilter.containerInclude.Match(cname) {
+			if len(d.ContainerExclude) == 0 || d.ContainerFilter.containerExclude.Match(cname) {
+				return nil
+			}
 		}
 	}
 
@@ -656,8 +680,32 @@ func parseSize(sizeStr string) (int64, error) {
 	return int64(size), nil
 }
 
+func (d *Docker) createContainerFilters() error {
+	if len(d.ContainerNames) > 0 {
+		d.ContainerInclude = append(d.ContainerInclude, d.ContainerNames...)
+	}
+
+	if len(d.ContainerInclude) != 0 {
+		var err error
+		d.ContainerFilter.containerInclude, err = filter.Compile(d.ContainerInclude)
+		if err != nil {
+			return err
+		}
+	}
+
+	if len(d.ContainerExclude) != 0 {
+		var err error
+		d.ContainerFilter.containerExclude, err = filter.Compile(d.ContainerExclude)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
 func (d *Docker) createLabelFilters() error {
-	if len(d.LabelInclude) != 0 && d.LabelFilter.labelInclude == nil {
+	if len(d.LabelInclude) != 0 {
 		var err error
 		d.LabelFilter.labelInclude, err = filter.Compile(d.LabelInclude)
 		if err != nil {
@@ -665,7 +713,7 @@ func (d *Docker) createLabelFilters() error {
 		}
 	}
 
-	if len(d.LabelExclude) != 0 && d.LabelFilter.labelExclude == nil {
+	if len(d.LabelExclude) != 0 {
 		var err error
 		d.LabelFilter.labelExclude, err = filter.Compile(d.LabelExclude)
 		if err != nil {
@@ -679,9 +727,9 @@ func (d *Docker) createLabelFilters() error {
 func init() {
 	inputs.Add("docker", func() telegraf.Input {
 		return &Docker{
-			PerDevice:           true,
-			Timeout:             internal.Duration{Duration: time.Second * 5},
-			labelFiltersCreated: false,
+			PerDevice:      true,
+			Timeout:        internal.Duration{Duration: time.Second * 5},
+			filtersCreated: false,
 		}
 	})
 }
diff --git a/plugins/inputs/docker/docker_test.go b/plugins/inputs/docker/docker_test.go
index 025d482f..45797df5 100644
--- a/plugins/inputs/docker/docker_test.go
+++ b/plugins/inputs/docker/docker_test.go
@@ -18,6 +18,7 @@ func TestDockerGatherContainerStats(t *testing.T) {
 		"container_name":  "redis",
 		"container_image": "redis/image",
 	}
+
 	gatherContainerStats(stats, &acc, tags, "123456789", true, true)
 
 	// test docker_container_net measurement
@@ -295,6 +296,73 @@ func TestDockerGatherLabels(t *testing.T) {
 	}
 }
 
+var gatherContainerNames = []struct {
+	include     []string
+	exclude     []string
+	expected    []string
+	notexpected []string
+}{
+	{[]string{}, []string{}, []string{"etcd", "etcd2"}, []string{}},
+	{[]string{"*"}, []string{}, []string{"etcd", "etcd2"}, []string{}},
+	{[]string{"etc*"}, []string{}, []string{"etcd", "etcd2"}, []string{}},
+	{[]string{"etcd"}, []string{}, []string{"etcd"}, []string{"etcd2"}},
+	{[]string{"etcd2*"}, []string{}, []string{"etcd2"}, []string{"etcd"}},
+	{[]string{}, []string{"etc*"}, []string{}, []string{"etcd", "etcd2"}},
+	{[]string{}, []string{"etcd"}, []string{"etcd2"}, []string{"etcd"}},
+	{[]string{"*"}, []string{"*"}, []string{"etcd", "etcd2"}, []string{}},
+	{[]string{}, []string{"*"}, []string{""}, []string{"etcd", "etcd2"}},
+}
+
+func TestContainerNames(t *testing.T) {
+	for _, tt := range gatherContainerNames {
+		var acc testutil.Accumulator
+
+		d := Docker{
+			client:           nil,
+			testing:          true,
+			ContainerInclude: tt.include,
+			ContainerExclude: tt.exclude,
+		}
+
+		err := d.Gather(&acc)
+		require.NoError(t, err)
+
+		for _, metric := range acc.Metrics {
+			if metric.Measurement == "docker_container_cpu" {
+				if val, ok := metric.Tags["container_name"]; ok {
+					var found bool = false
+					for _, cname := range tt.expected {
+						if val == cname {
+							found = true
+							break
+						}
+					}
+					if !found {
+						t.Errorf("Got unexpected container of %s. Test was -> Include: %s, Exclude: %s", val, tt.include, tt.exclude)
+					}
+				}
+			}
+		}
+
+		for _, metric := range acc.Metrics {
+			if metric.Measurement == "docker_container_cpu" {
+				if val, ok := metric.Tags["container_name"]; ok {
+					var found bool = false
+					for _, cname := range tt.notexpected {
+						if val == cname {
+							found = true
+							break
+						}
+					}
+					if found {
+						t.Errorf("Got unexpected container of %s. Test was -> Include: %s, Exclude: %s", val, tt.include, tt.exclude)
+					}
+				}
+			}
+		}
+	}
+}
+
 func TestDockerGatherInfo(t *testing.T) {
 	var acc testutil.Accumulator
 	d := Docker{
-- 
GitLab