Skip to content

Commit eee49a5

Browse files
committed
feat: add metrics to backend client
This commit adds two simple prometheus metrics to the http client that is being used by the backend; "requests_total" and "requests_duration_histogram_seconds". With that we should get some initial visibility into backend failures, response times and client requests per seconds as well. I decided to register everything in an `init` function to the `metrics.Gatherer`. Not perfect, but simple and probably good enough for a long time. I got to that `metrics.Gatherer` type by following the metrics-code of `controller-runtime`; I'm not sure if there's a better way to register metrics to that Registry, or if using a different registry would be fine as well and they'd simply get interlaced?... Additionally, controller-runtime also has a [`rest_client_requests_total` metric](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/metrics#pkg-constants) that it registers. As far as I can tell, this is for the default `http.Client` and comes from `client-go`. We could probably also make use of that, but would be missing a latency bucket. That latency bucket also exists, but is [disabled by default](kubernetes-sigs/controller-runtime#1587) because it created a cardinality explosion for some users, so I'm wary to enable it as well. By using a completely separate code-path and metrics-handler, we get metrics for only our backend, instead of them being interlaced with potential metrics from `client-go`. Additionally, we can start off with both latency and count-metrics, as I don't think we'll have issues with cardinality (we're only registering two labels - `client-go` also registered a "url" label which is not optimal).
1 parent 16c89a5 commit eee49a5

File tree

3 files changed

+114
-3
lines changed

3 files changed

+114
-3
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/go-git/go-git/v5 v5.6.0
77
github.com/go-logr/logr v1.2.3
88
github.com/google/go-github/v49 v49.0.0
9+
github.com/prometheus/client_golang v1.14.0
910
github.com/stretchr/testify v1.8.1
1011
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb
1112
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783
@@ -109,7 +110,6 @@ require (
109110
github.com/pjbgf/sha1cd v0.3.0 // indirect
110111
github.com/pkg/errors v0.9.1 // indirect
111112
github.com/pmezard/go-difflib v1.0.0 // indirect
112-
github.com/prometheus/client_golang v1.14.0 // indirect
113113
github.com/prometheus/client_model v0.3.0 // indirect
114114
github.com/prometheus/common v0.37.0 // indirect
115115
github.com/prometheus/procfs v0.8.0 // indirect

internal/backend/backend.go

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,52 @@ import (
2626

2727
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2828
"sigs.k8s.io/controller-runtime/pkg/client"
29+
"sigs.k8s.io/controller-runtime/pkg/metrics"
30+
31+
"github.com/prometheus/client_golang/prometheus"
32+
"github.com/prometheus/client_golang/prometheus/promhttp"
2933

3034
"github.com/snyk/kubernetes-scanner/internal/config"
3135
)
3236

37+
// the default transport automatically honors HTTP_PROXY settings.
38+
// this value is overwritten by init with a roundtripper that has prometheus metrics.
39+
var transport = http.DefaultTransport
40+
41+
func init() {
42+
commonLabels := []string{"code", "method"}
43+
// all supported metrics:
44+
var (
45+
requests = prometheus.NewCounterVec(
46+
prometheus.CounterOpts{
47+
Subsystem: "http_outgoing",
48+
Name: "requests_total",
49+
Help: "A counter for outgoing requests.",
50+
},
51+
commonLabels,
52+
)
53+
54+
durations = prometheus.NewHistogramVec(
55+
prometheus.HistogramOpts{
56+
Subsystem: "http_outgoing",
57+
Name: "request_duration_histogram_seconds",
58+
Help: "Request time duration.",
59+
Buckets: prometheus.DefBuckets,
60+
},
61+
commonLabels,
62+
)
63+
)
64+
65+
metrics.Registry.MustRegister(&httpMetricsCollector{
66+
metrics: []prometheus.Collector{requests, durations},
67+
})
68+
69+
transport = promhttp.InstrumentRoundTripperDuration(
70+
durations,
71+
promhttp.InstrumentRoundTripperCounter(requests, transport),
72+
)
73+
}
74+
3375
type Backend struct {
3476
apiEndpoint string
3577
clusterName string
@@ -45,8 +87,7 @@ func New(clusterName string, cfg *config.Egress) *Backend {
4587
authorizationKey: cfg.SnykServiceAccountToken,
4688

4789
client: &http.Client{
48-
// the default transport automatically honors HTTP_PROXY settings.
49-
Transport: http.DefaultTransport,
90+
Transport: transport,
5091
Timeout: cfg.HTTPClientTimeout.Duration,
5192
},
5293
}
@@ -131,3 +172,57 @@ type resource struct {
131172
ScannedAt metav1.Time `json:"scanned_at"`
132173
DeletedAt *metav1.Time `json:"deleted_at,omitempty"`
133174
}
175+
176+
//func instrumentClient(rt http.RoundTripper, reg prometheus.Registerer) (http.RoundTripper, error) {
177+
//commonLabels := []string{"code", "method"}
178+
//// all supported metrics:
179+
//var (
180+
//requests = prometheus.NewCounterVec(
181+
//prometheus.CounterOpts{
182+
//Subsystem: "http_outgoing",
183+
//Name: "requests_total",
184+
//Help: "A counter for outgoing requests.",
185+
//},
186+
//commonLabels,
187+
//)
188+
189+
//durations = prometheus.NewHistogramVec(
190+
//prometheus.HistogramOpts{
191+
//Subsystem: "http_outgoing",
192+
//Name: "request_duration_histogram_seconds",
193+
//Help: "Request time duration.",
194+
//Buckets: prometheus.DefBuckets,
195+
//},
196+
//commonLabels,
197+
//)
198+
//)
199+
200+
//h := &httpMetricsCollector{
201+
//metrics: []prometheus.Collector{requests, durations},
202+
//}
203+
//// unregister the handler to make sure it's never registered twice.
204+
//_ = reg.Unregister(h)
205+
206+
//return promhttp.InstrumentRoundTripperDuration(
207+
//durations,
208+
//promhttp.InstrumentRoundTripperCounter(requests, rt),
209+
//), reg.Register(h)
210+
//}
211+
212+
type httpMetricsCollector struct {
213+
metrics []prometheus.Collector
214+
}
215+
216+
// Describe implements prometheus.Collector interface.
217+
func (h *httpMetricsCollector) Describe(in chan<- *prometheus.Desc) {
218+
for _, m := range h.metrics {
219+
m.Describe(in)
220+
}
221+
}
222+
223+
// Collect implements prometheus.Collector interface.
224+
func (h *httpMetricsCollector) Collect(in chan<- prometheus.Metric) {
225+
for _, m := range h.metrics {
226+
m.Collect(in)
227+
}
228+
}

internal/backend/backend_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3333
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3434
"k8s.io/apimachinery/pkg/types"
35+
"sigs.k8s.io/controller-runtime/pkg/metrics"
3536

3637
"github.com/snyk/kubernetes-scanner/internal/config"
3738
)
@@ -71,6 +72,21 @@ func TestBackend(t *testing.T) {
7172
err = b.Upsert(ctx, pod, "v1", orgID, &metav1.Time{Time: now().Local()})
7273
require.NoError(t, err)
7374

75+
// some simple checks to make sure that the metrics show up.
76+
metrics, err := metrics.Registry.Gather()
77+
require.NoError(t, err)
78+
79+
var customMetrics int
80+
for _, metric := range metrics {
81+
switch *metric.Name {
82+
case "http_outgoing_requests_total":
83+
require.Equal(t, metric.GetMetric()[0].Counter.GetValue(), 2.0)
84+
customMetrics++
85+
case "http_outgoing_request_duration_histogram_seconds":
86+
customMetrics++
87+
}
88+
}
89+
require.Equal(t, customMetrics, 2)
7490
}
7591

7692
func TestBackendErrorHandling(t *testing.T) {

0 commit comments

Comments
 (0)