"context"
"fmt"
"html"
+ "net"
"net/http"
_ "net/http/pprof"
+ "net/url"
"os"
"os/signal"
+ "path"
"strconv"
+ "strings"
"syscall"
"time"
"github.com/go-kit/kit/log"
"github.com/go-kit/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"
timeoutOffset = kingpin.Flag("timeout-offset", "Offset to subtract from timeout in seconds.").Default("0.5").Float64()
configCheck = kingpin.Flag("config.check", "If true validate the config file and then exit.").Default().Bool()
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,
level.Info(logger).Log("msg", "Loaded config file")
+ // Infer or set Blackbox exporter externalURL
+ beURL, err := computeExternalURL(*externalURL, *listenAddress)
+ if err != nil {
+ level.Error(logger).Log("msg", "failed to determine external URL", "err", err)
+ return 1
+ }
+ level.Debug(logger).Log("externalURL", beURL.String())
+
+ // Default -web.route-prefix to path of -web.external-url.
+ if *routePrefix == "" {
+ *routePrefix = beURL.Path
+ }
+
+ // routePrefix must always be at least '/'.
+ *routePrefix = "/" + strings.Trim(*routePrefix, "/")
+ // routePrefix requires path to have trailing "/" in order
+ // for browsers to interpret the path-relative path correctly, instead of stripping it.
+ if *routePrefix != "/" {
+ *routePrefix = *routePrefix + "/"
+ }
+ level.Debug(logger).Log("routePrefix", *routePrefix)
+
hup := make(chan os.Signal, 1)
reloadCh := make(chan chan error)
signal.Notify(hup, syscall.SIGHUP)
}
}()
- http.HandleFunc("/-/reload",
+ // Match Prometheus behaviour and redirect over externalURL for root path only
+ // if routePrefix is different than "/"
+ if *routePrefix != "/" {
+ http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/" {
+ http.NotFound(w, r)
+ return
+ }
+ http.Redirect(w, r, beURL.String(), http.StatusFound)
+ })
+ }
+
+ http.HandleFunc(path.Join(*routePrefix, "/-/reload"),
func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.WriteHeader(http.StatusMethodNotAllowed)
http.Error(w, fmt.Sprintf("failed to reload config: %s", err), http.StatusInternalServerError)
}
})
- http.Handle("/metrics", promhttp.Handler())
- http.HandleFunc("/probe", func(w http.ResponseWriter, r *http.Request) {
+ http.Handle(path.Join(*routePrefix, "/metrics"), promhttp.Handler())
+ http.HandleFunc(path.Join(*routePrefix, "/probe"), func(w http.ResponseWriter, r *http.Request) {
sc.Lock()
conf := sc.C
sc.Unlock()
probeHandler(w, r, conf, logger, rh)
})
- http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ http.HandleFunc(*routePrefix, func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(`<html>
<head><title>Blackbox Exporter</title></head>
<body>
<h1>Blackbox Exporter</h1>
- <p><a href="/probe?target=prometheus.io&module=http_2xx">Probe prometheus.io for http_2xx</a></p>
- <p><a href="/probe?target=prometheus.io&module=http_2xx&debug=true">Debug probe prometheus.io for http_2xx</a></p>
- <p><a href="/metrics">Metrics</a></p>
- <p><a href="/config">Configuration</a></p>
+ <p><a href="probe?target=prometheus.io&module=http_2xx">Probe prometheus.io for http_2xx</a></p>
+ <p><a href="probe?target=prometheus.io&module=http_2xx&debug=true">Debug probe prometheus.io for http_2xx</a></p>
+ <p><a href="metrics">Metrics</a></p>
+ <p><a href="config">Configuration</a></p>
<h2>Recent Probes</h2>
<table border='1'><tr><th>Module</th><th>Target</th><th>Result</th><th>Debug</th>`))
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>",
+ fmt.Fprintf(w, "<tr><td>%s</td><td>%s</td><td>%s</td><td><a href='"+path.Join(*routePrefix, "/logs")+"?id=%d'>Logs</a></td></td>",
html.EscapeString(r.moduleName), html.EscapeString(r.target), success, r.id)
}
</html>`))
})
- http.HandleFunc("/logs", func(w http.ResponseWriter, r *http.Request) {
+ http.HandleFunc(path.Join(*routePrefix, "/logs"), func(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.URL.Query().Get("id"), 10, 64)
if err != nil {
http.Error(w, "Invalid probe id", 500)
w.Write([]byte(result.debugOutput))
})
- http.HandleFunc("/config", func(w http.ResponseWriter, r *http.Request) {
+ http.HandleFunc(path.Join(*routePrefix, "/config"), func(w http.ResponseWriter, r *http.Request) {
sc.RLock()
c, err := yaml.Marshal(sc.C)
sc.RUnlock()
return timeoutSeconds, nil
}
+
+func startsOrEndsWithQuote(s string) bool {
+ return strings.HasPrefix(s, "\"") || strings.HasPrefix(s, "'") ||
+ strings.HasSuffix(s, "\"") || strings.HasSuffix(s, "'")
+}
+
+// computeExternalURL computes a sanitized external URL from a raw input. It infers unset
+// URL parts from the OS and the given listen address.
+func computeExternalURL(u, listenAddr string) (*url.URL, error) {
+ if u == "" {
+ hostname, err := os.Hostname()
+ if err != nil {
+ return nil, err
+ }
+ _, port, err := net.SplitHostPort(listenAddr)
+ if err != nil {
+ return nil, err
+ }
+ u = fmt.Sprintf("http://%s:%s/", hostname, port)
+ }
+
+ if startsOrEndsWithQuote(u) {
+ return nil, errors.New("URL must not begin or end with quotes")
+ }
+
+ eu, err := url.Parse(u)
+ if err != nil {
+ return nil, err
+ }
+
+ ppref := strings.TrimRight(eu.Path, "/")
+ if ppref != "" && !strings.HasPrefix(ppref, "/") {
+ ppref = "/" + ppref
+ }
+ eu.Path = ppref
+
+ return eu, nil
+}