From 252a878a2c0e4b5961f04c765ca969224a8e0150 Mon Sep 17 00:00:00 2001 From: Sven Nebel Date: Wed, 11 Sep 2019 13:36:17 +0100 Subject: [PATCH] Adding web.external-url and web.route-prefix flags (#515) Signed-off-by: Sven Nebel --- go.mod | 2 +- main.go | 101 +++++++++++++++++++++++++++++++++++++++++++++------ main_test.go | 53 +++++++++++++++++++++++++++ 3 files changed, 144 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index bfae6ec..b60028b 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/main.go b/main.go index e4f9b75..3d95971 100644 --- a/main.go +++ b/main.go @@ -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" @@ -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("").String() + routePrefix = kingpin.Flag("web.route-prefix", "Prefix for the internal routes of web endpoints. Defaults to path of --web.external-url.").PlaceHolder("").String() Probers = map[string]prober.ProbeFn{ "http": prober.ProbeHTTP, @@ -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) @@ -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) @@ -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(` Blackbox Exporter

Blackbox Exporter

-

Probe prometheus.io for http_2xx

-

Debug probe prometheus.io for http_2xx

-

Metrics

-

Configuration

+

Probe prometheus.io for http_2xx

+

Debug probe prometheus.io for http_2xx

+

Metrics

+

Configuration

Recent Probes

`)) @@ -283,7 +324,7 @@ func run() int { if !r.success { success = "Failure" } - fmt.Fprintf(w, "", + fmt.Fprintf(w, "", html.EscapeString(r.moduleName), html.EscapeString(r.target), success, r.id) } @@ -291,7 +332,7 @@ func run() int { `)) }) - 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) @@ -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() @@ -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 +} diff --git a/main_test.go b/main_test.go index c2341ec..f9d536b 100644 --- a/main_test.go +++ b/main_test.go @@ -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) + } + } + } +} -- 2.25.1
ModuleTargetResultDebug
%s%s%sLogs
%s%s%sLogs