From 962ea8ccb2b60ab9bc5e2ffaae067a9b7f1ae559 Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Thu, 27 Jun 2024 21:58:16 -0700 Subject: [PATCH] Add kube-client metrics to operatorpkg --- metrics/metrics.go | 125 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 metrics/metrics.go diff --git a/metrics/metrics.go b/metrics/metrics.go new file mode 100644 index 0000000..4ce1dc7 --- /dev/null +++ b/metrics/metrics.go @@ -0,0 +1,125 @@ +package metrics + +import ( + "context" + "net/url" + "strings" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/samber/lo" + clientmetrics "k8s.io/client-go/tools/metrics" +) + +var ( + requestResult = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "client_go_request_total", + Help: "Number of HTTP requests, partitioned by status code and method.", + }, + []string{"code", "method"}, + ) + requestLatency = prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Name: "client_go_request_duration_seconds", + Help: "Request latency in seconds. Broken down by verb and URL.", + Buckets: prometheus.ExponentialBuckets(0.001, 2, 10), + }, []string{"verb", "group", "version", "kind", "subresource"}) +) + +// RegisterClientMetrics sets up the client latency metrics from client-go. +func RegisterClientMetrics(r prometheus.Registerer) { + // register the metrics with our registry + r.MustRegister(requestResult, requestLatency) + + clientmetrics.RequestLatency = &latencyAdapter{metric: requestLatency} + clientmetrics.RequestResult = &resultAdapter{metric: requestResult} +} + +type resultAdapter struct { + metric *prometheus.CounterVec +} + +func (r *resultAdapter) Increment(_ context.Context, code, method, _ string) { + r.metric.WithLabelValues(code, method).Inc() +} + +// latencyAdapter implements LatencyMetric. +type latencyAdapter struct { + metric *prometheus.HistogramVec +} + +// Observe increments the request latency metric for the given verb/group/version/kind/subresource. +func (l *latencyAdapter) Observe(_ context.Context, verb string, u url.URL, latency time.Duration) { + if data := parsePath(u.Path); data != nil { + // We update the "verb" to better reflect the action being taken by client-go + switch verb { + case "POST": + verb = "CREATE" + case "GET": + if !strings.Contains(u.Path, "{name}") { + verb = "LIST" + } + case "PUT": + if !strings.Contains(u.Path, "{name}") { + verb = "CREATE" + } else { + verb = "UPDATE" + } + } + l.metric.With(prometheus.Labels{ + "verb": verb, + "group": data.group, + "version": data.version, + "kind": data.kind, + "subresource": data.subresource, + }).Observe(latency.Seconds()) + } +} + +// pathData stores data parsed out from the URL path +type pathData struct { + group string + version string + kind string + subresource string +} + +// parsePath parses out the URL called from client-go to return back the group, version, kind, and subresource +// urls are formatted similar to /apis/coordination.k8s.io/v1/namespaces/{namespace}/leases/{name} or /apis/karpenter.sh/v1beta1/nodeclaims/{name} +func parsePath(path string) *pathData { + parts := strings.Split(path, "/")[1:] + + var groupIdx, versionIdx, kindIdx int + switch parts[0] { + case "api": + groupIdx = 0 + case "apis": + groupIdx = 1 + default: + return nil + } + // If the url is too short, then it's not interesting to us + if len(parts) < groupIdx+3 { + return nil + } + // This resource is namespaced + if parts[groupIdx+2] == "namespaces" { + versionIdx = groupIdx + 1 + kindIdx = versionIdx + 3 + } else { + versionIdx = groupIdx + 1 + kindIdx = versionIdx + 1 + } + + // If we have a subresource, it's going to be two indices after the kind + var subresource string + if len(parts) == kindIdx+3 { + subresource = parts[kindIdx+2] + } + return &pathData{ + group: lo.Ternary(groupIdx == 0, "", parts[groupIdx]), + version: parts[versionIdx], + kind: parts[kindIdx], + subresource: subresource, + } +}