-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add kube-client metrics to operatorpkg (#55)
- Loading branch information
1 parent
8746151
commit 2457d6a
Showing
1 changed file
with
129 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
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" | ||
) | ||
|
||
// This package adds client-go metrics that can be surfaced through the Prometheus metrics server | ||
// This is based on the reference implementation that was pulled out in controller-runtime in https://github.com/kubernetes-sigs/controller-runtime/pull/2298 | ||
|
||
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, group, version, kind, and subresource.", | ||
Buckets: prometheus.ExponentialBuckets(0.001, 2, 10), | ||
}, []string{"verb", "group", "version", "kind", "subresource"}) | ||
) | ||
|
||
// RegisterClientMetrics sets up the client latency and result 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{ | ||
// If the group index is 0, this is part of the core API, so there's no group | ||
group: lo.Ternary(groupIdx == 0, "", parts[groupIdx]), | ||
version: parts[versionIdx], | ||
kind: parts[kindIdx], | ||
subresource: subresource, | ||
} | ||
} |