├── .gitignore ├── .promu.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── VERSION ├── collector ├── bash.go ├── bash_test.go ├── collector.go ├── collector_suite_test.go ├── mysql.go ├── mysql_test.go ├── redis.go └── redis_test.go ├── config ├── config.go ├── config_suite_test.go └── config_test.go ├── custom_exporter.go ├── custom_exporter.png ├── custom_exporter_suite_test.go ├── custom_exporter_test.go ├── example.yml ├── example_shell.yml ├── example_with_error.yml ├── go.mod ├── go.sum ├── makeRelease.sh └── wrongYaml.yml /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | ![custom_exporter_working_schema](custom_exporter.png) 197 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 2.0.0 2 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 exporter

Custom 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 | -------------------------------------------------------------------------------- /custom_exporter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orange-cloudfoundry/custom_exporter/bd134ef9c5893e5e08401b54763f663ab8c91c0c/custom_exporter.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 exporter

Custom 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------