-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathprometheus.go
131 lines (115 loc) · 3.48 KB
/
prometheus.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
package middleware
import (
"bytes"
"fmt"
"log/slog"
"net/http"
"strings"
"time"
"github.com/gorilla/mux"
"github.com/jonmol/http-skeleton/util/logging"
"github.com/prometheus/client_golang/prometheus"
)
// TrackedWriter is just adding a bytes counter to http.ResponseWriter
// to be able to
type TrackedWriter struct {
http.ResponseWriter
Bytes int
}
func (t *TrackedWriter) Write(b []byte) (int, error) {
n, err := t.ResponseWriter.Write(b)
t.Bytes += n
return n, err
}
func (t *TrackedWriter) WriteHeader(code int) {
t.ResponseWriter.WriteHeader(code)
var buf bytes.Buffer
err := t.Header().Write(&buf)
if err != nil {
slog.Error("Failed to write response code", logging.Err(err))
}
t.Bytes += buf.Len()
}
// NewPromMiddleware creates a new Prometheus middleware. It excludes routes with {pathVariables} as they
// become "infinitely" many with for instance userID in the path. It can meassure 3 values:
// request count per endpoint (counter)
// response size per endpoint (gauge)
// response time per endpoint (gauge)
// one or two can be disabled, but it will panic if all three are, then you shouldn't use it
func NewPromMiddleware(appname, endpointType string, counted, sized, timed bool, paths []string) mux.MiddlewareFunc {
if !counted && !sized && !timed {
panic("All three measures are off, turn off the middleware instead!")
}
// only instrument paths without path variables
validP := make([]string, 0, len(paths))
for _, p := range paths {
if !strings.Contains(p, "{") {
validP = append(validP, p)
}
}
counter := prometheus.NewCounterVec(prometheus.CounterOpts{
Name: fmt.Sprintf("%s_%s_endpoint_counter", appname, endpointType),
Help: "Counter for the endpoints",
},
[]string{"endpoint"},
)
sizes := prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: fmt.Sprintf("%s_%s_endpoint_sizes", appname, endpointType),
Help: "The size of the responses",
}, []string{"endpoint"})
times := prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: fmt.Sprintf("%s_%s_endpoint_times", appname, endpointType),
Help: "The time it takes to send a response",
}, []string{"endpoint"})
registerMetrics(times, sizes, counter, timed, sized, counted, validP)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
inc(counter, counted, r.URL.Path)
d := time.Now()
tw := &TrackedWriter{ResponseWriter: w}
if sized {
next.ServeHTTP(tw, r)
} else {
next.ServeHTTP(w, r)
}
observe(times, timed, r.URL.Path, time.Since(d))
observe(sizes, sized, r.URL.Path, tw.Bytes)
})
}
}
func inc(counter *prometheus.CounterVec, active bool, path string) {
if active {
if c, err := counter.GetMetricWith(prometheus.Labels{"endpoint": path}); err == nil {
c.Inc()
}
}
}
func observe[V int | time.Duration](h *prometheus.HistogramVec, active bool, path string, val V) {
if active {
if c, err := h.GetMetricWith(prometheus.Labels{"endpoint": path}); err == nil {
c.Observe(float64(val))
}
}
}
func registerMetrics(t, s *prometheus.HistogramVec, c *prometheus.CounterVec, ta, sa, ca bool, paths []string) {
if ca {
prometheus.DefaultRegisterer.MustRegister(c)
}
if sa {
prometheus.DefaultRegisterer.MustRegister(s)
}
if ta {
prometheus.DefaultRegisterer.MustRegister(t)
}
for _, p := range paths {
if ca {
c.With(prometheus.Labels{"endpoint": p})
}
if sa {
s.With(prometheus.Labels{"endpoint": p})
}
if ta {
t.With(prometheus.Labels{"endpoint": p})
}
}
}