Merge branch 'master' into 14075-uploadfiles
[arvados.git] / sdk / go / httpserver / metrics.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: Apache-2.0
4
5 package httpserver
6
7 import (
8         "net/http"
9         "strconv"
10         "strings"
11         "time"
12
13         "git.curoverse.com/arvados.git/sdk/go/stats"
14         "github.com/Sirupsen/logrus"
15         "github.com/gogo/protobuf/jsonpb"
16         "github.com/prometheus/client_golang/prometheus"
17         "github.com/prometheus/client_golang/prometheus/promhttp"
18 )
19
20 type Handler interface {
21         http.Handler
22
23         // Returns an http.Handler that serves the Handler's metrics
24         // data at /metrics and /metrics.json, and passes other
25         // requests through to next.
26         ServeAPI(next http.Handler) http.Handler
27 }
28
29 type metrics struct {
30         next         http.Handler
31         logger       *logrus.Logger
32         registry     *prometheus.Registry
33         reqDuration  *prometheus.SummaryVec
34         timeToStatus *prometheus.SummaryVec
35         exportProm   http.Handler
36 }
37
38 func (*metrics) Levels() []logrus.Level {
39         return logrus.AllLevels
40 }
41
42 // Fire implements logrus.Hook in order to collect data points from
43 // request logs.
44 func (m *metrics) Fire(ent *logrus.Entry) error {
45         if tts, ok := ent.Data["timeToStatus"].(stats.Duration); !ok {
46         } else if method, ok := ent.Data["reqMethod"].(string); !ok {
47         } else if code, ok := ent.Data["respStatusCode"].(int); !ok {
48         } else {
49                 m.timeToStatus.WithLabelValues(strconv.Itoa(code), strings.ToLower(method)).Observe(time.Duration(tts).Seconds())
50         }
51         return nil
52 }
53
54 func (m *metrics) exportJSON(w http.ResponseWriter, req *http.Request) {
55         jm := jsonpb.Marshaler{Indent: "  "}
56         mfs, _ := m.registry.Gather()
57         w.Write([]byte{'['})
58         for i, mf := range mfs {
59                 if i > 0 {
60                         w.Write([]byte{','})
61                 }
62                 jm.Marshal(w, mf)
63         }
64         w.Write([]byte{']'})
65 }
66
67 // ServeHTTP implements http.Handler.
68 func (m *metrics) ServeHTTP(w http.ResponseWriter, req *http.Request) {
69         m.next.ServeHTTP(w, req)
70 }
71
72 // ServeAPI returns a new http.Handler that serves current data at
73 // metrics API endpoints (currently "GET /metrics(.json)?") and passes
74 // other requests through to next.
75 //
76 // Typical example:
77 //
78 //      m := Instrument(...)
79 //      srv := http.Server{Handler: m.ServeAPI(m)}
80 func (m *metrics) ServeAPI(next http.Handler) http.Handler {
81         return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
82                 switch {
83                 case req.Method != "GET" && req.Method != "HEAD":
84                         next.ServeHTTP(w, req)
85                 case req.URL.Path == "/metrics.json":
86                         m.exportJSON(w, req)
87                 case req.URL.Path == "/metrics":
88                         m.exportProm.ServeHTTP(w, req)
89                 default:
90                         next.ServeHTTP(w, req)
91                 }
92         })
93 }
94
95 // Instrument returns a new Handler that passes requests through to
96 // the next handler in the stack, and tracks metrics of those
97 // requests.
98 //
99 // For the metrics to be accurate, the caller must ensure every
100 // request passed to the Handler also passes through
101 // LogRequests(logger, ...), and vice versa.
102 //
103 // If registry is nil, a new registry is created.
104 //
105 // If logger is nil, logrus.StandardLogger() is used.
106 func Instrument(registry *prometheus.Registry, logger *logrus.Logger, next http.Handler) Handler {
107         if logger == nil {
108                 logger = logrus.StandardLogger()
109         }
110         if registry == nil {
111                 registry = prometheus.NewRegistry()
112         }
113         reqDuration := prometheus.NewSummaryVec(prometheus.SummaryOpts{
114                 Name: "request_duration_seconds",
115                 Help: "Summary of request duration.",
116         }, []string{"code", "method"})
117         timeToStatus := prometheus.NewSummaryVec(prometheus.SummaryOpts{
118                 Name: "time_to_status_seconds",
119                 Help: "Summary of request TTFB.",
120         }, []string{"code", "method"})
121         registry.MustRegister(timeToStatus)
122         registry.MustRegister(reqDuration)
123         m := &metrics{
124                 next:         promhttp.InstrumentHandlerDuration(reqDuration, next),
125                 logger:       logger,
126                 registry:     registry,
127                 reqDuration:  reqDuration,
128                 timeToStatus: timeToStatus,
129                 exportProm: promhttp.HandlerFor(registry, promhttp.HandlerOpts{
130                         ErrorLog: logger,
131                 }),
132         }
133         m.logger.AddHook(m)
134         return m
135 }