diff --git a/README.md b/README.md index b9bda7ee0..c373321d5 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ Go.d.plugin is shipped with Netdata. | [dnsmasq](https://github.com/netdata/go.d.plugin/tree/master/modules/dnsmasq) | Dnsmasq DNS Forwarder | | [dnsmasq_dhcp](https://github.com/netdata/go.d.plugin/tree/master/modules/dnsmasq_dhcp) | Dnsmasq DHCP | | [dns_query](https://github.com/netdata/go.d.plugin/tree/master/modules/dnsquery) | DNS Query RTT | +| [docker](https://github.com/netdata/go.d.plugin/tree/master/modules/docker) | Docker Engine | | [docker_engine](https://github.com/netdata/go.d.plugin/tree/master/modules/docker_engine) | Docker Engine | | [dockerhub](https://github.com/netdata/go.d.plugin/tree/master/modules/dockerhub) | Docker Hub | | [elasticsearch](https://github.com/netdata/go.d.plugin/tree/master/modules/elasticsearch) | Elasticsearch | diff --git a/config/go.d.conf b/config/go.d.conf index a4b3490ae..9280e21e8 100644 --- a/config/go.d.conf +++ b/config/go.d.conf @@ -28,6 +28,7 @@ modules: # dnsmasq: yes # dnsmasq_dhcp: yes # dns_query: yes +# docker: yes # docker_engine: yes # dockerhub: yes # elasticsearch: yes diff --git a/config/go.d/docker.conf b/config/go.d/docker.conf new file mode 100644 index 000000000..81e1d4a66 --- /dev/null +++ b/config/go.d/docker.conf @@ -0,0 +1,88 @@ +# netdata go.d.plugin configuration for docker +# +# This file is in YAML format. Generally the format is: +# +# name: value +# +# There are 2 sections: +# - GLOBAL +# - JOBS +# +# +# [ GLOBAL ] +# These variables set the defaults for all JOBs, however each JOB may define its own, overriding the defaults. +# +# The GLOBAL section format: +# param1: value1 +# param2: value2 +# +# Currently supported global parameters: +# - update_every +# Data collection frequency in seconds. Default: 1. +# +# - autodetection_retry +# Re-check interval in seconds. Attempts to start the job are made once every interval. +# Zero means not to schedule re-check. Default: 0. +# +# - priority +# Priority is the relative priority of the charts as rendered on the web page, +# lower numbers make the charts appear before the ones with higher numbers. Default: 70000. +# +# +# [ JOBS ] +# JOBS allow you to collect values from multiple sources. +# Each source will have its own set of charts. +# +# IMPORTANT: +# - Parameter 'name' is mandatory. +# - Jobs with the same name are mutually exclusive. Only one of them will be allowed running at any time. +# +# This allows autodetection to try several alternatives and pick the one that works. +# Any number of jobs is supported. +# +# The JOBS section format: +# +# jobs: +# - name: job1 +# param1: value1 +# param2: value2 +# +# - name: job2 +# param1: value1 +# param2: value2 +# +# - name: job2 +# param1: value1 +# +# +# [ List of JOB specific parameters ]: +# - address +# Docker daemon listen address. If using TCP socket: "tcp://[ip]:[port]". +# Syntax: +# address: 'unix:///var/run/docker.sock' +# +# - timeout +# Request timeout in seconds. +# Syntax: +# timeout: 1 +# +# +# [ JOB defaults ]: +# address: 'unix:///var/run/docker.sock' +# timeout: 1 +# +# +# [ JOB mandatory parameters ]: +# - name +# - address +# +# ------------------------------------------------MODULE-CONFIGURATION-------------------------------------------------- + +# update_every: 1 +# autodetection_retry: 0 +# priority: 70000 + +jobs: + - name: local + address: 'unix:///var/run/docker.sock' + timeout: 1 diff --git a/go.mod b/go.mod index 5df97644f..4df856941 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/blang/semver/v4 v4.0.0 github.com/cloudflare/cfssl v1.6.1 github.com/coreos/go-systemd/v22 v22.3.2 + github.com/docker/docker v20.10.17+incompatible github.com/facebook/time v0.0.0-20220713141651-bfc62ed6ec5f github.com/fsnotify/fsnotify v1.5.4 github.com/go-redis/redis/v8 v8.11.5 @@ -44,6 +45,8 @@ require ( require ( cloud.google.com/go/compute v1.6.1 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.5.1 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -57,6 +60,9 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/docker/distribution v2.7.1+incompatible // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.4.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect github.com/emicklei/go-restful v2.9.5+incompatible // indirect github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 // indirect @@ -112,6 +118,8 @@ require ( github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.0.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.12.2 // indirect diff --git a/go.sum b/go.sum index 392da9979..787f5b541 100644 --- a/go.sum +++ b/go.sum @@ -71,6 +71,8 @@ github.com/Azure/azure-sdk-for-go v29.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9mo github.com/Azure/azure-sdk-for-go v30.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-service-bus-go v0.9.1/go.mod h1:yzBx6/BUGfjfeqbRZny9AQIbIe3AcV9WZbAdpkoXOa0= github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-autorest v12.0.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= @@ -98,6 +100,8 @@ github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030I github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/sprig v2.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY= +github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= @@ -246,6 +250,14 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= +github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= +github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v20.10.17+incompatible h1:JYCuMrWaVNophQTOrMMoSwudOVEfcegoZZrleKc1xwE= +github.com/docker/docker v20.10.17+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= @@ -742,6 +754,7 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -752,6 +765,7 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= @@ -794,6 +808,10 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= @@ -1682,6 +1700,7 @@ gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/modules/docker/README.md b/modules/docker/README.md new file mode 100644 index 000000000..4181fdd3d --- /dev/null +++ b/modules/docker/README.md @@ -0,0 +1,65 @@ + + +# Docker monitoring with Netdata + +[Docker Engine](https://docs.docker.com/engine/) is an open source containerization technology for building and +containerizing your applications. + +This module monitors one or more Docker Engine instances, depending on your configuration. + +## Metrics + +All metrics have "docker." prefix. + +| Metric | Scope | Dimensions | Units | +|----------------------|:------:|:------------------------:|:----------:| +| containers_state | global | running, paused, stopped | containers | +| healthy_containers | global | healthy | containers | +| unhealthy_containers | global | unhealthy | containers | + +## Configuration + +Edit the `go.d/docker.conf` configuration file using `edit-config` from the +Netdata [config directory](https://learn.netdata.cloud/docs/configure/nodes), which is typically at `/etc/netdata`. + +```bash +cd /etc/netdata # Replace this path with your Netdata config directory +sudo ./edit-config go.d/docker.conf +``` + +```yaml +jobs: + - name: local + address: 'unix:///var/run/docker.sock' + + - name: remote + address: 'tcp://203.0.113.10:2375' +``` + +For all available options see +module [configuration file](https://github.com/netdata/go.d.plugin/blob/master/config/go.d/docker.conf). + +## Troubleshooting + +To troubleshoot issues with the `docker` collector, run the `go.d.plugin` with the debug option enabled. The output +should give you clues as to why the collector isn't working. + +First, navigate to your plugins' directory, usually at `/usr/libexec/netdata/plugins.d/`. If that's not the case on your +system, open `netdata.conf` and look for the setting `plugins directory`. Once you're in the plugin's directory, switch +to the `netdata` user. + +```bash +cd /usr/libexec/netdata/plugins.d/ +sudo -u netdata -s +``` + +You can now run the `go.d.plugin` to debug the collector: + +```bash +./go.d.plugin -d -m docker +``` diff --git a/modules/docker/charts.go b/modules/docker/charts.go new file mode 100644 index 000000000..6e0b906fa --- /dev/null +++ b/modules/docker/charts.go @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package docker + +import "github.com/netdata/go.d.plugin/agent/module" + +const ( + prioContainersState = module.Priority + iota + prioContainersHealthy + prioContainersUnhealthy +) + +var charts = module.Charts{ + containersStateChart.Copy(), + containersHealthyChart.Copy(), + containersUnhealthyChart.Copy(), +} + +var containersStateChart = module.Chart{ + ID: "containers_state", + Title: "Number of containers in different states", + Units: "containers", + Fam: "containers state", + Ctx: "docker.containers_state", + Priority: prioContainersState, + Type: module.Stacked, + Dims: module.Dims{ + {ID: "running_containers", Name: "running"}, + {ID: "paused_containers", Name: "paused"}, + {ID: "stopped_containers", Name: "stopped"}, + }, +} + +var containersHealthyChart = module.Chart{ + ID: "healthy_containers", + Title: "Number of healthy containers", + Units: "containers", + Fam: "containers health", + Ctx: "docker.healthy_containers", + Priority: prioContainersHealthy, + Dims: module.Dims{ + {ID: "healthy_containers", Name: "healthy"}, + }, +} + +var containersUnhealthyChart = module.Chart{ + ID: "unhealthy_containers", + Title: "Number of unhealthy containers", + Units: "containers", + Fam: "containers health", + Ctx: "docker.unhealthy_containers", + Priority: prioContainersUnhealthy, + Dims: module.Dims{ + {ID: "unhealthy_containers", Name: "unhealthy"}, + }, +} diff --git a/modules/docker/collect.go b/modules/docker/collect.go new file mode 100644 index 000000000..dcd4edb43 --- /dev/null +++ b/modules/docker/collect.go @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package docker + +import ( + "context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" +) + +func (d *Docker) collect() (map[string]int64, error) { + if d.client == nil { + client, err := d.newClient(d.Config) + if err != nil { + return nil, err + } + d.client = client + } + + defer func() { _ = d.client.Close() }() + + mx := make(map[string]int64) + + if err := d.collectInfo(mx); err != nil { + return nil, err + } + if err := d.collectContainersHealth(mx); err != nil { + return nil, err + } + + return mx, nil +} + +func (d *Docker) collectInfo(mx map[string]int64) error { + ctx, cancel := context.WithTimeout(context.Background(), d.Timeout.Duration) + defer cancel() + + info, err := d.client.Info(ctx) + if err != nil { + return err + } + + mx["running_containers"] = int64(info.ContainersRunning) + mx["paused_containers"] = int64(info.ContainersPaused) + mx["stopped_containers"] = int64(info.ContainersStopped) + + return nil +} + +func (d *Docker) collectContainersHealth(mx map[string]int64) error { + ctx1, cancel1 := context.WithTimeout(context.Background(), d.Timeout.Duration) + defer cancel1() + + args := filters.NewArgs(filters.KeyValuePair{Key: "health", Value: "healthy"}) + healthy, err := d.client.ContainerList(ctx1, types.ContainerListOptions{Filters: args}) + if err != nil { + return err + } + + ctx2, cancel2 := context.WithTimeout(context.Background(), d.Timeout.Duration) + defer cancel2() + + args = filters.NewArgs(filters.KeyValuePair{Key: "health", Value: "unhealthy"}) + unhealthy, err := d.client.ContainerList(ctx2, types.ContainerListOptions{Filters: args}) + if err != nil { + return err + } + + mx["healthy_containers"] = int64(len(healthy)) + mx["unhealthy_containers"] = int64(len(unhealthy)) + + return nil +} diff --git a/modules/docker/docker.go b/modules/docker/docker.go new file mode 100644 index 000000000..614826f56 --- /dev/null +++ b/modules/docker/docker.go @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package docker + +import ( + "context" + "time" + + "github.com/netdata/go.d.plugin/agent/module" + "github.com/netdata/go.d.plugin/pkg/web" + + "github.com/docker/docker/api/types" + docker "github.com/docker/docker/client" +) + +func init() { + module.Register("docker", module.Creator{ + Create: func() module.Module { return New() }, + }) +} + +func New() *Docker { + return &Docker{ + Config: Config{ + Address: docker.DefaultDockerHost, + Timeout: web.Duration{Duration: time.Second * 1}, + }, + charts: charts.Copy(), + newClient: func(cfg Config) (dockerClient, error) { + return docker.NewClientWithOpts(docker.WithHost(cfg.Address)) + }, + } +} + +type Config struct { + Timeout web.Duration `yaml:"timeout"` + Address string `yaml:"address"` +} + +type ( + Docker struct { + module.Base + Config `yaml:",inline"` + + charts *module.Charts + + newClient func(Config) (dockerClient, error) + client dockerClient + } + dockerClient interface { + Info(context.Context) (types.Info, error) + ContainerList(context.Context, types.ContainerListOptions) ([]types.Container, error) + Close() error + } +) + +func (d *Docker) Init() bool { + return true +} + +func (d *Docker) Check() bool { + return len(d.Collect()) > 0 +} + +func (d *Docker) Charts() *module.Charts { + return d.charts +} + +func (d *Docker) Collect() map[string]int64 { + mx, err := d.collect() + if err != nil { + d.Error(err) + } + + if len(mx) == 0 { + return nil + } + return mx +} + +func (d *Docker) Cleanup() { + if d.client == nil { + return + } + if err := d.client.Close(); err != nil { + d.Warningf("error on closing docker client: %v", err) + } + d.client = nil +} diff --git a/modules/docker/docker_test.go b/modules/docker/docker_test.go new file mode 100644 index 000000000..d10c51e8b --- /dev/null +++ b/modules/docker/docker_test.go @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package docker + +import ( + "context" + "errors" + "testing" + + "github.com/docker/docker/api/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDocker_Init(t *testing.T) { + tests := map[string]struct { + config Config + wantFail bool + }{ + "default config": { + wantFail: false, + config: New().Config, + }, + "unset 'address'": { + wantFail: false, + config: Config{ + Address: "", + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + d := New() + d.Config = test.config + + if test.wantFail { + assert.False(t, d.Init()) + } else { + assert.True(t, d.Init()) + } + }) + } +} + +func TestDocker_Charts(t *testing.T) { + assert.Equal(t, len(charts), len(*New().Charts())) +} + +func TestDocker_Cleanup(t *testing.T) { + tests := map[string]struct { + prepare func(d *Docker) + wantClose bool + }{ + "after New": { + wantClose: false, + prepare: func(d *Docker) {}, + }, + "after Init": { + wantClose: false, + prepare: func(d *Docker) { d.Init() }, + }, + "after Check": { + wantClose: true, + prepare: func(d *Docker) { d.Init(); d.Check() }, + }, + "after Collect": { + wantClose: true, + prepare: func(d *Docker) { d.Init(); d.Collect() }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + m := &mockClient{} + d := prepareDockerWithMock(m) + test.prepare(d) + + require.NotPanics(t, d.Cleanup) + + if test.wantClose { + assert.True(t, m.closeCalled) + } else { + assert.False(t, m.closeCalled) + } + }) + } + +} + +func TestDocker_Check(t *testing.T) { + tests := map[string]struct { + prepare func() *Docker + wantFail bool + }{ + "success when no errors on all calls": { + wantFail: false, + prepare: func() *Docker { return prepareDockerWithMock(&mockClient{}) }, + }, + "fail when error on creating docker client": { + wantFail: true, + prepare: func() *Docker { return prepareDockerWithMock(nil) }, + }, + "fail when error on Info()": { + wantFail: true, + prepare: func() *Docker { return prepareDockerWithMock(&mockClient{errOnInfo: true}) }, + }, + "fail when error on ContainerList()": { + wantFail: true, + prepare: func() *Docker { return prepareDockerWithMock(&mockClient{errOnContainerList: true}) }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + d := test.prepare() + + require.True(t, d.Init()) + + if test.wantFail { + assert.False(t, d.Check()) + } else { + assert.True(t, d.Check()) + } + }) + } +} + +func TestDocker_Collect(t *testing.T) { + tests := map[string]struct { + prepare func() *Docker + expected map[string]int64 + }{ + "success when no errors on all calls": { + prepare: func() *Docker { return prepareDockerWithMock(&mockClient{}) }, + expected: map[string]int64{ + "healthy_containers": 2, + "paused_containers": 5, + "running_containers": 4, + "stopped_containers": 6, + "unhealthy_containers": 3, + }, + }, + "fail when error on creating docker client": { + prepare: func() *Docker { return prepareDockerWithMock(nil) }, + expected: nil, + }, + "fail when error on Info()": { + prepare: func() *Docker { return prepareDockerWithMock(&mockClient{errOnInfo: true}) }, + expected: nil, + }, + "fail when error on ContainerList()": { + prepare: func() *Docker { return prepareDockerWithMock(&mockClient{errOnContainerList: true}) }, + expected: nil, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + d := test.prepare() + + require.True(t, d.Init()) + _ = d.Check() + + collected := d.Collect() + + assert.Equal(t, test.expected, collected) + }) + } +} + +func prepareDockerWithMock(m *mockClient) *Docker { + d := New() + if m == nil { + d.newClient = func(_ Config) (dockerClient, error) { return nil, errors.New("mock.newClient() error") } + } else { + d.newClient = func(_ Config) (dockerClient, error) { return m, nil } + } + return d +} + +type mockClient struct { + errOnInfo bool + errOnContainerList bool + closeCalled bool +} + +func (m *mockClient) Info(_ context.Context) (types.Info, error) { + if m.errOnInfo { + return types.Info{}, errors.New("mockClient.Info() error") + } + + return types.Info{ + ContainersRunning: 4, + ContainersPaused: 5, + ContainersStopped: 6, + }, nil +} + +func (m *mockClient) ContainerList(_ context.Context, opts types.ContainerListOptions) ([]types.Container, error) { + if m.errOnContainerList { + return nil, errors.New("mockClient.ContainerList() error") + } + + v := opts.Filters.Get("health") + + if len(v) == 0 { + return nil, errors.New("mockClient.ContainerList() error (expect 'health' filter)") + } + + switch v[0] { + case "healthy": + return []types.Container{{}, {}}, nil + case "unhealthy": + return []types.Container{{}, {}, {}}, nil + default: + return nil, nil + } +} + +func (m *mockClient) Close() error { + m.closeCalled = true + return nil +} diff --git a/modules/init.go b/modules/init.go index dfeeda838..909efdf61 100644 --- a/modules/init.go +++ b/modules/init.go @@ -16,6 +16,7 @@ import ( _ "github.com/netdata/go.d.plugin/modules/dnsmasq" _ "github.com/netdata/go.d.plugin/modules/dnsmasq_dhcp" _ "github.com/netdata/go.d.plugin/modules/dnsquery" + _ "github.com/netdata/go.d.plugin/modules/docker" _ "github.com/netdata/go.d.plugin/modules/docker_engine" _ "github.com/netdata/go.d.plugin/modules/dockerhub" _ "github.com/netdata/go.d.plugin/modules/elasticsearch"