tsweb, derp: add expvar http.Handler for Prometheus's format

And add some opinions.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2020-03-03 11:33:22 -08:00
committed by Brad Fitzpatrick
parent 89a2c3eb04
commit e371520cc5
4 changed files with 131 additions and 67 deletions

View File

@@ -18,8 +18,12 @@ import (
"time"
"tailscale.com/interfaces"
"tailscale.com/metrics"
)
// DevMode controls whether extra output in shown, for when the binary is being run in dev mode.
var DevMode bool
// NewMux returns a new ServeMux with debugHandler registered (and protected) at /debug/.
func NewMux(debugHandler http.Handler) *http.ServeMux {
mux := http.NewServeMux()
@@ -29,9 +33,10 @@ func NewMux(debugHandler http.Handler) *http.ServeMux {
}
func RegisterCommonDebug(mux *http.ServeMux) {
expvar.Publish("uptime", uptimeVar{})
expvar.Publish("counter_uptime_sec", expvar.Func(func() interface{} { return int64(Uptime().Seconds()) }))
mux.Handle("/debug/pprof/", Protected(http.DefaultServeMux)) // to net/http/pprof
mux.Handle("/debug/vars", Protected(http.DefaultServeMux)) // to expvar
mux.Handle("/debug/varz", Protected(http.HandlerFunc(varzHandler)))
}
func DefaultCertDir(leafDir string) string {
@@ -69,7 +74,12 @@ func AllowDebugAccess(r *http.Request) bool {
func Protected(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !AllowDebugAccess(r) {
http.Error(w, "debug access denied", http.StatusForbidden)
msg := "debug access denied"
if DevMode {
ipStr, _, _ := net.SplitHostPort(r.RemoteAddr)
msg += fmt.Sprintf("; to permit access, set ALLOW_DEBUG_IP=%v", ipStr)
}
http.Error(w, msg, http.StatusForbidden)
return
}
h.ServeHTTP(w, r)
@@ -80,10 +90,6 @@ var timeStart = time.Now()
func Uptime() time.Duration { return time.Since(timeStart).Round(time.Second) }
type uptimeVar struct{}
func (uptimeVar) String() string { return fmt.Sprint(int64(Uptime().Seconds())) }
// Port80Handler is the handler to be given to
// autocert.Manager.HTTPHandler. The inner handler is the mux
// returned by NewMux containing registered /debug handlers.
@@ -114,3 +120,64 @@ func stripPort(hostport string) string {
}
return net.JoinHostPort(host, "443")
}
// varzHandler is an HTTP handler to write expvar values into the
// prometheus export format:
//
// https://github.com/prometheus/docs/blob/master/content/docs/instrumenting/exposition_formats.md
//
// It makes the following assumptions:
//
// * *expvar.Int are counters.
// * a *tailscale/metrics.Set is descended into, joining keys with
// underscores. So use underscores as your metric names.
// * an expvar named starting with "gauge_" or "counter_" is of that
// Prometheus type, and has that prefix stripped.
// * anything else is untyped and thus not exported.
// * expvar.Func can return an int or int64 (for now) and anything else
// is not exported.
//
// This will evolve over time, or perhaps be replaced.
func varzHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; version=0.0.4")
var dump func(prefix string, kv expvar.KeyValue)
dump = func(prefix string, kv expvar.KeyValue) {
name := prefix + kv.Key
var typ string
switch v := kv.Value.(type) {
case *expvar.Int:
// Fast path for common value type.
fmt.Fprintf(w, "# TYPE %s counter\n%s %v\n", name, name, v.Value())
return
case *metrics.Set:
v.Do(func(kv expvar.KeyValue) {
dump(name+"_", kv)
})
return
}
if strings.HasPrefix(kv.Key, "gauge_") {
typ = "gauge"
name = prefix + strings.TrimPrefix(kv.Key, "gauge_")
} else if strings.HasPrefix(kv.Key, "counter_") {
typ = "counter"
name = prefix + strings.TrimPrefix(kv.Key, "counter_")
}
if fn, ok := kv.Value.(expvar.Func); ok {
val := fn()
switch val.(type) {
case int64, int:
if typ != "" {
fmt.Fprintf(w, "# TYPE %s %s\n%s %v\n", name, typ, name, val)
return
}
}
fmt.Fprintf(w, "# skipping expvar func %q returning unknown type %T\n", name, val)
return
}
fmt.Fprintf(w, "# skipping func %q returning unknown type %T\n", name, kv.Value)
}
expvar.Do(func(kv expvar.KeyValue) {
dump("", kv)
})
}