|  | package wrapngo | 
|  |  | 
|  | import ( | 
|  | "context" | 
|  | "errors" | 
|  | "fmt" | 
|  | "net/http" | 
|  | "regexp" | 
|  | "strings" | 
|  | "time" | 
|  |  | 
|  | "github.com/prometheus/client_golang/prometheus" | 
|  | "k8s.io/klog/v2" | 
|  | ) | 
|  |  | 
|  | // metricsSet contains all the Prometheus metrics collected by wrapngo. | 
|  | type metricsSet struct { | 
|  | requestLatencies *prometheus.HistogramVec | 
|  | waiting          prometheus.GaugeFunc | 
|  | inFlight         prometheus.GaugeFunc | 
|  | } | 
|  |  | 
|  | func newMetricsSet(ser *serializer) *metricsSet { | 
|  | return &metricsSet{ | 
|  | requestLatencies: prometheus.NewHistogramVec( | 
|  | prometheus.HistogramOpts{ | 
|  | Name: "equinix_api_latency", | 
|  | Help: "Equinix API request latency in seconds, partitioned by endpoint status code", | 
|  | }, | 
|  | []string{"endpoint", "status_code"}, | 
|  | ), | 
|  | waiting: prometheus.NewGaugeFunc( | 
|  | prometheus.GaugeOpts{ | 
|  | Name: "equinix_api_waiting", | 
|  | Help: "Number of API requests pending to be sent to Equinix but waiting on semaphore", | 
|  | }, | 
|  | func() float64 { | 
|  | _, waiting := ser.stats() | 
|  | return float64(waiting) | 
|  | }, | 
|  | ), | 
|  | inFlight: prometheus.NewGaugeFunc( | 
|  | prometheus.GaugeOpts{ | 
|  | Name: "equinix_api_in_flight", | 
|  | Help: "Number of API requests currently being processed by Equinix", | 
|  | }, | 
|  | func() float64 { | 
|  | inFlight, _ := ser.stats() | 
|  | return float64(inFlight) | 
|  | }, | 
|  | ), | 
|  | } | 
|  | } | 
|  |  | 
|  | // getEndpointForPath converts from an Equinix API method and path (eg. | 
|  | // /metal/v1/devices/deadbeef) into an 'endpoint' name, which is an imaginary, | 
|  | // Monogon-specific name for the API endpoint accessed by this call. | 
|  | // | 
|  | // If the given path is unknown and thus cannot be converted to an endpoint name, | 
|  | // 'Unknown' is return and a warning is logged. | 
|  | // | 
|  | // We use this function to partition request statistics per API 'endpoint'. An | 
|  | // alternative to this would be to record high-level packngo function names, but | 
|  | // one packngo function call might actually emit multiple HTTP API requests - so | 
|  | // we're stuck recording the low-level requests and gathering statistics from | 
|  | // there instead. | 
|  | func getEndpointForPath(method, path string) string { | 
|  | path = strings.TrimPrefix(path, "/metal/v1") | 
|  | for name, match := range endpointNames { | 
|  | if match.matches(method, path) { | 
|  | return name | 
|  | } | 
|  | } | 
|  | klog.Warningf("Unknown Equinix API %s %s - cannot determine metric endpoint name", method, path) | 
|  | return "Unknown" | 
|  | } | 
|  |  | 
|  | // requestMatch is used to match a HTTP request method/path. | 
|  | type requestMatch struct { | 
|  | method string | 
|  | regexp *regexp.Regexp | 
|  | } | 
|  |  | 
|  | func (r *requestMatch) matches(method, path string) bool { | 
|  | if r.method != method { | 
|  | return false | 
|  | } | 
|  | return r.regexp.MatchString(path) | 
|  | } | 
|  |  | 
|  | var ( | 
|  | endpointNames = map[string]requestMatch{ | 
|  | "GetDevice":           {"GET", regexp.MustCompile(`^/devices/[^/]+$`)}, | 
|  | "ListDevices":         {"GET", regexp.MustCompile(`^/(organizations|projects)/[^/]+/devices$`)}, | 
|  | "CreateDevice":        {"POST", regexp.MustCompile(`^/projects/[^/]+/devices$`)}, | 
|  | "ListReservations":    {"GET", regexp.MustCompile(`^/projects/[^/]+/hardware-reservations$`)}, | 
|  | "ListSSHKeys":         {"GET", regexp.MustCompile(`^/ssh-keys$`)}, | 
|  | "CreateSSHKey":        {"POST", regexp.MustCompile(`^/project/[^/]+/ssh-keys$`)}, | 
|  | "GetSSHKey":           {"GET", regexp.MustCompile(`^/ssh-keys/[^/]+$`)}, | 
|  | "UpdateSSHKey":        {"PATCH", regexp.MustCompile(`^/ssh-keys/[^/]+$`)}, | 
|  | "PerformDeviceAction": {"POST", regexp.MustCompile(`^/devices/[^/]+/actions$`)}, | 
|  | } | 
|  | ) | 
|  |  | 
|  | // onAPIRequestDone is called by the wrapngo code on every API response from | 
|  | // Equinix, and records the given parameters into metrics. | 
|  | func (m *metricsSet) onAPIRequestDone(req *http.Request, res *http.Response, err error, latency time.Duration) { | 
|  | if m == nil { | 
|  | return | 
|  | } | 
|  |  | 
|  | code := "unknown" | 
|  | if err == nil { | 
|  | code = fmt.Sprintf("%d", res.StatusCode) | 
|  | } else { | 
|  | switch { | 
|  | case errors.Is(err, context.Canceled): | 
|  | code = "ctx canceled" | 
|  | case errors.Is(err, context.DeadlineExceeded): | 
|  | code = "deadline exceeded" | 
|  | } | 
|  | } | 
|  | if code == "unknown" { | 
|  | klog.Warningf("Unexpected HTTP result: req %s %s, error: %v", req.Method, req.URL.Path, res) | 
|  | } | 
|  |  | 
|  | endpoint := getEndpointForPath(req.Method, req.URL.Path) | 
|  | m.requestLatencies.With(prometheus.Labels{"endpoint": endpoint, "status_code": code}).Observe(latency.Seconds()) | 
|  | } |