├── 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 | Build Status 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 | --------------------------------------------------------------------------------