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