├── collectd
├── docker.db
├── docker.conf
└── docker.conf.filters.example
├── .travis.yml
├── glide.yaml
├── .gitignore
├── LICENSE
├── Makefile
├── glide.lock
├── README.md
└── main.go
/collectd/docker.db:
--------------------------------------------------------------------------------
1 | network value:COUNTER:U:U
2 | disk value:COUNTER:U:U
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 | go:
3 | - 1.7
4 | cache:
5 | directories:
6 | - vendor
7 | install:
8 | - make deps
9 | - make setup-linter
10 | script:
11 | - make test
12 |
--------------------------------------------------------------------------------
/glide.yaml:
--------------------------------------------------------------------------------
1 | package: github.com/dustinblackman/collectd-docker-plugin
2 | import:
3 | - package: github.com/urfave/cli
4 | - package: github.com/davecgh/go-spew
5 | - package: github.com/fatih/structs
6 | - package: github.com/fsouza/go-dockerclient
7 |
--------------------------------------------------------------------------------
/collectd/docker.conf:
--------------------------------------------------------------------------------
1 | LoadPlugin exec
2 |
3 | Exec "nobody:docker" "/usr/local/bin/collectd-docker-plugin"
4 |
5 |
6 | # Add custom TypesDB for network counter stats
7 | TypesDB "/usr/share/collectd/types.db"
8 | TypesDB "/usr/share/collectd/docker.db"
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled Object files, Static and Dynamic libs (Shared Objects)
2 | *.o
3 | *.a
4 | *.so
5 |
6 | # Folders
7 | _obj
8 | _test
9 |
10 | # Architecture specific extensions/prefixes
11 | *.[568vq]
12 | [568vq].out
13 |
14 | *.cgo1.go
15 | *.cgo2.c
16 | _cgo_defun.c
17 | _cgo_gotypes.go
18 | _cgo_export.*
19 |
20 | _testmain.go
21 |
22 | *.exe
23 | *.test
24 | *.prof
25 |
26 | dist/
27 | tmp/
28 | vendor/
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Dustin Blackman
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/collectd/docker.conf.filters.example:
--------------------------------------------------------------------------------
1 | LoadPlugin exec
2 |
3 | Exec "nobody:docker" "/usr/local/bin/collectd-docker-plugin"
4 |
5 |
6 | # Add custom TypesDB for network counter stats
7 | TypesDB "/usr/share/collectd/types.db"
8 | TypesDB "/usr/share/collectd/docker.db"
9 |
10 | # Collectd Chains docs https://collectd.org/wiki/index.php/Chains
11 |
12 | # Collectd Naming schema https://collectd.org/wiki/index.php/Identifier
13 | # Example metric: docker.container_name.cpu.user_mode
14 | # Plugin: docker
15 | # PluginInstance: container_name
16 | # Type: cpu
17 | # TypeInstance: user_mode
18 |
19 | LoadPlugin match_regex
20 |
21 |
22 |
23 | Plugin "^docker$"
24 |
25 |
26 | Chain "FilterOutDetailedDockerStats"
27 |
28 |
29 |
30 | Target "write"
31 |
32 |
33 |
34 |
35 |
36 | Type "^cpu$"
37 | TypeInstance "^(kernel_mode|user_mode|percent_usage)$"
38 |
39 | Target "return"
40 |
41 |
42 |
43 | Type "^memory$"
44 | TypeInstance "^(total_rss|total_cache|hierarchical_memory_limit|percent_usage)$"
45 |
46 | Target "return"
47 |
48 |
49 |
50 | Type "^network$"
51 | TypeInstance "^(tx|rx)_bytes$"
52 |
53 | Target "return"
54 |
55 |
56 |
57 | Type "^disk$"
58 | TypeInstance "^(read|write)"
59 |
60 | Target "return"
61 |
62 |
63 | # Default (not match)
64 | Target "stop"
65 |
66 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | VERSION := 0.3.0
2 | GLIDE_COMMIT := 91d42a717b7202c55568a7da05be915488253b8d
3 | LINTER_COMMIT := 052c5941f855d3ffc9e8e8c446e0c0a8f0445410
4 |
5 | build:
6 | go build -ldflags="-X main.version=$(VERSION)" -o collectd-docker-plugin main.go
7 |
8 | deps:
9 | @if [ "$$(which glide)" = "" ]; then \
10 | go get -v github.com/Masterminds/glide; \
11 | cd $$GOPATH/src/github.com/Masterminds/glide;\
12 | git checkout $(GLIDE_COMMIT);\
13 | go install;\
14 | fi
15 | glide install
16 | go install
17 | glide install
18 |
19 | dist:
20 | which gox && echo "" || go get github.com/mitchellh/gox
21 | rm -rf tmp dist
22 | gox -os="linux windows freebsd openbsd" -output='tmp/{{.OS}}-{{.Arch}}-$(VERSION)/{{.Dir}}' -ldflags="-X main.version=$(VERSION)"
23 | mkdir dist
24 |
25 | # Create archives for Windows
26 | @for i in $$(find ./tmp -type f -name "collectd-docker-plugin.exe" | awk -F'/' '{print $$3}'); \
27 | do \
28 | zip -j "dist/collectd-docker-plugin-$$i.zip" "./tmp/$$i/collectd-docker-plugin.exe"; \
29 | done
30 |
31 | # Create achrives for everything else
32 | @for i in $$(find ./tmp -type f -not -name "collectd-docker-plugin.exe" | awk -F'/' '{print $$3}'); \
33 | do \
34 | chmod +x "./tmp/$$i/collectd-docker-plugin"; \
35 | tar -zcvf "dist/collectd-docker-plugin-$$i.tar.gz" --directory="./tmp/$$i" "./collectd-docker-plugin"; \
36 | done
37 |
38 | rm -rf tmp
39 |
40 | install: deps test
41 | go install -ldflags="-X main.version=$(VERSION)" main.go
42 |
43 | setup-linter:
44 | @if [ "$$(which gometalinter)" = "" ]; then \
45 | go get -v github.com/alecthomas/gometalinter; \
46 | cd $$GOPATH/src/github.com/alecthomas/gometalinter;\
47 | git checkout $(LINTER_COMMIT);\
48 | go install;\
49 | gometalinter --install;\
50 | fi
51 |
52 | test:
53 | make setup-linter
54 | gometalinter --vendor --fast --dupl-threshold=100 --cyclo-over=25 --min-occurrences=5 --disable=gas ./...
55 |
--------------------------------------------------------------------------------
/glide.lock:
--------------------------------------------------------------------------------
1 | hash: ee40a58eadbb0c71f94ffed3890dfd169a3815d4fbb0eb89aa9521013f3ec416
2 | updated: 2016-09-22T13:28:28.583198332-04:00
3 | imports:
4 | - name: github.com/davecgh/go-spew
5 | version: 6d212800a42e8ab5c146b8ace3490ee17e5225f9
6 | - name: github.com/docker/docker
7 | version: 79c1cd87ec74aa5ca71ceb52e12ab2f15efd8b24
8 | subpackages:
9 | - opts
10 | - pkg/archive
11 | - pkg/fileutils
12 | - pkg/homedir
13 | - pkg/stdcopy
14 | - pkg/idtools
15 | - pkg/ioutils
16 | - pkg/pools
17 | - pkg/promise
18 | - pkg/system
19 | - pkg/longpath
20 | - name: github.com/docker/engine-api
21 | version: 94a8f8f29307ab291abad6c6f2182d67089aae5d
22 | subpackages:
23 | - types/swarm
24 | - types/filters
25 | - types/mount
26 | - types/versions
27 | - name: github.com/docker/go-units
28 | version: 9b001659dd36225e356b4467c465d732e745f53d
29 | - name: github.com/fatih/camelcase
30 | version: f6a740d52f961c60348ebb109adde9f4635d7540
31 | - name: github.com/fatih/structs
32 | version: dc3312cb1a4513a366c4c9e622ad55c32df12ed3
33 | - name: github.com/fsouza/go-dockerclient
34 | version: 4c88c36676d7f65179f308949e16523d21573d9e
35 | - name: github.com/hashicorp/go-cleanhttp
36 | version: ad28ea4487f05916463e2423a55166280e8254b5
37 | - name: github.com/Microsoft/go-winio
38 | version: ce2922f643c8fd76b46cadc7f404a06282678b34
39 | - name: github.com/opencontainers/runc
40 | version: a2a6e828a9cafd11fb02b3589833a77c7af50b2f
41 | subpackages:
42 | - libcontainer/user
43 | - name: github.com/Sirupsen/logrus
44 | version: 26709e2714106fb8ad40b773b711ebce25b78914
45 | - name: github.com/urfave/cli
46 | version: d53eb991652b1d438abdd34ce4bfa3ef1539108e
47 | - name: golang.org/x/net
48 | version: 6d3beaea10370160dea67f5c9327ed791afd5389
49 | subpackages:
50 | - context
51 | - context/ctxhttp
52 | - name: golang.org/x/sys
53 | version: 8f0908ab3b2457e2e15403d3697c9ef5cb4b57a9
54 | subpackages:
55 | - windows
56 | devImports: []
57 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # collectd-docker-plugin
2 |
3 |
4 |
5 | Collectd plugin to tap in the Docker Stats streaming API using Collectd's [Exec](https://collectd.org/wiki/index.php/Plugin:Exec) plugin. Built with Go 1.7 and tested with Collectd 5.5 and Influx 1.0.
6 |
7 | ## Installation
8 |
9 | Example installation for a Ubuntu system. Make changes required to match your own OS.
10 |
11 | ```bash
12 | curl -Ls "https://github.com/dustinblackman/collectd-docker-plugin/releases/download/0.3.0/collectd-docker-plugin-linux-amd64-0.3.0.tar.gz" | tar xz -C /usr/local/bin/
13 | curl -o /usr/share/collectd https://raw.githubusercontent.com/dustinblackman/collectd-docker-plugin/master/collectd/docker.db
14 | curl -o /etc/collectd/collectd.conf.d https://raw.githubusercontent.com/dustinblackman/collectd-docker-plugin/master/collectd/docker.conf
15 | usermod -a -G docker nobody
16 | service collectd restart
17 | ```
18 |
19 | ## Parameters
20 | Parameters are available and can be added by modifying [docker.conf](./collectd/docker.conf) and appending parameters to the exec function.
21 |
22 |
23 | - `-d, --docker-host` - Docker socket path. Defaults to `unix:///var/run/docker.sock`
24 | - `-de, --docker-environment` - Boolean parameter to specifiy reading Docker parameters from environment variables
25 | - `-ch, --collectd-hostname` - Collectd hostname. This is automatically provided to the process from Collectd.
26 | - `-w, --wait-time` - Delay in seconds with how often metrics are submitted to Collectd. If wait time is set to `1`, it will use Docker Stats API streaming, otherwise it polls. Defaults to `5`.
27 |
28 | ## Build From Source
29 |
30 | Tested with Go 1.7. Versioning is done with [Glide](https://github.com/Masterminds/glide). The makefile will take care of installing it for you incase you don't have it.
31 |
32 | ```bash
33 | git pull https://github.com/dustinblackman/collectd-docker-plugin
34 | cd collectd-docker-plugin
35 | make install
36 | ```
37 |
38 | ## [License](./LICENSE)
39 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "strconv"
8 | "strings"
9 | "time"
10 |
11 | "github.com/fatih/camelcase"
12 | "github.com/fatih/structs"
13 | docker "github.com/fsouza/go-dockerclient"
14 | "github.com/urfave/cli"
15 | )
16 |
17 | var (
18 | client *docker.Client
19 | host = "localhost"
20 | version = "HEAD"
21 | waitTime int
22 | interval int
23 | )
24 |
25 | func printCollectD(containerName, statType, statTypeInstance string, value uint64) {
26 | valueString := strconv.FormatUint(value, 10)
27 | fmt.Printf("PUTVAL \"%s/docker-%s/%s-%s\" interval=%d N:%s\n", host, containerName, statType, statTypeInstance, interval, valueString)
28 | }
29 |
30 | func toUnderscore(key string) string {
31 | return strings.ToLower(strings.Join(camelcase.Split(key), "_"))
32 | }
33 |
34 | func processStats(containerName string, stats *docker.Stats) {
35 | // Memory
36 | printCollectD(containerName, "memory", "max_usage", stats.MemoryStats.MaxUsage)
37 | printCollectD(containerName, "memory", "usage", stats.MemoryStats.Usage)
38 | for key, value := range structs.Map(stats.MemoryStats.Stats) {
39 | printCollectD(containerName, "memory", toUnderscore(key), value.(uint64))
40 | }
41 | memoryPercent := (float64(stats.MemoryStats.Stats.TotalRss) * 100.0) / float64(stats.MemoryStats.Limit)
42 | printCollectD(containerName, "memory", "percent_usage", uint64(memoryPercent))
43 |
44 | // CPU
45 | printCollectD(containerName, "cpu", "total_usage", stats.CPUStats.CPUUsage.TotalUsage)
46 | // Borrowed from https://github.com/docker/docker/blob/c0699cd4a43ccc3b1e3624379e46e9ed94f7428c/cli/command/container/stats_helpers.go#L184-L197
47 | cpuPercent := 0.0
48 | cpuDelta := float64(stats.CPUStats.CPUUsage.TotalUsage) - float64(stats.PreCPUStats.CPUUsage.TotalUsage)
49 | systemDelta := float64(stats.CPUStats.SystemCPUUsage) - float64(stats.PreCPUStats.SystemCPUUsage)
50 | if systemDelta > 0.0 && cpuDelta > 0.0 {
51 | cpuPercent = (cpuDelta / systemDelta) * float64(len(stats.CPUStats.CPUUsage.PercpuUsage)) * 100.0
52 | }
53 | printCollectD(containerName, "cpu", "percent_usage", uint64(cpuPercent))
54 | printCollectD(containerName, "cpu", "kernel_mode", stats.CPUStats.CPUUsage.UsageInKernelmode-stats.PreCPUStats.CPUUsage.UsageInKernelmode)
55 | printCollectD(containerName, "cpu", "user_mode", stats.CPUStats.CPUUsage.UsageInUsermode-stats.PreCPUStats.CPUUsage.UsageInUsermode)
56 |
57 | // Network
58 | mergedNetworks := map[string]uint64{}
59 | for _, value := range stats.Networks {
60 | for networkKey, networkValue := range structs.Map(value) {
61 | if _, ok := mergedNetworks[networkKey]; ok {
62 | mergedNetworks[networkKey] += networkValue.(uint64)
63 | } else {
64 | mergedNetworks[networkKey] = networkValue.(uint64)
65 | }
66 | }
67 | }
68 | for key, value := range mergedNetworks {
69 | printCollectD(containerName, "network", toUnderscore(key), value)
70 | }
71 |
72 | // Block I/O
73 | var blkRead, blkWrite uint64
74 | for _, bioEntry := range stats.BlkioStats.IOServiceBytesRecursive {
75 | switch strings.ToLower(bioEntry.Op) {
76 | case "read":
77 | blkRead = blkRead + bioEntry.Value
78 | case "write":
79 | blkWrite = blkWrite + bioEntry.Value
80 | }
81 | }
82 | printCollectD(containerName, "disk", "read", blkRead)
83 | printCollectD(containerName, "disk", "write", blkWrite)
84 |
85 | }
86 |
87 | func callStats(container *docker.Container, containerName string, stream bool) error {
88 | errC := make(chan error, 1)
89 | statsC := make(chan *docker.Stats)
90 |
91 | go func() {
92 | errC <- client.Stats(docker.StatsOptions{ID: container.ID, Stats: statsC, Stream: stream})
93 | }()
94 |
95 | for {
96 | stats, ok := <-statsC
97 | if !ok {
98 | break
99 | }
100 | processStats(containerName, stats)
101 | }
102 |
103 | err := <-errC
104 | if stream && err != nil {
105 | log.Fatal(err)
106 | }
107 |
108 | return err
109 | }
110 |
111 | func getStats(containerID string) {
112 | container, err := client.InspectContainer(containerID)
113 | if err != nil {
114 | log.Fatal(err)
115 | }
116 | containerName := container.Name[1:len(container.Name)]
117 | // Docker Stats API emits stats every second.
118 | if waitTime == 1 {
119 | callStats(container, containerName, true)
120 | } else {
121 | for {
122 | err := callStats(container, containerName, false)
123 | if err != nil {
124 | break
125 | }
126 | time.Sleep(time.Duration(waitTime) * time.Second)
127 | }
128 | }
129 | }
130 |
131 | func listContainers(ctx *cli.Context) {
132 | host = ctx.String("collectd-hostname")
133 | waitTime = ctx.Int("wait-time")
134 | interval = ctx.Int("interval")
135 |
136 | var err interface{}
137 | if ctx.Bool("docker-environment") {
138 | client, err = docker.NewClientFromEnv()
139 | } else {
140 | client, err = docker.NewClient(ctx.String("docker-host"))
141 | }
142 |
143 | if err != nil {
144 | log.Fatal(err)
145 | return
146 | }
147 |
148 | containers, err := client.ListContainers(docker.ListContainersOptions{All: false, Size: false})
149 | if err != nil {
150 | log.Fatal(err)
151 | return
152 | }
153 |
154 | for _, container := range containers {
155 | go getStats(container.ID)
156 | }
157 |
158 | dockerEvents := make(chan *docker.APIEvents, 100)
159 | client.AddEventListener(dockerEvents)
160 | for event := range dockerEvents {
161 | if event.Status == "start" {
162 | go getStats(event.ID)
163 | }
164 | }
165 | }
166 |
167 | func main() {
168 | app := cli.NewApp()
169 | app.Name = "collectd-docker-plugin"
170 | app.Usage = "A collectd plugin to submit metrics from the docker stats API"
171 | app.Version = version
172 | app.Author = "Dustin Blackman"
173 | app.Copyright = "(c) 2016 " + app.Author
174 | app.EnableBashCompletion = true
175 | app.Action = listContainers
176 |
177 | app.Flags = []cli.Flag{
178 | cli.StringFlag{
179 | Name: "docker-host, d",
180 | Usage: "Docker host",
181 | Value: "unix:///var/run/docker.sock",
182 | },
183 | cli.BoolFlag{
184 | Name: "docker-environment, de",
185 | Usage: "Use environment docker variables instead of passing docker socket path",
186 | },
187 | cli.StringFlag{
188 | Name: "collectd-hostname, ch",
189 | Usage: "Docker host",
190 | EnvVar: "COLLECTD_HOSTNAME",
191 | Value: "localhost",
192 | },
193 | cli.IntFlag{
194 | Name: "wait-time, w",
195 | Usage: "Wait time between how often stats should be requested from the Docker stats API",
196 | Value: 5,
197 | },
198 | cli.IntFlag{
199 | Name: "interval, i",
200 | Usage: "Set interval for collecting metrics",
201 | Value: 60,
202 | },
203 | }
204 |
205 | app.Run(os.Args)
206 | }
207 |
--------------------------------------------------------------------------------