Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding web.external-url and web.route-prefix flags #515

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
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
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
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)
}
}
}
}