diff --git a/cmd/main.go b/cmd/main.go index 473e088..8cc1e6e 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -18,7 +18,6 @@ import ( "encoding/json" "net/http" "os" - "time" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" @@ -78,7 +77,7 @@ func Run() { func probeHandler(w http.ResponseWriter, r *http.Request, logger log.Logger, config config.Config) { - ctx, cancel := context.WithTimeout(r.Context(), time.Duration(config.Global.TimeoutSeconds*float64(time.Second))) + ctx, cancel := context.WithCancel(r.Context()) defer cancel() r = r.WithContext(ctx) diff --git a/cmd/main_test.go b/cmd/main_test.go new file mode 100644 index 0000000..70df1de --- /dev/null +++ b/cmd/main_test.go @@ -0,0 +1,220 @@ +// Copyright 2020 The Prometheus Authors +// 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. + +package cmd + +import ( + "encoding/base64" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-kit/kit/log" + "github.com/prometheus-community/json_exporter/config" + pconfig "github.com/prometheus/common/config" +) + +func TestFailIfSelfSignedCA(t *testing.T) { + target := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + })) + defer target.Close() + + req := httptest.NewRequest("GET", "http://example.com/foo"+"?target="+target.URL, nil) + recorder := httptest.NewRecorder() + probeHandler(recorder, req, log.NewNopLogger(), config.Config{}) + + resp := recorder.Result() + body, _ := ioutil.ReadAll(resp.Body) + + if resp.StatusCode != http.StatusServiceUnavailable { + t.Fatalf("Fail if (not strict) selfsigned CA test fails unexpectedly, got %s", body) + } +} + +func TestSucceedIfSelfSignedCA(t *testing.T) { + c := config.Config{} + c.HTTPClientConfig.TLSConfig.InsecureSkipVerify = true + target := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + })) + defer target.Close() + + req := httptest.NewRequest("GET", "http://example.com/foo"+"?target="+target.URL, nil) + recorder := httptest.NewRecorder() + probeHandler(recorder, req, log.NewNopLogger(), c) + + resp := recorder.Result() + body, _ := ioutil.ReadAll(resp.Body) + + if resp.StatusCode != http.StatusOK { + t.Fatalf("Succeed if (not strict) selfsigned CA test fails unexpectedly, got %s", body) + } +} + +func TestFailIfTargetMissing(t *testing.T) { + req := httptest.NewRequest("GET", "http://example.com/foo", nil) + recorder := httptest.NewRecorder() + probeHandler(recorder, req, log.NewNopLogger(), config.Config{}) + + resp := recorder.Result() + body, _ := ioutil.ReadAll(resp.Body) + + if resp.StatusCode != http.StatusBadRequest { + t.Fatalf("Fail if 'target' query parameter missing test fails unexpectedly, got %s", body) + } +} + +func TestDefaultAcceptHeader(t *testing.T) { + target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + expected := "application/json" + if got := r.Header.Get("Accept"); got != expected { + t.Errorf("Default 'Accept' header mismatch, got %s, expected: %s", got, expected) + w.WriteHeader(http.StatusNotAcceptable) + } + })) + defer target.Close() + + req := httptest.NewRequest("GET", "http://example.com/foo"+"?target="+target.URL, nil) + recorder := httptest.NewRecorder() + probeHandler(recorder, req, log.NewNopLogger(), config.Config{}) + + resp := recorder.Result() + body, _ := ioutil.ReadAll(resp.Body) + + if resp.StatusCode != http.StatusOK { + t.Fatalf("Default 'Accept: application/json' header test fails unexpectedly, got %s", body) + } +} + +func TestCorrectResponse(t *testing.T) { + tests := []struct { + ConfigFile string + ServeFile string + ResponseFile string + ShouldSucceed bool + }{ + {"../test/config/good.yml", "/serve/good.json", "../test/response/good.txt", true}, + {"../test/config/good.yml", "/serve/repeat-metric.json", "../test/response/good.txt", false}, + } + + target := httptest.NewServer(http.FileServer(http.Dir("../test"))) + defer target.Close() + + for i, test := range tests { + c, err := config.LoadConfig(test.ConfigFile) + if err != nil { + t.Fatalf("Failed to load config file %s", test.ConfigFile) + } + + req := httptest.NewRequest("GET", "http://example.com/foo"+"?target="+target.URL+test.ServeFile, nil) + recorder := httptest.NewRecorder() + probeHandler(recorder, req, log.NewNopLogger(), c) + + resp := recorder.Result() + body, _ := ioutil.ReadAll(resp.Body) + + expected, _ := ioutil.ReadFile(test.ResponseFile) + + if test.ShouldSucceed && string(body) != string(expected) { + t.Fatalf("Correct response validation test %d fails unexpectedly.\nGOT:\n%s\nEXPECTED:\n%s", i, body, expected) + } + } +} + +func TestBasicAuth(t *testing.T) { + username := "myUser" + password := "mySecretPassword" + expected := "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password)) + target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if got := r.Header.Get("Authorization"); got != expected { + t.Errorf("BasicAuth mismatch, got: %s, expected: %s", got, expected) + w.WriteHeader(http.StatusUnauthorized) + } + })) + defer target.Close() + + req := httptest.NewRequest("GET", "http://example.com/foo"+"?target="+target.URL, nil) + recorder := httptest.NewRecorder() + c := config.Config{} + auth := &pconfig.BasicAuth{ + Username: username, + Password: pconfig.Secret(password), + } + + c.HTTPClientConfig.BasicAuth = auth + probeHandler(recorder, req, log.NewNopLogger(), c) + + resp := recorder.Result() + body, _ := ioutil.ReadAll(resp.Body) + + if resp.StatusCode != http.StatusOK { + t.Fatalf("BasicAuth test fails unexpectedly. Got: %s", body) + } +} + +func TestBearerToken(t *testing.T) { + token := "mySecretToken" + target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + expected := "Bearer " + token + if got := r.Header.Get("Authorization"); got != expected { + t.Errorf("BearerToken mismatch, got: %s, expected: %s", got, expected) + w.WriteHeader(http.StatusUnauthorized) + } + })) + defer target.Close() + + req := httptest.NewRequest("GET", "http://example.com/foo"+"?target="+target.URL, nil) + recorder := httptest.NewRecorder() + c := config.Config{} + + c.HTTPClientConfig.BearerToken = pconfig.Secret(token) + probeHandler(recorder, req, log.NewNopLogger(), c) + + resp := recorder.Result() + body, _ := ioutil.ReadAll(resp.Body) + + if resp.StatusCode != http.StatusOK { + t.Fatalf("BearerToken test fails unexpectedly. Got: %s", body) + } +} + +func TestHTTPHeaders(t *testing.T) { + headers := map[string]string{ + "X-Dummy": "test", + "User-Agent": "unsuspicious user", + "Accept-Language": "en-US", + } + target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + for key, value := range headers { + if got := r.Header.Get(key); got != value { + t.Errorf("Unexpected value of header %q: expected %q, got %q", key, value, got) + } + } + w.WriteHeader(http.StatusOK) + })) + defer target.Close() + + req := httptest.NewRequest("GET", "http://example.com/foo"+"?target="+target.URL, nil) + recorder := httptest.NewRecorder() + c := config.Config{} + c.Headers = headers + + probeHandler(recorder, req, log.NewNopLogger(), c) + + resp := recorder.Result() + body, _ := ioutil.ReadAll(resp.Body) + + if resp.StatusCode != http.StatusOK { + t.Fatalf("Setting custom headers failed unexpectedly. Got: %s", body) + } +} diff --git a/config/config.go b/config/config.go index 6f32bd7..db7e799 100644 --- a/config/config.go +++ b/config/config.go @@ -41,14 +41,9 @@ const ( type Config struct { Headers map[string]string `yaml:"headers,omitempty"` Metrics []Metric `yaml:"metrics"` - Global GlobalConfig `yaml:"global_config,omitempty"` HTTPClientConfig pconfig.HTTPClientConfig `yaml:"http_client_config,omitempty"` } -type GlobalConfig struct { - TimeoutSeconds float64 `yaml:"timeout_seconds,omitempty"` -} - func LoadConfig(configPath string) (Config, error) { var config Config data, err := ioutil.ReadFile(configPath) @@ -70,9 +65,6 @@ func LoadConfig(configPath string) (Config, error) { config.Metrics[i].Help = config.Metrics[i].Name } } - if config.Global.TimeoutSeconds == 0 { - config.Global.TimeoutSeconds = 10 - } return config, nil } diff --git a/examples/config.yml b/examples/config.yml index 9252d78..c0451cc 100644 --- a/examples/config.yml +++ b/examples/config.yml @@ -31,6 +31,3 @@ headers: # username: myuser # #password: veryverysecret # password_file: /tmp/mysecret.txt -# -# global_config: -# timeout_seconds: 30 // defaults to 10 diff --git a/test/config/good.yml b/test/config/good.yml new file mode 100644 index 0000000..0110ff4 --- /dev/null +++ b/test/config/good.yml @@ -0,0 +1,21 @@ +--- +metrics: +- name: example_global_value + path: $.counter + help: Example of a top-level global value scrape in the json + labels: + environment: beta # static label + location: $.location # dynamic label + +- name: example_value + type: object + help: Example of sub-level value scrapes from a json + path: $.values[*]?(@.state == "ACTIVE") + labels: + environment: beta # static label + id: $.id # dynamic label + values: + active: 1 # static value + count: $.count # dynamic value + boolean: $.some_boolean + diff --git a/test/response/good.txt b/test/response/good.txt new file mode 100644 index 0000000..5ca3360 --- /dev/null +++ b/test/response/good.txt @@ -0,0 +1,15 @@ +# HELP example_global_value Example of a top-level global value scrape in the json +# TYPE example_global_value untyped +example_global_value{environment="beta",location="mars"} 1234 +# HELP example_value_active Example of sub-level value scrapes from a json +# TYPE example_value_active untyped +example_value_active{environment="beta",id="id-A"} 1 +example_value_active{environment="beta",id="id-C"} 1 +# HELP example_value_boolean Example of sub-level value scrapes from a json +# TYPE example_value_boolean untyped +example_value_boolean{environment="beta",id="id-A"} 1 +example_value_boolean{environment="beta",id="id-C"} 0 +# HELP example_value_count Example of sub-level value scrapes from a json +# TYPE example_value_count untyped +example_value_count{environment="beta",id="id-A"} 1 +example_value_count{environment="beta",id="id-C"} 3 diff --git a/test/serve/good.json b/test/serve/good.json new file mode 100644 index 0000000..93dea69 --- /dev/null +++ b/test/serve/good.json @@ -0,0 +1,24 @@ +{ + "counter": 1234, + "values": [ + { + "id": "id-A", + "count": 1, + "some_boolean": true, + "state": "ACTIVE" + }, + { + "id": "id-B", + "count": 2, + "some_boolean": true, + "state": "INACTIVE" + }, + { + "id": "id-C", + "count": 3, + "some_boolean": false, + "state": "ACTIVE" + } + ], + "location": "mars" +} diff --git a/test/serve/repeat-metric.json b/test/serve/repeat-metric.json new file mode 100644 index 0000000..649a2f5 --- /dev/null +++ b/test/serve/repeat-metric.json @@ -0,0 +1,30 @@ +{ + "counter": 1234, + "values": [ + { + "id": "id-A", + "count": 1, + "some_boolean": true, + "state": "ACTIVE" + }, + { + "id": "id-B", + "count": 2, + "some_boolean": true, + "state": "INACTIVE" + }, + { + "id": "id-C", + "count": 3, + "some_boolean": true, + "state": "ACTIVE" + }, + { + "id": "id-C", + "count": 4, + "some_boolean": false, + "state": "ACTIVE" + } + ], + "location": "mars" +}