1package search
2
3import (
4 "errors"
5 "net/http"
6 "strconv"
7 "strings"
8 "time"
9
10 "github.com/labstack/echo/v4"
11 "github.com/prometheus/client_golang/prometheus"
12 "github.com/prometheus/client_golang/prometheus/promauto"
13)
14
15var postsReceived = promauto.NewCounter(prometheus.CounterOpts{
16 Name: "search_posts_received",
17 Help: "Number of posts received",
18})
19
20var postsIndexed = promauto.NewCounter(prometheus.CounterOpts{
21 Name: "search_posts_indexed",
22 Help: "Number of posts indexed",
23})
24
25var postsFailed = promauto.NewCounter(prometheus.CounterOpts{
26 Name: "search_posts_failed",
27 Help: "Number of posts that failed indexing",
28})
29
30var postsDeleted = promauto.NewCounter(prometheus.CounterOpts{
31 Name: "search_posts_deleted",
32 Help: "Number of posts deleted",
33})
34
35var profilesReceived = promauto.NewCounter(prometheus.CounterOpts{
36 Name: "search_profiles_received",
37 Help: "Number of profiles received",
38})
39
40var profilesIndexed = promauto.NewCounter(prometheus.CounterOpts{
41 Name: "search_profiles_indexed",
42 Help: "Number of profiles indexed",
43})
44
45var profilesFailed = promauto.NewCounter(prometheus.CounterOpts{
46 Name: "search_profiles_failed",
47 Help: "Number of profiles that failed indexing",
48})
49
50var profilesDeleted = promauto.NewCounter(prometheus.CounterOpts{
51 Name: "search_profiles_deleted",
52 Help: "Number of profiles deleted",
53})
54
55var currentSeq = promauto.NewGauge(prometheus.GaugeOpts{
56 Name: "search_current_seq",
57 Help: "Current sequence number",
58})
59
60var reqSz = promauto.NewHistogramVec(prometheus.HistogramOpts{
61 Name: "http_request_size_bytes",
62 Help: "A histogram of request sizes for requests.",
63 Buckets: prometheus.ExponentialBuckets(100, 10, 8),
64}, []string{"code", "method", "path", "extras"})
65
66var reqDur = promauto.NewHistogramVec(prometheus.HistogramOpts{
67 Name: "http_request_duration_seconds",
68 Help: "A histogram of latencies for requests.",
69 Buckets: prometheus.ExponentialBuckets(0.0001, 2, 18),
70}, []string{"code", "method", "path", "extras"})
71
72var reqCnt = promauto.NewCounterVec(prometheus.CounterOpts{
73 Name: "http_requests_total",
74 Help: "A counter for requests to the wrapped handler.",
75}, []string{"code", "method", "path", "extras"})
76
77var resSz = promauto.NewHistogramVec(prometheus.HistogramOpts{
78 Name: "http_response_size_bytes",
79 Help: "A histogram of response sizes for requests.",
80 Buckets: prometheus.ExponentialBuckets(100, 10, 8),
81}, []string{"code", "method", "path", "extras"})
82
83// MetricsMiddleware defines handler function for metrics middleware
84func MetricsMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
85 return func(c echo.Context) error {
86 path := c.Path()
87 if path == "/metrics" || path == "/_health" {
88 return next(c)
89 }
90
91 start := time.Now()
92 requestSize := computeApproximateRequestSize(c.Request())
93
94 err := next(c)
95
96 status := c.Response().Status
97 if err != nil {
98 var httpError *echo.HTTPError
99 if errors.As(err, &httpError) {
100 status = httpError.Code
101 }
102 if status == 0 || status == http.StatusOK {
103 status = http.StatusInternalServerError
104 }
105 }
106
107 elapsed := float64(time.Since(start)) / float64(time.Second)
108
109 statusStr := strconv.Itoa(status)
110 method := c.Request().Method
111
112 responseSize := float64(c.Response().Size)
113
114 // Custom label for Typeahead search queries
115 typeahead := false
116 if q := strings.TrimSpace(c.QueryParam("typeahead")); q == "true" || q == "1" || q == "y" {
117 typeahead = true
118 }
119
120 labels := []string{statusStr, method, path}
121 if typeahead {
122 labels = append(labels, "typeahead")
123 } else {
124 labels = append(labels, "_none")
125 }
126
127 reqDur.WithLabelValues(labels...).Observe(elapsed)
128 reqCnt.WithLabelValues(labels...).Inc()
129 reqSz.WithLabelValues(labels...).Observe(float64(requestSize))
130 resSz.WithLabelValues(labels...).Observe(responseSize)
131
132 return err
133 }
134}
135
136func computeApproximateRequestSize(r *http.Request) int {
137 s := 0
138 if r.URL != nil {
139 s = len(r.URL.Path)
140 }
141
142 s += len(r.Method)
143 s += len(r.Proto)
144 for name, values := range r.Header {
145 s += len(name)
146 for _, value := range values {
147 s += len(value)
148 }
149 }
150 s += len(r.Host)
151
152 // N.B. r.Form and r.MultipartForm are assumed to be included in r.URL.
153
154 if r.ContentLength != -1 {
155 s += int(r.ContentLength)
156 }
157 return s
158}