Skip to content

Commit b91346f

Browse files
ayrusloresperuri
andauthored
feat/metrics : add new metrics for measuring skipper latency (#3506)
This change involves adding a new metric to measure the overall skipper latency of a request. i.e., the time taken by a request in skipper itself, so this does not include time taken for serving for the response to client and the time taken for the backend round trip. skipper latency = serve duration - response duration - backend duration --------- Signed-off-by: speruri <surya.srikar.peruri@zalando.de> Co-authored-by: speruri <surya.srikar.peruri@zalando.de>
1 parent 1d84ea6 commit b91346f

15 files changed

Lines changed: 585 additions & 6 deletions

config/config.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ type Config struct {
9393
ServeMethodMetric bool `yaml:"serve-method-metric"`
9494
ServeStatusCodeMetric bool `yaml:"serve-status-code-metric"`
9595
BackendHostMetrics bool `yaml:"backend-host-metrics"`
96+
ProxyRequestMetrics bool `yaml:"proxy-request-metrics"`
97+
ProxyResponseMetrics bool `yaml:"proxy-response-metrics"`
9698
AllFiltersMetrics bool `yaml:"all-filters-metrics"`
9799
CombinedResponseMetrics bool `yaml:"combined-response-metrics"`
98100
RouteResponseMetrics bool `yaml:"route-response-metrics"`
@@ -409,6 +411,8 @@ func NewConfig() *Config {
409411
flag.BoolVar(&cfg.ServeMethodMetric, "serve-method-metric", true, "enables the HTTP method as a domain of the total serve time metric. It affects both route and host split metrics")
410412
flag.BoolVar(&cfg.ServeStatusCodeMetric, "serve-status-code-metric", true, "enables the HTTP response status code as a domain of the total serve time metric. It affects both route and host split metrics")
411413
flag.BoolVar(&cfg.BackendHostMetrics, "backend-host-metrics", false, "enables reporting total serve time metrics for each backend")
414+
flag.BoolVar(&cfg.ProxyRequestMetrics, "proxy-request-metrics", false, "enables reporting latency / time spent in handling the request part of the proxy operation i.e., the duration from entry till before the backend round trip")
415+
flag.BoolVar(&cfg.ProxyResponseMetrics, "proxy-response-metrics", false, "enables reporting latency / time spent in handling the reponse part of the proxy operation i.e., the duration from after the backend round trip till the response is served")
412416
flag.BoolVar(&cfg.AllFiltersMetrics, "all-filters-metrics", false, "enables reporting combined filter metrics for each route")
413417
flag.BoolVar(&cfg.CombinedResponseMetrics, "combined-response-metrics", false, "enables reporting combined response time metrics")
414418
flag.BoolVar(&cfg.RouteResponseMetrics, "route-response-metrics", false, "enables reporting response time metrics for each route")
@@ -822,6 +826,8 @@ func (c *Config) ToOptions() skipper.Options {
822826
EnableServeHostCounter: c.ServeHostCounter,
823827
EnableServeMethodMetric: c.ServeMethodMetric,
824828
EnableServeStatusCodeMetric: c.ServeStatusCodeMetric,
829+
EnableProxyRequestMetrics: c.ProxyRequestMetrics,
830+
EnableProxyResponseMetrics: c.ProxyResponseMetrics,
825831
EnableBackendHostMetrics: c.BackendHostMetrics,
826832
EnableAllFiltersMetrics: c.AllFiltersMetrics,
827833
EnableCombinedResponseMetrics: c.CombinedResponseMetrics,

docs/operation/operation.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ common metrics endpoint :9911/metrics.
210210

211211
To monitor skipper we recommend the following queries:
212212

213+
- P99 Proxy latency: `histogram_quantile(0.99, sum(rate(skipper_proxy_total_duration_seconds_bucket{}[1m])) by (le))`
213214
- P99 backend latency: `histogram_quantile(0.99, sum(rate(skipper_serve_host_duration_seconds_bucket{}[1m])) by (le))`
214215
- HTTP 2xx rate: `histogram_quantile(0.99, sum(rate(skipper_serve_host_duration_seconds_bucket{code =~ "2.*"}[1m])) by (le) )`
215216
- HTTP 4xx rate: `histogram_quantile(0.99, sum(rate(skipper_serve_host_duration_seconds_bucket{code =~ "4.*"}[1m])) by (le) )`
@@ -225,6 +226,85 @@ To monitor skipper we recommend the following queries:
225226

226227
You may add static metrics labels like `version` using Prometheus [relabeling feature](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config).
227228

229+
### Proxy Metrics
230+
231+
Skipper Proxy Metrics provides information about the time spent by skipper in processing a request i.e., the time spent by a request inside skipper (this excludes the response application of filters to a req/res, the backend roundtrip and serving the response). The total proxy metrics are enabled by default and these metrics can be used to build KPIs / SLOs, so as to understand and monitor the performance of skipper.
232+
233+
The Proxy Metrics exludes the filter processing as this is dependent on which filters the user decides to use for a particular route. The backend round trip time depends on the backend application and the operation being performed. And the serve response depends on the client. These are operations are not in control of skipper and are hence excluded to solely monitor the performance of Skipper.
234+
235+
These metrics are exposed in /metrics, the example json structure looks like this:
236+
237+
```json
238+
{
239+
"timers" : {
240+
"skipper.proxy.total": {
241+
"15m.rate": 0.2,
242+
"1m.rate": 0.2,
243+
"5m.rate": 0.2,
244+
"75%": 288375,
245+
"95%": 288375,
246+
"99%": 288375,
247+
"99.9%": 288375,
248+
"count": 1,
249+
"max": 288375,
250+
"mean": 288375,
251+
"mean.rate": 0.7268368234069077,
252+
"median": 288375,
253+
"min": 288375,
254+
"stddev": 0
255+
},
256+
}
257+
}
258+
```
259+
260+
The proxy metrics can also be fetched in more detail, i.e., splits the proxy total metrics to get the proxy request metrics and proxy response metrics. The Proxy Request Metrics provides the duration / time taken from the start of ServeHTTP till the backend round trip. The Proxy Response Metrics provides the duration / time taken from after the backend round trip till the response is served.
261+
262+
-proxy-request-metrics
263+
enbales the collection proxy request metrics
264+
-proxy-response-metrics
265+
enables the collection proxy response metrics
266+
267+
If enabled these metrics are also exposed in /metrics, and the example json structure would like the following:
268+
269+
```json
270+
{
271+
"timers": {
272+
"skipper.proxy.request": {
273+
"15m.rate": 0.2,
274+
"1m.rate": 0.2,
275+
"5m.rate": 0.2,
276+
"75%": 0,
277+
"95%": 0,
278+
"99%": 0,
279+
"99.9%": 0,
280+
"count": 1,
281+
"max": 0,
282+
"mean": 0,
283+
"mean.rate": 0.7268396413261223,
284+
"median": 0,
285+
"min": 0,
286+
"stddev": 0
287+
},
288+
"skipper.proxy.response": {
289+
"15m.rate": 0.2,
290+
"1m.rate": 0.2,
291+
"5m.rate": 0.2,
292+
"75%": 288375,
293+
"95%": 288375,
294+
"99%": 288375,
295+
"99.9%": 288375,
296+
"count": 1,
297+
"max": 288375,
298+
"mean": 288375,
299+
"mean.rate": 0.7268397290232465,
300+
"median": 288375,
301+
"min": 288375,
302+
"stddev": 0
303+
},
304+
}
305+
}
306+
```
307+
228308
### Connection metrics
229309

230310
This option will enable known loadbalancer connections metrics, like

metrics/all_kind.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ func (a *All) MeasureResponse(code int, method string, routeId string, start tim
8989
a.codaHale.MeasureResponse(code, method, routeId, start)
9090
}
9191

92+
func (a *All) MeasureProxy(requestDuration, responseDuration time.Duration) {
93+
a.prometheus.MeasureProxy(requestDuration, responseDuration)
94+
a.codaHale.MeasureProxy(requestDuration, responseDuration)
95+
}
96+
9297
func (a *All) MeasureServe(routeId, host, method string, code int, start time.Time) {
9398
a.prometheus.MeasureServe(routeId, host, method, code, start)
9499
a.codaHale.MeasureServe(routeId, host, method, code, start)

metrics/codahale.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ const (
2727
KeyResponse = "response.%d.%s.skipper.%s"
2828
KeyResponseCombined = "all.response.%d.%s.skipper"
2929
Key5xxsBackend = "all.backend.5xx"
30+
KeyProxyTotal = "proxy.total"
31+
KeyProxyRequest = "proxy.request"
32+
KeyProxyResponse = "proxy.response"
3033

3134
KeyErrorsBackend = "errors.backend.%s"
3235
KeyErrorsStreaming = "errors.streaming.%s"
@@ -182,6 +185,17 @@ func (c *CodaHale) MeasureResponse(code int, method string, routeId string, star
182185
}
183186
}
184187

188+
func (c *CodaHale) MeasureProxy(requestDuration, responseDuration time.Duration) {
189+
skipperDuration := requestDuration + responseDuration
190+
c.updateTimer(KeyProxyTotal, skipperDuration)
191+
if c.options.EnableProxyRequestMetrics {
192+
c.updateTimer(KeyProxyRequest, requestDuration)
193+
}
194+
if c.options.EnableProxyResponseMetrics {
195+
c.updateTimer(KeyProxyResponse, responseDuration)
196+
}
197+
}
198+
185199
func (c *CodaHale) MeasureServe(routeId, host, method string, code int, start time.Time) {
186200
if !(c.options.EnableServeRouteMetrics || c.options.EnableServeHostMetrics) {
187201
return

metrics/codahale_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,3 +525,100 @@ func TestCodaHaleServeMetrics(t *testing.T) {
525525
})
526526
}
527527
}
528+
529+
type latencyMetricsTestItem struct {
530+
msg string
531+
requestDuration time.Duration
532+
responseDuration time.Duration
533+
enabledRequest bool
534+
enabledResponse bool
535+
}
536+
537+
func TestCodaHaleProxyLatencyMetrics(t *testing.T) {
538+
if testing.Short() {
539+
t.Skip()
540+
}
541+
542+
for _, ti := range []latencyMetricsTestItem{
543+
{
544+
msg: "request and response disabled",
545+
requestDuration: 100 * time.Millisecond,
546+
responseDuration: 200 * time.Millisecond,
547+
enabledRequest: false,
548+
enabledResponse: false,
549+
},
550+
{
551+
msg: "request enabled, response disabled",
552+
requestDuration: 10 * time.Millisecond,
553+
responseDuration: 20 * time.Millisecond,
554+
enabledRequest: true,
555+
enabledResponse: false,
556+
},
557+
{
558+
msg: "request disabled, response enabled",
559+
requestDuration: 300 * time.Millisecond,
560+
responseDuration: 100 * time.Millisecond,
561+
enabledRequest: false,
562+
enabledResponse: true,
563+
},
564+
{
565+
msg: "request and response enabled",
566+
requestDuration: 150 * time.Millisecond,
567+
responseDuration: 200 * time.Millisecond,
568+
enabledRequest: true,
569+
enabledResponse: true,
570+
},
571+
} {
572+
t.Run(ti.msg, func(t *testing.T) {
573+
checkMetrics := func(m *CodaHale, key string, enabled bool, count int64, duration time.Duration) (bool, string) {
574+
v := m.reg.Get(key)
575+
576+
switch {
577+
case enabled && v == nil:
578+
return false, "metric not found in the registry"
579+
case !enabled && v != nil:
580+
return false, "unexpected metrics"
581+
case !enabled && v == nil:
582+
return true, ""
583+
}
584+
585+
tr, ok := v.(metrics.Timer)
586+
if !ok {
587+
return false, "invalid metric type"
588+
}
589+
590+
trs := tr.Snapshot()
591+
592+
if trs.Count() != count {
593+
return false, fmt.Sprintf("failed to get the right count: %d instead of %d", trs.Count(), count)
594+
}
595+
596+
if trs.Max() > int64(duration) || trs.Min() < int64(duration) {
597+
return false, "failed to get the right duration"
598+
}
599+
600+
return true, ""
601+
}
602+
603+
o := Options{
604+
EnableProxyRequestMetrics: ti.enabledRequest,
605+
EnableProxyResponseMetrics: ti.enabledResponse,
606+
}
607+
c := NewCodaHale(o)
608+
defer c.Close()
609+
610+
c.MeasureProxy(ti.requestDuration, ti.responseDuration)
611+
time.Sleep(10 * time.Millisecond)
612+
613+
if ok, reason := checkMetrics(c, KeyProxyTotal, true, 1, ti.requestDuration+ti.responseDuration); !ok {
614+
t.Errorf("error processing metric '%s': %s", KeyProxyTotal, reason)
615+
}
616+
if ok, reason := checkMetrics(c, KeyProxyRequest, ti.enabledRequest, 1, ti.requestDuration); !ok {
617+
t.Errorf("error processing metric '%s': %s", KeyProxyRequest, reason)
618+
}
619+
if ok, reason := checkMetrics(c, KeyProxyResponse, ti.enabledResponse, 1, ti.responseDuration); !ok {
620+
t.Errorf("error processing metric '%s': %s", KeyProxyResponse, reason)
621+
}
622+
})
623+
}
624+
}

metrics/metrics.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ type Metrics interface {
7070
MeasureFilterResponse(filterName string, start time.Time)
7171
MeasureAllFiltersResponse(routeId string, start time.Time)
7272
MeasureResponse(code int, method string, routeId string, start time.Time)
73+
MeasureProxy(requestDuration, responseDuration time.Duration)
7374
MeasureServe(routeId, host, method string, code int, start time.Time)
7475
IncRoutingFailures()
7576
IncErrorsBackend(routeId string)
@@ -130,6 +131,18 @@ type Options struct {
130131
// both route and host split metrics.
131132
EnableServeStatusCodeMetric bool
132133

134+
// If set, the total request handling time taken by skipper will be
135+
// collected. It measures the duration taken by skipper to process
136+
// the request, from the start excluding the filters processing and
137+
// until the backend round trip is started.
138+
EnableProxyRequestMetrics bool
139+
140+
// If set, the total response handling time take by skipper will be
141+
// collected. It measures the duration taken by skipper to process the
142+
// response, from after the backend round trip is finished, excluding
143+
// the filters processing and until the before the response is served.
144+
EnableProxyResponseMetrics bool
145+
133146
// If set, detailed response time metrics will be collected
134147
// for each backend host
135148
EnableBackendHostMetrics bool

metrics/metricstest/metricsmock.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,22 @@ func (m *MockMetrics) MeasureResponse(code int, method string, routeId string, s
148148
// implement me
149149
}
150150

151+
func (m *MockMetrics) MeasureProxy(requestDuration, responseDuration time.Duration) {
152+
totalDuration := requestDuration + responseDuration
153+
totalDurationKey := fmt.Sprintf("%sproxy.total.duration", m.Prefix)
154+
m.WithMeasures(func(measures map[string][]time.Duration) {
155+
measures[totalDurationKey] = append(measures[totalDurationKey], totalDuration)
156+
})
157+
requestDurationKey := fmt.Sprintf("%sproxy.request.duration", m.Prefix)
158+
m.WithMeasures(func(measures map[string][]time.Duration) {
159+
measures[requestDurationKey] = append(measures[requestDurationKey], requestDuration)
160+
})
161+
responseDurationKey := fmt.Sprintf("%sproxy.response.duration", m.Prefix)
162+
m.WithMeasures(func(measures map[string][]time.Duration) {
163+
measures[responseDurationKey] = append(measures[responseDurationKey], responseDuration)
164+
})
165+
}
166+
151167
func (m *MockMetrics) MeasureServe(routeId, host, method string, code int, start time.Time) {
152168
// implement me
153169
}

0 commit comments

Comments
 (0)