├── VERSION
├── custom_exporter.png
├── .gitignore
├── example_shell.yml
├── CHANGELOG.md
├── wrongYaml.yml
├── .promu.yml
├── makeRelease.sh
├── example.yml
├── config
├── config_suite_test.go
├── config_test.go
└── config.go
├── custom_exporter_suite_test.go
├── go.mod
├── example_with_error.yml
├── collector
├── collector_suite_test.go
├── bash_test.go
├── redis_test.go
├── collector.go
├── mysql_test.go
├── bash.go
├── mysql.go
└── redis.go
├── Makefile
├── custom_exporter.go
├── custom_exporter_test.go
├── README.md
├── LICENSE
└── go.sum
/VERSION:
--------------------------------------------------------------------------------
1 | 2.0.0
2 |
--------------------------------------------------------------------------------
/custom_exporter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orange-cloudfoundry/custom_exporter/HEAD/custom_exporter.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | #binary
2 | /custom_exporter
3 |
4 | #cover files
5 | *.cover*
6 | */*.cover*
7 | **/*.cover*
8 |
9 | #IDE jetbrains
10 | .idea
11 | .idea/
12 |
13 | # Vendoring folder
14 | vendor/
15 |
16 |
--------------------------------------------------------------------------------
/example_shell.yml:
--------------------------------------------------------------------------------
1 | ---
2 | credentials:
3 | - name: shell_root
4 | type: bash
5 | metrics:
6 | - name: custom_metric_shell
7 | commands:
8 | - ls -ahl
9 | - pwd
10 | - echo -e 1\tchicken\t128\n2\tbeef\t256\n3\tsnails\t14\n
11 | credential: shell_root
12 | mapping:
13 | - id
14 | - animals
15 | separator: "\t"
16 | value_type: UNTYPED
17 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## v1.0.0 / 2017-03-27:
2 |
3 | First release of this exporter
4 |
5 | #### NOTE: Collector Information
6 | * `mysql` - this collectors are not still tested on context
7 |
8 |
9 | #### Collectors included :
10 | * `bash` - run custom command into the shell process (| are not allowed, so each result of command are include as stdin for the next command)
11 | * `redis` - run custom query to redis and parse result as JSON
12 | * `mysql` - run custom SQL Query and expose last columns as value
13 |
--------------------------------------------------------------------------------
/wrongYaml.yml:
--------------------------------------------------------------------------------
1 | custom_exporter:
2 | credentials:
3 | - name: shell_root
4 | type: bash
5 | - name: mysql_connector
6 | type: mysql
7 | dsn: mysql://root:passwor@127.0.0.1:3306/mydb
8 | metrics:
9 | - name: custom_metric_shell
10 | commands:
11 | - pwd -P
12 | - ls
13 | - echo -e 1\tchicken\t128\n2\tbeef\t256\n3\tsnails\t14\n
14 | credential: shell_root
15 | mapping:
16 | - id
17 | - animals
18 | separator: "\t"
19 | value_type: UNTYPED
20 | - name: custom_metric_mysql
21 | commands:
22 | - SELECT aml_id,aml_name,aml_number FROM animals
23 | credential: mysql_connector
24 | mapping:
25 | - id
26 | - name
27 | value_type: UNTYPED
28 |
--------------------------------------------------------------------------------
/.promu.yml:
--------------------------------------------------------------------------------
1 | go:
2 | cgo: true
3 |
4 | repository:
5 | path: github.com/orange-cloudfoundry/custom_exporter
6 |
7 | build:
8 | ldflags: |
9 | -X github.com/prometheus/common/version.Version=1.0.1
10 | -X github.com/prometheus/common/version.Revision=159a029e2d320e99bca683c0c8baa8bf0b5dfb68
11 | -X github.com/prometheus/common/version.Branch=release-1.0
12 | -X github.com/prometheus/common/version.BuildUser=Nicolas.Juhel
13 | -X github.com/prometheus/common/version.BuildDate=2017-03-29.16:41:21.+0200
14 |
15 | tarball:
16 | files:
17 | - README.md
18 | - example.yml
19 | - LICENSE
20 | - NOTICE
21 |
22 | crossbuild:
23 | platforms:
24 |
25 |
--------------------------------------------------------------------------------
/makeRelease.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e
4 |
5 | vi VERSION
6 |
7 | BRANCH=$(git rev-parse --abbrev-ref HEAD)
8 | REVISION=$(git rev-parse HEAD)
9 | VERSION=$(cat VERSION)
10 | OWNER=$(git log -1 --format="%cN" $REVISION | tr " " ".")
11 | DATE=$(git log -1 --format="%ci" $REVISION | tr " " ".")
12 |
13 | sed -i -r "s|^([ ]{4}\-X github\.com/prometheus/common/version\.Version\=).*$|\1$VERSION|" .promu.yml
14 | sed -i -r "s|^([ ]{4}\-X github\.com/prometheus/common/version\.Revision\=).*$|\1$REVISION|" .promu.yml
15 | sed -i -r "s|^([ ]{4}\-X github\.com/prometheus/common/version\.Branch\=).*$|\1$BRANCH|" .promu.yml
16 | sed -i -r "s|^([ ]{4}\-X github\.com/prometheus/common/version\.BuildUser\=).*$|\1$OWNER|" .promu.yml
17 | sed -i -r "s|^([ ]{4}\-X github\.com/prometheus/common/version\.BuildDate\=).*$|\1$DATE|" .promu.yml
18 |
19 | git status
20 |
21 |
--------------------------------------------------------------------------------
/example.yml:
--------------------------------------------------------------------------------
1 | ---
2 | credentials:
3 | - name: shell_root
4 | type: bash
5 | user: root
6 | - name: mysql_connector
7 | type: mysql
8 | dsn: mysql://root:password@127.0.0.1:3306/mydb
9 | - name: redis_connector
10 | type: redis
11 | dsn: tcp://:password@127.0.0.1:6789/0
12 | metrics:
13 | - name: custom_metric_shell
14 | commands:
15 | - ls -ahl
16 | - pwd
17 | - echo -e 1\tchicken\t128\n2\tbeef\t256\n3\tsnails\t14\n
18 | credential: shell_root
19 | mapping:
20 | - id
21 | - animals
22 | separator: "\t"
23 | value_type: UNTYPED
24 | - name: custom_metric_mysql
25 | commands:
26 | - SELECT aml_id,aml_name,aml_number FROM animals
27 | credential: mysql_connector
28 | mapping:
29 | - id
30 | - name
31 | value_type: UNTYPED
32 | - name: custom_metric_redis
33 | commands:
34 | - GET foo*
35 | credential: redis_connector
36 | mapping:
37 | - role
38 | value_name: value
39 | value_type: UNTYPED
40 |
41 |
--------------------------------------------------------------------------------
/config/config_suite_test.go:
--------------------------------------------------------------------------------
1 | package config_test
2 |
3 | import (
4 | . "github.com/onsi/ginkgo"
5 | . "github.com/onsi/gomega"
6 | "github.com/onsi/gomega/gexec"
7 |
8 | "testing"
9 | )
10 |
11 | /*
12 | Copyright 2017 Orange
13 |
14 | Licensed under the Apache License, Version 2.0 (the "License");
15 | you may not use this file except in compliance with the License.
16 | You may obtain a copy of the License at
17 |
18 | http://www.apache.org/licenses/LICENSE-2.0
19 |
20 | Unless required by applicable law or agreed to in writing, software
21 | distributed under the License is distributed on an "AS IS" BASIS,
22 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
23 | See the License for the specific language governing permissions and
24 | limitations under the License.
25 | */
26 |
27 | var binaryPath string
28 |
29 | func TestCustomExporter(t *testing.T) {
30 | RegisterFailHandler(Fail)
31 | RunSpecs(t, "Custom Config Test Suite")
32 | }
33 |
34 | var _ = SynchronizedBeforeSuite(func() []byte {
35 | var err error
36 | binaryPath, err = gexec.Build("github.com/orange-cloudfoundry/custom_exporter", "-race")
37 | Expect(err).NotTo(HaveOccurred())
38 |
39 | return []byte(binaryPath)
40 | }, func(bytes []byte) {
41 | binaryPath = string(bytes)
42 | })
43 |
--------------------------------------------------------------------------------
/custom_exporter_suite_test.go:
--------------------------------------------------------------------------------
1 | package main_test
2 |
3 | import (
4 | . "github.com/onsi/ginkgo"
5 | . "github.com/onsi/gomega"
6 | "github.com/onsi/gomega/gexec"
7 |
8 | "testing"
9 | )
10 |
11 | /*
12 | Copyright 2017 Orange
13 |
14 | Licensed under the Apache License, Version 2.0 (the "License");
15 | you may not use this file except in compliance with the License.
16 | You may obtain a copy of the License at
17 |
18 | http://www.apache.org/licenses/LICENSE-2.0
19 |
20 | Unless required by applicable law or agreed to in writing, software
21 | distributed under the License is distributed on an "AS IS" BASIS,
22 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
23 | See the License for the specific language governing permissions and
24 | limitations under the License.
25 | */
26 |
27 | var binaryPath string
28 |
29 | func TestCustomExporter(t *testing.T) {
30 | RegisterFailHandler(Fail)
31 | RunSpecs(t, "Custom Exporter Main Suite")
32 | }
33 |
34 | var _ = SynchronizedBeforeSuite(func() []byte {
35 | var err error
36 | binaryPath, err = gexec.Build("github.com/orange-cloudfoundry/custom_exporter", "-race")
37 | Expect(err).NotTo(HaveOccurred())
38 |
39 | return []byte(binaryPath)
40 | }, func(bytes []byte) {
41 | binaryPath = string(bytes)
42 | })
43 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/orange-cloudfoundry/custom_exporter
2 |
3 | go 1.13
4 |
5 | require (
6 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect
7 | github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6 // indirect
8 | github.com/alicebob/miniredis v2.5.0+incompatible
9 | github.com/go-sql-driver/mysql v1.4.1
10 | github.com/gomodule/redigo v2.0.0+incompatible // indirect
11 | github.com/google/go-github/v25 v25.1.3 // indirect
12 | github.com/onsi/ginkgo v1.11.0
13 | github.com/onsi/gomega v1.8.1
14 | github.com/prometheus/client_golang v1.3.0
15 | github.com/prometheus/common v0.7.0
16 | github.com/prometheus/promu v0.5.0 // indirect
17 | github.com/tedsuo/ifrit v0.0.0-20191009134036-9a97d0632f00
18 | github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb // indirect
19 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect
20 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 // indirect
21 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8 // indirect
22 | golang.org/x/tools v0.0.0-20191230220329-2aa90c603ae3 // indirect
23 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect
24 | google.golang.org/appengine v1.6.5 // indirect
25 | gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0
26 | gopkg.in/redis.v5 v5.2.9
27 | gopkg.in/yaml.v2 v2.2.7
28 | honnef.co/go/tools v0.0.1-2019.2.3 // indirect
29 | )
30 |
--------------------------------------------------------------------------------
/example_with_error.yml:
--------------------------------------------------------------------------------
1 | ---
2 | credentials:
3 | - name: shell_root
4 | type: bash
5 | - name: mysql_connector
6 | type: mysql
7 | dsn: mysql://root:password@tcp(127.0.0.1:3306)/mydb
8 | - name: redis_connector
9 | type: redis
10 | dsn: tcp://:password@127.0.0.1:6789/0
11 | metrics:
12 | - name: custom_metric_shell
13 | commands:
14 | - ls -ahl
15 | - pwd
16 | - echo -e 1\tchicken\t128\n2\tbeef\t256\n3\tsnails\t14\n
17 | credential: shell_root
18 | mapping:
19 | - id
20 | - animals
21 | separator: "\t"
22 | value_type: UNTYPED
23 | - name: custom_metric_shell_error
24 | commands:
25 | - ls -ahl
26 | - fake1234
27 | - echo -e 1\tchicken\t128\n2\tbeef\t256\n3\tsnails\t14\n
28 | credential: shell_root
29 | mapping:
30 | - id
31 | - animals
32 | separator: "\t"
33 | value_type: UNTYPED
34 | - name: custom_metric_mysql
35 | commands:
36 | - SELECT aml_id,aml_name,aml_number FROM animals
37 | credential: mysql_connector
38 | mapping:
39 | - id
40 | - name
41 | value_type: UNTYPED
42 | - name: custom_metric_mysql_error
43 | commands:
44 | - SELECT "id", "name", 1, FROM animals
45 | credential: mysql_connector
46 | mapping:
47 | - id
48 | - name
49 | value_type: UNTYPED
50 | - name: custom_metric_redis
51 | commands:
52 | - GET foo1
53 | credential: redis_connector
54 | mapping:
55 | - role
56 | value_name: value
57 | value_type: UNTYPED
58 | - name: custom_metric_redis_error
59 | commands:
60 | - ERROR_COMMAND
61 | credential: redis_connector
62 | mapping:
63 | - role
64 | value_name: error
65 | value_type: UNTYPED
66 |
--------------------------------------------------------------------------------
/collector/collector_suite_test.go:
--------------------------------------------------------------------------------
1 | package collector_test
2 |
3 | import (
4 | . "github.com/onsi/ginkgo"
5 | . "github.com/onsi/gomega"
6 |
7 | "sync"
8 | "testing"
9 |
10 | "github.com/alicebob/miniredis"
11 | "github.com/prometheus/client_golang/prometheus"
12 | "github.com/prometheus/common/log"
13 | )
14 |
15 | /*
16 | Copyright 2017 Orange
17 |
18 | Licensed under the Apache License, Version 2.0 (the "License");
19 | you may not use this file except in compliance with the License.
20 | You may obtain a copy of the License at
21 |
22 | http://www.apache.org/licenses/LICENSE-2.0
23 |
24 | Unless required by applicable law or agreed to in writing, software
25 | distributed under the License is distributed on an "AS IS" BASIS,
26 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
27 | See the License for the specific language governing permissions and
28 | limitations under the License.
29 | */
30 |
31 | var (
32 | redisAddr string
33 | redisServer *miniredis.Miniredis
34 | ch chan prometheus.Metric
35 | ds chan *prometheus.Desc
36 | wg sync.WaitGroup
37 | )
38 |
39 | func TestCustomExporter(t *testing.T) {
40 | RegisterFailHandler(Fail)
41 | RunSpecs(t, "Custom Config Test Suite")
42 | }
43 |
44 | var _ = SynchronizedBeforeSuite(func() []byte {
45 | var err error
46 |
47 | if redisServer, err = miniredis.Run(); err != nil {
48 | println(err.Error())
49 | panic(err)
50 | }
51 |
52 | redisAddr = redisServer.Addr()
53 | log.Infof("Miniredis started and listinning on Addr \"%s\" ...", redisAddr)
54 |
55 | return []byte(redisAddr)
56 | }, func(byte []byte) {
57 | ch = make(chan prometheus.Metric)
58 | ds = make(chan *prometheus.Desc)
59 | log.Infoln("Channels openned...")
60 |
61 | redisServer.FlushAll()
62 | redisServer.RequireAuth("password")
63 | redisServer.Set("foo1", "{\"test\":1,\"role\":\"master\",\"value\":\"14.258\"}")
64 | redisServer.Set("foo2", "{\"test\":2,\"role\":\"master\",\"value\":\"6843.119\"}")
65 | redisServer.Set("foo3", "{\"test\":3,\"role\":\"master\",\"value\":\"18.1244\"}")
66 | redisServer.Set("foo4", "{\"test\":4,\"role\":\"master\",\"value\":\"15.2234841e+12\"}")
67 | })
68 |
69 | var _ = SynchronizedAfterSuite(func() {
70 | log.Infof("Stopping Miniredis listinning on Addr \"%s\" ...", redisAddr)
71 | redisServer.Close()
72 |
73 | }, func() {})
74 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # Copyright 2017 Orange
2 | # Licensed under the Apache License, Version 2.0 (the "License");
3 | # you may not use this file except in compliance with the License.
4 | # You may obtain a copy of the License at
5 | #
6 | # http://www.apache.org/licenses/LICENSE-2.0
7 | #
8 | # Unless required by applicable law or agreed to in writing, software
9 | # distributed under the License is distributed on an "AS IS" BASIS,
10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | # See the License for the specific language governing permissions and
12 | # limitations under the License.
13 |
14 | GO ?= CGO_ENABLED=1 go
15 | GOPATH := $(firstword $(subst :, ,$(shell $(GO) env GOPATH)))
16 | SCPATH := $(GOPATH)/src/github.com/orange-cloudfoundry/custom_exporter
17 | LCPATH := $(shell pwd)
18 |
19 | PROMU ?= $(GOPATH)/bin/promu
20 | STATICCHECK ?= $(GOPATH)/bin/staticcheck
21 | pkgs = $(shell $(GO) list ./... )
22 |
23 | CUR_DIR ?= $(shell basename $(pwd))
24 | BIN_DIR ?= $(GOPATH)/bin
25 | SRC_DIR ?= $(GOPATH)/src
26 | PKG_DIR ?= $(GOPATH)/pkg
27 |
28 | PREFIX ?= $(shell pwd)
29 |
30 | DOCKER_IMAGE_NAME ?= custom_exporter
31 | DOCKER_IMAGE_TAG ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD))
32 |
33 | ifeq ($(OS),Windows_NT)
34 | OS_detected := Windows
35 | else
36 | OS_detected := $(shell uname -s)
37 | endif
38 |
39 | all: format vet test staticcheck build
40 |
41 | clean:
42 | @echo ">> cleanning"
43 | @$(GO) clean -x -r -testcache -modcache
44 |
45 | pre-build:
46 | @echo ">> get dependancies"
47 | @$(GO) mod download
48 | @$(GO) mod vendor
49 |
50 | style: pre-build
51 | @echo ">> checking code style"
52 | @! gofmt -d $(shell find . -prune -o -name '*.go' -print) | grep '^'
53 |
54 | test: pre-build
55 | @echo ">> running tests"
56 | @$(GO) test -race -short $(pkgs)
57 |
58 | format: pre-build
59 | @echo ">> formatting code"
60 | @$(GO) fmt $(pkgs)
61 |
62 | vet: pre-build
63 | @echo ">> vetting code"
64 | @$(GO) vet $(pkgs)
65 |
66 | staticcheck: $(STATICCHECK)
67 | @echo ">> running staticcheck"
68 | @$(STATICCHECK) $(pkgs)
69 |
70 | buildbin: $(PROMU)
71 | @echo ">> building binaries"
72 | @$(PROMU) build --prefix $(PREFIX)
73 |
74 | build: clean pre-build buildbin
75 |
76 | tarball: $(PROMU)
77 | @echo ">> building release tarball"
78 | @$(PROMU) tarball --prefix $(PREFIX)
79 |
80 | $(GOPATH)/bin/promu promu:
81 | @GOOS= GOARCH= $(GO) get -u github.com/prometheus/promu
82 |
83 | $(GOPATH)/bin/staticcheck:
84 | @GOOS= GOARCH= $(GO) get -u honnef.co/go/tools/cmd/staticcheck
85 |
86 |
87 | .PHONY: all style format build test vet tarball docker promu staticcheck clean
88 |
89 | # Declaring the binaries at their default locations as PHONY targets is a hack
90 | # to ensure the latest version is downloaded on every make execution.
91 | # If this is not desired, copy/symlink these binaries to a different path and
92 | # set the respective environment variables.
93 | .PHONY: $(GOPATH)/bin/promu $(GOPATH)/bin/staticcheck
94 |
95 |
--------------------------------------------------------------------------------
/config/config_test.go:
--------------------------------------------------------------------------------
1 | package config_test
2 |
3 | import (
4 | . "github.com/onsi/ginkgo"
5 | . "github.com/onsi/gomega"
6 | "github.com/orange-cloudfoundry/custom_exporter/config"
7 | )
8 |
9 | /*
10 | Copyright 2017 Orange
11 |
12 | Licensed under the Apache License, Version 2.0 (the "License");
13 | you may not use this file except in compliance with the License.
14 | You may obtain a copy of the License at
15 |
16 | http://www.apache.org/licenses/LICENSE-2.0
17 |
18 | Unless required by applicable law or agreed to in writing, software
19 | distributed under the License is distributed on an "AS IS" BASIS,
20 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21 | See the License for the specific language governing permissions and
22 | limitations under the License.
23 | */
24 |
25 | var _ = Describe("Testing Custom Export, Staging Config Test: ", func() {
26 | var (
27 | filePath string
28 | cnf *config.Config
29 | err error
30 | )
31 |
32 | JustBeforeEach(func() {
33 | cnf, err = config.NewConfig(filePath)
34 | })
35 |
36 | Context("When miss the file config path", func() {
37 | BeforeEach(func() {
38 | filePath = ""
39 | })
40 |
41 | It("shound occures an error", func() {
42 | Expect(err).To(HaveOccurred())
43 | })
44 | })
45 |
46 | Context("When give a wrong file path", func() {
47 | BeforeEach(func() {
48 | filePath = "/test/me/wrong.yml"
49 | })
50 |
51 | It("shound occures an error", func() {
52 | Expect(err).To(HaveOccurred())
53 | })
54 | })
55 |
56 | Context("When give a good config file path and wrong yaml formatted", func() {
57 | BeforeEach(func() {
58 | filePath = "../wrongYaml.yml"
59 | })
60 |
61 | It("shound occures an error", func() {
62 | Expect(err).To(HaveOccurred())
63 | })
64 | })
65 |
66 | Context("When give a good config file path and well formatted yaml", func() {
67 | BeforeEach(func() {
68 | filePath = "../example.yml"
69 | })
70 |
71 | It("shound not occures an error", func() {
72 | Expect(err).NotTo(HaveOccurred())
73 | })
74 |
75 | It("should return a config struct of 6 metrics", func() {
76 | Expect(len(cnf.Metrics)).To(Equal(3))
77 | })
78 |
79 | It("should return metrics custom_metric_shell well composed", func() {
80 | name := "custom_metric_shell"
81 |
82 | Expect(cnf.Metrics[name].Name).To(Equal(name))
83 | Expect(len(cnf.Metrics[name].Commands)).To(Equal(3))
84 | Expect(cnf.Metrics[name].Credential.Name).To(Equal("shell_root"))
85 | Expect(cnf.Metrics[name].Credential.Collector).To(Equal("bash"))
86 | })
87 |
88 | It("should return metrics custom_metric_shell well composed", func() {
89 | name := "custom_metric_mysql"
90 |
91 | Expect(cnf.Metrics[name].Name).To(Equal(name))
92 | Expect(len(cnf.Metrics[name].Commands)).To(Equal(1))
93 | Expect(cnf.Metrics[name].Credential.Name).To(Equal("mysql_connector"))
94 | Expect(cnf.Metrics[name].Credential.Collector).To(Equal("mysql"))
95 | })
96 |
97 | It("should return metrics custom_metric_shell well composed", func() {
98 | name := "custom_metric_redis"
99 |
100 | Expect(cnf.Metrics[name].Name).To(Equal(name))
101 | Expect(len(cnf.Metrics[name].Commands)).To(Equal(1))
102 | Expect(cnf.Metrics[name].Credential.Name).To(Equal("redis_connector"))
103 | Expect(cnf.Metrics[name].Credential.Collector).To(Equal("redis"))
104 | })
105 | })
106 | })
107 |
--------------------------------------------------------------------------------
/collector/bash_test.go:
--------------------------------------------------------------------------------
1 | package collector_test
2 |
3 | import (
4 | . "github.com/onsi/ginkgo"
5 | . "github.com/onsi/gomega"
6 | "github.com/orange-cloudfoundry/custom_exporter/collector"
7 | "github.com/orange-cloudfoundry/custom_exporter/config"
8 | "github.com/prometheus/common/log"
9 | "sync"
10 | )
11 |
12 | /*
13 | Copyright 2017 Orange
14 |
15 | Licensed under the Apache License, Version 2.0 (the "License");
16 | you may not use this file except in compliance with the License.
17 | You may obtain a copy of the License at
18 |
19 | http://www.apache.org/licenses/LICENSE-2.0
20 |
21 | Unless required by applicable law or agreed to in writing, software
22 | distributed under the License is distributed on an "AS IS" BASIS,
23 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
24 | See the License for the specific language governing permissions and
25 | limitations under the License.
26 | */
27 |
28 | var _ = Describe("Testing Custom Export, Staging Config Test: ", func() {
29 | var (
30 | cnf *config.Config
31 | colBash *collector.CollectorBash
32 | metric config.MetricsItem
33 |
34 | isOk bool
35 | err error
36 | )
37 |
38 | BeforeEach(func() {
39 | wg = sync.WaitGroup{}
40 | wg.Add(1)
41 |
42 | cnf, err = config.NewConfig("../example_with_error.yml")
43 | })
44 |
45 | Context("When giving a valid config file with custom_metric_shell", func() {
46 |
47 | It("should have a valid config object", func() {
48 | Expect(err).NotTo(HaveOccurred())
49 | })
50 |
51 | Context("And giving an invalid config metric object", func() {
52 | It("should found the invalid metric object", func() {
53 | metric, isOk = cnf.Metrics["custom_metric_mysql"]
54 | Expect(isOk).To(BeTrue())
55 | })
56 | It("should return an error when creating the collector", func() {
57 | _, err = collector.NewPrometheusBashCollector(metric)
58 | Expect(err).To(HaveOccurred())
59 | })
60 | })
61 |
62 | Context("And giving an valid config metric object with invalid command", func() {
63 | It("should found the valid metric object", func() {
64 | metric, isOk = cnf.Metrics["custom_metric_shell_error"]
65 | Expect(isOk).To(BeTrue())
66 | })
67 |
68 | It("should not return an error when creating the collector", func() {
69 | _, err = collector.NewPrometheusBashCollector(metric)
70 | Expect(err).NotTo(HaveOccurred())
71 | })
72 |
73 | It("should return a valid Bash collector", func() {
74 | colBash = collector.NewCollectorBash(metric)
75 | Expect(colBash.Config()).To(Equal(metric))
76 | Expect(colBash.Name()).To(Equal(collector.CollectorBashName))
77 | Expect(colBash.Desc()).To(Equal(collector.CollectorBashDesc))
78 | })
79 |
80 | It("should return an error when call Run", func() {
81 | go func() {
82 | defer func() {
83 | GinkgoRecover()
84 | wg.Done()
85 | }()
86 | log.Infoln("Calling Run")
87 | Expect(colBash.Run(ch)).To(HaveOccurred())
88 | log.Infoln("Run called...")
89 | }()
90 |
91 | wg.Wait()
92 | })
93 | })
94 |
95 | Context("And giving a valid config metric object", func() {
96 | It("should found the valid metric object", func() {
97 | metric, isOk = cnf.Metrics["custom_metric_shell"]
98 | Expect(isOk).To(BeTrue())
99 | })
100 |
101 | It("should not return an error when creating the collector", func() {
102 | _, err = collector.NewPrometheusBashCollector(metric)
103 | Expect(err).NotTo(HaveOccurred())
104 | })
105 |
106 | It("should return a valid Bash collector", func() {
107 | colBash = collector.NewCollectorBash(metric)
108 | Expect(colBash.Config()).To(Equal(metric))
109 | Expect(colBash.Name()).To(Equal(collector.CollectorBashName))
110 | Expect(colBash.Desc()).To(Equal(collector.CollectorBashDesc))
111 | })
112 |
113 | It("should not return an error when call Run", func() {
114 | go func() {
115 | defer func() {
116 | GinkgoRecover()
117 | wg.Done()
118 | }()
119 | log.Debugln("Calling Run")
120 | Expect(colBash.Run(ch)).ToNot(HaveOccurred())
121 | log.Debugln("Run called...")
122 | }()
123 |
124 | wg.Wait()
125 | })
126 | })
127 | })
128 | })
129 |
--------------------------------------------------------------------------------
/custom_exporter.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "net/http"
7 | "os"
8 |
9 | "github.com/orange-cloudfoundry/custom_exporter/collector"
10 | "github.com/orange-cloudfoundry/custom_exporter/config"
11 | "github.com/prometheus/client_golang/prometheus"
12 | "github.com/prometheus/client_golang/prometheus/promhttp"
13 | "github.com/prometheus/common/log"
14 | "github.com/prometheus/common/version"
15 | )
16 |
17 | /*
18 | Copyright 2017 Orange
19 |
20 | Licensed under the Apache License, Version 2.0 (the "License");
21 | you may not use this file except in compliance with the License.
22 | You may obtain a copy of the License at
23 |
24 | http://www.apache.org/licenses/LICENSE-2.0
25 |
26 | Unless required by applicable law or agreed to in writing, software
27 | distributed under the License is distributed on an "AS IS" BASIS,
28 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
29 | See the License for the specific language governing permissions and
30 | limitations under the License.
31 | */
32 |
33 | var ArgsRequire []string
34 | var ArgsSeen map[string]bool
35 |
36 | var showVersion = flag.Bool(
37 | "version",
38 | false,
39 | "Print version information.",
40 | )
41 |
42 | var listenAddress = flag.String(
43 | "web.listen-address",
44 | ":9213",
45 | "Address to listen on for web interface and telemetry.",
46 | )
47 |
48 | var metricPath = flag.String(
49 | "web.telemetry-path",
50 | "/metrics",
51 | "Path under which to expose metrics.",
52 | )
53 |
54 | var configFile = flag.String(
55 | "collector.config",
56 | "",
57 | "Path to config.yml file to read custom exporter definition.",
58 | )
59 |
60 | func init() {
61 | ArgsRequire = []string{
62 | "collector.config",
63 | }
64 |
65 | ArgsSeen = make(map[string]bool)
66 |
67 | prometheus.MustRegister(version.NewCollector(config.Namespace + "_" + config.Exporter))
68 | }
69 |
70 | func main() {
71 | fmt.Fprintln(os.Stdout, version.Info())
72 | fmt.Fprintln(os.Stdout, version.BuildContext())
73 |
74 | flag.Parse()
75 |
76 | if *showVersion {
77 | os.Exit(0)
78 | }
79 |
80 | if ok := checkRequireArgs(); !ok {
81 | os.Exit(2)
82 | }
83 |
84 | if _, err := os.Stat(*configFile); err != nil {
85 | log.Errorln("Error:", err.Error())
86 | os.Exit(2)
87 | }
88 |
89 | var myConfig *config.Config
90 |
91 | if cnf, err := config.NewConfig(*configFile); err != nil {
92 | log.Fatalf("FATAL: %s", err.Error())
93 | } else {
94 | myConfig = cnf
95 | }
96 |
97 | prometheus.MustRegister(createListCollectors(myConfig)...)
98 |
99 | http.Handle(*metricPath, promhttp.Handler())
100 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
101 | w.Write([]byte(`
Custom exporterCustom exporter
Metrics
`))
102 | })
103 |
104 | log.Infoln("Listening on", *listenAddress)
105 | log.Fatal(http.ListenAndServe(*listenAddress, nil))
106 | }
107 |
108 | func checkRequireArgs() bool {
109 | var res bool
110 |
111 | res = true
112 |
113 | flag.Visit(func(f *flag.Flag) { ArgsSeen[f.Name] = true })
114 |
115 | for _, req := range ArgsRequire {
116 | if !ArgsSeen[req] {
117 | fmt.Fprintf(os.Stderr, "missing required -%s argument/flag\n", req)
118 | res = false
119 | }
120 | }
121 |
122 | if !res {
123 | fmt.Fprintf(os.Stdout, "")
124 | fmt.Fprintf(os.Stdout, "")
125 | flag.Usage()
126 | }
127 |
128 | return res
129 | }
130 |
131 | func createListCollectors(c *config.Config) []prometheus.Collector {
132 | var result []prometheus.Collector
133 |
134 | for _, cnf := range c.Metrics {
135 | if col := createNewCollector(&cnf); col != nil {
136 | result = append(result, col)
137 | }
138 | }
139 |
140 | if len(result) < 1 {
141 | log.Fatalf("Error : the metrics list is empty !!")
142 | }
143 |
144 | return result
145 | }
146 |
147 | func createNewCollector(m *config.MetricsItem) prometheus.Collector {
148 | var col prometheus.Collector
149 | var err error
150 |
151 | switch m.Credential.Collector {
152 | case "bash":
153 | col, err = collector.NewPrometheusBashCollector(*m)
154 | case "mysql":
155 | col, err = collector.NewPrometheusMysqlCollector(*m)
156 | case "redis":
157 | col, err = collector.NewPrometheusRedisCollector(*m)
158 | default:
159 | return nil
160 | }
161 |
162 | if err != nil {
163 | log.Errorf("Error: %v", err)
164 | return nil
165 | }
166 |
167 | return col
168 | }
169 |
--------------------------------------------------------------------------------
/collector/redis_test.go:
--------------------------------------------------------------------------------
1 | package collector_test
2 |
3 | import (
4 | . "github.com/onsi/ginkgo"
5 | . "github.com/onsi/gomega"
6 | "github.com/orange-cloudfoundry/custom_exporter/collector"
7 | "github.com/orange-cloudfoundry/custom_exporter/config"
8 | "github.com/prometheus/common/log"
9 | "net/url"
10 | "sync"
11 | )
12 |
13 | /*
14 | Copyright 2017 Orange
15 |
16 | Licensed under the Apache License, Version 2.0 (the "License");
17 | you may not use this file except in compliance with the License.
18 | You may obtain a copy of the License at
19 |
20 | http://www.apache.org/licenses/LICENSE-2.0
21 |
22 | Unless required by applicable law or agreed to in writing, software
23 | distributed under the License is distributed on an "AS IS" BASIS,
24 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
25 | See the License for the specific language governing permissions and
26 | limitations under the License.
27 | */
28 |
29 | var _ = Describe("Testing Custom Export, Staging Config Test: ", func() {
30 | var (
31 | cnf *config.Config
32 | colRedis *collector.CollectorRedis
33 | metric config.MetricsItem
34 |
35 | isOk bool
36 | err error
37 | )
38 |
39 | BeforeEach(func() {
40 |
41 | wg = sync.WaitGroup{}
42 | wg.Add(1)
43 |
44 | cnf, err = config.NewConfig("../example_with_error.yml")
45 |
46 | var dsn *url.URL
47 | for k, m := range cnf.Metrics {
48 | if m.Credential.Collector != collector.CollectorRedisName {
49 | continue
50 | }
51 |
52 | if dsn, err = url.Parse(m.Credential.Dsn); err != nil {
53 | continue
54 | }
55 |
56 | dsn.Host = redisAddr
57 |
58 | m.Credential.Dsn = dsn.String()
59 | cnf.Metrics[k] = m
60 | }
61 | })
62 |
63 | Context("When giving a valid config file with custom_metric_redis", func() {
64 |
65 | It("should have a valid config object", func() {
66 | Expect(err).NotTo(HaveOccurred())
67 | })
68 |
69 | Context("And giving an invalid config metric object", func() {
70 | It("should found the invalid metric object", func() {
71 | metric, isOk = cnf.Metrics["custom_metric_mysql"]
72 | Expect(isOk).To(BeTrue())
73 | })
74 | It("should return an error when creating the collector", func() {
75 | _, err = collector.NewPrometheusRedisCollector(metric)
76 | Expect(err).To(HaveOccurred())
77 | })
78 | })
79 |
80 | Context("And giving an valid config metric object with invalid command", func() {
81 | It("should found the valid metric object", func() {
82 | metric, isOk = cnf.Metrics["custom_metric_redis_error"]
83 | Expect(isOk).To(BeTrue())
84 | })
85 |
86 | It("should not return an error when creating the collector", func() {
87 | _, err = collector.NewPrometheusRedisCollector(metric)
88 | Expect(err).NotTo(HaveOccurred())
89 | })
90 |
91 | It("should return a valid Bash collector", func() {
92 | colRedis = collector.NewCollectorRedis(metric)
93 | Expect(colRedis.Config()).To(Equal(metric))
94 | Expect(colRedis.Name()).To(Equal(collector.CollectorRedisName))
95 | Expect(colRedis.Desc()).To(Equal(collector.CollectorRedisDesc))
96 | })
97 |
98 | It("should return an error when call Run", func() {
99 | go func() {
100 | defer func() {
101 | GinkgoRecover()
102 | wg.Done()
103 | }()
104 | log.Infoln("Calling Run")
105 | Expect(colRedis.Run(ch)).To(HaveOccurred())
106 | log.Infoln("Run called...")
107 | }()
108 |
109 | wg.Wait()
110 | })
111 | })
112 |
113 | Context("And giving a valid config metric object", func() {
114 | It("should found the valid metric object", func() {
115 | metric, isOk = cnf.Metrics["custom_metric_redis"]
116 | Expect(isOk).To(BeTrue())
117 | })
118 |
119 | It("should not return an error when creating the collector", func() {
120 | _, err = collector.NewPrometheusRedisCollector(metric)
121 | Expect(err).NotTo(HaveOccurred())
122 | })
123 |
124 | It("should return a valid redis collector", func() {
125 | colRedis = collector.NewCollectorRedis(metric)
126 | Expect(colRedis.Config()).To(Equal(metric))
127 | Expect(colRedis.Name()).To(Equal(collector.CollectorRedisName))
128 | Expect(colRedis.Desc()).To(Equal(collector.CollectorRedisDesc))
129 | })
130 |
131 | It("should not return an error when call Run", func() {
132 | go func() {
133 | defer func() {
134 | GinkgoRecover()
135 | wg.Done()
136 | }()
137 | log.Infoln("Calling Run")
138 | Expect(colRedis.Run(ch)).ToNot(HaveOccurred())
139 | log.Infoln("Run called...")
140 | }()
141 |
142 | wg.Wait()
143 | })
144 | })
145 | })
146 | })
147 |
--------------------------------------------------------------------------------
/collector/collector.go:
--------------------------------------------------------------------------------
1 | package collector
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | "time"
7 |
8 | "github.com/orange-cloudfoundry/custom_exporter/config"
9 | "github.com/prometheus/client_golang/prometheus"
10 | "github.com/prometheus/common/log"
11 | )
12 |
13 | /*
14 | Copyright 2017 Orange
15 |
16 | Licensed under the Apache License, Version 2.0 (the "License");
17 | you may not use this file except in compliance with the License.
18 | You may obtain a copy of the License at
19 |
20 | http://www.apache.org/licenses/LICENSE-2.0
21 |
22 | Unless required by applicable law or agreed to in writing, software
23 | distributed under the License is distributed on an "AS IS" BASIS,
24 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
25 | See the License for the specific language governing permissions and
26 | limitations under the License.
27 | */
28 |
29 | // Exporter collects MySQL metrics. It implements prometheus.Collector.
30 | type CollectorHelper struct {
31 | duration, error prometheus.Gauge
32 | totalScrapes prometheus.Counter
33 | scrapeErrors *prometheus.CounterVec
34 | collectorCustom CollectorCustom
35 | }
36 | type CollectorCustom interface {
37 | Name() string
38 | Desc() string
39 | Run(ch chan<- prometheus.Metric) error
40 | Config() config.MetricsItem
41 | }
42 |
43 | func NewCollectorHelper(collectorCustom CollectorCustom) *CollectorHelper {
44 | configName := collectorCustom.Config().Name
45 |
46 | helper := &CollectorHelper{
47 | duration: prometheus.NewGauge(prometheus.GaugeOpts{
48 | Namespace: config.Namespace,
49 | Subsystem: configName,
50 | Name: "last_scrape_duration_seconds",
51 | Help: "Duration of the last scrape of metrics from " + configName,
52 | }),
53 |
54 | error: prometheus.NewGauge(prometheus.GaugeOpts{
55 | Namespace: config.Namespace,
56 | Subsystem: configName,
57 | Name: "last_scrape_error",
58 | Help: "Whether the last scrape of metrics from " + configName + " resulted in an error (1 for error, 0 for success).",
59 | }),
60 |
61 | totalScrapes: prometheus.NewCounter(prometheus.CounterOpts{
62 | Namespace: config.Namespace,
63 | Subsystem: configName,
64 | Name: "scrapes_total",
65 | Help: "Total number of times " + configName + " was scraped for metrics.",
66 | }),
67 |
68 | scrapeErrors: prometheus.NewCounterVec(prometheus.CounterOpts{
69 | Namespace: config.Namespace,
70 | Subsystem: configName,
71 | Name: "scrape_errors_total",
72 | Help: "Total number of times an error occurred scraping a " + configName,
73 | }, []string{"collector"}),
74 |
75 | collectorCustom: collectorCustom,
76 | }
77 |
78 | return helper
79 | }
80 |
81 | func (e CollectorHelper) Check(err error) error {
82 | config := e.collectorCustom.Config()
83 | name := e.collectorCustom.Name()
84 |
85 | if config.Credential.Collector != name {
86 | err = fmt.Errorf("mismatching collector type : config type = %s & current type = %s",
87 | config.Credential.Collector,
88 | name,
89 | )
90 | log.Errorln("Error:", err)
91 | }
92 |
93 | if len(config.Commands) < 1 {
94 | err = fmt.Errorf("empty commands to run")
95 | log.Errorln("Error:", err)
96 | }
97 |
98 | return err
99 | }
100 |
101 | func (e *CollectorHelper) Describe(ch chan<- *prometheus.Desc) {
102 | log.Debugln("Call Shell Describe")
103 |
104 | metricCh := make(chan prometheus.Metric)
105 | doneCh := make(chan struct{})
106 |
107 | go func() {
108 | for m := range metricCh {
109 | ch <- m.Desc()
110 | }
111 | close(doneCh)
112 | }()
113 |
114 | e.Collect(metricCh)
115 | close(metricCh)
116 | <-doneCh
117 | }
118 |
119 | // Collect implements prometheus.Collector.
120 | func (e *CollectorHelper) Collect(ch chan<- prometheus.Metric) {
121 | log.Debugln("Call Generic Collect")
122 | e.scrape(ch)
123 | ch <- e.duration
124 | ch <- e.totalScrapes
125 | ch <- e.error
126 | e.scrapeErrors.Collect(ch)
127 | }
128 |
129 | func (e *CollectorHelper) scrape(ch chan<- prometheus.Metric) {
130 | log.Debugln("Call Shell scrape")
131 | e.totalScrapes.Inc()
132 |
133 | var err error
134 |
135 | defer func(begun time.Time) {
136 | e.duration.Set(time.Since(begun).Seconds())
137 | if err == nil {
138 | e.error.Set(0)
139 | } else {
140 | e.error.Set(1)
141 | }
142 | }(time.Now())
143 |
144 | err = e.collectorCustom.Run(ch)
145 | }
146 |
147 | func PromDesc(collectorCustom CollectorCustom) string {
148 | log.Debugln("Call Generic PromDesc")
149 |
150 | var namespace string
151 | var subsystem string
152 | var name string
153 |
154 | namespace = config.Namespace
155 | //subsystem = collectorCustom.Name()
156 | name = strings.ToLower(collectorCustom.Config().Name)
157 |
158 | log.Debugf("Calling PromDesc with namespace \"%s\", subsystem \"%s\" and name \"%s\"", namespace, subsystem, name)
159 | return prometheus.BuildFQName(namespace, subsystem, name)
160 | }
161 |
--------------------------------------------------------------------------------
/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "io/ioutil"
5 | "os/user"
6 | "strconv"
7 | "strings"
8 |
9 | "github.com/prometheus/client_golang/prometheus"
10 | "github.com/prometheus/common/log"
11 | "gopkg.in/yaml.v2"
12 | )
13 |
14 | /*
15 | Copyright 2017 Orange
16 |
17 | Licensed under the Apache License, Version 2.0 (the "License");
18 | you may not use this file except in compliance with the License.
19 | You may obtain a copy of the License at
20 |
21 | http://www.apache.org/licenses/LICENSE-2.0
22 |
23 | Unless required by applicable law or agreed to in writing, software
24 | distributed under the License is distributed on an "AS IS" BASIS,
25 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
26 | See the License for the specific language governing permissions and
27 | limitations under the License.
28 | */
29 |
30 | // Metric name parts.
31 | const (
32 | // Namespace for all metrics.
33 | Namespace = "custom"
34 | // Subsystem(s).
35 | Exporter = "exporter"
36 | )
37 |
38 | type CredentialsItem struct {
39 | Name string `yaml:"name"`
40 | Collector string `yaml:"type"`
41 |
42 | User string `yaml:"user,omitempty"`
43 | Dsn string `yaml:"dsn,omitempty"`
44 | Uri string `yaml:"uri,omitempty"`
45 | Path string `yaml:"path,omitempty"`
46 |
47 | //@TODO add user to allow run command as this user... for shell need uid/gid
48 | }
49 |
50 | type MetricsItem struct {
51 | Name string
52 | Commands []string
53 |
54 | Credential CredentialsItem
55 |
56 | Mapping []string
57 | Separator string
58 | Value_name string
59 | Value_type prometheus.ValueType
60 | }
61 |
62 | type MetricsItemYaml struct {
63 | Name string `yaml:"name"`
64 | Commands []string `yaml:"commands"`
65 |
66 | Credential string `yaml:"credential"`
67 |
68 | Mapping []string `yaml:"mapping"`
69 | Separator string `yaml:"separator,omitempty"`
70 | Value_name string `yaml:"value_name,omitempty"`
71 | Value_type string `yaml:"value_type"`
72 | }
73 |
74 | type ConfigYaml struct {
75 | Credentials []CredentialsItem `yaml:"credentials"`
76 | Metrics []MetricsItemYaml `yaml:"metrics"`
77 | }
78 |
79 | type Config struct {
80 | Metrics map[string]MetricsItem
81 | }
82 |
83 | type CredentialsUser struct {
84 | user.User
85 | }
86 |
87 | func NewConfig(configFile string) (*Config, error) {
88 | var contentFile []byte
89 | var err error
90 |
91 | if contentFile, err = ioutil.ReadFile(configFile); err != nil {
92 | return nil, err
93 | }
94 |
95 | ymlCnf := ConfigYaml{}
96 |
97 | if err = yaml.Unmarshal(contentFile, &ymlCnf); err != nil {
98 | return nil, err
99 | }
100 |
101 | myCnf := new(Config)
102 | myCnf.metricsList(ymlCnf)
103 |
104 | //log.Debugln("config loaded:\n", string(contentFile))
105 |
106 | return myCnf, nil
107 | }
108 |
109 | func (c Config) credentialsList(yaml ConfigYaml) map[string]CredentialsItem {
110 | var result = make(map[string]CredentialsItem)
111 |
112 | for _, v := range yaml.Credentials {
113 | result[v.Name] = CredentialsItem{
114 | Name: v.Name,
115 | Collector: v.Collector,
116 | Dsn: v.Dsn,
117 | Path: v.Path,
118 | Uri: v.Uri,
119 | }
120 | }
121 |
122 | return result
123 | }
124 |
125 | func (e Config) ValueType(Value_type string) prometheus.ValueType {
126 |
127 | switch Value_type {
128 | case "COUNTER":
129 | return prometheus.CounterValue
130 | case "GAUGE":
131 | return prometheus.GaugeValue
132 | }
133 |
134 | return prometheus.UntypedValue
135 | }
136 |
137 | func (c *Config) metricsList(yaml ConfigYaml) {
138 | var result = make(map[string]MetricsItem)
139 | var credentials = c.credentialsList(yaml)
140 |
141 | for _, v := range yaml.Metrics {
142 | if cred, ok := credentials[v.Credential]; ok {
143 | result[v.Name] = MetricsItem{
144 | Name: v.Name,
145 | Commands: v.Commands,
146 | Credential: cred,
147 | Mapping: v.Mapping,
148 | Separator: v.Separator,
149 | Value_name: v.Value_name,
150 | Value_type: c.ValueType(v.Value_type),
151 | }
152 | } else {
153 | log.Fatalf("error credential, collector type not found : %s", v.Credential)
154 | }
155 | }
156 |
157 | c.Metrics = result
158 | }
159 |
160 | func (m MetricsItem) SeparatorValue() string {
161 | sep := m.Separator
162 |
163 | if len(sep) < 1 {
164 | sep = " "
165 | }
166 |
167 | return sep
168 | }
169 |
170 | func (m MetricsItem) CredentialUser() *CredentialsUser {
171 | usr := strings.TrimSpace(m.Credential.User)
172 |
173 | if len(usr) == 0 {
174 | return currentUser()
175 | }
176 |
177 | if myUser, err := user.LookupId(usr); err == nil {
178 | return &CredentialsUser{User: *myUser}
179 | }
180 |
181 | if myUser, err := user.Lookup(usr); err == nil {
182 | return &CredentialsUser{User: *myUser}
183 | }
184 |
185 | return currentUser()
186 | }
187 |
188 | func currentUser() *CredentialsUser {
189 | var myUser *user.User
190 | var err error
191 |
192 | if myUser, err = user.Current(); err != nil {
193 | log.Fatalf("Error on retrieve current system user : %s", err.Error())
194 | }
195 |
196 | return &CredentialsUser{User: *myUser}
197 | }
198 |
199 | func (c CredentialsUser) UidInt() uint32 {
200 | if uid, err := strconv.ParseUint(c.Uid, 10, 32); err == nil {
201 | return uint32(uid)
202 | }
203 | return 0
204 | }
205 |
206 | func (c CredentialsUser) GidInt() uint32 {
207 | if gid, err := strconv.ParseUint(c.Gid, 10, 32); err == nil {
208 | return uint32(gid)
209 | }
210 | return 0
211 | }
212 |
--------------------------------------------------------------------------------
/collector/mysql_test.go:
--------------------------------------------------------------------------------
1 | package collector_test
2 |
3 | import (
4 | . "github.com/onsi/ginkgo"
5 | . "github.com/onsi/gomega"
6 |
7 | "sync"
8 |
9 | "database/sql"
10 | "database/sql/driver"
11 | "errors"
12 | "github.com/orange-cloudfoundry/custom_exporter/collector"
13 | "github.com/orange-cloudfoundry/custom_exporter/config"
14 | "github.com/prometheus/common/log"
15 | "gopkg.in/DATA-DOG/go-sqlmock.v1"
16 | )
17 |
18 | /*
19 | Copyright 2017 Orange
20 |
21 | Licensed under the Apache License, Version 2.0 (the "License");
22 | you may not use this file except in compliance with the License.
23 | You may obtain a copy of the License at
24 |
25 | http://www.apache.org/licenses/LICENSE-2.0
26 |
27 | Unless required by applicable law or agreed to in writing, software
28 | distributed under the License is distributed on an "AS IS" BASIS,
29 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
30 | See the License for the specific language governing permissions and
31 | limitations under the License.
32 | */
33 |
34 | var _ = Describe("Testing Custom Export, Staging Config Test: ", func() {
35 | var (
36 | cnf *config.Config
37 | colMysql *collector.CollectorMysql
38 | metric config.MetricsItem
39 |
40 | DBclient *sql.DB
41 | DBmock sqlmock.Sqlmock
42 |
43 | isOk bool
44 | err error
45 | )
46 |
47 | BeforeEach(func() {
48 | wg = sync.WaitGroup{}
49 | wg.Add(1)
50 |
51 | cnf, err = config.NewConfig("../example_with_error.yml")
52 |
53 | if DBclient, DBmock, err = sqlmock.New(); err != nil {
54 | log.Fatalf("Error while trying to mock DB Mysql connection : %v", err)
55 | }
56 | })
57 |
58 | Context("When giving a valid config file with custom_metric_mysql", func() {
59 |
60 | It("should have a valid config object", func() {
61 | Expect(err).NotTo(HaveOccurred())
62 | })
63 |
64 | Context("And giving an invalid config metric object", func() {
65 | It("should found the invalid metric object", func() {
66 | metric, isOk = cnf.Metrics["custom_metric_shell"]
67 | Expect(isOk).To(BeTrue())
68 | })
69 | It("should return an error when creating the collector", func() {
70 | _, err = collector.NewPrometheusMysqlCollector(metric)
71 | Expect(err).To(HaveOccurred())
72 | })
73 | })
74 |
75 | Context("And giving an valid config metric object with invalid command", func() {
76 | It("should found the valid metric object", func() {
77 | metric, isOk = cnf.Metrics["custom_metric_mysql_error"]
78 | Expect(isOk).To(BeTrue())
79 | })
80 |
81 | It("should not return an error when creating the collector", func() {
82 | _, err = collector.NewPrometheusMysqlCollector(metric)
83 | Expect(err).NotTo(HaveOccurred())
84 | })
85 |
86 | It("should return a valid Bash collector", func() {
87 | colMysql = collector.NewCollectorMysql(metric)
88 | Expect(colMysql.Config()).To(Equal(metric))
89 | Expect(colMysql.Name()).To(Equal(collector.CollectorMysqlName))
90 | Expect(colMysql.Desc()).To(Equal(collector.CollectorMysqlDesc))
91 | })
92 |
93 | It("should return an error when call Run", func() {
94 | colMysql.StoreDBClient(DBclient)
95 | DBmock.ExpectQuery("SELECT \"id\", \"name\", 1, FROM animals").WithArgs().WillReturnError(errors.New("Generated SQL Error in mock object"))
96 |
97 | go func() {
98 | defer func() {
99 | GinkgoRecover()
100 | wg.Done()
101 | }()
102 | log.Infoln("Calling Run")
103 | Expect(colMysql.Run(ch)).To(HaveOccurred())
104 | log.Infoln("Run called...")
105 | }()
106 | wg.Wait()
107 | })
108 | })
109 |
110 | Context("And giving a valid config metric object", func() {
111 | It("should found the valid metric object", func() {
112 | metric, isOk = cnf.Metrics["custom_metric_mysql"]
113 | Expect(isOk).To(BeTrue())
114 | })
115 |
116 | It("should not return an error when creating the collector", func() {
117 | _, err = collector.NewPrometheusMysqlCollector(metric)
118 | Expect(err).NotTo(HaveOccurred())
119 | })
120 |
121 | It("should return a valid mysql collector", func() {
122 | colMysql = collector.NewCollectorMysql(metric)
123 | Expect(colMysql.Config()).To(Equal(metric))
124 | Expect(colMysql.Name()).To(Equal(collector.CollectorMysqlName))
125 | Expect(colMysql.Desc()).To(Equal(collector.CollectorMysqlDesc))
126 | })
127 |
128 | It("should not return an error when call Run", func() {
129 | colMysql.StoreDBClient(DBclient)
130 |
131 | var rows *sqlmock.Rows
132 | var rowValues []driver.Value
133 |
134 | rows = sqlmock.NewRows([]string{"id", "name", "count"})
135 |
136 | rowValues = make([]driver.Value, 0)
137 | rowValues = append(rowValues, 1)
138 | rowValues = append(rowValues, "chicken")
139 | rowValues = append(rowValues, 128)
140 | rows.AddRow(rowValues...)
141 |
142 | rowValues = make([]driver.Value, 0)
143 | rowValues = append(rowValues, 2)
144 | rowValues = append(rowValues, "beef")
145 | rowValues = append(rowValues, 256)
146 | rows.AddRow(rowValues...)
147 |
148 | rowValues = make([]driver.Value, 0)
149 | rowValues = append(rowValues, 3)
150 | rowValues = append(rowValues, "snails")
151 | rowValues = append(rowValues, 14)
152 | rows.AddRow(rowValues...)
153 |
154 | DBmock.ExpectQuery("SELECT aml_id,aml_name,aml_number FROM animals").WillReturnRows(rows)
155 |
156 | go func() {
157 | defer func() {
158 | GinkgoRecover()
159 | wg.Done()
160 | }()
161 | log.Infoln("Calling Run")
162 | err := colMysql.Run(ch)
163 |
164 | if err != nil {
165 | log.Errorf("Error : %v", err)
166 | }
167 |
168 | Expect(err).ToNot(HaveOccurred())
169 | log.Infoln("Run called...")
170 | }()
171 |
172 | wg.Wait()
173 | })
174 | })
175 | })
176 | })
177 |
--------------------------------------------------------------------------------
/collector/bash.go:
--------------------------------------------------------------------------------
1 | package collector
2 |
3 | import (
4 | "github.com/orange-cloudfoundry/custom_exporter/config"
5 | "github.com/prometheus/client_golang/prometheus"
6 | "github.com/prometheus/common/log"
7 | "os"
8 | "os/exec"
9 | "regexp"
10 | "strconv"
11 | "strings"
12 | "syscall"
13 | )
14 |
15 | /*
16 | Copyright 2017 Orange
17 |
18 | Licensed under the Apache License, Version 2.0 (the "License");
19 | you may not use this file except in compliance with the License.
20 | You may obtain a copy of the License at
21 |
22 | http://www.apache.org/licenses/LICENSE-2.0
23 |
24 | Unless required by applicable law or agreed to in writing, software
25 | distributed under the License is distributed on an "AS IS" BASIS,
26 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
27 | See the License for the specific language governing permissions and
28 | limitations under the License.
29 | */
30 |
31 | const (
32 | CollectorBashName = "bash"
33 | CollectorBashDesc = "Metrics from shell collector in the custom exporter."
34 | )
35 |
36 | type CollectorBash struct {
37 | metricsConfig config.MetricsItem
38 | }
39 |
40 | func NewCollectorBash(config config.MetricsItem) *CollectorBash {
41 | return &CollectorBash{
42 | metricsConfig: config,
43 | }
44 | }
45 |
46 | func NewPrometheusBashCollector(config config.MetricsItem) (prometheus.Collector, error) {
47 | myCol := NewCollectorHelper(
48 | NewCollectorBash(config),
49 | )
50 |
51 | log.Infof("Collector Added: Type '%s' / Name '%s' / Credentials '%s'", CollectorBashName, config.Name, config.Credential.Name)
52 |
53 | return myCol, myCol.Check(nil)
54 | }
55 |
56 | func (e CollectorBash) Config() config.MetricsItem {
57 | return e.metricsConfig
58 | }
59 |
60 | func (e CollectorBash) Name() string {
61 | return CollectorBashName
62 | }
63 |
64 | func (e CollectorBash) Desc() string {
65 | return CollectorBashDesc
66 | }
67 |
68 | func (e CollectorBash) Run(ch chan<- prometheus.Metric) error {
69 | var output []byte
70 | var err error
71 | var command string
72 | var args []string
73 | var cmd *exec.Cmd
74 | var sysCred syscall.SysProcAttr
75 | var useCred bool
76 |
77 | os.Setenv("CREDENTIALS_NAME", e.metricsConfig.Credential.Name)
78 | os.Setenv("CREDENTIALS_COLLECTOR", e.metricsConfig.Credential.Collector)
79 | os.Setenv("CREDENTIALS_DSN", e.metricsConfig.Credential.Dsn)
80 | os.Setenv("CREDENTIALS_PATH", e.metricsConfig.Credential.Path)
81 | os.Setenv("CREDENTIALS_URI", e.metricsConfig.Credential.Uri)
82 |
83 | if e.metricsConfig.Credential.User != "" {
84 | useCred = true
85 | creduser := e.metricsConfig.CredentialUser()
86 | sysCred = syscall.SysProcAttr{Credential: &syscall.Credential{Uid: creduser.UidInt(), Gid: creduser.GidInt()}}
87 | } else {
88 | useCred = false
89 | }
90 |
91 | regexCmd := regexp.MustCompile("'.+'|\".+\"|\\S+")
92 |
93 | for _, c := range e.metricsConfig.Commands {
94 |
95 | args = regexCmd.FindAllString(c, -1)
96 | command, args = args[0], args[1:]
97 |
98 | log.Debugf("Parsed command : %s -- %v", command, args)
99 | log.Debugf("Checking command/script exists : \"%s\"...", command)
100 |
101 | _, err = exec.LookPath(command)
102 | if err != nil {
103 | log.Errorf("Error with metric \"%s\" while checking command exists \"%s\" : %s", e.metricsConfig.Name, c, err.Error())
104 | return err
105 | }
106 |
107 | log.Debugf("Running command \"%s\" with params \"%s\"...", command, args)
108 |
109 | //config the command statement, stding (use last output) and the env vars
110 | cmd = exec.Command(command, args...)
111 | cmd.Env = os.Environ()
112 | cmd.Stdin = strings.NewReader(string(output))
113 |
114 | if useCred {
115 | cmd.SysProcAttr = &sysCred
116 | }
117 |
118 | // run the command
119 | output, err = cmd.CombinedOutput()
120 |
121 | if err != nil {
122 | log.Errorf("Error with metric \"%s\" while running command \"%s\" : %v : %v", e.metricsConfig.Name, c, err, string(output))
123 | return err
124 | }
125 |
126 | log.Debugf("Result command \"%s\" : \"%s\"", command, string(output))
127 | }
128 |
129 | log.Debugf("Run metric \"%s\" command '%s'", e.metricsConfig.Name, command)
130 | log.Debugln("Result:", "\n"+string(output))
131 |
132 | return e.parse(ch, string(output))
133 | }
134 |
135 | func (e CollectorBash) parse(ch chan<- prometheus.Metric, output string) error {
136 | var err error
137 |
138 | err = nil
139 | sep := e.metricsConfig.Separator
140 | nb := len(e.metricsConfig.Mapping) + 1
141 |
142 | for _, l := range strings.Split(output, "\n") {
143 | if len(strings.TrimSpace(l)) < nb {
144 | continue
145 | }
146 |
147 | log.Debugf("Parsing line: \"%s\"...", l)
148 |
149 | // prevents first and last char are a separator
150 | l = strings.Trim(strings.TrimSpace(l), sep)
151 |
152 | if errline := e.parseLine(ch, strings.Split(l, sep)); errline != nil {
153 | log.Errorf("Error with metric \"%s\" while parsing line : %s", e.metricsConfig.Name, errline.Error())
154 | err = errline
155 | }
156 | }
157 |
158 | return err
159 | }
160 |
161 | func (e *CollectorBash) parseLine(ch chan<- prometheus.Metric, fields []string) error {
162 | var (
163 | mapping []string
164 | labelVal []string
165 | metricVal float64
166 | err error
167 | )
168 |
169 | mapping = e.metricsConfig.Mapping
170 | labelVal = make([]string, len(mapping))
171 | err = nil
172 |
173 | for i, value := range fields {
174 |
175 | value = strings.TrimSpace(value)
176 |
177 | if (i + 1) > len(mapping) {
178 | if metricVal, err = strconv.ParseFloat(value, 64); err != nil {
179 | metricVal = float64(0)
180 | }
181 | } else {
182 | labelVal[i] = value
183 | }
184 | }
185 |
186 | if err != nil {
187 | log.Debugf("Return error : '%s'", err.Error())
188 | return err
189 | }
190 |
191 | prom_desc := PromDesc(e)
192 | log.Debugf("Add Metric \"%s\" : Tag '%s' / TagValue '%s' / Value '%v'", prom_desc, mapping, labelVal, metricVal)
193 |
194 | metric := prometheus.MustNewConstMetric(
195 | prometheus.NewDesc(prom_desc, e.metricsConfig.Name, mapping, nil),
196 | e.metricsConfig.Value_type, metricVal, labelVal...,
197 | )
198 |
199 | select {
200 | case ch <- metric:
201 | log.Debug("Return no error...")
202 | return nil
203 | default:
204 | log.Info("Cannot write to channel...")
205 | }
206 |
207 | return err
208 | }
209 |
--------------------------------------------------------------------------------
/collector/mysql.go:
--------------------------------------------------------------------------------
1 | package collector
2 |
3 | import (
4 | "database/sql"
5 | "fmt"
6 | "strings"
7 |
8 | "github.com/orange-cloudfoundry/custom_exporter/config"
9 | "github.com/prometheus/client_golang/prometheus"
10 | "github.com/prometheus/common/log"
11 |
12 | _ "github.com/go-sql-driver/mysql"
13 | )
14 |
15 | /*
16 | Copyright 2017 Orange
17 |
18 | Licensed under the Apache License, Version 2.0 (the "License");
19 | you may not use this file except in compliance with the License.
20 | You may obtain a copy of the License at
21 |
22 | http://www.apache.org/licenses/LICENSE-2.0
23 |
24 | Unless required by applicable law or agreed to in writing, software
25 | distributed under the License is distributed on an "AS IS" BASIS,
26 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
27 | See the License for the specific language governing permissions and
28 | limitations under the License.
29 | */
30 |
31 | const (
32 | CollectorMysqlName = "mysql"
33 | CollectorMysqlDesc = "Metrics from mysql collector in the custom exporter."
34 | )
35 |
36 | type CollectorMysql struct {
37 | client *sql.DB
38 | metricsConfig config.MetricsItem
39 | }
40 |
41 | func NewCollectorMysql(config config.MetricsItem) *CollectorMysql {
42 | return &CollectorMysql{
43 | metricsConfig: config,
44 | }
45 | }
46 |
47 | func NewPrometheusMysqlCollector(config config.MetricsItem) (prometheus.Collector, error) {
48 | myCol := NewCollectorHelper(NewCollectorMysql(config))
49 |
50 | log.Infof("Collector Added: Type '%s' / Name '%s' / Credentials '%s'", CollectorMysqlName, config.Name, config.Credential.Name)
51 | return myCol, myCol.Check(nil)
52 | }
53 |
54 | func (e CollectorMysql) Config() config.MetricsItem {
55 | return e.metricsConfig
56 | }
57 |
58 | func (e CollectorMysql) Name() string {
59 | return CollectorMysqlName
60 | }
61 |
62 | func (e CollectorMysql) Desc() string {
63 | return CollectorMysqlDesc
64 | }
65 |
66 | func (e *CollectorMysql) Run(ch chan<- prometheus.Metric) error {
67 | var (
68 | err error
69 | out *sql.Rows
70 | )
71 |
72 | err = nil
73 |
74 | if e.client == nil {
75 | if err = e.DBClient(); err != nil {
76 | log.Errorf("Error for metrics \"%s\" while creating DB client \"%s\": %v", e.metricsConfig.Name, e.metricsConfig.Credential.Dsn, err)
77 | return err
78 | }
79 | }
80 |
81 | defer func() {
82 | e.client.Close()
83 | e.client = nil
84 | }()
85 |
86 | if err = e.client.Ping(); err != nil {
87 | log.Errorf("Error for metrics \"%s\" while trying to ping DB server \"%s\": %v", e.metricsConfig.Name, e.metricsConfig.Credential.Dsn, err)
88 | return err
89 | }
90 |
91 | log.Debugln("Calling Mysql Commands... ")
92 |
93 | for _, c := range e.metricsConfig.Commands {
94 | c = strings.TrimSpace(c)
95 |
96 | if len(c) < 1 {
97 | continue
98 | }
99 |
100 | if out, err = e.client.Query(c); err != nil {
101 | log.Errorf("Error for metrics \"%s\" while calling query \"%s\": %v", e.metricsConfig.Name, c, err)
102 | return err
103 | }
104 | }
105 |
106 | return e.parseResult(ch, out)
107 | }
108 |
109 | func (e *CollectorMysql) parseResult(ch chan<- prometheus.Metric, res *sql.Rows) error {
110 | var (
111 | err error
112 | nbCols int
113 | colMapping map[int]string
114 | tagLabels []string
115 | tagValues []string
116 | valMetric float64
117 | )
118 |
119 | if colList, err := res.Columns(); err != nil {
120 | log.Errorf("Error for metrics \"%s\" while retrieve columns names : %v", e.metricsConfig.Name, err)
121 | return err
122 | } else {
123 | nbCols = len(colList)
124 | colMapping = e.mapColumsConfig(colList, e.metricsConfig.Mapping)
125 | }
126 |
127 | log.Debugf("Metrics \"%s\" - Colums lists : %v", e.metricsConfig.Name, colMapping)
128 |
129 | for res.Next() {
130 | ptrMapping := make([]interface{}, nbCols)
131 | rawMapping := make([][]byte, nbCols)
132 |
133 | tagLabels = make([]string, 0)
134 | tagValues = make([]string, 0)
135 | valMetric = float64(0)
136 |
137 | for i := range colMapping {
138 | if (i + 1) < len(colMapping) {
139 | ptrMapping[i] = &rawMapping[i]
140 | } else {
141 | ptrMapping[i] = &valMetric
142 | }
143 | }
144 |
145 | if errRow := res.Scan(ptrMapping...); errRow != nil {
146 | log.Errorf("Error for metrics \"%s\", while parsing result : %v", e.metricsConfig.Name, errRow)
147 | err = errRow
148 | continue
149 | }
150 |
151 | for i, k := range colMapping {
152 | if (i + 1) < len(colMapping) {
153 | if k != "" {
154 | tagLabels = append(tagLabels, k)
155 | tagValues = append(tagValues, string(rawMapping[i]))
156 | }
157 | }
158 | }
159 |
160 | prom_desc := PromDesc(e)
161 | log.Debugf("Add Metric \"%s\" : Tag '%s' / TagValue '%s' / Value '%v'", prom_desc, tagLabels, tagValues, valMetric)
162 |
163 | metric := prometheus.MustNewConstMetric(
164 | prometheus.NewDesc(prom_desc, e.metricsConfig.Name, tagLabels, nil),
165 | e.metricsConfig.Value_type, valMetric, tagValues...,
166 | )
167 |
168 | select {
169 | case ch <- metric:
170 | log.Debug("Return no error...")
171 | default:
172 | log.Info("Cannot write to channel...")
173 | }
174 | }
175 |
176 | return err
177 | }
178 |
179 | func (e *CollectorMysql) mapColumsConfig(colums, config []string) map[int]string {
180 | var res = make(map[int]string)
181 |
182 | for i, c := range colums {
183 |
184 | c = strings.TrimSpace(c)
185 |
186 | for _, k := range config {
187 |
188 | k = strings.TrimSpace(k)
189 |
190 | if k == c {
191 | res[i] = k
192 | }
193 | }
194 |
195 | if _, ok := res[i]; !ok {
196 | res[i] = ""
197 | }
198 | }
199 |
200 | return res
201 | }
202 |
203 | func (e CollectorMysql) DsnPart() (string, string, error) {
204 | dsn := strings.TrimSpace(e.metricsConfig.Credential.Dsn)
205 |
206 | if len(dsn) < 1 {
207 | return "", "", fmt.Errorf("cannot find a valid dsn : %s", e.metricsConfig.Credential.Dsn)
208 | }
209 |
210 | dsnPart := strings.SplitN(e.metricsConfig.Credential.Dsn, "://", 2)
211 |
212 | if dsnPart[0] == "" {
213 | return "", "", fmt.Errorf("cannot find a valid dsn : %s", e.metricsConfig.Credential.Dsn)
214 | }
215 |
216 | if len(dsnPart[1]) < 3 {
217 | return "", "", fmt.Errorf("cannot find a valid dsn : %s", e.metricsConfig.Credential.Dsn)
218 | }
219 |
220 | return dsnPart[0], dsnPart[1], nil
221 | }
222 |
223 | func (e *CollectorMysql) DBClient() error {
224 | var (
225 | dsnstr string
226 | driver string
227 | client *sql.DB
228 | err error
229 | )
230 |
231 | if driver, dsnstr, err = e.DsnPart(); err != nil {
232 | return err
233 | }
234 |
235 | if client, err = sql.Open(driver, dsnstr); err != nil {
236 | return err
237 | }
238 |
239 | e.StoreDBClient(client)
240 |
241 | return nil
242 | }
243 |
244 | func (e *CollectorMysql) StoreDBClient(client *sql.DB) {
245 | e.client = client
246 | }
247 |
--------------------------------------------------------------------------------
/custom_exporter_test.go:
--------------------------------------------------------------------------------
1 | package main_test
2 |
3 | import (
4 | "io"
5 | "net/http"
6 | "os/exec"
7 | "strconv"
8 |
9 | "fmt"
10 |
11 | "os"
12 | "time"
13 |
14 | "github.com/onsi/gomega/gbytes"
15 | "github.com/onsi/gomega/gexec"
16 | "github.com/tedsuo/ifrit"
17 | "github.com/tedsuo/ifrit/ginkgomon"
18 |
19 | . "github.com/onsi/ginkgo"
20 | . "github.com/onsi/gomega"
21 | "io/ioutil"
22 | )
23 |
24 | /*
25 | Copyright 2017 Orange
26 |
27 | Licensed under the Apache License, Version 2.0 (the "License");
28 | you may not use this file except in compliance with the License.
29 | You may obtain a copy of the License at
30 |
31 | http://www.apache.org/licenses/LICENSE-2.0
32 |
33 | Unless required by applicable law or agreed to in writing, software
34 | distributed under the License is distributed on an "AS IS" BASIS,
35 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
36 | See the License for the specific language governing permissions and
37 | limitations under the License.
38 | */
39 |
40 | type failRunner struct {
41 | Command *exec.Cmd
42 | Name string
43 | AnsiColorCode string
44 | StartCheck string
45 | StartCheckTimeout time.Duration
46 | Cleanup func()
47 | session *gexec.Session
48 | sessionReady chan struct{}
49 | existStatus int
50 | }
51 |
52 | var (
53 | args []string
54 | listenAddr string
55 | metricRoute string
56 | configPath string
57 | logLevel string
58 |
59 | process ifrit.Process
60 | )
61 |
62 | func (r failRunner) Run(sigChan <-chan os.Signal, ready chan<- struct{}) error {
63 | defer GinkgoRecover()
64 |
65 | var err error
66 |
67 | allOutput := gbytes.NewBuffer()
68 |
69 | debugWriter := gexec.NewPrefixedWriter(
70 | fmt.Sprintf("\x1b[32m[d]\x1b[%s[%s]\x1b[0m ", r.AnsiColorCode, r.Name),
71 | GinkgoWriter,
72 | )
73 |
74 | r.session, err = gexec.Start(
75 | r.Command,
76 | gexec.NewPrefixedWriter(
77 | fmt.Sprintf("\x1b[32m[o]\x1b[%s[%s]\x1b[0m ", r.AnsiColorCode, r.Name),
78 | io.MultiWriter(allOutput, GinkgoWriter),
79 | ),
80 | gexec.NewPrefixedWriter(
81 | fmt.Sprintf("\x1b[91m[e]\x1b[%s[%s]\x1b[0m ", r.AnsiColorCode, r.Name),
82 | io.MultiWriter(allOutput, GinkgoWriter),
83 | ),
84 | )
85 |
86 | Ω(err).ShouldNot(HaveOccurred())
87 |
88 | fmt.Fprintf(debugWriter, "spawned %s (pid: %d)\n", r.Command.Path, r.Command.Process.Pid)
89 |
90 | if r.sessionReady != nil {
91 | close(r.sessionReady)
92 | }
93 |
94 | startCheckDuration := r.StartCheckTimeout
95 | if startCheckDuration == 0 {
96 | startCheckDuration = 5 * time.Second
97 | }
98 |
99 | var startCheckTimeout <-chan time.Time
100 | if r.StartCheck != "" {
101 | startCheckTimeout = time.After(startCheckDuration)
102 | }
103 |
104 | detectStartCheck := allOutput.Detect(r.StartCheck)
105 |
106 | for {
107 | select {
108 | case <-detectStartCheck: // works even with empty string
109 | allOutput.CancelDetects()
110 | startCheckTimeout = nil
111 | detectStartCheck = nil
112 | close(ready)
113 |
114 | case <-startCheckTimeout:
115 | // clean up hanging process
116 | r.session.Kill().Wait()
117 |
118 | // fail to start
119 | return fmt.Errorf(
120 | "did not see %s in command's output within %s. full output:\n\n%s",
121 | r.StartCheck,
122 | startCheckDuration,
123 | string(allOutput.Contents()),
124 | )
125 |
126 | case signal := <-sigChan:
127 | r.session.Signal(signal)
128 |
129 | case <-r.session.Exited:
130 | if r.Cleanup != nil {
131 | r.Cleanup()
132 | }
133 |
134 | Expect(string(allOutput.Contents())).To(ContainSubstring(r.StartCheck))
135 | Expect(r.session.ExitCode()).To(Equal(r.existStatus), fmt.Sprintf("Expected process to exit with %d, got: %d", r.existStatus, r.session.ExitCode()))
136 | return nil
137 | }
138 | }
139 | }
140 |
141 | var _ = Describe("Custom Export Main Test", func() {
142 | BeforeEach(func() {
143 | logLevel = "debug"
144 | })
145 |
146 | AfterEach(func() {
147 | ginkgomon.Kill(process)
148 | })
149 |
150 | Context("Missing required args", func() {
151 | It("shows usage", func() {
152 | var args []string
153 |
154 | // args = append(args, "-log.level="+logLevel)
155 |
156 | exporter := failRunner{
157 | Name: "custom_exporter",
158 | Command: exec.Command(binaryPath, args...),
159 | StartCheck: " missing required -collector.config argument/flag",
160 | existStatus: 2,
161 | }
162 | process = ifrit.Invoke(exporter)
163 | })
164 | })
165 |
166 | Context("Given a wrong required args", func() {
167 | It("shows usage", func() {
168 | var args []string
169 |
170 | args = append(args, "-collector.config=wrong.err")
171 | // args = append(args, "-log.level="+logLevel)
172 |
173 | exporter := failRunner{
174 | Name: "custom_exporter",
175 | Command: exec.Command(binaryPath, args...),
176 | StartCheck: "no such file or directory",
177 | existStatus: 2,
178 | }
179 |
180 | process = ifrit.Invoke(exporter)
181 | })
182 | })
183 |
184 | Context("Has required args", func() {
185 | BeforeEach(func() {
186 | listenAddr = "0.0.0.0:" + strconv.Itoa(9213+GinkgoParallelNode())
187 | configPath = "example_shell.yml"
188 | metricRoute = "/metrics"
189 |
190 | args = append(args, "-web.listen-address="+listenAddr)
191 | args = append(args, "-collector.config="+configPath)
192 | args = append(args, "-web.telemetry-path="+metricRoute)
193 | // args = append(args, "-log.level="+logLevel)
194 |
195 | exporter := failRunner{
196 | Name: "custom_exporter",
197 | Command: exec.Command(binaryPath, args...),
198 | StartCheck: "Listening",
199 | StartCheckTimeout: 30 * time.Second,
200 | existStatus: 137,
201 | }
202 |
203 | process = ifrit.Invoke(exporter)
204 | })
205 |
206 | It("should listen on the given address and return the landing page", func() {
207 |
208 | landingPage := []byte(`Custom exporterCustom exporter
Metrics
`)
209 |
210 | req, err := http.NewRequest("GET", "http://"+listenAddr+"/", nil)
211 | Expect(err).NotTo(HaveOccurred())
212 |
213 | resp, err := http.DefaultClient.Do(req)
214 | Expect(err).NotTo(HaveOccurred())
215 | Expect(resp.StatusCode).To(Equal(200))
216 |
217 | body, err := ioutil.ReadAll(resp.Body)
218 | Expect(err).NotTo(HaveOccurred())
219 | Expect(body).To(Equal(landingPage))
220 | })
221 |
222 | It("should listen on the given address and return the metrics route", func() {
223 |
224 | req, err := http.NewRequest("GET", "http://"+listenAddr+"/"+metricRoute, nil)
225 | Expect(err).NotTo(HaveOccurred())
226 |
227 | resp, err := http.DefaultClient.Do(req)
228 | Expect(err).NotTo(HaveOccurred())
229 | Expect(resp.StatusCode).To(Equal(200))
230 |
231 | body, err := ioutil.ReadAll(resp.Body)
232 |
233 | Expect(err).NotTo(HaveOccurred())
234 |
235 | //println(string(body))
236 |
237 | Expect(string(body)).To(ContainSubstring("custom_custom_metric_shell{animals=\"beef\",id=\"2\"} 256"))
238 | Expect(string(body)).To(ContainSubstring("custom_custom_metric_shell{animals=\"chicken\",id=\"1\"} 128"))
239 | Expect(string(body)).To(ContainSubstring("custom_custom_metric_shell{animals=\"snails\",id=\"3\"} 14"))
240 | })
241 |
242 | })
243 | })
244 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Prometheus custom_exporter
2 |
3 | ## Intro
4 |
5 | This project is aimed to retrieve specific metrics that can't be found in dedicated exporters.
6 |
7 | ## Concepts
8 | This exporter work with a dedicated config’s file that’s contain all needed metrics.
9 |
10 | Each dedicated metrics will use a collector to access to data and a custom commands list that's will be run into this collector.
11 | The final command must expose a ready to be parsed result set to extract metric value and tags value.
12 | A mapping config allows to specify the tags included otherwise, only last data on each row or record will be extract.
13 |
14 | A collector is define by his name and include the type (bash, mysql ...) and credential (data source name, user, password, uri ...).
15 |
16 | The dedicated metrics are exposed with a prefix "custom" and the name of this metrics extract from the config file.
17 |
18 | ## How it's work
19 | The main process will load the config file and will register a dedicated collector into Prometheus client framework for each metrics. Each metric is composed of a collector helper who's include a specific type collector defined into the config file
20 |
21 | On each call to the metrics path of the exporter (i.e. http://localhost:9213/metrics/), the main process will call each registered Prometheus collectors in multithreading and grab all results to expose them to the caller.
22 |
23 | If a metrics is not available (errors on running command, result empty ...) a minimal result will be exposing. When this metric’s commands rise up, the result will appear. If the config of a metrics is not well defined, the metrics will be not registered into the main process. If no metrics are registered, the main process will exit with an error status.
24 |
25 | ## Build from source
26 |
27 | > Requirement : go version >= 1.13 (using go mod)
28 |
29 | To build from source, a makefile is available in the repos, so the easiest build process is :
30 | ```bash
31 |
32 | go get -u github.com/orange-cloudfoundry/custom_exporter
33 | cd $GOPATH/src/github.com/orange-cloudfoundry/custom_exporter
34 |
35 | make
36 |
37 | ```
38 |
39 | or building with [`promu` tools](https://github.com/prometheus/promu):
40 | ```bash
41 |
42 | go get -u github.com/prometheus/promu
43 | cd $GOPATH/src/github.com/prometheus/promu
44 |
45 | make
46 |
47 | go get -u github.com/orange-cloudfoundry/custom_exporter
48 | cd $GOPATH/src/github.com/orange-cloudfoundry/custom_exporter
49 |
50 | $GOPATH/bin/promu build --prefix $GOPATH/bin
51 |
52 | ```
53 | Note: [a bosh release for cloudfoundry](https://github.com/orange-cloudfoundry/custom_exporter-boshrelease) is available at github
54 |
55 | ## Configuration
56 |
57 | The configuration is split in 2 separate parts:
58 | * **credentials**: provide credentials an data type to the custom export.
59 | * **metrics**: provide commands that are to be run to retrieve metrics and key-value mapping
60 |
61 | #### Credential
62 | The credential section is composed at least as:
63 |
64 | * **name**: name of the credential
65 | * **type**: collector type (one of existing collector : redis, mysql, bash, ...). If the type is not understand the metrics connected to this credential will be ignored
66 |
67 | This other options depends of collectors:
68 |
69 | | Option Name | Description | Collector |
70 | | :---------: | :---------- | :-------: |
71 | | dsn | the DSN (Data Source Name) is an URL like string usually use to connect to database | mysql, redis |
72 | | user | the user to run command in shell process | bash |
73 |
74 | The DSN form example for each collector:
75 |
76 | * mysql: driver://user:password@protocol(addr:port|[addr_ip_v6]:port|socket)/database
77 | * redis: protocol://:password@host:port/database
78 |
79 | #### Metric
80 | The metrics section is composed at least as:
81 |
82 | * **name**: name of the metrics
83 | * **commands**: list of command to run to retrieve the metrics tags and value
84 | * **credential**: the credential's name to use in this metrics (cannot be null : collector type is include in the credential)
85 | * **value_type**: the prometheus value type (COUNTER, GAUGE, UNTYPED)
86 |
87 | This others options are optionals:
88 |
89 | | Option Name | Description | Collector |
90 | | :---------: | :---------- | :-------: |
91 | | mapping | the list of tags to be found in result set | all |
92 | | separator | the separator used in some collector like bash | bash |
93 | | value_name | the name of the metric value key who's be found in result of command | redis |
94 |
95 | ## Manifest & result examples
96 | ### First example
97 | #### Manifest
98 | ```yaml
99 | custom_exporter:
100 | credentials:
101 | - name: mysql_credential_tcp
102 | type: mysql
103 | dsn: mysql://user:password@tcp(127.0.0.1:3306)/database_name
104 | - name: mysql_credential_socket
105 | type: mysql
106 | dsn: mysql://user:password@unix(/var/lib/mysql/mysql.sock)/database_name
107 | - name: shell_credential
108 | type: bash
109 | user: root
110 | - name: redis_credential
111 | type: redis
112 | dsn: tcp://:password@127.0.0.1:1234/0
113 | metrics:
114 | - name: node_database_size_bytes
115 | commands:
116 | - find /var/vcap/store/mysql/ -type d -name cf* -exec du -sb {} ;
117 | - sed -ne s/^\([0-9]\+\)\t\(\/var\/vcap\/store\/mysql\/\)\(.*\)$/\3 \1/p
118 | credential: shell_credential
119 | mapping:
120 | - database
121 | separator: ' '
122 | value_type: UNTYPED
123 | - name: node_database_provisioning_bytes
124 | commands:
125 | - select db_name,max_storage_mb*1024*1024 FROM mysql_broker.service_instances;
126 | credential: mysql_credential
127 | mapping:
128 | - database
129 | value_type: UNTYPED
130 | - name: node_redis_info
131 | commands:
132 | - INFO REPLICATION
133 | credential: redis_credential
134 | mapping:
135 | - role
136 | value_name: value
137 | value_type: UNTYPED
138 | ```
139 |
140 | #### Results returned in the custom exporter
141 |
142 | ```bash
143 | [08:53:09] BOSH MySQL ~ # curl -s 10.234.250.202:9100/metrics | grep -i 'node_database'
144 | # HELP node_database_provisioning_bytes Metric read from /var/vcap/jobs/node_exporter/config/database_provisioning.prom
145 | # TYPE node_database_provisioning_bytes untyped
146 | custom_node_database_provisioning_bytes{database="cf_74df5b8f_e7fe_4151_8ec3_741296d42fbc"} 1.048576e+09
147 | custom_node_database_provisioning_bytes{database="cf_d7161ef3_e6fc_4a05_9631_834525f0f7ba"} 1.048576e+09
148 | custom_node_database_provisioning_bytes{database="cf_fa61054d_5c08_4734_a31e_4f2e6065897b"} 1.048576e+08
149 | # HELP node_database_size_bytes Metric read from /var/vcap/jobs/node_exporter/config/database_size.prom
150 | # TYPE node_database_size_bytes untyped
151 | custom_node_database_size_bytes{database="cf_74df5b8f_e7fe_4151_8ec3_741296d42fbc"} 4157
152 | custom_node_database_size_bytes{database="cf_d7161ef3_e6fc_4a05_9631_834525f0f7ba"} 4157
153 | custom_node_database_size_bytes{database="cf_fa61054d_5c08_4734_a31e_4f2e6065897b"} 4157
154 | ```
155 |
156 | ### Another example
157 | #### Manifest
158 | ```yaml
159 | custom_exporter:
160 | credentials:
161 | - name: mysql_connector
162 | type: mysql ##Possible types are for the moment shell mysql redis
163 | dsn: mysql://root:password@1.2.3.4:1234/mydb
164 | metrics:
165 | - name: custom_metric
166 | commands:
167 | - 1
168 | - 2
169 | - 3
170 | credential: mysql_connector
171 | mapping:
172 | - tag1
173 | - tag2
174 | value_type: UNTYPED
175 | separator: \t #useless for MySQL but can be usefull for shell
176 | ```
177 |
178 | #### Result example (MySQL view)
179 | ```mysql
180 | | 1 | chicken | 128 |
181 | | 2 | beef | 256 |
182 | | 3 | snails | 14 |
183 | ```
184 |
185 | #### Result example (Exporter view)
186 | ```bash
187 | custom_metric{tag1="1",tag2="chicken",instance="ip:port",job="custom_exporter"} 128
188 | custom_metric{tag1="2",tag2="beef",instance="ip:port",job="custom_exporter"} 256
189 | custom_metric{tag1="3",tag2="snails",instance="ip:port",job="custom_exporter"} 14
190 | ```
191 |
192 | ## Port binding
193 | According to https://github.com/prometheus/prometheus/wiki/Default-port-allocations we will use TCP/9209
194 |
195 | ## WIP : Working schema
196 | 
197 |
--------------------------------------------------------------------------------
/collector/redis.go:
--------------------------------------------------------------------------------
1 | package collector
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "net/url"
7 | "strconv"
8 | "strings"
9 |
10 | "github.com/orange-cloudfoundry/custom_exporter/config"
11 | "github.com/prometheus/client_golang/prometheus"
12 | "github.com/prometheus/common/log"
13 | "gopkg.in/redis.v5"
14 | )
15 |
16 | /*
17 | Copyright 2017 Orange
18 |
19 | Licensed under the Apache License, Version 2.0 (the "License");
20 | you may not use this file except in compliance with the License.
21 | You may obtain a copy of the License at
22 |
23 | http://www.apache.org/licenses/LICENSE-2.0
24 |
25 | Unless required by applicable law or agreed to in writing, software
26 | distributed under the License is distributed on an "AS IS" BASIS,
27 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
28 | See the License for the specific language governing permissions and
29 | limitations under the License.
30 | */
31 |
32 | const (
33 | CollectorRedisName = "redis"
34 | CollectorRedisDesc = "Metrics from redis collector in the custom exporter."
35 | )
36 |
37 | type CollectorRedis struct {
38 | metricsConfig config.MetricsItem
39 | }
40 |
41 | func NewCollectorRedis(config config.MetricsItem) *CollectorRedis {
42 | return &CollectorRedis{
43 | metricsConfig: config,
44 | }
45 | }
46 |
47 | func NewPrometheusRedisCollector(config config.MetricsItem) (prometheus.Collector, error) {
48 | var err error
49 |
50 | myCol := NewCollectorHelper(NewCollectorRedis(config))
51 |
52 | log.Infof("Collector Added: Type '%s' / Name '%s' / Credentials '%s'", CollectorRedisName, config.Name, config.Credential.Name)
53 |
54 | if len(config.Value_name) < 1 {
55 | err = fmt.Errorf("keymapping not present for collector %s", CollectorRedisName)
56 | log.Errorln("Error:", err)
57 | }
58 |
59 | return myCol, myCol.Check(err)
60 | }
61 |
62 | func (e CollectorRedis) Config() config.MetricsItem {
63 | return e.metricsConfig
64 | }
65 |
66 | func (e CollectorRedis) Name() string {
67 | return CollectorRedisName
68 | }
69 |
70 | func (e CollectorRedis) Desc() string {
71 | return CollectorRedisDesc
72 | }
73 |
74 | func (e *CollectorRedis) Run(ch chan<- prometheus.Metric) error {
75 | var (
76 | red *redis.Client
77 | jsn map[string]interface{}
78 | res map[string]string
79 | labelVal []string
80 | mapping []string
81 | err error
82 | out []byte
83 | )
84 |
85 | err = nil
86 | mapping = e.metricsConfig.Mapping
87 | labelVal = make([]string, len(mapping))
88 |
89 | if red, err = e.redisClient(); err != nil {
90 | log.Errorf("Error when get Redis Client for metric \"%s\" : %s", e.metricsConfig.Name, err.Error())
91 | return err
92 | }
93 |
94 | defer red.Close()
95 |
96 | log.Debugln("Calling Redis Commands... ")
97 |
98 | for _, c := range e.metricsConfig.Commands {
99 | c = strings.TrimSpace(c)
100 |
101 | if len(c) < 1 {
102 | continue
103 | }
104 |
105 | cmd := e.redisRun(red, c)
106 |
107 | if cmd.Err() != nil {
108 | log.Errorf("Error for metrics \"%s\" while running redis command \"%s\": %s", e.metricsConfig.Name, c, cmd.Err().Error())
109 | return cmd.Err()
110 | }
111 |
112 | out = []byte(cmd.Val().(string))
113 | jsn = make(map[string]interface{})
114 |
115 | if err = json.Unmarshal(out, &jsn); err != nil {
116 | log.Errorf("Error for metrics \"%s\" while parsing json result of redis command \"%s\": %s", e.metricsConfig.Name, c, err.Error())
117 | return err
118 | }
119 | }
120 |
121 | res = make(map[string]string)
122 | for k, v := range jsn {
123 | res[k] = e.interface2String(v)
124 | }
125 |
126 | log.Debugln("Filtering Redis Label Value... ")
127 |
128 | for i, k := range mapping {
129 | if val, isOk := res[k]; !isOk {
130 | log.Debugln("TagValue not found :", k)
131 | labelVal[i] = ""
132 | } else {
133 | labelVal[i] = val
134 | }
135 | }
136 |
137 | log.Debugln("Filtering Redis Metric Value... ")
138 | metricVal := float64(0)
139 |
140 | if val, isOk := res[e.metricsConfig.Value_name]; !isOk {
141 | err = fmt.Errorf("keymapping not found in resultSet for collector %s and command [ %s ]", CollectorRedisName, strings.Join(e.metricsConfig.Commands, ", "))
142 | } else {
143 | if metricVal, err = strconv.ParseFloat(val, 64); err != nil {
144 | metricVal = float64(0)
145 | }
146 | }
147 |
148 | prom_desc := PromDesc(e)
149 | log.Debugf("Add Metric \"%s\" : Tag '%s' / TagValue '%s' / Value '%v'", prom_desc, mapping, labelVal, metricVal)
150 |
151 | metric := prometheus.MustNewConstMetric(
152 | prometheus.NewDesc(prom_desc, e.metricsConfig.Name, mapping, nil),
153 | e.metricsConfig.Value_type, metricVal, labelVal...,
154 | )
155 |
156 | select {
157 | case ch <- metric:
158 | log.Debug("Return no error...")
159 | return nil
160 | default:
161 | log.Info("Cannot write to channel...")
162 | }
163 |
164 | return err
165 | }
166 |
167 | func (e CollectorRedis) interface2String(input interface{}) string {
168 |
169 | if val, ok := input.(float64); ok {
170 | return strconv.FormatFloat(val, 'f', -1, 64)
171 | }
172 |
173 | if val, ok := input.(float32); ok {
174 | return strconv.FormatFloat(float64(val), 'f', -1, 32)
175 | }
176 |
177 | if val, ok := input.(int); ok {
178 | return strconv.FormatInt(int64(val), 10)
179 | }
180 |
181 | if val, ok := input.(bool); ok {
182 | return strconv.FormatBool(val)
183 | }
184 |
185 | if val, ok := input.(string); ok {
186 | return string(val)
187 | }
188 |
189 | return ""
190 | }
191 |
192 | func (e CollectorRedis) DsnPart() (map[string]interface{}, error) {
193 | var (
194 | dbn int64
195 | dsn *url.URL
196 | pss string
197 | err error
198 | isOk bool
199 | res map[string]interface{}
200 | )
201 |
202 | res = make(map[string]interface{})
203 |
204 | if dsn, err = url.Parse(e.metricsConfig.Credential.Dsn); err != nil {
205 | return res, err
206 | }
207 |
208 | if pss, isOk = dsn.User.Password(); !isOk {
209 | pss = ""
210 | }
211 |
212 | if strings.Trim(dsn.Path, "/") == "" {
213 | dbn = 0
214 | } else {
215 | if dbn, err = strconv.ParseInt(strings.Trim(dsn.Path, "/"), 10, 64); err != nil {
216 | return res, fmt.Errorf("db identifier not well formatted (int value required) : %s", err.Error())
217 | }
218 | }
219 |
220 | res["addr"] = string(dsn.Host)
221 | res["pass"] = string(pss)
222 | res["dbnum"] = int(dbn)
223 |
224 | return res, nil
225 | }
226 |
227 | func (e CollectorRedis) redisClient() (*redis.Client, error) {
228 | var (
229 | clt *redis.Client
230 | dsn map[string]interface{}
231 | err error
232 | )
233 |
234 | if dsn, err = e.DsnPart(); err != nil {
235 | return clt, err
236 | }
237 |
238 | var redisOpt = redis.Options{
239 | Addr: dsn["addr"].(string),
240 | }
241 |
242 | if _, ok := dsn["pass"]; ok && len(strings.TrimSpace(dsn["pass"].(string))) > 0 {
243 | redisOpt.Password = strings.TrimSpace(dsn["pass"].(string))
244 | }
245 |
246 | if _, ok := dsn["dbnum"]; ok {
247 | redisOpt.DB = dsn["dbnum"].(int)
248 | }
249 |
250 | if redisOpt.Password == "" && redisOpt.DB == 0 {
251 | redisOpt.ReadOnly = true
252 | }
253 |
254 | log.Debugf("Starting client redis for metrics \"%s\", with params : %v", e.metricsConfig.Name, redisOpt)
255 | clt = redis.NewClient(&redisOpt)
256 |
257 | return clt, e.redisPing(clt)
258 | }
259 |
260 | func (e CollectorRedis) redisPing(client *redis.Client) error {
261 | if _, err := client.Ping().Result(); err != nil {
262 | return err
263 | }
264 |
265 | return nil
266 | }
267 |
268 | func (e CollectorRedis) redisRun(client *redis.Client, command string) *redis.Cmd {
269 | var (
270 | arg []interface{}
271 | res *redis.Cmd
272 | )
273 |
274 | cmd := strings.Split(command, " ")
275 | arg = make([]interface{}, len(cmd))
276 |
277 | for k, v := range cmd {
278 | arg[k] = v
279 | }
280 |
281 | log.Debugf("Prepare command for metrics \"%s\" : %v", e.metricsConfig.Name, arg)
282 | res = redis.NewCmd(arg...)
283 |
284 | if res.Err() != nil {
285 | log.Errorf("Error with metrics \"%s\" for command \"%s\" : %s", e.metricsConfig.Name, command, res.Err().Error())
286 | return res
287 | }
288 |
289 | log.Debugf("Proceed command for metrics \"%s\"...", e.metricsConfig.Name)
290 |
291 | client.Process(res)
292 | log.Debugf("Proceded command for metrics \"%s\" : %v", e.metricsConfig.Name, res)
293 |
294 | return res
295 | }
296 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
4 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
5 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
6 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
7 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
8 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E=
9 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
10 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
11 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
12 | github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6 h1:45bxf7AZMwWcqkLzDAQugVEwedisr5nRJ1r+7LYnv0U=
13 | github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
14 | github.com/alicebob/miniredis v2.5.0+incompatible h1:yBHoLpsyjupjz3NL3MhKMVkR41j82Yjf3KFv7ApYzUI=
15 | github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk=
16 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
17 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
18 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
19 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
20 | github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
21 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
22 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
23 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
24 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
25 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
26 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
27 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
28 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
29 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
30 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
31 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
32 | github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
33 | github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
34 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
35 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
36 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
37 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
38 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
39 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
40 | github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
41 | github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
42 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
43 | github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
44 | github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
45 | github.com/google/go-github/v25 v25.0.0 h1:y/oM3M5B1Y5wUD2lFU6qRVwxFGU580oy/2zPFBQxCCc=
46 | github.com/google/go-github/v25 v25.0.0/go.mod h1:XMRvWvLBf2K0UaSNFDIBtB5BgBYRV5g+0b7k32sqrME=
47 | github.com/google/go-github/v25 v25.1.3 h1:Ht4YIQgUh4l4lc80fvGnw60khXysXvlgPxPP8uJG3EA=
48 | github.com/google/go-github/v25 v25.1.3/go.mod h1:6z5pC69qHtrPJ0sXPsj4BLnd82b+r6sLB7qcBoRZqpw=
49 | github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
50 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
51 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
52 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
53 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
54 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
55 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
56 | github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
57 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
58 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
59 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
60 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
61 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
62 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
63 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
64 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
65 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
66 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
67 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
68 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
69 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
70 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
71 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
72 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
73 | github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw=
74 | github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
75 | github.com/onsi/gomega v1.8.1 h1:C5Dqfs/LeauYDX0jJXIe2SWmwCbGzx9yF8C8xy3Lh34=
76 | github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
77 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
78 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
79 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
80 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
81 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
82 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
83 | github.com/prometheus/client_golang v1.3.0 h1:miYCvYqFXtl/J9FIy8eNpBfYthAEFg+Ys0XyUVEcDsc=
84 | github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
85 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
86 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
87 | github.com/prometheus/client_model v0.1.0 h1:ElTg5tNp4DqfV7UQjDqv2+RJlNzsDtvNAWccbItceIE=
88 | github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
89 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
90 | github.com/prometheus/common v0.5.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
91 | github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY=
92 | github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
93 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
94 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
95 | github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8=
96 | github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
97 | github.com/prometheus/promu v0.5.0 h1:q7GkmIdBZ+ulL+6v4EDsZL+cW9UCW9J3DHA89bFI83c=
98 | github.com/prometheus/promu v0.5.0/go.mod h1:sXydR89lpo0YkCrYK1EhYjaJUesenzLhd9CNRAwN+bI=
99 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
100 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
101 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
102 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
103 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
104 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
105 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
106 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
107 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
108 | github.com/tedsuo/ifrit v0.0.0-20191009134036-9a97d0632f00 h1:mujcChM89zOHwgZBBNr5WZ77mBXP1yR+gLThGCYZgAg=
109 | github.com/tedsuo/ifrit v0.0.0-20191009134036-9a97d0632f00/go.mod h1:eyZnKCc955uh98WQvzOm0dgAeLnf2O0Rz0LPoC5ze+0=
110 | github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb h1:ZkM6LRnq40pR1Ox0hTHlnpkcOTuFIDQpZ1IN8rKKhX0=
111 | github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
112 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
113 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
114 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
115 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
116 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
117 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
118 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
119 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
120 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
121 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
122 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
123 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
124 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
125 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU=
126 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
127 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
128 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
129 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
130 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
131 | golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a h1:tImsplftrFpALCYumobsd0K86vlAs/eXGFms2txfJfA=
132 | golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
133 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 h1:pE8b58s1HRDMi8RDc79m0HISf9D4TzseP40cEA6IGfs=
134 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
135 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
136 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
137 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
138 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
139 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
140 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
141 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
142 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
143 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
144 | golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
145 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
146 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
147 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
148 | golang.org/x/sys v0.0.0-20191220142924-d4481acd189f h1:68K/z8GLUxV76xGSqwTWw2gyk/jwn79LUL43rES2g8o=
149 | golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
150 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8 h1:JA8d3MPx/IToSyXZG/RhwYEtfrKO1Fxrqe8KrkiLXKM=
151 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
152 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
153 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
154 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
155 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
156 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
157 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac h1:MQEvx39qSf8vyrx3XRaOe+j1UDIzKwkYOVObRgGPVqI=
158 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
159 | golang.org/x/tools v0.0.0-20191230220329-2aa90c603ae3 h1:2+KluhQfJ1YhW+TB1KrISS2SfiG1pLEoseB0D4VF/bo=
160 | golang.org/x/tools v0.0.0-20191230220329-2aa90c603ae3/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
161 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
162 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
163 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
164 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
165 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
166 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
167 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
168 | google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
169 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
170 | gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 h1:FVCohIoYO7IJoDDVpV2pdq7SgrMH6wHnuTyrdrxJNoY=
171 | gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw=
172 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
173 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
174 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
175 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
176 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
177 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
178 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
179 | gopkg.in/redis.v5 v5.2.9 h1:MNZYOLPomQzZMfpN3ZtD1uyJ2IDonTTlxYiV/pEApiw=
180 | gopkg.in/redis.v5 v5.2.9/go.mod h1:6gtv0/+A4iM08kdRfocWYB3bLX2tebpNtfKlFT6H4mY=
181 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
182 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
183 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
184 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
185 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
186 | gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
187 | gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
188 | honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
189 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
190 |
--------------------------------------------------------------------------------