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
)
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
package main
import (
- "bytes"
- "context"
"fmt"
"html"
"net"
"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"
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() {
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())
}
}()
- // 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) {
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")
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>
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) {
}
-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, "'")
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 {
}
}
}
-
-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)
- }
-}
--- /dev/null
+// 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
+}
--- /dev/null
+// 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)
+ }
+}
// 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()
}
// 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
}
}
// See the License for the specific language governing permissions and
// limitations under the License.
-package main
+package prober
import (
"fmt"
)
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)
}
}
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)
}
}
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)
}
}
}