add basic expvar metrics and prometheus-compatible exporter (#5)
This commit is contained in:
parent
1ef560dcac
commit
20f59cf226
@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -13,13 +14,56 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"github.com/sixt/gomodproxy/pkg/api"
|
"github.com/sixt/gomodproxy/pkg/api"
|
||||||
|
|
||||||
_ "expvar"
|
"expvar"
|
||||||
_ "net/http/pprof"
|
_ "net/http/pprof"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func prometheusExpose(w io.Writer, name string, v interface{}) {
|
||||||
|
// replace all invalid symbols with underscores
|
||||||
|
name = strings.Map(func(r rune) rune {
|
||||||
|
r = unicode.ToLower(r)
|
||||||
|
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
return '_'
|
||||||
|
}, name)
|
||||||
|
// expvar does not have concepts of counters and gauges,
|
||||||
|
// so we tell one from another based on the name suffix.
|
||||||
|
counter := strings.HasSuffix(name, "_total")
|
||||||
|
if f, ok := v.(float64); ok {
|
||||||
|
if counter {
|
||||||
|
fmt.Fprintf(w, "# TYPE %s counter\n", name)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(w, "# TYPE %s gauge\n", name)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "%s %f\n", name, f)
|
||||||
|
} else if m, ok := v.(map[string]interface{}); ok {
|
||||||
|
for k, v := range m {
|
||||||
|
// for composite maps we construct metric names by joining the parent map
|
||||||
|
// name and the key name.
|
||||||
|
s := strings.TrimSuffix(name, "_total") + "_" + k
|
||||||
|
if counter {
|
||||||
|
s = s + "_total"
|
||||||
|
}
|
||||||
|
prometheusExpose(w, s, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func prometheusHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
expvar.Do(func(kv expvar.KeyValue) {
|
||||||
|
var v interface{}
|
||||||
|
if err := json.Unmarshal([]byte(kv.Value.String()), &v); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
prometheusExpose(w, kv.Key, v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func prettyLog(v ...interface{}) {
|
func prettyLog(v ...interface{}) {
|
||||||
s := ""
|
s := ""
|
||||||
msg := ""
|
msg := ""
|
||||||
@ -56,6 +100,7 @@ func main() {
|
|||||||
|
|
||||||
addr := flag.String("addr", ":0", "http server address")
|
addr := flag.String("addr", ":0", "http server address")
|
||||||
verbose := flag.Bool("v", false, "verbose logging")
|
verbose := flag.Bool("v", false, "verbose logging")
|
||||||
|
prometheus := flag.String("prometheus", "", "prometheus address")
|
||||||
debug := flag.Bool("debug", false, "enable debug HTTP API (pprof/expvar)")
|
debug := flag.Bool("debug", false, "enable debug HTTP API (pprof/expvar)")
|
||||||
json := flag.Bool("json", false, "json structured logging")
|
json := flag.Bool("json", false, "json structured logging")
|
||||||
dir := flag.String("dir", filepath.Join(os.Getenv("HOME"), ".gomodproxy/cache"), "modules cache directory")
|
dir := flag.String("dir", filepath.Join(os.Getenv("HOME"), ".gomodproxy/cache"), "modules cache directory")
|
||||||
@ -103,6 +148,14 @@ func main() {
|
|||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.Handle("/", api.New(options...))
|
mux.Handle("/", api.New(options...))
|
||||||
|
if *prometheus != "" {
|
||||||
|
if *prometheus == *addr {
|
||||||
|
mux.HandleFunc("/metrics", prometheusHandler)
|
||||||
|
} else {
|
||||||
|
srv := &http.Server{Handler: http.HandlerFunc(prometheusHandler), Addr: *prometheus}
|
||||||
|
go srv.ListenAndServe()
|
||||||
|
}
|
||||||
|
}
|
||||||
if *debug {
|
if *debug {
|
||||||
mux.Handle("/debug/vars", http.DefaultServeMux)
|
mux.Handle("/debug/vars", http.DefaultServeMux)
|
||||||
mux.Handle("/debug/pprof/heap", http.DefaultServeMux)
|
mux.Handle("/debug/pprof/heap", http.DefaultServeMux)
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"expvar"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -42,6 +43,14 @@ var (
|
|||||||
apiZip = regexp.MustCompile(`^/(?P<module>.*)/@v/(?P<version>.*).zip$`)
|
apiZip = regexp.MustCompile(`^/(?P<module>.*)/@v/(?P<version>.*).zip$`)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
cacheHits = expvar.NewMap("cache_hits_total")
|
||||||
|
cacheMisses = expvar.NewMap("cache_misses_total")
|
||||||
|
httpRequests = expvar.NewMap("http_requests_total")
|
||||||
|
httpErrors = expvar.NewMap("http_errors_total")
|
||||||
|
httpRequestDurations = expvar.NewMap("http_request_duration_seconds")
|
||||||
|
)
|
||||||
|
|
||||||
// New returns a configured http.Handler which implements GOPROXY API.
|
// New returns a configured http.Handler which implements GOPROXY API.
|
||||||
func New(options ...Option) http.Handler {
|
func New(options ...Option) http.Handler {
|
||||||
api := &api{log: func(...interface{}) {}}
|
api := &api{log: func(...interface{}) {}}
|
||||||
@ -113,13 +122,14 @@ func (api *api) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
defer func() { api.log("api.ServeHTTP", "method", r.Method, "url", r.URL, "time", time.Since(now)) }()
|
defer func() { api.log("api.ServeHTTP", "method", r.Method, "url", r.URL, "time", time.Since(now)) }()
|
||||||
|
|
||||||
for _, route := range []struct {
|
for _, route := range []struct {
|
||||||
|
id string
|
||||||
regexp *regexp.Regexp
|
regexp *regexp.Regexp
|
||||||
handler func(w http.ResponseWriter, r *http.Request, module, version string)
|
handler func(w http.ResponseWriter, r *http.Request, module, version string)
|
||||||
}{
|
}{
|
||||||
{apiList, api.list},
|
{"list", apiList, api.list},
|
||||||
{apiInfo, api.info},
|
{"info", apiInfo, api.info},
|
||||||
{apiMod, api.mod},
|
{"api", apiMod, api.mod},
|
||||||
{apiZip, api.zip},
|
{"zip", apiZip, api.zip},
|
||||||
} {
|
} {
|
||||||
if m := route.regexp.FindStringSubmatch(r.URL.Path); m != nil {
|
if m := route.regexp.FindStringSubmatch(r.URL.Path); m != nil {
|
||||||
module, version := m[1], ""
|
module, version := m[1], ""
|
||||||
@ -127,11 +137,18 @@ func (api *api) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
version = m[2]
|
version = m[2]
|
||||||
}
|
}
|
||||||
module = decodeBangs(module)
|
module = decodeBangs(module)
|
||||||
|
httpRequests.Add(route.id, 1)
|
||||||
|
defer func() {
|
||||||
|
v := &expvar.Float{}
|
||||||
|
v.Set(time.Since(now).Seconds())
|
||||||
|
httpRequestDurations.Set(route.id, v)
|
||||||
|
}()
|
||||||
route.handler(w, r, module, version)
|
route.handler(w, r, module, version)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
httpRequests.Add("not_found", 1)
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,9 +164,11 @@ func (api *api) vcs(ctx context.Context, module string) vcs.VCS {
|
|||||||
func (api *api) module(ctx context.Context, module string, version vcs.Version) ([]byte, time.Time, error) {
|
func (api *api) module(ctx context.Context, module string, version vcs.Version) ([]byte, time.Time, error) {
|
||||||
for _, store := range api.stores {
|
for _, store := range api.stores {
|
||||||
if snapshot, err := store.Get(ctx, module, version); err == nil {
|
if snapshot, err := store.Get(ctx, module, version); err == nil {
|
||||||
|
cacheHits.Add(module, 1)
|
||||||
return snapshot.Data, snapshot.Timestamp, nil
|
return snapshot.Data, snapshot.Timestamp, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
cacheMisses.Add(module, 1)
|
||||||
|
|
||||||
timestamp, err := api.vcs(ctx, module).Timestamp(ctx, version)
|
timestamp, err := api.vcs(ctx, module).Timestamp(ctx, version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -187,6 +206,7 @@ func (api *api) list(w http.ResponseWriter, r *http.Request, module, version str
|
|||||||
list, err := api.vcs(r.Context(), module).List(r.Context())
|
list, err := api.vcs(r.Context(), module).List(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.log("api.list", "module", module, "error", err)
|
api.log("api.list", "module", module, "error", err)
|
||||||
|
httpErrors.Add(module, 1)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -202,6 +222,7 @@ func (api *api) info(w http.ResponseWriter, r *http.Request, module, version str
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.log("api.info", "module", module, "version", version, "error", err)
|
api.log("api.info", "module", module, "version", version, "error", err)
|
||||||
|
httpErrors.Add(module, 1)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -236,6 +257,7 @@ func (api *api) zip(w http.ResponseWriter, r *http.Request, module, version stri
|
|||||||
b, _, err := api.module(r.Context(), module, vcs.Version(version))
|
b, _, err := api.module(r.Context(), module, vcs.Version(version))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.log("api.zip", "module", module, "version", version, "error", err)
|
api.log("api.zip", "module", module, "version", version, "error", err)
|
||||||
|
httpErrors.Add(module, 1)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user