From 3446984d6a6e7d2fc75c4bdf014cdc232e9bf10a Mon Sep 17 00:00:00 2001 From: =?utf8?q?Marc=20Tudur=C3=AD?= Date: Tue, 20 Sep 2022 17:40:26 +0200 Subject: [PATCH] Refactor proberHandler as an exported function (#957) * Refactor proberHandler as an exported function Signed-off-by: Marc Tuduri --- go.mod | 2 +- main.go | 212 +------------------- main_test.go | 191 +----------------- prober/handler.go | 234 ++++++++++++++++++++++ prober/handler_test.go | 205 +++++++++++++++++++ history.go => prober/history.go | 57 +++--- history_test.go => prober/history_test.go | 36 ++-- 7 files changed, 494 insertions(+), 443 deletions(-) create mode 100644 prober/handler.go create mode 100644 prober/handler_test.go rename history.go => prober/history.go (66%) rename history_test.go => prober/history_test.go (72%) diff --git a/go.mod b/go.mod index 75d1aca..f584f77 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( golang.org/x/text v0.3.7 google.golang.org/grpc v1.48.0 gopkg.in/alecthomas/kingpin.v2 v2.2.6 + gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -36,7 +37,6 @@ require ( google.golang.org/appengine v1.6.6 // indirect google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 // indirect google.golang.org/protobuf v1.27.1 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect ) go 1.17 diff --git a/main.go b/main.go index bdc73ae..23f690f 100644 --- a/main.go +++ b/main.go @@ -14,8 +14,6 @@ package main import ( - "bytes" - "context" "fmt" "html" "net" @@ -28,21 +26,16 @@ import ( "strconv" "strings" "syscall" - "time" - "github.com/go-kit/log" "github.com/go-kit/log/level" "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/prometheus/common/expfmt" "github.com/prometheus/common/promlog" "github.com/prometheus/common/promlog/flag" "github.com/prometheus/common/version" "github.com/prometheus/exporter-toolkit/web" webflag "github.com/prometheus/exporter-toolkit/web/kingpinflag" - "golang.org/x/text/cases" - "golang.org/x/text/language" "gopkg.in/alecthomas/kingpin.v2" "gopkg.in/yaml.v3" @@ -63,180 +56,10 @@ var ( historyLimit = kingpin.Flag("history.limit", "The maximum amount of items to keep in the history.").Default("100").Uint() externalURL = kingpin.Flag("web.external-url", "The URL under which Blackbox exporter is externally reachable (for example, if Blackbox exporter is served via a reverse proxy). Used for generating relative and absolute links back to Blackbox exporter itself. If the URL has a path portion, it will be used to prefix all HTTP endpoints served by Blackbox exporter. If omitted, relevant URL components will be derived automatically.").PlaceHolder("").String() routePrefix = kingpin.Flag("web.route-prefix", "Prefix for the internal routes of web endpoints. Defaults to path of --web.external-url.").PlaceHolder("").String() - - Probers = map[string]prober.ProbeFn{ - "http": prober.ProbeHTTP, - "tcp": prober.ProbeTCP, - "icmp": prober.ProbeICMP, - "dns": prober.ProbeDNS, - "grpc": prober.ProbeGRPC, - } - - moduleUnknownCounter = prometheus.NewCounter(prometheus.CounterOpts{ - Name: "blackbox_module_unknown_total", - Help: "Count of unknown modules requested by probes", - }) - - caser = cases.Title(language.Und) ) -func probeHandler(w http.ResponseWriter, r *http.Request, c *config.Config, logger log.Logger, rh *resultHistory) { - moduleName := r.URL.Query().Get("module") - if moduleName == "" { - moduleName = "http_2xx" - } - module, ok := c.Modules[moduleName] - if !ok { - http.Error(w, fmt.Sprintf("Unknown module %q", moduleName), http.StatusBadRequest) - level.Debug(logger).Log("msg", "Unknown module", "module", moduleName) - moduleUnknownCounter.Add(1) - return - } - - timeoutSeconds, err := getTimeout(r, module, *timeoutOffset) - if err != nil { - http.Error(w, fmt.Sprintf("Failed to parse timeout from Prometheus header: %s", err), http.StatusInternalServerError) - return - } - - ctx, cancel := context.WithTimeout(r.Context(), time.Duration(timeoutSeconds*float64(time.Second))) - defer cancel() - r = r.WithContext(ctx) - - probeSuccessGauge := prometheus.NewGauge(prometheus.GaugeOpts{ - Name: "probe_success", - Help: "Displays whether or not the probe was a success", - }) - probeDurationGauge := prometheus.NewGauge(prometheus.GaugeOpts{ - Name: "probe_duration_seconds", - Help: "Returns how long the probe took to complete in seconds", - }) - - params := r.URL.Query() - target := params.Get("target") - if target == "" { - http.Error(w, "Target parameter is missing", http.StatusBadRequest) - return - } - - prober, ok := Probers[module.Prober] - if !ok { - http.Error(w, fmt.Sprintf("Unknown prober %q", module.Prober), http.StatusBadRequest) - return - } - - hostname := params.Get("hostname") - if module.Prober == "http" && hostname != "" { - err = setHTTPHost(hostname, &module) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - } - - sl := newScrapeLogger(logger, moduleName, target) - level.Info(sl).Log("msg", "Beginning probe", "probe", module.Prober, "timeout_seconds", timeoutSeconds) - - start := time.Now() - registry := prometheus.NewRegistry() - registry.MustRegister(probeSuccessGauge) - registry.MustRegister(probeDurationGauge) - success := prober(ctx, target, module, registry, sl) - duration := time.Since(start).Seconds() - probeDurationGauge.Set(duration) - if success { - probeSuccessGauge.Set(1) - level.Info(sl).Log("msg", "Probe succeeded", "duration_seconds", duration) - } else { - level.Error(sl).Log("msg", "Probe failed", "duration_seconds", duration) - } - - debugOutput := DebugOutput(&module, &sl.buffer, registry) - rh.Add(moduleName, target, debugOutput, success) - - if r.URL.Query().Get("debug") == "true" { - w.Header().Set("Content-Type", "text/plain") - w.Write([]byte(debugOutput)) - return - } - - h := promhttp.HandlerFor(registry, promhttp.HandlerOpts{}) - h.ServeHTTP(w, r) -} - -func setHTTPHost(hostname string, module *config.Module) error { - // By creating a new hashmap and copying values there we - // ensure that the initial configuration remain intact. - headers := make(map[string]string) - if module.HTTP.Headers != nil { - for name, value := range module.HTTP.Headers { - if caser.String(name) == "Host" && value != hostname { - return fmt.Errorf("host header defined both in module configuration (%s) and with URL-parameter 'hostname' (%s)", value, hostname) - } - headers[name] = value - } - } - headers["Host"] = hostname - module.HTTP.Headers = headers - return nil -} - -type scrapeLogger struct { - next log.Logger - buffer bytes.Buffer - bufferLogger log.Logger -} - -func newScrapeLogger(logger log.Logger, module string, target string) *scrapeLogger { - logger = log.With(logger, "module", module, "target", target) - sl := &scrapeLogger{ - next: logger, - buffer: bytes.Buffer{}, - } - bl := log.NewLogfmtLogger(&sl.buffer) - sl.bufferLogger = log.With(bl, "ts", log.DefaultTimestampUTC, "caller", log.Caller(6), "module", module, "target", target) - return sl -} - -func (sl scrapeLogger) Log(keyvals ...interface{}) error { - sl.bufferLogger.Log(keyvals...) - kvs := make([]interface{}, len(keyvals)) - copy(kvs, keyvals) - // Switch level to debug for application output. - for i := 0; i < len(kvs); i += 2 { - if kvs[i] == level.Key() { - kvs[i+1] = level.DebugValue() - } - } - return sl.next.Log(kvs...) -} - -// DebugOutput returns plaintext debug output for a probe. -func DebugOutput(module *config.Module, logBuffer *bytes.Buffer, registry *prometheus.Registry) string { - buf := &bytes.Buffer{} - fmt.Fprintf(buf, "Logs for the probe:\n") - logBuffer.WriteTo(buf) - fmt.Fprintf(buf, "\n\n\nMetrics that would have been returned:\n") - mfs, err := registry.Gather() - if err != nil { - fmt.Fprintf(buf, "Error gathering metrics: %s\n", err) - } - for _, mf := range mfs { - expfmt.MetricFamilyToText(buf, mf) - } - fmt.Fprintf(buf, "\n\n\nModule configuration:\n") - c, err := yaml.Marshal(module) - if err != nil { - fmt.Fprintf(buf, "Error marshalling config: %s\n", err) - } - buf.Write(c) - - return buf.String() -} - func init() { prometheus.MustRegister(version.NewCollector("blackbox_exporter")) - prometheus.MustRegister(moduleUnknownCounter) } func main() { @@ -251,7 +74,7 @@ func run() int { kingpin.HelpFlag.Short('h') kingpin.Parse() logger := promlog.New(promlogConfig) - rh := &resultHistory{maxResults: *historyLimit} + rh := &prober.ResultHistory{MaxResults: *historyLimit} level.Info(logger).Log("msg", "Starting blackbox_exporter", "version", version.Info()) level.Info(logger).Log("build_context", version.BuildContext()) @@ -314,7 +137,7 @@ func run() int { } }() - // Match Prometheus behaviour and redirect over externalURL for root path only + // Match Prometheus behavior and redirect over externalURL for root path only // if routePrefix is different than "/" if *routePrefix != "/" { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { @@ -349,7 +172,7 @@ func run() int { sc.Lock() conf := sc.C sc.Unlock() - probeHandler(w, r, conf, logger, rh) + prober.Handler(w, r, conf, logger, rh, *timeoutOffset, nil) }) http.HandleFunc(*routePrefix, func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") @@ -369,11 +192,11 @@ func run() int { for i := len(results) - 1; i >= 0; i-- { r := results[i] success := "Success" - if !r.success { + if !r.Success { success = "Failure" } fmt.Fprintf(w, "%s%s%sLogs", - html.EscapeString(r.moduleName), html.EscapeString(r.target), success, r.id) + html.EscapeString(r.ModuleName), html.EscapeString(r.Target), success, r.Id) } w.Write([]byte(` @@ -392,7 +215,7 @@ func run() int { return } w.Header().Set("Content-Type", "text/plain") - w.Write([]byte(result.debugOutput)) + w.Write([]byte(result.DebugOutput)) }) http.HandleFunc(path.Join(*routePrefix, "/config"), func(w http.ResponseWriter, r *http.Request) { @@ -433,29 +256,6 @@ func run() int { } -func getTimeout(r *http.Request, module config.Module, offset float64) (timeoutSeconds float64, err error) { - // If a timeout is configured via the Prometheus header, add it to the request. - if v := r.Header.Get("X-Prometheus-Scrape-Timeout-Seconds"); v != "" { - var err error - timeoutSeconds, err = strconv.ParseFloat(v, 64) - if err != nil { - return 0, err - } - } - if timeoutSeconds == 0 { - timeoutSeconds = 120 - } - - var maxTimeoutSeconds = timeoutSeconds - offset - if module.Timeout.Seconds() < maxTimeoutSeconds && module.Timeout.Seconds() > 0 || maxTimeoutSeconds < 0 { - timeoutSeconds = module.Timeout.Seconds() - } else { - timeoutSeconds = maxTimeoutSeconds - } - - return timeoutSeconds, nil -} - func startsOrEndsWithQuote(s string) bool { return strings.HasPrefix(s, "\"") || strings.HasPrefix(s, "'") || strings.HasSuffix(s, "\"") || strings.HasSuffix(s, "'") diff --git a/main_test.go b/main_test.go index e7ba5c6..45c9abe 100644 --- a/main_test.go +++ b/main_test.go @@ -13,131 +13,7 @@ package main -import ( - "bytes" - "fmt" - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" - - "github.com/go-kit/log" - "github.com/prometheus/client_golang/prometheus" - pconfig "github.com/prometheus/common/config" - - "github.com/prometheus/blackbox_exporter/config" -) - -var c = &config.Config{ - Modules: map[string]config.Module{ - "http_2xx": config.Module{ - Prober: "http", - Timeout: 10 * time.Second, - HTTP: config.HTTPProbe{ - HTTPClientConfig: pconfig.HTTPClientConfig{ - BearerToken: "mysecret", - }, - }, - }, - }, -} - -func TestPrometheusTimeoutHTTP(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - time.Sleep(2 * time.Second) - })) - defer ts.Close() - - req, err := http.NewRequest("GET", "?target="+ts.URL, nil) - if err != nil { - t.Fatal(err) - } - req.Header.Set("X-Prometheus-Scrape-Timeout-Seconds", "1") - - rr := httptest.NewRecorder() - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - probeHandler(w, r, c, log.NewNopLogger(), &resultHistory{}) - }) - - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("probe request handler returned wrong status code: %v, want %v", status, http.StatusOK) - } -} - -func TestPrometheusConfigSecretsHidden(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - time.Sleep(2 * time.Second) - })) - defer ts.Close() - - req, err := http.NewRequest("GET", "?debug=true&target="+ts.URL, nil) - if err != nil { - t.Fatal(err) - } - rr := httptest.NewRecorder() - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - probeHandler(w, r, c, log.NewNopLogger(), &resultHistory{}) - }) - handler.ServeHTTP(rr, req) - - body := rr.Body.String() - if strings.Contains(body, "mysecret") { - t.Errorf("Secret exposed in debug config output: %v", body) - } - if !strings.Contains(body, "") { - t.Errorf("Hidden secret missing from debug config output: %v", body) - } -} - -func TestDebugOutputSecretsHidden(t *testing.T) { - module := c.Modules["http_2xx"] - out := DebugOutput(&module, &bytes.Buffer{}, prometheus.NewRegistry()) - - if strings.Contains(out, "mysecret") { - t.Errorf("Secret exposed in debug output: %v", out) - } - if !strings.Contains(out, "") { - t.Errorf("Hidden secret missing from debug output: %v", out) - } -} - -func TestTimeoutIsSetCorrectly(t *testing.T) { - var tests = []struct { - inModuleTimeout time.Duration - inPrometheusTimeout string - inOffset float64 - outTimeout float64 - }{ - {0 * time.Second, "15", 0.5, 14.5}, - {0 * time.Second, "15", 0, 15}, - {20 * time.Second, "15", 0.5, 14.5}, - {20 * time.Second, "15", 0, 15}, - {5 * time.Second, "15", 0, 5}, - {5 * time.Second, "15", 0.5, 5}, - {10 * time.Second, "", 0.5, 10}, - {10 * time.Second, "10", 0.5, 9.5}, - {9500 * time.Millisecond, "", 0.5, 9.5}, - {9500 * time.Millisecond, "", 1, 9.5}, - {0 * time.Second, "", 0.5, 119.5}, - {0 * time.Second, "", 0, 120}, - } - - for _, v := range tests { - request, _ := http.NewRequest("GET", "", nil) - request.Header.Set("X-Prometheus-Scrape-Timeout-Seconds", v.inPrometheusTimeout) - module := config.Module{ - Timeout: v.inModuleTimeout, - } - - timeout, _ := getTimeout(request, module, v.inOffset) - if timeout != v.outTimeout { - t.Errorf("timeout is incorrect: %v, want %v", timeout, v.outTimeout) - } - } -} +import "testing" func TestComputeExternalURL(t *testing.T) { tests := []struct { @@ -191,68 +67,3 @@ func TestComputeExternalURL(t *testing.T) { } } } - -func TestHostnameParam(t *testing.T) { - headers := map[string]string{} - c := &config.Config{ - Modules: map[string]config.Module{ - "http_2xx": config.Module{ - Prober: "http", - Timeout: 10 * time.Second, - HTTP: config.HTTPProbe{ - Headers: headers, - IPProtocolFallback: true, - }, - }, - }, - } - - // check that 'hostname' parameter make its way to Host header - hostname := "foo.example.com" - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Host != hostname { - t.Errorf("Unexpected Host: expected %q, got %q.", hostname, r.Host) - } - w.WriteHeader(http.StatusOK) - })) - defer ts.Close() - - requrl := fmt.Sprintf("?debug=true&hostname=%s&target=%s", hostname, ts.URL) - - req, err := http.NewRequest("GET", requrl, nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - probeHandler(w, r, c, log.NewNopLogger(), &resultHistory{}) - }) - - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("probe request handler returned wrong status code: %v, want %v", status, http.StatusOK) - } - - // check that ts got the request to perform header check - if !strings.Contains(rr.Body.String(), "probe_success 1") { - t.Errorf("probe failed, response body: %v", rr.Body.String()) - } - - // check that host header both in config and in parameter will result in 400 - c.Modules["http_2xx"].HTTP.Headers["Host"] = hostname + ".something" - - handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - probeHandler(w, r, c, log.NewNopLogger(), &resultHistory{}) - }) - - rr = httptest.NewRecorder() - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusBadRequest { - t.Errorf("probe request handler returned wrong status code: %v, want %v", status, http.StatusBadRequest) - } -} diff --git a/prober/handler.go b/prober/handler.go new file mode 100644 index 0000000..51f6189 --- /dev/null +++ b/prober/handler.go @@ -0,0 +1,234 @@ +// Copyright 2016 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 prober + +import ( + "bytes" + "context" + "fmt" + "net/http" + "net/url" + "strconv" + "time" + + "github.com/go-kit/log" + "github.com/go-kit/log/level" + "github.com/prometheus/blackbox_exporter/config" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/prometheus/common/expfmt" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "gopkg.in/yaml.v2" +) + +var ( + Probers = map[string]ProbeFn{ + "http": ProbeHTTP, + "tcp": ProbeTCP, + "icmp": ProbeICMP, + "dns": ProbeDNS, + "grpc": ProbeGRPC, + } + moduleUnknownCounter = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "blackbox_module_unknown_total", + Help: "Count of unknown modules requested by probes", + }) + + caser = cases.Title(language.Und) +) + +func init() { + prometheus.MustRegister(moduleUnknownCounter) +} + +func Handler(w http.ResponseWriter, r *http.Request, c *config.Config, logger log.Logger, + rh *ResultHistory, timeoutOffset float64, params url.Values) { + if params == nil { + params = r.URL.Query() + } + moduleName := params.Get("module") + if moduleName == "" { + moduleName = "http_2xx" + } + module, ok := c.Modules[moduleName] + if !ok { + http.Error(w, fmt.Sprintf("Unknown module %q", moduleName), http.StatusBadRequest) + level.Debug(logger).Log("msg", "Unknown module", "module", moduleName) + moduleUnknownCounter.Add(1) + return + } + + timeoutSeconds, err := getTimeout(r, module, timeoutOffset) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to parse timeout from Prometheus header: %s", err), http.StatusInternalServerError) + return + } + + ctx, cancel := context.WithTimeout(r.Context(), time.Duration(timeoutSeconds*float64(time.Second))) + defer cancel() + r = r.WithContext(ctx) + + probeSuccessGauge := prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "probe_success", + Help: "Displays whether or not the probe was a success", + }) + probeDurationGauge := prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "probe_duration_seconds", + Help: "Returns how long the probe took to complete in seconds", + }) + + target := params.Get("target") + if target == "" { + http.Error(w, "Target parameter is missing", http.StatusBadRequest) + return + } + + prober, ok := Probers[module.Prober] + if !ok { + http.Error(w, fmt.Sprintf("Unknown prober %q", module.Prober), http.StatusBadRequest) + return + } + + hostname := params.Get("hostname") + if module.Prober == "http" && hostname != "" { + err = setHTTPHost(hostname, &module) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + } + + sl := newScrapeLogger(logger, moduleName, target) + level.Info(sl).Log("msg", "Beginning probe", "probe", module.Prober, "timeout_seconds", timeoutSeconds) + + start := time.Now() + registry := prometheus.NewRegistry() + registry.MustRegister(probeSuccessGauge) + registry.MustRegister(probeDurationGauge) + success := prober(ctx, target, module, registry, sl) + duration := time.Since(start).Seconds() + probeDurationGauge.Set(duration) + if success { + probeSuccessGauge.Set(1) + level.Info(sl).Log("msg", "Probe succeeded", "duration_seconds", duration) + } else { + level.Error(sl).Log("msg", "Probe failed", "duration_seconds", duration) + } + + debugOutput := DebugOutput(&module, &sl.buffer, registry) + rh.Add(moduleName, target, debugOutput, success) + + if r.URL.Query().Get("debug") == "true" { + w.Header().Set("Content-Type", "text/plain") + w.Write([]byte(debugOutput)) + return + } + + h := promhttp.HandlerFor(registry, promhttp.HandlerOpts{}) + h.ServeHTTP(w, r) +} + +func setHTTPHost(hostname string, module *config.Module) error { + // By creating a new hashmap and copying values there we + // ensure that the initial configuration remain intact. + headers := make(map[string]string) + if module.HTTP.Headers != nil { + for name, value := range module.HTTP.Headers { + if caser.String(name) == "Host" && value != hostname { + return fmt.Errorf("host header defined both in module configuration (%s) and with URL-parameter 'hostname' (%s)", value, hostname) + } + headers[name] = value + } + } + headers["Host"] = hostname + module.HTTP.Headers = headers + return nil +} + +type scrapeLogger struct { + next log.Logger + buffer bytes.Buffer + bufferLogger log.Logger +} + +func newScrapeLogger(logger log.Logger, module string, target string) *scrapeLogger { + logger = log.With(logger, "module", module, "target", target) + sl := &scrapeLogger{ + next: logger, + buffer: bytes.Buffer{}, + } + bl := log.NewLogfmtLogger(&sl.buffer) + sl.bufferLogger = log.With(bl, "ts", log.DefaultTimestampUTC, "caller", log.Caller(6), "module", module, "target", target) + return sl +} + +func (sl scrapeLogger) Log(keyvals ...interface{}) error { + sl.bufferLogger.Log(keyvals...) + kvs := make([]interface{}, len(keyvals)) + copy(kvs, keyvals) + // Switch level to debug for application output. + for i := 0; i < len(kvs); i += 2 { + if kvs[i] == level.Key() { + kvs[i+1] = level.DebugValue() + } + } + return sl.next.Log(kvs...) +} + +// DebugOutput returns plaintext debug output for a probe. +func DebugOutput(module *config.Module, logBuffer *bytes.Buffer, registry *prometheus.Registry) string { + buf := &bytes.Buffer{} + fmt.Fprintf(buf, "Logs for the probe:\n") + logBuffer.WriteTo(buf) + fmt.Fprintf(buf, "\n\n\nMetrics that would have been returned:\n") + mfs, err := registry.Gather() + if err != nil { + fmt.Fprintf(buf, "Error gathering metrics: %s\n", err) + } + for _, mf := range mfs { + expfmt.MetricFamilyToText(buf, mf) + } + fmt.Fprintf(buf, "\n\n\nModule configuration:\n") + c, err := yaml.Marshal(module) + if err != nil { + fmt.Fprintf(buf, "Error marshalling config: %s\n", err) + } + buf.Write(c) + + return buf.String() +} + +func getTimeout(r *http.Request, module config.Module, offset float64) (timeoutSeconds float64, err error) { + // If a timeout is configured via the Prometheus header, add it to the request. + if v := r.Header.Get("X-Prometheus-Scrape-Timeout-Seconds"); v != "" { + var err error + timeoutSeconds, err = strconv.ParseFloat(v, 64) + if err != nil { + return 0, err + } + } + if timeoutSeconds == 0 { + timeoutSeconds = 120 + } + + var maxTimeoutSeconds = timeoutSeconds - offset + if module.Timeout.Seconds() < maxTimeoutSeconds && module.Timeout.Seconds() > 0 || maxTimeoutSeconds < 0 { + timeoutSeconds = module.Timeout.Seconds() + } else { + timeoutSeconds = maxTimeoutSeconds + } + + return timeoutSeconds, nil +} diff --git a/prober/handler_test.go b/prober/handler_test.go new file mode 100644 index 0000000..a144fb9 --- /dev/null +++ b/prober/handler_test.go @@ -0,0 +1,205 @@ +// Copyright 2016 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 prober + +import ( + "bytes" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/go-kit/log" + "github.com/prometheus/client_golang/prometheus" + pconfig "github.com/prometheus/common/config" + + "github.com/prometheus/blackbox_exporter/config" +) + +var c = &config.Config{ + Modules: map[string]config.Module{ + "http_2xx": { + Prober: "http", + Timeout: 10 * time.Second, + HTTP: config.HTTPProbe{ + HTTPClientConfig: pconfig.HTTPClientConfig{ + BearerToken: "mysecret", + }, + }, + }, + }, +} + +func TestPrometheusTimeoutHTTP(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + time.Sleep(2 * time.Second) + })) + defer ts.Close() + + req, err := http.NewRequest("GET", "?target="+ts.URL, nil) + if err != nil { + t.Fatal(err) + } + req.Header.Set("X-Prometheus-Scrape-Timeout-Seconds", "1") + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + Handler(w, r, c, log.NewNopLogger(), &ResultHistory{}, 0.5, nil) + }) + + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + t.Errorf("probe request handler returned wrong status code: %v, want %v", status, http.StatusOK) + } +} + +func TestPrometheusConfigSecretsHidden(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + time.Sleep(2 * time.Second) + })) + defer ts.Close() + + req, err := http.NewRequest("GET", "?debug=true&target="+ts.URL, nil) + if err != nil { + t.Fatal(err) + } + rr := httptest.NewRecorder() + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + Handler(w, r, c, log.NewNopLogger(), &ResultHistory{}, 0.5, nil) + }) + handler.ServeHTTP(rr, req) + + body := rr.Body.String() + if strings.Contains(body, "mysecret") { + t.Errorf("Secret exposed in debug config output: %v", body) + } + if !strings.Contains(body, "") { + t.Errorf("Hidden secret missing from debug config output: %v", body) + } +} + +func TestDebugOutputSecretsHidden(t *testing.T) { + module := c.Modules["http_2xx"] + out := DebugOutput(&module, &bytes.Buffer{}, prometheus.NewRegistry()) + + if strings.Contains(out, "mysecret") { + t.Errorf("Secret exposed in debug output: %v", out) + } + if !strings.Contains(out, "") { + t.Errorf("Hidden secret missing from debug output: %v", out) + } +} + +func TestTimeoutIsSetCorrectly(t *testing.T) { + var tests = []struct { + inModuleTimeout time.Duration + inPrometheusTimeout string + inOffset float64 + outTimeout float64 + }{ + {0 * time.Second, "15", 0.5, 14.5}, + {0 * time.Second, "15", 0, 15}, + {20 * time.Second, "15", 0.5, 14.5}, + {20 * time.Second, "15", 0, 15}, + {5 * time.Second, "15", 0, 5}, + {5 * time.Second, "15", 0.5, 5}, + {10 * time.Second, "", 0.5, 10}, + {10 * time.Second, "10", 0.5, 9.5}, + {9500 * time.Millisecond, "", 0.5, 9.5}, + {9500 * time.Millisecond, "", 1, 9.5}, + {0 * time.Second, "", 0.5, 119.5}, + {0 * time.Second, "", 0, 120}, + } + + for _, v := range tests { + request, _ := http.NewRequest("GET", "", nil) + request.Header.Set("X-Prometheus-Scrape-Timeout-Seconds", v.inPrometheusTimeout) + module := config.Module{ + Timeout: v.inModuleTimeout, + } + + timeout, _ := getTimeout(request, module, v.inOffset) + if timeout != v.outTimeout { + t.Errorf("timeout is incorrect: %v, want %v", timeout, v.outTimeout) + } + } +} + +func TestHostnameParam(t *testing.T) { + headers := map[string]string{} + c := &config.Config{ + Modules: map[string]config.Module{ + "http_2xx": { + Prober: "http", + Timeout: 10 * time.Second, + HTTP: config.HTTPProbe{ + Headers: headers, + IPProtocolFallback: true, + }, + }, + }, + } + + // check that 'hostname' parameter make its way to Host header + hostname := "foo.example.com" + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Host != hostname { + t.Errorf("Unexpected Host: expected %q, got %q.", hostname, r.Host) + } + w.WriteHeader(http.StatusOK) + })) + defer ts.Close() + + requrl := fmt.Sprintf("?debug=true&hostname=%s&target=%s", hostname, ts.URL) + + req, err := http.NewRequest("GET", requrl, nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + Handler(w, r, c, log.NewNopLogger(), &ResultHistory{}, 0.5, nil) + }) + + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + t.Errorf("probe request handler returned wrong status code: %v, want %v", status, http.StatusOK) + } + + // check that ts got the request to perform header check + if !strings.Contains(rr.Body.String(), "probe_success 1") { + t.Errorf("probe failed, response body: %v", rr.Body.String()) + } + + // check that host header both in config and in parameter will result in 400 + c.Modules["http_2xx"].HTTP.Headers["Host"] = hostname + ".something" + + handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + Handler(w, r, c, log.NewNopLogger(), &ResultHistory{}, 0.5, nil) + }) + + rr = httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusBadRequest { + t.Errorf("probe request handler returned wrong status code: %v, want %v", status, http.StatusBadRequest) + } +} diff --git a/history.go b/prober/history.go similarity index 66% rename from history.go rename to prober/history.go index 95b686d..306eb41 100644 --- a/history.go +++ b/prober/history.go @@ -11,65 +11,66 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package prober import ( "sync" ) -type result struct { - id int64 - moduleName string - target string - debugOutput string - success bool +// Result contains the result of the execution of a probe +type Result struct { + Id int64 + ModuleName string + Target string + DebugOutput string + Success bool } -// resultHistory contains two history slices: `results` contains most recent `maxResults` results. +// ResultHistory contains two history slices: `results` contains most recent `maxResults` results. // After they expire out of `results`, failures will be saved in `preservedFailedResults`. This // ensures that we are always able to see debug information about recent failures. -type resultHistory struct { +type ResultHistory struct { mu sync.Mutex nextId int64 - results []*result - preservedFailedResults []*result - maxResults uint + results []*Result + preservedFailedResults []*Result + MaxResults uint } // Add a result to the history. -func (rh *resultHistory) Add(moduleName, target, debugOutput string, success bool) { +func (rh *ResultHistory) Add(moduleName, target, debugOutput string, success bool) { rh.mu.Lock() defer rh.mu.Unlock() - r := &result{ - id: rh.nextId, - moduleName: moduleName, - target: target, - debugOutput: debugOutput, - success: success, + r := &Result{ + Id: rh.nextId, + ModuleName: moduleName, + Target: target, + DebugOutput: debugOutput, + Success: success, } rh.nextId++ rh.results = append(rh.results, r) - if uint(len(rh.results)) > rh.maxResults { + if uint(len(rh.results)) > rh.MaxResults { // If we are about to remove a failure, add it to the failed result history, then // remove the oldest failed result, if needed. - if !rh.results[0].success { + if !rh.results[0].Success { rh.preservedFailedResults = append(rh.preservedFailedResults, rh.results[0]) - if uint(len(rh.preservedFailedResults)) > rh.maxResults { - preservedFailedResults := make([]*result, len(rh.preservedFailedResults)-1) + if uint(len(rh.preservedFailedResults)) > rh.MaxResults { + preservedFailedResults := make([]*Result, len(rh.preservedFailedResults)-1) copy(preservedFailedResults, rh.preservedFailedResults[1:]) rh.preservedFailedResults = preservedFailedResults } } - results := make([]*result, len(rh.results)-1) + results := make([]*Result, len(rh.results)-1) copy(results, rh.results[1:]) rh.results = results } } // List returns a list of all results. -func (rh *resultHistory) List() []*result { +func (rh *ResultHistory) List() []*Result { rh.mu.Lock() defer rh.mu.Unlock() @@ -78,17 +79,17 @@ func (rh *resultHistory) List() []*result { } // Get returns a given result. -func (rh *resultHistory) Get(id int64) *result { +func (rh *ResultHistory) Get(id int64) *Result { rh.mu.Lock() defer rh.mu.Unlock() for _, r := range rh.preservedFailedResults { - if r.id == id { + if r.Id == id { return r } } for _, r := range rh.results { - if r.id == id { + if r.Id == id { return r } } diff --git a/history_test.go b/prober/history_test.go similarity index 72% rename from history_test.go rename to prober/history_test.go index 3fb3c49..b964c53 100644 --- a/history_test.go +++ b/prober/history_test.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package prober import ( "fmt" @@ -19,42 +19,42 @@ import ( ) func TestHistoryKeepsLatestResults(t *testing.T) { - history := &resultHistory{maxResults: 3} + history := &ResultHistory{MaxResults: 3} for i := 0; i < 4; i++ { history.Add("module", "target", fmt.Sprintf("result %d", i), true) } savedResults := history.List() for i := 0; i < len(savedResults); i++ { - if savedResults[i].debugOutput != fmt.Sprintf("result %d", i+1) { + if savedResults[i].DebugOutput != fmt.Sprintf("result %d", i+1) { t.Errorf("History contained the wrong result at index %d", i) } } } -func FillHistoryWithMaxSuccesses(h *resultHistory) { - for i := uint(0); i < h.maxResults; i++ { +func FillHistoryWithMaxSuccesses(h *ResultHistory) { + for i := uint(0); i < h.MaxResults; i++ { h.Add("module", "target", fmt.Sprintf("result %d", h.nextId), true) } } -func FillHistoryWithMaxPreservedFailures(h *resultHistory) { - for i := uint(0); i < h.maxResults; i++ { +func FillHistoryWithMaxPreservedFailures(h *ResultHistory) { + for i := uint(0); i < h.MaxResults; i++ { h.Add("module", "target", fmt.Sprintf("result %d", h.nextId), false) } } func TestHistoryPreservesExpiredFailedResults(t *testing.T) { - history := &resultHistory{maxResults: 3} + history := &ResultHistory{MaxResults: 3} - // Success are expired, no failues are expired + // Success are expired, no failures are expired FillHistoryWithMaxSuccesses(history) FillHistoryWithMaxPreservedFailures(history) savedResults := history.List() for i := uint(0); i < uint(len(savedResults)); i++ { - expectedDebugOutput := fmt.Sprintf("result %d", i+history.maxResults) - if savedResults[i].debugOutput != expectedDebugOutput { - t.Errorf("History contained the wrong result at index %d. Expected: %s, Actual: %s", i, expectedDebugOutput, savedResults[i].debugOutput) + expectedDebugOutput := fmt.Sprintf("result %d", i+history.MaxResults) + if savedResults[i].DebugOutput != expectedDebugOutput { + t.Errorf("History contained the wrong result at index %d. Expected: %s, Actual: %s", i, expectedDebugOutput, savedResults[i].DebugOutput) } } @@ -62,9 +62,9 @@ func TestHistoryPreservesExpiredFailedResults(t *testing.T) { FillHistoryWithMaxPreservedFailures(history) savedResults = history.List() for i := uint(0); i < uint(len(savedResults)); i++ { - expectedDebugOutput := fmt.Sprintf("result %d", i+history.maxResults) - if savedResults[i].debugOutput != expectedDebugOutput { - t.Errorf("History contained the wrong result at index %d. Expected: %s, Actual: %s", i, expectedDebugOutput, savedResults[i].debugOutput) + expectedDebugOutput := fmt.Sprintf("result %d", i+history.MaxResults) + if savedResults[i].DebugOutput != expectedDebugOutput { + t.Errorf("History contained the wrong result at index %d. Expected: %s, Actual: %s", i, expectedDebugOutput, savedResults[i].DebugOutput) } } @@ -73,9 +73,9 @@ func TestHistoryPreservesExpiredFailedResults(t *testing.T) { FillHistoryWithMaxSuccesses(history) savedResults = history.List() for i := uint(0); i < uint(len(savedResults)); i++ { - expectedDebugOutput := fmt.Sprintf("result %d", i+history.maxResults*3) - if savedResults[i].debugOutput != expectedDebugOutput { - t.Errorf("History contained the wrong result at index %d. Expected: %s, Actual: %s", i, expectedDebugOutput, savedResults[i].debugOutput) + expectedDebugOutput := fmt.Sprintf("result %d", i+history.MaxResults*3) + if savedResults[i].DebugOutput != expectedDebugOutput { + t.Errorf("History contained the wrong result at index %d. Expected: %s, Actual: %s", i, expectedDebugOutput, savedResults[i].DebugOutput) } } } -- 2.25.1