Refactor proberHandler as an exported function (#957)
authorMarc Tudurí <marctc@protonmail.com>
Tue, 20 Sep 2022 15:40:26 +0000 (17:40 +0200)
committerGitHub <noreply@github.com>
Tue, 20 Sep 2022 15:40:26 +0000 (09:40 -0600)
* Refactor proberHandler as an exported function

Signed-off-by: Marc Tuduri <marctc@protonmail.com>
go.mod
main.go
main_test.go
prober/handler.go [new file with mode: 0644]
prober/handler_test.go [new file with mode: 0644]
prober/history.go [moved from history.go with 66% similarity]
prober/history_test.go [moved from history_test.go with 72% similarity]

diff --git a/go.mod b/go.mod
index 75d1aca9e6bad168c349e946e168e550a913ada9..f584f779af25206eb3e052f10351d86dbc267d19 100644 (file)
--- 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 bdc73ae066304f4a5bf5db63f1e9805f3ad37614..23f690fb8b59fc64e97c33239e9efdf6ece8cb3a 100644 (file)
--- 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("<url>").String()
        routePrefix   = kingpin.Flag("web.route-prefix", "Prefix for the internal routes of web endpoints. Defaults to path of --web.external-url.").PlaceHolder("<path>").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 = "<strong>Failure</strong>"
                        }
                        fmt.Fprintf(w, "<tr><td>%s</td><td>%s</td><td>%s</td><td><a href='logs?id=%d'>Logs</a></td></td>",
-                               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(`</table></body>
@@ -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, "'")
index e7ba5c68a276ede7eb53375dcc786d1cde8f5052..45c9abe1485e582e9d44a9b0fc38c4ed92f3e348 100644 (file)
 
 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, "<secret>") {
-               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, "<secret>") {
-               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 (file)
index 0000000..51f6189
--- /dev/null
@@ -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 (file)
index 0000000..a144fb9
--- /dev/null
@@ -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, "<secret>") {
+               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, "<secret>") {
+               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)
+       }
+}
similarity index 66%
rename from history.go
rename to prober/history.go
index 95b686deb4f87ab9bd016a729bfd0dfdee4ecce4..306eb419eba2cc1ff9a20e8f12747053789b210f 100644 (file)
 // 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
                }
        }
similarity index 72%
rename from history_test.go
rename to prober/history_test.go
index 3fb3c49fdbdc13453ce8311e97fa70a18c7b31b0..b964c537febd790ae2d1ff1c802e5b82aba33b93 100644 (file)
@@ -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)
                }
        }
 }