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 1 commit
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
3 changes: 2 additions & 1 deletion go.mod
Expand Up @@ -6,10 +6,11 @@ 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
github.com/stretchr/testify v1.3.0
golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56 // indirect
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980
golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect
Expand Down
93 changes: 77 additions & 16 deletions main.go
Expand Up @@ -18,16 +18,20 @@ import (
"context"
"fmt"
"html"
"net"
"net/http"
_ "net/http/pprof"
"net/url"
"os"
"os/signal"
"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 +55,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 +223,25 @@ func run() int {

level.Info(logger).Log("msg", "Loaded config file")

// Infer or set Blackbox exporter externalURL
beURL, err := extURL(logger, os.Hostname, *listenAddress, *externalURL)
if err != nil {
level.Error(logger).Log("msg", "failed to determine external URL", "err", err)
return 1
}
level.Debug(logger).Log("externalURL", beURL.String())

// Make routePrefix default to externalURL path if empty string.
if *routePrefix == "" {
*routePrefix = beURL.Path
}
*routePrefix = "/" + strings.Trim(*routePrefix, "/")
level.Debug(logger).Log("routePrefix", *routePrefix)

if *routePrefix == "/" {
*routePrefix = ""
}

hup := make(chan os.Signal, 1)
reloadCh := make(chan chan error)
signal.Notify(hup, syscall.SIGHUP)
Expand All @@ -241,7 +266,7 @@ func run() int {
}
}()

http.HandleFunc("/-/reload",
http.HandleFunc(fmt.Sprintf("%s/-/reload", *routePrefix),
func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.WriteHeader(http.StatusMethodNotAllowed)
Expand All @@ -255,25 +280,27 @@ 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(fmt.Sprintf("%s/metrics", *routePrefix), promhttp.Handler())
http.HandleFunc(fmt.Sprintf("%s/probe", *routePrefix), 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(fmt.Sprintf("%s/", *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>
<h2>Recent Probes</h2>
<table border='1'><tr><th>Module</th><th>Target</th><th>Result</th><th>Debug</th>`))
w.Write([]byte("<html>\n"))
w.Write([]byte(" <head><title>Blackbox Exporter</title></head>\n"))
w.Write([]byte(" <body>\n"))
w.Write([]byte(" <h1>Blackbox Exporter</h1>\n"))

w.Write([]byte(fmt.Sprintf(" <p><a href=\"%s/probe?target=prometheus.io&module=http_2xx\">Probe prometheus.io for http_2xx</a></p>\n", *routePrefix)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use relative paths here, the blackbox exporter doesn't do anything fancy.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, since they are already relative urls I'll keep the code in this area the same then

w.Write([]byte(fmt.Sprintf(" <p><a href=\"%s/probe?target=prometheus.io&module=http_2xx&debug=true\">Debug probe prometheus.io for http_2xx</a></p>\n", *routePrefix)))
w.Write([]byte(fmt.Sprintf(" <p><a href=\"%s/metrics\">Metrics</a></p>\n", *routePrefix)))
w.Write([]byte(fmt.Sprintf(" <p><a href=\"%s/config\">Configuration</a></p>\n", *routePrefix)))

w.Write([]byte(" <h2>Recent Probes</h2>\n"))
w.Write([]byte(" <table border='1'><tr><th>Module</th><th>Target</th><th>Result</th><th>Debug</th>\n"))

results := rh.List()

Expand All @@ -291,7 +318,7 @@ func run() int {
</html>`))
})

http.HandleFunc("/logs", func(w http.ResponseWriter, r *http.Request) {
http.HandleFunc(fmt.Sprintf("%s/logs", *routePrefix), 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 +333,7 @@ func run() int {
w.Write([]byte(result.debugOutput))
})

http.HandleFunc("/config", func(w http.ResponseWriter, r *http.Request) {
http.HandleFunc(fmt.Sprintf("%s/config", *routePrefix), func(w http.ResponseWriter, r *http.Request) {
sc.RLock()
c, err := yaml.Marshal(sc.C)
sc.RUnlock()
Expand Down Expand Up @@ -366,3 +393,37 @@ func getTimeout(r *http.Request, module config.Module, offset float64) (timeoutS

return timeoutSeconds, nil
}

func extURL(logger log.Logger, hostnamef func() (string, error), listen, external string) (*url.URL, error) {
if external == "" {
hostname, err := hostnamef()
if err != nil {
return nil, err
}
_, port, err := net.SplitHostPort(listen)
if err != nil {
return nil, err
}
if port == "" {
level.Warn(logger).Log("msg", "no port found for listen address", "address", listen)
}

external = fmt.Sprintf("http://%s:%s/", hostname, port)
}

u, err := url.Parse(external)
if err != nil {
return nil, err
}
if u.Scheme != "http" && u.Scheme != "https" {
return nil, errors.Errorf("%q: invalid %q scheme, only 'http' and 'https' are supported", u.String(), u.Scheme)
}

ppref := strings.TrimRight(u.Path, "/")
if ppref != "" && !strings.HasPrefix(ppref, "/") {
ppref = "/" + ppref
}
u.Path = ppref

return u, nil
}
79 changes: 79 additions & 0 deletions main_test.go
Expand Up @@ -15,12 +15,15 @@ package main

import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"

"github.com/stretchr/testify/require"

"github.com/go-kit/kit/log"
"github.com/prometheus/client_golang/prometheus"
pconfig "github.com/prometheus/common/config"
Expand Down Expand Up @@ -137,3 +140,79 @@ func TestTimeoutIsSetCorrectly(t *testing.T) {
}
}
}

func TestExternalURL(t *testing.T) {
hostname := "foo"
for _, tc := range []struct {
hostnameResolver func() (string, error)
external string
listen string

expURL string
err bool
}{
{
listen: ":9093",
expURL: "http://" + hostname + ":9093",
},
{
listen: "localhost:9093",
expURL: "http://" + hostname + ":9093",
},
{
listen: "localhost:",
expURL: "http://" + hostname + ":",
},
{
external: "https://host.example.com",
expURL: "https://host.example.com",
},
{
external: "https://host.example.com/",
expURL: "https://host.example.com",
},
{
external: "http://host.example.com/alertmanager",
expURL: "http://host.example.com/alertmanager",
},
{
external: "http://host.example.com/alertmanager/",
expURL: "http://host.example.com/alertmanager",
},
{
external: "http://host.example.com/////alertmanager//",
expURL: "http://host.example.com/////alertmanager",
},
{
err: true,
},
{
hostnameResolver: func() (string, error) { return "", fmt.Errorf("some error") },
err: true,
},
{
external: "://broken url string",
err: true,
},
{
external: "host.example.com:8080",
err: true,
},
} {
tc := tc
if tc.hostnameResolver == nil {
tc.hostnameResolver = func() (string, error) {
return hostname, nil
}
}
t.Run(fmt.Sprintf("external=%q,listen=%q", tc.external, tc.listen), func(t *testing.T) {
u, err := extURL(log.NewNopLogger(), tc.hostnameResolver, tc.listen, tc.external)
if tc.err {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tc.expURL, u.String())
})
}
}
15 changes: 15 additions & 0 deletions vendor/github.com/davecgh/go-spew/LICENSE

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.