From 20f59cf2269455bb1355b964aa5317183a26a83a Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Fri, 12 Oct 2018 13:34:07 +0200 Subject: [PATCH] add basic expvar metrics and prometheus-compatible exporter (#5) --- cmd/gomodproxy/main.go | 55 +++++++++++++++++++++++++++++++++++++++++- pkg/api/api.go | 30 ++++++++++++++++++++--- 2 files changed, 80 insertions(+), 5 deletions(-) diff --git a/cmd/gomodproxy/main.go b/cmd/gomodproxy/main.go index d55e048..46cf1fc 100644 --- a/cmd/gomodproxy/main.go +++ b/cmd/gomodproxy/main.go @@ -5,6 +5,7 @@ import ( "encoding/json" "flag" "fmt" + "io" "log" "net" "net/http" @@ -13,13 +14,56 @@ import ( "path/filepath" "strings" "time" + "unicode" "github.com/sixt/gomodproxy/pkg/api" - _ "expvar" + "expvar" _ "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{}) { s := "" msg := "" @@ -56,6 +100,7 @@ func main() { addr := flag.String("addr", ":0", "http server address") verbose := flag.Bool("v", false, "verbose logging") + prometheus := flag.String("prometheus", "", "prometheus address") debug := flag.Bool("debug", false, "enable debug HTTP API (pprof/expvar)") json := flag.Bool("json", false, "json structured logging") dir := flag.String("dir", filepath.Join(os.Getenv("HOME"), ".gomodproxy/cache"), "modules cache directory") @@ -103,6 +148,14 @@ func main() { mux := http.NewServeMux() 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 { mux.Handle("/debug/vars", http.DefaultServeMux) mux.Handle("/debug/pprof/heap", http.DefaultServeMux) diff --git a/pkg/api/api.go b/pkg/api/api.go index ad7a4c1..1bd7153 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -5,6 +5,7 @@ import ( "bytes" "context" "encoding/json" + "expvar" "fmt" "io" "net/http" @@ -42,6 +43,14 @@ var ( apiZip = regexp.MustCompile(`^/(?P.*)/@v/(?P.*).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. func New(options ...Option) http.Handler { 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)) }() for _, route := range []struct { + id string regexp *regexp.Regexp handler func(w http.ResponseWriter, r *http.Request, module, version string) }{ - {apiList, api.list}, - {apiInfo, api.info}, - {apiMod, api.mod}, - {apiZip, api.zip}, + {"list", apiList, api.list}, + {"info", apiInfo, api.info}, + {"api", apiMod, api.mod}, + {"zip", apiZip, api.zip}, } { if m := route.regexp.FindStringSubmatch(r.URL.Path); m != nil { module, version := m[1], "" @@ -127,11 +137,18 @@ func (api *api) ServeHTTP(w http.ResponseWriter, r *http.Request) { version = m[2] } 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) return } } + httpRequests.Add("not_found", 1) 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) { for _, store := range api.stores { if snapshot, err := store.Get(ctx, module, version); err == nil { + cacheHits.Add(module, 1) return snapshot.Data, snapshot.Timestamp, nil } } + cacheMisses.Add(module, 1) timestamp, err := api.vcs(ctx, module).Timestamp(ctx, version) 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()) if err != nil { api.log("api.list", "module", module, "error", err) + httpErrors.Add(module, 1) http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -202,6 +222,7 @@ func (api *api) info(w http.ResponseWriter, r *http.Request, module, version str if err != nil { api.log("api.info", "module", module, "version", version, "error", err) + httpErrors.Add(module, 1) http.Error(w, err.Error(), http.StatusInternalServerError) 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)) if err != nil { api.log("api.zip", "module", module, "version", version, "error", err) + httpErrors.Add(module, 1) http.Error(w, err.Error(), http.StatusInternalServerError) return }