From 6fc47365a0f5e8d2e6e70c94c3a6f42ac06b8cea Mon Sep 17 00:00:00 2001 From: John Bryan Sazon Date: Wed, 24 Apr 2019 22:25:01 +0200 Subject: [PATCH 1/2] Make the flags configurable using environment variables --- Gopkg.lock | 51 ++++- Gopkg.toml | 4 + clickhouse_exporter.go | 22 +- vendor/github.com/peterbourgon/ff/.gitignore | 14 ++ vendor/github.com/peterbourgon/ff/.travis.yml | 7 + vendor/github.com/peterbourgon/ff/LICENSE | 201 +++++++++++++++++ vendor/github.com/peterbourgon/ff/README.md | 54 +++++ .../peterbourgon/ff/fftoml/fftoml.go | 46 ++++ .../peterbourgon/ff/fftoml/fftoml_test.go | 114 ++++++++++ vendor/github.com/peterbourgon/ff/go.mod | 3 + vendor/github.com/peterbourgon/ff/go.sum | 2 + vendor/github.com/peterbourgon/ff/json.go | 65 ++++++ .../github.com/peterbourgon/ff/json_test.go | 113 ++++++++++ vendor/github.com/peterbourgon/ff/parse.go | 202 ++++++++++++++++++ .../github.com/peterbourgon/ff/parse_test.go | 176 +++++++++++++++ 15 files changed, 1061 insertions(+), 13 deletions(-) create mode 100644 vendor/github.com/peterbourgon/ff/.gitignore create mode 100644 vendor/github.com/peterbourgon/ff/.travis.yml create mode 100644 vendor/github.com/peterbourgon/ff/LICENSE create mode 100644 vendor/github.com/peterbourgon/ff/README.md create mode 100644 vendor/github.com/peterbourgon/ff/fftoml/fftoml.go create mode 100644 vendor/github.com/peterbourgon/ff/fftoml/fftoml_test.go create mode 100644 vendor/github.com/peterbourgon/ff/go.mod create mode 100644 vendor/github.com/peterbourgon/ff/go.sum create mode 100644 vendor/github.com/peterbourgon/ff/json.go create mode 100644 vendor/github.com/peterbourgon/ff/json_test.go create mode 100644 vendor/github.com/peterbourgon/ff/parse.go create mode 100644 vendor/github.com/peterbourgon/ff/parse_test.go diff --git a/Gopkg.lock b/Gopkg.lock index 87dd011..39fc1e6 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -2,68 +2,111 @@ [[projects]] + digest = "1:b15e65d54f9b9020afe2609ba1082ba6770c429cbb4617c0dfdcad0e19496fa4" name = "github.com/Sirupsen/logrus" packages = ["."] + pruneopts = "" revision = "7f976d3a76720c4c27af2ba716b85d2e0a7e38b1" version = "v1.0.1" [[projects]] branch = "master" + digest = "1:0c5485088ce274fac2e931c1b979f2619345097b39d91af3239977114adf0320" name = "github.com/beorn7/perks" packages = ["quantile"] + pruneopts = "" revision = "4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9" [[projects]] branch = "master" + digest = "1:0556762750ea0b2441468ebb355f15e1139de0af71adf5472731ec07a23e62db" name = "github.com/golang/protobuf" packages = ["proto"] + pruneopts = "" revision = "0a4f71a498b7c4812f64969510bcb4eca251e33a" [[projects]] + digest = "1:4c23ced97a470b17d9ffd788310502a077b9c1f60221a85563e49696276b4147" name = "github.com/matttproud/golang_protobuf_extensions" packages = ["pbutil"] + pruneopts = "" revision = "3247c84500bff8d9fb6d579d800f20b3e091582c" version = "v1.0.0" [[projects]] + digest = "1:1adf389cdedae3e38d664fc387aa63718f8ad8397e0cce8e2f3c7450833ab650" + name = "github.com/peterbourgon/ff" + packages = ["."] + pruneopts = "" + revision = "52d851567994fcca73bf3668fc3099cfa7be3768" + version = "v1.2.0" + +[[projects]] + digest = "1:4142d94383572e74b42352273652c62afec5b23f325222ed09198f46009022d1" name = "github.com/prometheus/client_golang" - packages = ["prometheus"] + packages = [ + "prometheus", + "prometheus/promhttp", + ] + pruneopts = "" revision = "c5b7fccd204277076155f10851dad72b76a49317" version = "v0.8.0" [[projects]] branch = "master" + digest = "1:2c2e0c749aa376a90cd48f4b62b55763214f3bb16d2de7eef981062892f61619" name = "github.com/prometheus/client_model" packages = ["go"] + pruneopts = "" revision = "6f3806018612930941127f2a7c6c453ba2c527d2" [[projects]] branch = "master" + digest = "1:bb155e0e83eb27465503e71a486a950a1b9de7de9413cf411927a3f05334c7b6" name = "github.com/prometheus/common" - packages = ["expfmt","internal/bitbucket.org/ww/goautoneg","model"] + packages = [ + "expfmt", + "internal/bitbucket.org/ww/goautoneg", + "model", + ] + pruneopts = "" revision = "3e6a7635bac6573d43f49f97b47eb9bda195dba8" [[projects]] branch = "master" + digest = "1:52c2f5326f3f79596064c8fb73185d544143f7670d840a31b3a655b64bbc1217" name = "github.com/prometheus/log" packages = ["."] + pruneopts = "" revision = "9a3136781e1ff7bc42736ba4acb81339b1422551" [[projects]] branch = "master" + digest = "1:e5e51236fd16fb017b660ea5f473c5fc51ab3f5926285e645c4ad52bd78b12b3" name = "github.com/prometheus/procfs" - packages = [".","xfs"] + packages = [ + ".", + "xfs", + ] + pruneopts = "" revision = "e645f4e5aaa8506fc71d6edbc5c4ff02c04c46f2" [[projects]] branch = "master" + digest = "1:b78daec10cf61db3be8d7121bdaf68f8b181aba5ac1c958f239fbbb5c75daa4f" name = "golang.org/x/sys" packages = ["unix"] + pruneopts = "" revision = "abf9c25f54453410d0c6668e519582a9e1115027" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "66a89abcc4db7160ad4b91ca6c16049a616004060b3c42d4a13a8d98f5838d90" + input-imports = [ + "github.com/peterbourgon/ff", + "github.com/prometheus/client_golang/prometheus", + "github.com/prometheus/client_golang/prometheus/promhttp", + "github.com/prometheus/log", + ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index e69de29..e6f0a87 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -0,0 +1,4 @@ + +[[constraint]] + name = "github.com/peterbourgon/ff" + version = "1.2.0" diff --git a/clickhouse_exporter.go b/clickhouse_exporter.go index cb853d7..ef16113 100644 --- a/clickhouse_exporter.go +++ b/clickhouse_exporter.go @@ -7,28 +7,32 @@ import ( "os" "github.com/f1yegor/clickhouse_exporter/exporter" + "github.com/peterbourgon/ff" "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/log" "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/prometheus/log" ) var ( - listeningAddress = flag.String("telemetry.address", ":9116", "Address on which to expose metrics.") - metricsEndpoint = flag.String("telemetry.endpoint", "/metrics", "Path under which to expose metrics.") - clickhouseScrapeURI = flag.String("scrape_uri", "http://localhost:8123/", "URI to clickhouse http endpoint") - insecure = flag.Bool("insecure", true, "Ignore server certificate if using https") - user = os.Getenv("CLICKHOUSE_USER") - password = os.Getenv("CLICKHOUSE_PASSWORD") + fs = flag.NewFlagSet("clickhouse-exporter", flag.ExitOnError) + listeningAddress = fs.String("telemetry.address", ":9116", "Address on which to expose metrics. Override environment (CLICKHOUSE_TELEMETRY_ADDRESS)") + metricsEndpoint = fs.String("telemetry.endpoint", "/metrics", "Path under which to expose metrics. Override environment (CLICKHOUSE_TELEMETRY_ENDPOINT)") + clickhouseScrapeURI = fs.String("scrape_uri", "http://localhost:8123/", "URI to clickhouse http endpoint. Override environment (CLICKHOUSE_SCRAPE_URI)") + insecure = fs.Bool("insecure", true, "Ignore server certificate if using https. Override environment (CLICKHOUSE_INSECURE)") + user = fs.String("user", "", "Clickhouse user. Override environment (CLICKHOUSE_USER)") + password = fs.String("password", "", "Clickhouse password. Override environment (CLICKHOUSE_PASSWORD)") ) func main() { - flag.Parse() + if err := ff.Parse(fs, os.Args[1:], ff.WithEnvVarPrefix("CLICKHOUSE")); err != nil { + panic(err) + } uri, err := url.Parse(*clickhouseScrapeURI) if err != nil { log.Fatal(err) } - e := exporter.NewExporter(*uri, *insecure, user, password) + e := exporter.NewExporter(*uri, *insecure, *user, *password) prometheus.MustRegister(e) log.Printf("Starting Server: %s", *listeningAddress) diff --git a/vendor/github.com/peterbourgon/ff/.gitignore b/vendor/github.com/peterbourgon/ff/.gitignore new file mode 100644 index 0000000..a1338d6 --- /dev/null +++ b/vendor/github.com/peterbourgon/ff/.gitignore @@ -0,0 +1,14 @@ +# Binaries for programs and plugins +*.exe +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 +.glide/ diff --git a/vendor/github.com/peterbourgon/ff/.travis.yml b/vendor/github.com/peterbourgon/ff/.travis.yml new file mode 100644 index 0000000..4860021 --- /dev/null +++ b/vendor/github.com/peterbourgon/ff/.travis.yml @@ -0,0 +1,7 @@ +language: go + +script: go test -race -v ./... + +go: + - 1.11.x + - tip diff --git a/vendor/github.com/peterbourgon/ff/LICENSE b/vendor/github.com/peterbourgon/ff/LICENSE new file mode 100644 index 0000000..8dada3e --- /dev/null +++ b/vendor/github.com/peterbourgon/ff/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/peterbourgon/ff/README.md b/vendor/github.com/peterbourgon/ff/README.md new file mode 100644 index 0000000..3d46af3 --- /dev/null +++ b/vendor/github.com/peterbourgon/ff/README.md @@ -0,0 +1,54 @@ +# ff [![Latest Release](https://img.shields.io/github/release/peterbourgon/ff.svg?style=flat-square)](https://github.com/peterbourgon/ff/releases/latest) [![GoDoc](https://godoc.org/github.com/peterbourgon/ff?status.svg)](https://godoc.org/github.com/peterbourgon/ff) [![Travis CI](https://travis-ci.org/peterbourgon/ff.svg?branch=master)](https://travis-ci.org/peterbourgon/ff) + +ff stands for flags-first, and provides an opinionated way to populate a +[flag.FlagSet](https://golang.org/pkg/flag#FlagSet) with configuration data from +the environment. Specifically, it allows data to be parsed from commandline +args, a configuration file, and environment variables, in that priority order. + +## Usage + +Define a flag.FlagSet in your func main. + +```go +func main() { + fs := flag.NewFlagSet("my-program", flag.ExitOnError) + var ( + listenAddr = fs.String("listen-addr", "localhost:8080", "listen address") + refresh = fs.Duration("refresh", 15*time.Second, "refresh interval") + debug = fs.Bool("debug", false, "log debug information") + _ = fs.String("config", "", "config file (optional)") + ) +``` + +Then, call ff.Parse instead of fs.Parse. + +```go + ff.Parse(fs, os.Args[1:], + ff.WithConfigFileFlag("config"), + ff.WithConfigFileParser(ff.PlainParser), + ff.WithEnvVarPrefix("MY_PROGRAM"), + ) +``` + +This example will parse flags from the commandline args, just like regular +package flag, with the highest priority. If a `-config` file is specified, it +will try to parse it using the PlainParser, which expects files in this format: + +``` +listen-addr localhost:8080 +refresh 30s +debug true +``` + +It's simple to write your own config file parser. + +```go +// ConfigFileParser interprets the config file represented by the reader +// and calls the set function for each parsed flag pair. +type ConfigFileParser func(r io.Reader, set func(name, value string) error) error +``` + +Finally, it will look in the environment for variables with a `MY_PROGRAM` +prefix. Flag names are capitalized, and separator characters are converted to +underscores. In this case, for example, `MY_PROGRAM_LISTEN_ADDR` would match to +`listen-addr`. diff --git a/vendor/github.com/peterbourgon/ff/fftoml/fftoml.go b/vendor/github.com/peterbourgon/ff/fftoml/fftoml.go new file mode 100644 index 0000000..b52b96d --- /dev/null +++ b/vendor/github.com/peterbourgon/ff/fftoml/fftoml.go @@ -0,0 +1,46 @@ +package fftoml + +import ( + "fmt" + "io" + "strconv" + + "github.com/BurntSushi/toml" +) + +// Parser is a parser for TOML file format. Flags and their values are read +// from the key/value pairs defined in the config file +func Parser(r io.Reader, set func(name, value string) error) error { + var m map[string]interface{} + _, err := toml.DecodeReader(r, &m) + if err != nil { + return fmt.Errorf("error parsing TOML config: %v", err) + } + for key, val := range m { + value, err := valToStr(val) + if err != nil { + return err + } + if err = set(key, value); err != nil { + return err + } + } + return nil +} + +func valToStr(val interface{}) (string, error) { + switch v := val.(type) { + case string: + return v, nil + case bool: + return strconv.FormatBool(v), nil + case uint64: + return strconv.FormatUint(v, 10), nil + case int64: + return strconv.FormatInt(v, 10), nil + case float64: + return strconv.FormatFloat(v, 'g', -1, 64), nil + default: + return "", fmt.Errorf("could not convert %q (type %T) to string", val, val) + } +} diff --git a/vendor/github.com/peterbourgon/ff/fftoml/fftoml_test.go b/vendor/github.com/peterbourgon/ff/fftoml/fftoml_test.go new file mode 100644 index 0000000..81e513f --- /dev/null +++ b/vendor/github.com/peterbourgon/ff/fftoml/fftoml_test.go @@ -0,0 +1,114 @@ +package fftoml_test + +import ( + "flag" + "fmt" + "math/rand" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/peterbourgon/ff" + "github.com/peterbourgon/ff/fftoml" +) + +func TestParser(t *testing.T) { + type want struct { + s string + i int + b bool + d time.Duration + f float64 + err string + } + + for _, testcase := range []struct { + name string + args []string + file string + want want + }{ + { + name: "empty input", + args: []string{}, + file: ``, + want: want{d: time.Second}, + }, + { + name: "basic KV pairs", + args: []string{}, + file: ` + s = "s" + i = 10 + b = true + d = "5s" + f = 3.14e10 + `, + want: want{"s", 10, true, 5 * time.Second, 3.14e10, ""}, + }, + { + name: "bad TOML file", + args: []string{}, + file: `{`, + want: want{d: time.Second, err: "error parsing TOML config"}, + }, + } { + t.Run(testcase.name, func(t *testing.T) { + fs := flag.NewFlagSet("test", flag.ExitOnError) + var ( + s = fs.String("s", "", "string") + i = fs.Int("i", 0, "int") + b = fs.Bool("b", false, "bool") + d = fs.Duration("d", time.Second, "time.Duration") + f = fs.Float64("f", 0.0, "float64") + ) + + var options []ff.Option + { + filename := filepath.Join(os.TempDir(), "TestParse"+fmt.Sprint(10000*rand.Intn(10000))) + f, err := os.Create(filename) + if err != nil { + t.Fatal(err) + } + defer os.Remove(f.Name()) + f.Write([]byte(testcase.file)) + f.Close() + + options = append(options, ff.WithConfigFile(f.Name()), ff.WithConfigFileParser(fftoml.Parser)) + } + + err := ff.Parse(fs, testcase.args, options...) + if testcase.want.err == "" { + if err != nil { + t.Fatal(err) + } + } else { + want, have := testcase.want.err, "" + if err != nil { + have = err.Error() + } + if !strings.Contains(have, want) { + t.Errorf("missing expected error: want %q, have %q", want, have) + } + } + + if want, have := testcase.want.s, *s; want != have { + t.Errorf("s: want %q, have %q", want, have) + } + if want, have := testcase.want.i, *i; want != have { + t.Errorf("i: want %d, have %d", want, have) + } + if want, have := testcase.want.b, *b; want != have { + t.Errorf("b: want %v, have %v", want, have) + } + if want, have := testcase.want.d, *d; want != have { + t.Errorf("d: want %s, have %s", want, have) + } + if want, have := testcase.want.f, *f; want != have { + t.Errorf("d: want %f, have %f", want, have) + } + }) + } +} diff --git a/vendor/github.com/peterbourgon/ff/go.mod b/vendor/github.com/peterbourgon/ff/go.mod new file mode 100644 index 0000000..5cb823f --- /dev/null +++ b/vendor/github.com/peterbourgon/ff/go.mod @@ -0,0 +1,3 @@ +module github.com/peterbourgon/ff + +require github.com/BurntSushi/toml v0.3.1 diff --git a/vendor/github.com/peterbourgon/ff/go.sum b/vendor/github.com/peterbourgon/ff/go.sum new file mode 100644 index 0000000..9cb2df8 --- /dev/null +++ b/vendor/github.com/peterbourgon/ff/go.sum @@ -0,0 +1,2 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= diff --git a/vendor/github.com/peterbourgon/ff/json.go b/vendor/github.com/peterbourgon/ff/json.go new file mode 100644 index 0000000..c09aef7 --- /dev/null +++ b/vendor/github.com/peterbourgon/ff/json.go @@ -0,0 +1,65 @@ +package ff + +import ( + "encoding/json" + "fmt" + "io" + "strconv" +) + +// JSONParser is a parser for config files in JSON format. Input should be +// an object. The object's keys are treated as flag names, and the object's +// values as flag values. If the value is an array, the flag will be set +// multiple times. +func JSONParser(r io.Reader, set func(name, value string) error) error { + var m map[string]interface{} + d := json.NewDecoder(r) + d.UseNumber() // must set UseNumber for stringifyValue to work + if err := d.Decode(&m); err != nil { + return fmt.Errorf("error parsing JSON config: %v", err) + } + for key, val := range m { + values, err := stringifySlice(val) + if err != nil { + return fmt.Errorf("error parsing JSON config: %v", err) + } + for _, value := range values { + if err := set(key, value); err != nil { + return err + } + } + } + return nil +} + +func stringifySlice(val interface{}) ([]string, error) { + if vals, ok := val.([]interface{}); ok { + ss := make([]string, len(vals)) + for i := range vals { + s, err := stringifyValue(vals[i]) + if err != nil { + return nil, err + } + ss[i] = s + } + return ss, nil + } + s, err := stringifyValue(val) + if err != nil { + return nil, err + } + return []string{s}, nil +} + +func stringifyValue(val interface{}) (string, error) { + switch v := val.(type) { + case string: + return v, nil + case json.Number: + return v.String(), nil + case bool: + return strconv.FormatBool(v), nil + default: + return "", fmt.Errorf("could not convert %q (type %T) to string", val, val) + } +} diff --git a/vendor/github.com/peterbourgon/ff/json_test.go b/vendor/github.com/peterbourgon/ff/json_test.go new file mode 100644 index 0000000..7b9516f --- /dev/null +++ b/vendor/github.com/peterbourgon/ff/json_test.go @@ -0,0 +1,113 @@ +package ff + +import ( + "flag" + "fmt" + "math/rand" + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +func TestJSONParser(t *testing.T) { + type want struct { + s string + i int + b bool + d time.Duration + err string + } + + for _, testcase := range []struct { + name string + args []string + file string + want want + }{ + { + name: "empty input", + args: []string{}, + file: `{}`, + want: want{d: time.Second}, + }, + { + name: "basic KV pairs", + args: []string{}, + file: `{"s": "s", "i": 10, "b": true, "d": "5s"}`, + want: want{"s", 10, true, 5 * time.Second, ""}, + }, + { + name: "Key with array of values", + args: []string{}, + file: ` + { + "s": ["t", "s"], + "i": ["11", "10"], + "b": [false, true], + "d": ["10m", "5s"] + } + `, + want: want{"s", 10, true, 5 * time.Second, ""}, + }, + { + name: "bad JSON file", + args: []string{}, + file: `{`, + want: want{d: time.Second, err: "error parsing JSON config"}, + }, + } { + t.Run(testcase.name, func(t *testing.T) { + fs := flag.NewFlagSet("test", flag.ExitOnError) + var ( + s = fs.String("s", "", "string") + i = fs.Int("i", 0, "int") + b = fs.Bool("b", false, "bool") + d = fs.Duration("d", time.Second, "time.Duration") + ) + + var options []Option + { + filename := filepath.Join(os.TempDir(), "TestParse"+fmt.Sprint(10000*rand.Intn(10000))) + f, err := os.Create(filename) + if err != nil { + t.Fatal(err) + } + defer os.Remove(f.Name()) + f.Write([]byte(testcase.file)) + f.Close() + + options = append(options, WithConfigFile(f.Name()), WithConfigFileParser(JSONParser)) + } + + err := Parse(fs, testcase.args, options...) + if testcase.want.err == "" { + if err != nil { + t.Fatal(err) + } + } else { + want, have := testcase.want.err, "" + if err != nil { + have = err.Error() + } + if !strings.Contains(have, want) { + t.Errorf("missing expected error: want %q, have %q", want, have) + } + } + + if want, have := testcase.want.s, *s; want != have { + t.Errorf("s: want %q, have %q", want, have) + } + if want, have := testcase.want.i, *i; want != have { + t.Errorf("i: want %d, have %d", want, have) + } + if want, have := testcase.want.b, *b; want != have { + t.Errorf("b: want %v, have %v", want, have) + } + if want, have := testcase.want.d, *d; want != have { + t.Errorf("d: want %s, have %s", want, have) + } + }) + } +} diff --git a/vendor/github.com/peterbourgon/ff/parse.go b/vendor/github.com/peterbourgon/ff/parse.go new file mode 100644 index 0000000..69e5c67 --- /dev/null +++ b/vendor/github.com/peterbourgon/ff/parse.go @@ -0,0 +1,202 @@ +package ff + +import ( + "bufio" + "flag" + "fmt" + "io" + "os" + "strings" +) + +// Parse the flags in the flag set from the provided (presumably commandline) +// args. Additional options may be provided to parse from a config file and/or +// environment variables in that priority order. +func Parse(fs *flag.FlagSet, args []string, options ...Option) error { + var c Context + for _, option := range options { + option(&c) + } + + if err := fs.Parse(args); err != nil { + return fmt.Errorf("error parsing commandline args: %v", err) + } + + provided := map[string]bool{} + fs.Visit(func(f *flag.Flag) { + provided[f.Name] = true + }) + + if c.configFile == "" && c.configFileFlagName != "" { + if f := fs.Lookup(c.configFileFlagName); f != nil { + c.configFile = f.Value.String() + } + } + + if c.configFile != "" && c.configFileParser != nil { + f, err := os.Open(c.configFile) + if err != nil { + return err + } + defer f.Close() + + err = c.configFileParser(f, func(name, value string) error { + if fs.Lookup(name) == nil { + return fmt.Errorf("config file flag %q not defined in flag set", name) + } + + if provided[name] { + return nil // commandline args take precedence + } + + if err := fs.Set(name, value); err != nil { + return fmt.Errorf("error setting flag %q from config file: %v", name, err) + } + + return nil + }) + if err != nil { + return err + } + } + + fs.Visit(func(f *flag.Flag) { + provided[f.Name] = true + }) + + if c.envVarPrefix != "" || c.envVarNoPrefix { + var errs []string + fs.VisitAll(func(f *flag.Flag) { + if provided[f.Name] { + return // commandline args and config file take precedence + } + + var key string + { + key = strings.ToUpper(f.Name) + key = envVarReplacer.Replace(key) + if !c.envVarNoPrefix { + key = strings.ToUpper(c.envVarPrefix) + "_" + key + } + } + if value := os.Getenv(key); value != "" { + for _, individual := range strings.Split(value, ",") { + if err := fs.Set(f.Name, strings.TrimSpace(individual)); err != nil { + errs = append(errs, fmt.Sprintf("error setting flag %q from env var %q: %v", f.Name, key, err)) + } + } + } + }) + if len(errs) > 0 { + return fmt.Errorf("error parsing env vars: %s", strings.Join(errs, "; ")) + } + } + + return nil +} + +// Context contains private fields used during parsing. +type Context struct { + configFile string + configFileFlagName string + configFileParser ConfigFileParser + envVarPrefix string + envVarNoPrefix bool +} + +// Option controls some aspect of parse behavior. +type Option func(*Context) + +// WithConfigFile tells parse to read the provided filename as a config file. +// Requires WithConfigFileParser, and overrides WithConfigFileFlag. +func WithConfigFile(filename string) Option { + return func(c *Context) { + c.configFile = filename + } +} + +// WithConfigFileFlag tells parse to treat the flag with the given name as a +// config file. Requires WithConfigFileParser, and is overridden by WithConfigFile. +func WithConfigFileFlag(flagname string) Option { + return func(c *Context) { + c.configFileFlagName = flagname + } +} + +// WithConfigFileParser tells parse how to interpret the config file provided via +// WithConfigFile or WithConfigFileFlag. +func WithConfigFileParser(p ConfigFileParser) Option { + return func(c *Context) { + c.configFileParser = p + } +} + +// WithEnvVarPrefix tells parse to look in the environment for variables with +// the given prefix. Flag names are converted to environment variables by +// capitalizing them, and replacing separator characters like periods or hyphens +// with underscores. Additionally, if the environment variable's value contains +// commas, each comma-delimited token is treated as a separate instance of the +// associated flag name. +func WithEnvVarPrefix(prefix string) Option { + return func(c *Context) { + c.envVarPrefix = prefix + } +} + +// WithEnvVarNoPrefix tells parse to look in the environment for variables with +// no prefix. See WithEnvVarPrefix for an explanation of how flag names are +// converted to environment variables names. +func WithEnvVarNoPrefix() Option { + return func(c *Context) { + c.envVarNoPrefix = true + } +} + +// ConfigFileParser interprets the config file represented by the reader +// and calls the set function for each parsed flag pair. +type ConfigFileParser func(r io.Reader, set func(name, value string) error) error + +// PlainParser is a parser for config files in an extremely simple format. Each +// line is tokenized as a single key/value pair. The first whitespace-delimited +// token in the line is interpreted as the flag name, and all remaining tokens +// are interpreted as the value. Any leading hyphens on the flag name are +// ignored. +func PlainParser(r io.Reader, set func(name, value string) error) error { + s := bufio.NewScanner(r) + for s.Scan() { + line := strings.TrimSpace(s.Text()) + if line == "" { + continue // skip empties + } + + if line[0] == '#' { + continue // skip comments + } + + var ( + name string + value string + index = strings.IndexRune(line, ' ') + ) + if index < 0 { + name, value = line, "true" // boolean option + } else { + name, value = line[:index], strings.TrimSpace(line[index:]) + } + + if i := strings.Index(value, " #"); i >= 0 { + value = strings.TrimSpace(value[:i]) + } + + if err := set(name, value); err != nil { + return err + } + } + return nil +} + +var envVarReplacer = strings.NewReplacer( + "-", "_", + ".", "_", + "/", "_", +) diff --git a/vendor/github.com/peterbourgon/ff/parse_test.go b/vendor/github.com/peterbourgon/ff/parse_test.go new file mode 100644 index 0000000..95c7a12 --- /dev/null +++ b/vendor/github.com/peterbourgon/ff/parse_test.go @@ -0,0 +1,176 @@ +package ff + +import ( + "flag" + "fmt" + "math/rand" + "os" + "path/filepath" + "testing" + "time" +) + +func TestParsePriority(t *testing.T) { + type want struct { + s string + i int + b bool + d time.Duration + } + + for _, testcase := range []struct { + name string + args []string + file string + env map[string]string + want want + }{ + { + name: "args only", + args: []string{"-s", "foo", "-i", "123", "-b", "-d", "24m"}, + want: want{"foo", 123, true, 24 * time.Minute}, + }, + { + name: "file only", + file: "s bar\ni 99\nb true\nd 1h", + want: want{"bar", 99, true, time.Hour}, + }, + { + name: "env only", + env: map[string]string{"TEST_PARSE_S": "baz", "TEST_PARSE_D": "100s"}, + want: want{"baz", 0, false, 100 * time.Second}, + }, + { + name: "args and file", + args: []string{"-s", "foo", "-i", "1234"}, + file: "\ns should be overridden\n\nd 3s\n", + want: want{"foo", 1234, false, 3 * time.Second}, + }, + { + name: "args and env", + args: []string{"-s", "explicit wins", "-i", "7"}, + env: map[string]string{"TEST_PARSE_S": "should be overridden", "TEST_PARSE_B": "true"}, + want: want{"explicit wins", 7, true, time.Second}, + }, + { + name: "file and env", + file: "s bar\ni 99\n\nd 34s\n\n # comment line\n", + env: map[string]string{"TEST_PARSE_S": "should be overridden", "TEST_PARSE_B": "true"}, + want: want{"bar", 99, true, 34 * time.Second}, + }, + { + name: "args file env", + args: []string{"-s", "from arg", "-i", "100"}, + file: "s from file\ni 200 # comment\n\nd 1m\n\n\n", + env: map[string]string{"TEST_PARSE_S": "from env", "TEST_PARSE_I": "300", "TEST_PARSE_B": "true", "TEST_PARSE_D": "1h"}, + want: want{"from arg", 100, true, time.Minute}, + }, + } { + t.Run(testcase.name, func(t *testing.T) { + fs := flag.NewFlagSet("test", flag.ExitOnError) + var ( + s = fs.String("s", "", "string") + i = fs.Int("i", 0, "int") + b = fs.Bool("b", false, "bool") + d = fs.Duration("d", time.Second, "time.Duration") + ) + + var options []Option + + if testcase.file != "" { + filename := filepath.Join(os.TempDir(), "TestParsePriority"+fmt.Sprint(10000*rand.Intn(10000))) + f, err := os.Create(filename) + if err != nil { + t.Fatal(err) + } + defer os.Remove(f.Name()) + f.Write([]byte(testcase.file)) + f.Close() + + options = append(options, WithConfigFile(f.Name()), WithConfigFileParser(PlainParser)) + } + + if len(testcase.env) > 0 { + for k, v := range testcase.env { + os.Setenv(k, v) + defer os.Setenv(k, "") + } + + options = append(options, WithEnvVarPrefix("TEST_PARSE")) + } + + if err := Parse(fs, testcase.args, options...); err != nil { + t.Fatal(err) + } + + if want, have := testcase.want.s, *s; want != have { + t.Errorf("s: want %q, have %q", want, have) + } + if want, have := testcase.want.i, *i; want != have { + t.Errorf("i: want %d, have %d", want, have) + } + if want, have := testcase.want.b, *b; want != have { + t.Errorf("b: want %v, have %v", want, have) + } + if want, have := testcase.want.d, *d; want != have { + t.Errorf("d: want %s, have %s", want, have) + } + }) + } +} + +func TestParseIssue16(t *testing.T) { + for _, testcase := range []struct { + name string + file string + want string + }{ + { + name: "hash in value", + file: "foo bar#baz", + want: "bar#baz", + }, + { + name: "EOL comment with space", + file: "foo bar # baz", + want: "bar", + }, + { + name: "EOL comment no space", + file: "foo bar #baz", + want: "bar", + }, + { + name: "only comment with space", + file: "# foo bar\n", + want: "", + }, + { + name: "only comment no space", + file: "#foo bar\n", + want: "", + }, + } { + t.Run(testcase.name, func(t *testing.T) { + fs := flag.NewFlagSet("test", flag.ExitOnError) + foo := fs.String("foo", "", "the value of foo") + + filename := filepath.Join(os.TempDir(), "TestParseIssue16"+fmt.Sprint(10000*rand.Intn(10000))) + f, err := os.Create(filename) + if err != nil { + t.Fatal(err) + } + defer os.Remove(f.Name()) + f.Write([]byte(testcase.file)) + f.Close() + + if err := Parse(fs, []string{}, WithConfigFile(filename), WithConfigFileParser(PlainParser)); err != nil { + t.Fatal(err) + } + + if want, have := testcase.want, *foo; want != have { + t.Errorf("want %q, have %q", want, have) + } + }) + } +} From 3da84bd7669518a203e18aa4758ac152f80f76c9 Mon Sep 17 00:00:00 2001 From: John Bryan Sazon Date: Wed, 24 Apr 2019 22:33:50 +0200 Subject: [PATCH 2/2] Update documentation --- README.md | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 61ef42d..be85112 100644 --- a/README.md +++ b/README.md @@ -12,17 +12,26 @@ To run it: ./clickhouse_exporter [flags] ``` -Help on flags: +Flags are also configurable using environment variables. See the usage with: + ```bash ./clickhouse_exporter --help ``` -Credentials(if not default): - -via environment variables ``` -CLICKHOUSE_USER -CLICKHOUSE_PASSWORD +Usage of clickhouse-exporter: + -insecure + Ignore server certificate if using https. Override environment (CLICKHOUSE_INSECURE) (default true) + -password string + Clickhouse password. Override environment (CLICKHOUSE_PASSWORD) + -scrape_uri string + URI to clickhouse http endpoint. Override environment (CLICKHOUSE_SCRAPE_URI) (default "http://localhost:8123/") + -telemetry.address string + Address on which to expose metrics. Override environment (CLICKHOUSE_TELEMETRY_ADDRESS) (default ":9116") + -telemetry.endpoint string + Path under which to expose metrics. Override environment (CLICKHOUSE_TELEMETRY_ENDPOINT) (default "/metrics") + -user string + Clickhouse user. Override environment (CLICKHOUSE_USER) ``` ## Using Docker @@ -30,5 +39,7 @@ CLICKHOUSE_PASSWORD ``` docker run -d -p 9116:9116 f1yegor/clickhouse-exporter -scrape_uri=http://clickhouse.service.consul:8123/ ``` + ## Sample dashboard + Grafana dashboard could be a start for inspiration https://grafana.net/dashboards/882