Skip to content

Commit f006419

Browse files
committed
metrics: set to 0 when routes are fixed
- Modify UpdateInvalidRoute to set previously seen metrics to 0 when not present in current counts - Refactor processRouteDefs to return reasonCounts instead of calling metrics internally - Update receiveRouteMatcher to aggregate all validation errors and call UpdateInvalidRoute once Signed-off-by: Veronika Volokitina <v.volokitinaa@gmail.com>
1 parent e6d3f0c commit f006419

6 files changed

Lines changed: 242 additions & 22 deletions

File tree

metrics/codahale.go

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,20 +44,23 @@ const (
4444

4545
// CodaHale is the CodaHale format backend, implements Metrics interface in DropWizard's CodaHale metrics format.
4646
type CodaHale struct {
47-
reg metrics.Registry
48-
createTimer func() metrics.Timer
49-
createCounter func() metrics.Counter
50-
createGauge func() metrics.GaugeFloat64
51-
options Options
52-
handler http.Handler
53-
quit chan struct{}
47+
reg metrics.Registry
48+
createTimer func() metrics.Timer
49+
createCounter func() metrics.Counter
50+
createGauge func() metrics.GaugeFloat64
51+
options Options
52+
handler http.Handler
53+
quit chan struct{}
54+
invalidRouteReasons map[string]bool
5455
}
5556

5657
// NewCodaHale returns a new CodaHale backend of metrics.
5758
func NewCodaHale(o Options) *CodaHale {
5859
o = applyCompatibilityDefaults(o)
5960

60-
c := &CodaHale{}
61+
c := &CodaHale{
62+
invalidRouteReasons: make(map[string]bool),
63+
}
6164

6265
c.quit = make(chan struct{})
6366
c.reg = metrics.NewRegistry()
@@ -266,8 +269,17 @@ func (c *CodaHale) IncErrorsStreaming(routeId string) {
266269
}
267270

268271
func (c *CodaHale) UpdateInvalidRoute(reasonCounts map[string]int) {
272+
// Set current counts
269273
for reason, count := range reasonCounts {
270274
c.UpdateGauge(fmt.Sprintf(KeyInvalidRoutes, reason), float64(count))
275+
c.invalidRouteReasons[reason] = true
276+
}
277+
278+
// Set previously seen reasons to 0 if they're not in current counts
279+
for reason := range c.invalidRouteReasons {
280+
if _, exists := reasonCounts[reason]; !exists {
281+
c.UpdateGauge(fmt.Sprintf(KeyInvalidRoutes, reason), 0)
282+
}
271283
}
272284
}
273285

metrics/codahale_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,3 +622,103 @@ func TestCodaHaleProxyLatencyMetrics(t *testing.T) {
622622
})
623623
}
624624
}
625+
626+
func TestCodaHaleRouteValidationMetricsRealRoutingBehavior(t *testing.T) {
627+
c := NewCodaHale(Options{})
628+
defer c.Close()
629+
630+
getGaugeValue := func(reason string) float64 {
631+
key := fmt.Sprintf(KeyInvalidRoutes, reason)
632+
gauge := c.getGauge(key)
633+
return gauge.Value()
634+
}
635+
636+
// Phase 1: Initial state - no metrics present
637+
t.Log("Phase 1: Initial state - no metrics should have values")
638+
639+
// Phase 2: Routes with validation errors (simulating receiveRouteMatcher)
640+
t.Log("Phase 2: Routes with validation errors")
641+
reasonCounts2 := map[string]int{
642+
"unknown_filter": 2,
643+
"invalid_matcher": 1,
644+
}
645+
c.UpdateInvalidRoute(reasonCounts2)
646+
647+
if got := getGaugeValue("unknown_filter"); got != 2 {
648+
t.Errorf("Expected unknown_filter gauge to be 2, got %f", got)
649+
}
650+
if got := getGaugeValue("invalid_matcher"); got != 1 {
651+
t.Errorf("Expected invalid_matcher gauge to be 1, got %f", got)
652+
}
653+
654+
// Phase 3: Routes are fixed - receiveRouteMatcher calls UpdateInvalidRoute with fresh empty/nil map
655+
t.Log("Phase 3: Routes are fixed - empty reasonCounts map")
656+
reasonCounts3 := map[string]int{}
657+
c.UpdateInvalidRoute(reasonCounts3)
658+
659+
if got := getGaugeValue("unknown_filter"); got != 0 {
660+
t.Errorf("Expected unknown_filter gauge to be 0 after routes fixed, got %f", got)
661+
}
662+
if got := getGaugeValue("invalid_matcher"); got != 0 {
663+
t.Errorf("Expected invalid_matcher gauge to be 0 after routes fixed, got %f", got)
664+
}
665+
666+
t.Log("Key insight: CodaHale metrics are now properly set to 0 when routes are fixed")
667+
t.Log("Previously seen invalid routes are tracked and reset to 0 when not present in new counts")
668+
}
669+
670+
func TestCodaHaleRouteValidationMetricsLifecycleBehavior(t *testing.T) {
671+
c := NewCodaHale(Options{})
672+
defer c.Close()
673+
674+
getGaugeValue := func(reason string) float64 {
675+
key := fmt.Sprintf(KeyInvalidRoutes, reason)
676+
gauge := c.getGauge(key)
677+
return gauge.Value()
678+
}
679+
680+
t.Log("Scenario 1: Initial route errors")
681+
c.UpdateInvalidRoute(map[string]int{
682+
"unknown_filter": 3,
683+
"invalid_matcher": 2,
684+
})
685+
686+
if got := getGaugeValue("unknown_filter"); got != 3 {
687+
t.Errorf("Expected unknown_filter gauge to be 3, got %f", got)
688+
}
689+
if got := getGaugeValue("invalid_matcher"); got != 2 {
690+
t.Errorf("Expected invalid_matcher gauge to be 2, got %f", got)
691+
}
692+
693+
t.Log("Scenario 2: Some routes fixed, some errors increase, new errors appear")
694+
c.UpdateInvalidRoute(map[string]int{
695+
"unknown_filter": 5,
696+
"invalid_predicate": 1,
697+
})
698+
699+
if got := getGaugeValue("unknown_filter"); got != 5 {
700+
t.Errorf("Expected unknown_filter gauge to be 5, got %f", got)
701+
}
702+
if got := getGaugeValue("invalid_predicate"); got != 1 {
703+
t.Errorf("Expected invalid_predicate gauge to be 1, got %f", got)
704+
}
705+
if got := getGaugeValue("invalid_matcher"); got != 0 {
706+
t.Errorf("Expected invalid_matcher gauge to be 0 after being fixed, got %f", got)
707+
}
708+
709+
t.Log("Scenario 3: All routes become valid")
710+
c.UpdateInvalidRoute(map[string]int{})
711+
712+
if got := getGaugeValue("unknown_filter"); got != 0 {
713+
t.Errorf("Expected unknown_filter gauge to be 0, got %f", got)
714+
}
715+
if got := getGaugeValue("invalid_predicate"); got != 0 {
716+
t.Errorf("Expected invalid_predicate gauge to be 0, got %f", got)
717+
}
718+
if got := getGaugeValue("invalid_matcher"); got != 0 {
719+
t.Errorf("Expected invalid_matcher gauge to be 0, got %f", got)
720+
}
721+
722+
t.Log("Answer: When routes are fixed, CodaHale metrics are properly set to 0")
723+
t.Log("This happens because UpdateInvalidRoute now tracks previously seen reasons and resets them")
724+
}

metrics/prometheus.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ type Prometheus struct {
7272
customCounterM *prometheus.CounterVec
7373
customGaugeM *prometheus.GaugeVec
7474
invalidRouteM *prometheus.GaugeVec
75+
invalidRouteReasons map[string]bool
7576

7677
opts Options
7778
registry *prometheus.Registry
@@ -83,8 +84,9 @@ func NewPrometheus(opts Options) *Prometheus {
8384
opts = applyCompatibilityDefaults(opts)
8485

8586
p := &Prometheus{
86-
registry: opts.PrometheusRegistry,
87-
opts: opts,
87+
registry: opts.PrometheusRegistry,
88+
opts: opts,
89+
invalidRouteReasons: make(map[string]bool),
8890
}
8991

9092
if p.registry == nil {
@@ -527,6 +529,14 @@ func (p *Prometheus) IncErrorsStreaming(routeID string) {
527529
func (p *Prometheus) UpdateInvalidRoute(reasonCounts map[string]int) {
528530
for reason, count := range reasonCounts {
529531
p.invalidRouteM.WithLabelValues(reason).Set(float64(count))
532+
p.invalidRouteReasons[reason] = true
533+
}
534+
535+
// Set previously seen reasons to 0 if they're not in current counts
536+
for reason := range p.invalidRouteReasons {
537+
if _, exists := reasonCounts[reason]; !exists {
538+
p.invalidRouteM.WithLabelValues(reason).Set(0)
539+
}
530540
}
531541
}
532542

metrics/prometheus_test.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1302,3 +1302,104 @@ func TestPrometheusMetricsStartTimestamp(t *testing.T) {
13021302
checkMetric(`skipper_serve_host_count{code="201",host="bar_test",method="POST",start="(\d+)"} 2`)
13031303
checkMetric(`skipper_route_error_total{start="(\d+)"} 3`)
13041304
}
1305+
1306+
func TestRouteValidationMetricsRealRoutingBehavior(t *testing.T) {
1307+
pm := metrics.NewPrometheus(metrics.Options{})
1308+
path := "/metrics"
1309+
1310+
mux := http.NewServeMux()
1311+
pm.RegisterHandler(path, mux)
1312+
1313+
getMetricsOutput := func() string {
1314+
req := httptest.NewRequest("GET", path, nil)
1315+
w := httptest.NewRecorder()
1316+
mux.ServeHTTP(w, req)
1317+
1318+
resp := w.Result()
1319+
require.Equal(t, http.StatusOK, resp.StatusCode)
1320+
1321+
body, err := io.ReadAll(resp.Body)
1322+
require.NoError(t, err)
1323+
return string(body)
1324+
}
1325+
1326+
t.Log("Phase 1: Initial state - no metrics present")
1327+
output1 := getMetricsOutput()
1328+
assert.NotContains(t, output1, `skipper_route_invalid{reason="unknown_filter"}`)
1329+
assert.NotContains(t, output1, `skipper_route_invalid{reason="invalid_matcher"}`)
1330+
1331+
t.Log("Phase 2: Routes with validation errors (simulating receiveRouteMatcher)")
1332+
reasonCounts2 := map[string]int{
1333+
"unknown_filter": 2,
1334+
"invalid_matcher": 1,
1335+
}
1336+
pm.UpdateInvalidRoute(reasonCounts2)
1337+
1338+
output2 := getMetricsOutput()
1339+
assert.Contains(t, output2, `skipper_route_invalid{reason="unknown_filter"} 2`)
1340+
assert.Contains(t, output2, `skipper_route_invalid{reason="invalid_matcher"} 1`)
1341+
1342+
t.Log("Phase 3: Routes are fixed - receiveRouteMatcher calls UpdateInvalidRoute with fresh empty/nil map")
1343+
reasonCounts3 := map[string]int{}
1344+
pm.UpdateInvalidRoute(reasonCounts3)
1345+
1346+
output3 := getMetricsOutput()
1347+
assert.Contains(t, output3, `skipper_route_invalid{reason="unknown_filter"} 0`)
1348+
assert.Contains(t, output3, `skipper_route_invalid{reason="invalid_matcher"} 0`)
1349+
1350+
t.Log("Key insight: Metrics are now properly set to 0 when routes are fixed")
1351+
t.Log("Previously seen invalid routes are tracked and reset to 0 when not present in new counts")
1352+
}
1353+
1354+
func TestRouteValidationMetricsLifecycleBehavior(t *testing.T) {
1355+
pm := metrics.NewPrometheus(metrics.Options{})
1356+
path := "/metrics"
1357+
1358+
mux := http.NewServeMux()
1359+
pm.RegisterHandler(path, mux)
1360+
1361+
getMetricsOutput := func() string {
1362+
req := httptest.NewRequest("GET", path, nil)
1363+
w := httptest.NewRecorder()
1364+
mux.ServeHTTP(w, req)
1365+
1366+
resp := w.Result()
1367+
require.Equal(t, http.StatusOK, resp.StatusCode)
1368+
1369+
body, err := io.ReadAll(resp.Body)
1370+
require.NoError(t, err)
1371+
return string(body)
1372+
}
1373+
1374+
t.Log("Scenario 1: Initial route errors")
1375+
pm.UpdateInvalidRoute(map[string]int{
1376+
"unknown_filter": 3,
1377+
"invalid_matcher": 2,
1378+
})
1379+
1380+
output1 := getMetricsOutput()
1381+
assert.Contains(t, output1, `skipper_route_invalid{reason="unknown_filter"} 3`)
1382+
assert.Contains(t, output1, `skipper_route_invalid{reason="invalid_matcher"} 2`)
1383+
1384+
t.Log("Scenario 2: Some routes fixed, some errors increase, new errors appear")
1385+
pm.UpdateInvalidRoute(map[string]int{
1386+
"unknown_filter": 5,
1387+
"invalid_predicate": 1,
1388+
})
1389+
1390+
output2 := getMetricsOutput()
1391+
assert.Contains(t, output2, `skipper_route_invalid{reason="unknown_filter"} 5`)
1392+
assert.Contains(t, output2, `skipper_route_invalid{reason="invalid_predicate"} 1`)
1393+
assert.Contains(t, output2, `skipper_route_invalid{reason="invalid_matcher"} 0`)
1394+
1395+
t.Log("Scenario 3: All routes become valid")
1396+
pm.UpdateInvalidRoute(map[string]int{})
1397+
1398+
output3 := getMetricsOutput()
1399+
assert.Contains(t, output3, `skipper_route_invalid{reason="unknown_filter"} 0`)
1400+
assert.Contains(t, output3, `skipper_route_invalid{reason="invalid_predicate"} 0`)
1401+
assert.Contains(t, output3, `skipper_route_invalid{reason="invalid_matcher"} 0`)
1402+
1403+
t.Log("Answer: When routes are fixed, metrics are properly set to 0")
1404+
t.Log("This happens because UpdateInvalidRoute now tracks previously seen reasons and resets them")
1405+
}

routing/datasource.go

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -524,10 +524,8 @@ func mapPredicates(cps []PredicateSpec) map[string]PredicateSpec {
524524
}
525525

526526
// processes a set of route definitions for the routing table
527-
func processRouteDefs(o *Options, defs []*eskip.Route) (routes []*Route, invalidDefs []*eskip.Route) {
527+
func processRouteDefs(o *Options, defs []*eskip.Route) (routes []*Route, invalidDefs []*eskip.Route, reasonCounts map[string]int) {
528528
cpm := mapPredicates(o.Predicates)
529-
reasonCounts := make(map[string]int)
530-
531529
for _, def := range defs {
532530
route, err := processRouteDef(o, cpm, def)
533531
if err == nil {
@@ -545,11 +543,6 @@ func processRouteDefs(o *Options, defs []*eskip.Route) (routes []*Route, invalid
545543
reasonCounts[reason]++
546544
}
547545
}
548-
549-
if o.Metrics != nil {
550-
o.Metrics.UpdateInvalidRoute(reasonCounts)
551-
}
552-
553546
return
554547
}
555548

@@ -602,7 +595,7 @@ func receiveRouteMatcher(o Options, out chan<- *routeTable, quit <-chan struct{}
602595
defs = o.PreProcessors[i].Do(defs)
603596
}
604597

605-
routes, invalidRoutes := processRouteDefs(&o, defs)
598+
routes, invalidRoutes, reasonCounts := processRouteDefs(&o, defs)
606599

607600
for i := range o.PostProcessors {
608601
routes = o.PostProcessors[i].Do(routes)
@@ -618,8 +611,12 @@ func receiveRouteMatcher(o Options, out chan<- *routeTable, quit <-chan struct{}
618611
invalidRouteIds[err.ID] = struct{}{}
619612
}
620613

614+
if len(errs) > 0 {
615+
reasonCounts[errInvalidMatcher.Code()] = len(errs)
616+
}
617+
621618
if o.Metrics != nil {
622-
o.Metrics.UpdateInvalidRoute(map[string]int{errInvalidMatcher.Code(): len(errs)})
619+
o.Metrics.UpdateInvalidRoute(reasonCounts)
623620
}
624621

625622
for i := range routes {

routing/matcher_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ func docToRoutes(doc string) ([]*Route, error) {
101101
if err != nil {
102102
return nil, err
103103
}
104-
routes, _ := processRouteDefs(&Options{Predicates: []PredicateSpec{&truePredicate{}}}, defs)
104+
routes, _, _ := processRouteDefs(&Options{Predicates: []PredicateSpec{&truePredicate{}}}, defs)
105105
return routes, nil
106106
}
107107

@@ -193,7 +193,7 @@ func generateRoutes(paths []string) []*Route {
193193
defs[i] = &eskip.Route{Id: fmt.Sprintf("route%d", i), Path: p, Backend: p}
194194
}
195195

196-
routes, _ := processRouteDefs(&Options{}, defs)
196+
routes, _, _ := processRouteDefs(&Options{}, defs)
197197
return routes
198198
}
199199

0 commit comments

Comments
 (0)