13198: Move metrics to httpserver package.
[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
24 type metrics struct {
25         next         http.Handler
26         logger       *logrus.Logger
27         registry     *prometheus.Registry
28         reqDuration  *prometheus.SummaryVec
29         timeToStatus *prometheus.SummaryVec
30         exportProm   http.Handler
31 }
32
33 func (*metrics) Levels() []logrus.Level {
34         return logrus.AllLevels
35 }
36
37 func (m *metrics) Fire(ent *logrus.Entry) error {
38         if tts, ok := ent.Data["timeToStatus"].(stats.Duration); !ok {
39         } else if method, ok := ent.Data["reqMethod"].(string); !ok {
40         } else if code, ok := ent.Data["respStatusCode"].(int); !ok {
41         } else {
42                 m.timeToStatus.WithLabelValues(strconv.Itoa(code), strings.ToLower(method)).Observe(time.Duration(tts).Seconds())
43         }
44         return nil
45 }
46
47 func (m *metrics) exportJSON(w http.ResponseWriter, req *http.Request) {
48         jm := jsonpb.Marshaler{Indent: "  "}
49         mfs, _ := m.registry.Gather()
50         w.Write([]byte{'['})
51         for i, mf := range mfs {
52                 if i > 0 {
53                         w.Write([]byte{','})
54                 }
55                 jm.Marshal(w, mf)
56         }
57         w.Write([]byte{']'})
58 }
59
60 func (m *metrics) ServeHTTP(w http.ResponseWriter, req *http.Request) {
61         switch {
62         case req.Method != "GET" && req.Method != "HEAD":
63                 m.next.ServeHTTP(w, req)
64         case req.URL.Path == "/metrics.json":
65                 m.exportJSON(w, req)
66         case req.URL.Path == "/metrics":
67                 m.exportProm.ServeHTTP(w, req)
68         default:
69                 m.next.ServeHTTP(w, req)
70         }
71 }
72
73 func Instrument(logger *logrus.Logger, next http.Handler) Handler {
74         if logger == nil {
75                 logger = logrus.StandardLogger()
76         }
77         reqDuration := prometheus.NewSummaryVec(prometheus.SummaryOpts{
78                 Name: "request_duration_seconds",
79                 Help: "Summary of request duration.",
80         }, []string{"code", "method"})
81         timeToStatus := prometheus.NewSummaryVec(prometheus.SummaryOpts{
82                 Name: "time_to_status_seconds",
83                 Help: "Summary of request TTFB.",
84         }, []string{"code", "method"})
85         registry := prometheus.NewRegistry()
86         registry.MustRegister(timeToStatus)
87         registry.MustRegister(reqDuration)
88         m := &metrics{
89                 next:         promhttp.InstrumentHandlerDuration(reqDuration, next),
90                 logger:       logger,
91                 registry:     registry,
92                 reqDuration:  reqDuration,
93                 timeToStatus: timeToStatus,
94                 exportProm: promhttp.HandlerFor(registry, promhttp.HandlerOpts{
95                         ErrorLog: logger,
96                 }),
97         }
98         m.logger.AddHook(m)
99         return m
100 }