Skip to content

Commit

Permalink
Adding web.external-url and web.route-prefix flags (#515)
Browse files Browse the repository at this point in the history
Signed-off-by: Sven Nebel <[email protected]>
  • Loading branch information
snebel29 authored and brian-brazil committed Sep 11, 2019
1 parent d0c9b46 commit 252a878
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 12 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require (
github.com/go-kit/kit v0.8.0
github.com/kr/pretty v0.1.0 // indirect
github.com/miekg/dns v1.1.14
github.com/pkg/errors v0.8.1 // indirect
github.com/pkg/errors v0.8.1
github.com/prometheus/client_golang v1.0.0
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90
github.com/prometheus/common v0.6.0
Expand Down
101 changes: 90 additions & 11 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,21 @@ import (
"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"
Expand All @@ -51,6 +56,8 @@ var (
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,
Expand Down Expand Up @@ -217,6 +224,28 @@ func run() int {

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)
Expand All @@ -241,7 +270,19 @@ func run() int {
}
}()

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)
Expand All @@ -255,23 +296,23 @@ func run() int {
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>`))

Expand All @@ -283,15 +324,15 @@ func run() int {
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)
}

w.Write([]byte(`</table></body>
</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)
Expand All @@ -306,7 +347,7 @@ func run() int {
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()
Expand Down Expand Up @@ -366,3 +407,41 @@ func getTimeout(r *http.Request, module config.Module, offset float64) (timeoutS

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
}
53 changes: 53 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,56 @@ func TestTimeoutIsSetCorrectly(t *testing.T) {
}
}
}

func TestComputeExternalURL(t *testing.T) {
tests := []struct {
input string
valid bool
}{
{
input: "",
valid: true,
},
{
input: "http://proxy.com/prometheus",
valid: true,
},
{
input: "'https://url/prometheus'",
valid: false,
},
{
input: "'relative/path/with/quotes'",
valid: false,
},
{
input: "http://alertmanager.company.com",
valid: true,
},
{
input: "https://double--dash.de",
valid: true,
},
{
input: "'http://starts/with/quote",
valid: false,
},
{
input: "ends/with/quote\"",
valid: false,
},
}

for _, test := range tests {
_, err := computeExternalURL(test.input, "0.0.0.0:9090")
if test.valid {
if err != nil {
t.Errorf("unexpected error %v", err)
}
} else {
if err == nil {
t.Errorf("expected error computing %s got none", test.input)
}
}
}
}

0 comments on commit 252a878

Please sign in to comment.