1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: Apache-2.0
13 "git.curoverse.com/arvados.git/sdk/go/auth"
14 "git.curoverse.com/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"
21 type Handler interface {
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
33 registry *prometheus.Registry
34 reqDuration *prometheus.SummaryVec
35 timeToStatus *prometheus.SummaryVec
36 exportProm http.Handler
39 func (*metrics) Levels() []logrus.Level {
40 return logrus.AllLevels
43 // Fire implements logrus.Hook in order to collect data points from
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 {
50 m.timeToStatus.WithLabelValues(strconv.Itoa(code), strings.ToLower(method)).Observe(time.Duration(tts).Seconds())
55 func (m *metrics) exportJSON(w http.ResponseWriter, req *http.Request) {
56 jm := jsonpb.Marshaler{Indent: " "}
57 mfs, _ := m.registry.Gather()
59 for i, mf := range mfs {
68 // ServeHTTP implements http.Handler.
69 func (m *metrics) ServeHTTP(w http.ResponseWriter, req *http.Request) {
70 m.next.ServeHTTP(w, req)
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.
77 // If the given token is not empty, that token must be supplied by a
78 // client in order to access the metrics endpoints.
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) {
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)
96 next.ServeHTTP(w, req)
101 // Instrument returns a new Handler that passes requests through to
102 // the next handler in the stack, and tracks metrics of those
105 // For the metrics to be accurate, the caller must ensure every
106 // request passed to the Handler also passes through
107 // LogRequests(logger, ...), and vice versa.
109 // If registry is nil, a new registry is created.
111 // If logger is nil, logrus.StandardLogger() is used.
112 func Instrument(registry *prometheus.Registry, logger *logrus.Logger, next http.Handler) Handler {
114 logger = logrus.StandardLogger()
117 registry = prometheus.NewRegistry()
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)
130 next: promhttp.InstrumentHandlerDuration(reqDuration, next),
133 reqDuration: reqDuration,
134 timeToStatus: timeToStatus,
135 exportProm: promhttp.HandlerFor(registry, promhttp.HandlerOpts{