blob: 0f4cc94bfce52ec3bd327fa14ef7749ad2df3dc8 [file] [log] [blame]
Serge Bazanskidea7cd02023-04-26 13:58:17 +02001package wrapngo
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "net/http"
8 "regexp"
9 "strings"
10 "time"
11
12 "github.com/prometheus/client_golang/prometheus"
13 "k8s.io/klog/v2"
14)
15
16// metricsSet contains all the Prometheus metrics collected by wrapngo.
17type metricsSet struct {
18 requestLatencies *prometheus.HistogramVec
19 waiting prometheus.GaugeFunc
20 inFlight prometheus.GaugeFunc
21}
22
23func newMetricsSet(ser *serializer) *metricsSet {
24 return &metricsSet{
25 requestLatencies: prometheus.NewHistogramVec(
26 prometheus.HistogramOpts{
27 Name: "equinix_api_latency",
28 Help: "Equinix API request latency in seconds, partitioned by endpoint status code",
29 },
30 []string{"endpoint", "status_code"},
31 ),
32 waiting: prometheus.NewGaugeFunc(
33 prometheus.GaugeOpts{
34 Name: "equinix_api_waiting",
35 Help: "Number of API requests pending to be sent to Equinix but waiting on semaphore",
36 },
37 func() float64 {
38 _, waiting := ser.stats()
39 return float64(waiting)
40 },
41 ),
42 inFlight: prometheus.NewGaugeFunc(
43 prometheus.GaugeOpts{
44 Name: "equinix_api_in_flight",
45 Help: "Number of API requests currently being processed by Equinix",
46 },
47 func() float64 {
48 inFlight, _ := ser.stats()
49 return float64(inFlight)
50 },
51 ),
52 }
53}
54
55// getEndpointForPath converts from an Equinix API method and path (eg.
56// /metal/v1/devices/deadbeef) into an 'endpoint' name, which is an imaginary,
57// Monogon-specific name for the API endpoint accessed by this call.
58//
59// If the given path is unknown and thus cannot be converted to an endpoint name,
60// 'Unknown' is return and a warning is logged.
61//
62// We use this function to partition request statistics per API 'endpoint'. An
63// alternative to this would be to record high-level packngo function names, but
64// one packngo function call might actually emit multiple HTTP API requests - so
65// we're stuck recording the low-level requests and gathering statistics from
66// there instead.
67func getEndpointForPath(method, path string) string {
68 path = strings.TrimPrefix(path, "/metal/v1")
69 for name, match := range endpointNames {
70 if match.matches(method, path) {
71 return name
72 }
73 }
74 klog.Warningf("Unknown Equinix API %s %s - cannot determine metric endpoint name", method, path)
75 return "Unknown"
76}
77
78// requestMatch is used to match a HTTP request method/path.
79type requestMatch struct {
80 method string
81 regexp *regexp.Regexp
82}
83
84func (r *requestMatch) matches(method, path string) bool {
85 if r.method != method {
86 return false
87 }
88 return r.regexp.MatchString(path)
89}
90
91var (
92 endpointNames = map[string]requestMatch{
93 "GetDevice": {"GET", regexp.MustCompile(`^/devices/[^/]+$`)},
94 "ListDevices": {"GET", regexp.MustCompile(`^/(organizations|projects)/[^/]+/devices$`)},
95 "CreateDevice": {"POST", regexp.MustCompile(`^/projects/[^/]+/devices$`)},
96 "ListReservations": {"GET", regexp.MustCompile(`^/project/[^/]+/hardware-reservations$`)},
97 "ListSSHKeys": {"GET", regexp.MustCompile(`^/ssh-keys$`)},
98 "CreateSSHKey": {"POST", regexp.MustCompile(`^/project/[^/]+/ssh-keys$`)},
99 "GetSSHKey": {"GET", regexp.MustCompile(`^/ssh-keys/[^/]+$`)},
100 "UpdateSSHKey": {"PATCH", regexp.MustCompile(`^/ssh-keys/[^/]+$`)},
101 "PerformDeviceAction": {"POST", regexp.MustCompile(`^/devices/[^/]+/actions$`)},
102 }
103)
104
105// onAPIRequestDone is called by the wrapngo code on every API response from
106// Equinix, and records the given parameters into metrics.
107func (m *metricsSet) onAPIRequestDone(req *http.Request, res *http.Response, err error, latency time.Duration) {
108 if m == nil {
109 return
110 }
111
112 code := "unknown"
113 if err == nil {
114 code = fmt.Sprintf("%d", res.StatusCode)
115 } else {
116 switch {
117 case errors.Is(err, context.Canceled):
118 code = "ctx canceled"
119 case errors.Is(err, context.DeadlineExceeded):
120 code = "deadline exceeded"
121 }
122 }
123 if code == "unknown" {
124 klog.Warningf("Unexpected HTTP result: req %s %s, error: %v", req.Method, req.URL.Path, res)
125 }
126
127 endpoint := getEndpointForPath(req.Method, req.URL.Path)
128 m.requestLatencies.With(prometheus.Labels{"endpoint": endpoint, "status_code": code}).Observe(latency.Seconds())
129}