1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: Apache-2.0
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"
20 type Handler interface {
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
32 registry *prometheus.Registry
33 reqDuration *prometheus.SummaryVec
34 timeToStatus *prometheus.SummaryVec
35 exportProm http.Handler
38 func (*metrics) Levels() []logrus.Level {
39 return logrus.AllLevels
42 // Fire implements logrus.Hook in order to collect data points from
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 {
49 m.timeToStatus.WithLabelValues(strconv.Itoa(code), strings.ToLower(method)).Observe(time.Duration(tts).Seconds())
54 func (m *metrics) exportJSON(w http.ResponseWriter, req *http.Request) {
55 jm := jsonpb.Marshaler{Indent: " "}
56 mfs, _ := m.registry.Gather()
58 for i, mf := range mfs {
67 // ServeHTTP implements http.Handler.
68 func (m *metrics) ServeHTTP(w http.ResponseWriter, req *http.Request) {
69 m.next.ServeHTTP(w, req)
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.
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) {
83 case req.Method != "GET" && req.Method != "HEAD":
84 next.ServeHTTP(w, req)
85 case req.URL.Path == "/metrics.json":
87 case req.URL.Path == "/metrics":
88 m.exportProm.ServeHTTP(w, req)
90 next.ServeHTTP(w, req)
95 // Instrument returns a new Handler that passes requests through to
96 // the next handler in the stack, and tracks metrics of those
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 func Instrument(logger *logrus.Logger, next http.Handler) Handler {
104 logger = logrus.StandardLogger()
106 reqDuration := prometheus.NewSummaryVec(prometheus.SummaryOpts{
107 Name: "request_duration_seconds",
108 Help: "Summary of request duration.",
109 }, []string{"code", "method"})
110 timeToStatus := prometheus.NewSummaryVec(prometheus.SummaryOpts{
111 Name: "time_to_status_seconds",
112 Help: "Summary of request TTFB.",
113 }, []string{"code", "method"})
114 registry := prometheus.NewRegistry()
115 registry.MustRegister(timeToStatus)
116 registry.MustRegister(reqDuration)
118 next: promhttp.InstrumentHandlerDuration(reqDuration, next),
121 reqDuration: reqDuration,
122 timeToStatus: timeToStatus,
123 exportProm: promhttp.HandlerFor(registry, promhttp.HandlerOpts{