浏览代码

refact cscli metric processing (#2816)

* typos
* refact cscli metric processing
* lint
mmetc 1 年之前
父节点
当前提交
af1df0696b
共有 3 个文件被更改,包括 240 次插入164 次删除
  1. 1 1
      .golangci.yml
  2. 48 137
      cmd/crowdsec-cli/metrics.go
  3. 191 26
      cmd/crowdsec-cli/metrics_table.go

+ 1 - 1
.golangci.yml

@@ -22,7 +22,7 @@ linters-settings:
 
   gocognit:
     # lower this after refactoring
-    min-complexity: 150
+    min-complexity: 145
 
   gocyclo:
     # lower this after refactoring

+ 48 - 137
cmd/crowdsec-cli/metrics.go

@@ -2,6 +2,7 @@ package main
 
 import (
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io"
 	"net/http"
@@ -42,8 +43,14 @@ type (
 	}
 )
 
+var (
+	ErrMissingConfig = errors.New("prometheus section missing, can't show metrics")
+	ErrMetricsDisabled = errors.New("prometheus is not enabled, can't show metrics")
+
+)
+
 type metricSection interface {
-	Table(io.Writer, bool, bool)
+	Table(out io.Writer, noUnit bool, showEmpty bool)
 	Description() (string, string)
 }
 
@@ -154,6 +161,9 @@ func (ms metricStore) Fetch(url string) error {
 			origin := metric.Labels["origin"]
 			action := metric.Labels["action"]
 
+			appsecEngine := metric.Labels["appsec_engine"]
+			appsecRule := metric.Labels["rule_name"]
+
 			mtype := metric.Labels["type"]
 
 			fval, err := strconv.ParseFloat(value, 32)
@@ -162,178 +172,78 @@ func (ms metricStore) Fetch(url string) error {
 			}
 
 			ival := int(fval)
+
 			switch fam.Name {
 			//
 			// buckets
 			//
 			case "cs_bucket_created_total":
-				if _, ok := mBucket[name]; !ok {
-					mBucket[name] = make(map[string]int)
-				}
-				mBucket[name]["instantiation"] += ival
+				mBucket.Process(name, "instantiation", ival)
 			case "cs_buckets":
-				if _, ok := mBucket[name]; !ok {
-					mBucket[name] = make(map[string]int)
-				}
-				mBucket[name]["curr_count"] += ival
+				mBucket.Process(name, "curr_count", ival)
 			case "cs_bucket_overflowed_total":
-				if _, ok := mBucket[name]; !ok {
-					mBucket[name] = make(map[string]int)
-				}
-				mBucket[name]["overflow"] += ival
+				mBucket.Process(name, "overflow", ival)
 			case "cs_bucket_poured_total":
-				if _, ok := mBucket[name]; !ok {
-					mBucket[name] = make(map[string]int)
-				}
-				if _, ok := mAcquis[source]; !ok {
-					mAcquis[source] = make(map[string]int)
-				}
-				mBucket[name]["pour"] += ival
-				mAcquis[source]["pour"] += ival
+				mBucket.Process(name, "pour", ival)
+				mAcquis.Process(source, "pour", ival)
 			case "cs_bucket_underflowed_total":
-				if _, ok := mBucket[name]; !ok {
-					mBucket[name] = make(map[string]int)
-				}
-				mBucket[name]["underflow"] += ival
+				mBucket.Process(name, "underflow", ival)
 			//
 			// parsers
 			//
 			case "cs_parser_hits_total":
-				if _, ok := mAcquis[source]; !ok {
-					mAcquis[source] = make(map[string]int)
-				}
-				mAcquis[source]["reads"] += ival
+				mAcquis.Process(source, "reads", ival)
 			case "cs_parser_hits_ok_total":
-				if _, ok := mAcquis[source]; !ok {
-					mAcquis[source] = make(map[string]int)
-				}
-				mAcquis[source]["parsed"] += ival
+				mAcquis.Process(source, "parsed", ival)
 			case "cs_parser_hits_ko_total":
-				if _, ok := mAcquis[source]; !ok {
-					mAcquis[source] = make(map[string]int)
-				}
-				mAcquis[source]["unparsed"] += ival
+				mAcquis.Process(source, "unparsed", ival)
 			case "cs_node_hits_total":
-				if _, ok := mParser[name]; !ok {
-					mParser[name] = make(map[string]int)
-				}
-				mParser[name]["hits"] += ival
+				mParser.Process(name, "hits", ival)
 			case "cs_node_hits_ok_total":
-				if _, ok := mParser[name]; !ok {
-					mParser[name] = make(map[string]int)
-				}
-				mParser[name]["parsed"] += ival
+				mParser.Process(name, "parsed", ival)
 			case "cs_node_hits_ko_total":
-				if _, ok := mParser[name]; !ok {
-					mParser[name] = make(map[string]int)
-				}
-				mParser[name]["unparsed"] += ival
+				mParser.Process(name, "unparsed", ival)
 			//
 			// whitelists
 			//
 			case "cs_node_wl_hits_total":
-				if _, ok := mWhitelist[name]; !ok {
-					mWhitelist[name] = make(map[string]map[string]int)
-				}
-				if _, ok := mWhitelist[name][reason]; !ok {
-					mWhitelist[name][reason] = make(map[string]int)
-				}
-				mWhitelist[name][reason]["hits"] += ival
+				mWhitelist.Process(name, reason, "hits", ival)
 			case "cs_node_wl_hits_ok_total":
-				if _, ok := mWhitelist[name]; !ok {
-					mWhitelist[name] = make(map[string]map[string]int)
-				}
-				if _, ok := mWhitelist[name][reason]; !ok {
-					mWhitelist[name][reason] = make(map[string]int)
-				}
-				mWhitelist[name][reason]["whitelisted"] += ival
+				mWhitelist.Process(name, reason, "whitelisted", ival)
 				// track as well whitelisted lines at acquis level
-				if _, ok := mAcquis[source]; !ok {
-					mAcquis[source] = make(map[string]int)
-				}
-				mAcquis[source]["whitelisted"] += ival
+				mAcquis.Process(source, "whitelisted", ival)
 			//
 			// lapi
 			//
 			case "cs_lapi_route_requests_total":
-				if _, ok := mLapi[route]; !ok {
-					mLapi[route] = make(map[string]int)
-				}
-				mLapi[route][method] += ival
+				mLapi.Process(route, method, ival)
 			case "cs_lapi_machine_requests_total":
-				if _, ok := mLapiMachine[machine]; !ok {
-					mLapiMachine[machine] = make(map[string]map[string]int)
-				}
-				if _, ok := mLapiMachine[machine][route]; !ok {
-					mLapiMachine[machine][route] = make(map[string]int)
-				}
-				mLapiMachine[machine][route][method] += ival
+				mLapiMachine.Process(machine, route, method, ival)
 			case "cs_lapi_bouncer_requests_total":
-				if _, ok := mLapiBouncer[bouncer]; !ok {
-					mLapiBouncer[bouncer] = make(map[string]map[string]int)
-				}
-				if _, ok := mLapiBouncer[bouncer][route]; !ok {
-					mLapiBouncer[bouncer][route] = make(map[string]int)
-				}
-				mLapiBouncer[bouncer][route][method] += ival
+				mLapiBouncer.Process(bouncer, route, method, ival)
 			case "cs_lapi_decisions_ko_total", "cs_lapi_decisions_ok_total":
-				if _, ok := mLapiDecision[bouncer]; !ok {
-					mLapiDecision[bouncer] = struct {
-						NonEmpty int
-						Empty    int
-					}{}
-				}
-				x := mLapiDecision[bouncer]
-				if fam.Name == "cs_lapi_decisions_ko_total" {
-					x.Empty += ival
-				} else if fam.Name == "cs_lapi_decisions_ok_total" {
-					x.NonEmpty += ival
-				}
-				mLapiDecision[bouncer] = x
+				mLapiDecision.Process(bouncer, fam.Name, ival)
 			//
 			// decisions
 			//
 			case "cs_active_decisions":
-				if _, ok := mDecision[reason]; !ok {
-					mDecision[reason] = make(map[string]map[string]int)
-				}
-				if _, ok := mDecision[reason][origin]; !ok {
-					mDecision[reason][origin] = make(map[string]int)
-				}
-				mDecision[reason][origin][action] += ival
+				mDecision.Process(reason, origin, action, ival)
 			case "cs_alerts":
-				mAlert[reason] += ival
+				mAlert.Process(reason, ival)
 			//
 			// stash
 			//
 			case "cs_cache_size":
-				mStash[name] = struct {
-					Type  string
-					Count int
-				}{Type: mtype, Count: ival}
+				mStash.Process(name, mtype, ival)
 			//
 			// appsec
 			//
 			case "cs_appsec_reqs_total":
-				if _, ok := mAppsecEngine[metric.Labels["appsec_engine"]]; !ok {
-					mAppsecEngine[metric.Labels["appsec_engine"]] = make(map[string]int, 0)
-				}
-				mAppsecEngine[metric.Labels["appsec_engine"]]["processed"] = ival
+				mAppsecEngine.Process(appsecEngine, "processed", ival)
 			case "cs_appsec_block_total":
-				if _, ok := mAppsecEngine[metric.Labels["appsec_engine"]]; !ok {
-					mAppsecEngine[metric.Labels["appsec_engine"]] = make(map[string]int, 0)
-				}
-				mAppsecEngine[metric.Labels["appsec_engine"]]["blocked"] = ival
+				mAppsecEngine.Process(appsecEngine, "blocked", ival)
 			case "cs_appsec_rule_hits":
-				appsecEngine := metric.Labels["appsec_engine"]
-				ruleID := metric.Labels["rule_name"]
-				if _, ok := mAppsecRule[appsecEngine]; !ok {
-					mAppsecRule[appsecEngine] = make(map[string]map[string]int, 0)
-				}
-				if _, ok := mAppsecRule[appsecEngine][ruleID]; !ok {
-					mAppsecRule[appsecEngine][ruleID] = make(map[string]int, 0)
-				}
-				mAppsecRule[appsecEngine][ruleID]["triggered"] = ival
+				mAppsecRule.Process(appsecEngine, appsecRule, "triggered", ival)
 			default:
 				log.Debugf("unknown: %+v", fam.Name)
 				continue
@@ -380,13 +290,13 @@ func (ms metricStore) Format(out io.Writer, sections []string, formatType string
 	case "json":
 		x, err := json.MarshalIndent(want, "", " ")
 		if err != nil {
-			return fmt.Errorf("failed to unmarshal metrics : %v", err)
+			return fmt.Errorf("failed to marshal metrics: %w", err)
 		}
 		out.Write(x)
 	case "raw":
 		x, err := yaml.Marshal(want)
 		if err != nil {
-			return fmt.Errorf("failed to unmarshal metrics : %v", err)
+			return fmt.Errorf("failed to marshal metrics: %w", err)
 		}
 		out.Write(x)
 	default:
@@ -404,11 +314,11 @@ func (cli *cliMetrics) show(sections []string, url string, noUnit bool) error {
 	}
 
 	if cfg.Prometheus == nil {
-		return fmt.Errorf("prometheus section missing, can't show metrics")
+		return ErrMissingConfig
 	}
 
 	if !cfg.Prometheus.Enabled {
-		return fmt.Errorf("prometheus is not enabled, can't show metrics")
+		return ErrMetricsDisabled
 	}
 
 	ms := NewMetricStore()
@@ -427,6 +337,7 @@ func (cli *cliMetrics) show(sections []string, url string, noUnit bool) error {
 	if err := ms.Format(color.Output, sections, cfg.Cscli.Output, noUnit); err != nil {
 		return err
 	}
+
 	return nil
 }
 
@@ -468,6 +379,7 @@ cscli metrics list`,
 // expandAlias returns a list of sections. The input can be a list of sections or alias.
 func (cli *cliMetrics) expandSectionGroups(args []string) []string {
 	ret := []string{}
+
 	for _, section := range args {
 		switch section {
 		case "engine":
@@ -522,8 +434,8 @@ cscli metrics show acquisition parsers buckets stash -o json`,
 
 func (cli *cliMetrics) list() error {
 	type metricType struct {
-		Type        string `json:"type" yaml:"type"`
-		Title       string `json:"title" yaml:"title"`
+		Type        string `json:"type"        yaml:"type"`
+		Title       string `json:"title"       yaml:"title"`
 		Description string `json:"description" yaml:"description"`
 	}
 
@@ -553,13 +465,13 @@ func (cli *cliMetrics) list() error {
 	case "json":
 		x, err := json.MarshalIndent(allMetrics, "", " ")
 		if err != nil {
-			return fmt.Errorf("failed to unmarshal metrics: %w", err)
+			return fmt.Errorf("failed to marshal metric types: %w", err)
 		}
 		fmt.Println(string(x))
 	case "raw":
 		x, err := yaml.Marshal(allMetrics)
 		if err != nil {
-			return fmt.Errorf("failed to unmarshal metrics: %w", err)
+			return fmt.Errorf("failed to marshal metric types: %w", err)
 		}
 		fmt.Println(string(x))
 	}
@@ -575,8 +487,7 @@ func (cli *cliMetrics) newListCmd() *cobra.Command {
 		Args:              cobra.ExactArgs(0),
 		DisableAutoGenTag: true,
 		RunE: func(_ *cobra.Command, _ []string) error {
-			cli.list()
-			return nil
+			return cli.list()
 		},
 	}
 

+ 191 - 26
cmd/crowdsec-cli/metrics_table.go

@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"io"
 	"sort"
+	"strconv"
 
 	"github.com/aquasecurity/table"
 	log "github.com/sirupsen/logrus"
@@ -11,17 +12,21 @@ import (
 	"github.com/crowdsecurity/go-cs-lib/maptools"
 )
 
+// ErrNilTable means a nil pointer was passed instead of a table instance. This is a programming error.
+var ErrNilTable = fmt.Errorf("nil table")
+
 func lapiMetricsToTable(t *table.Table, stats map[string]map[string]map[string]int) int {
 	// stats: machine -> route -> method -> count
-
 	// sort keys to keep consistent order when printing
 	machineKeys := []string{}
 	for k := range stats {
 		machineKeys = append(machineKeys, k)
 	}
+
 	sort.Strings(machineKeys)
 
 	numRows := 0
+
 	for _, machine := range machineKeys {
 		// oneRow: route -> method -> count
 		machineRow := stats[machine]
@@ -33,53 +38,60 @@ func lapiMetricsToTable(t *table.Table, stats map[string]map[string]map[string]i
 					methodName,
 				}
 				if count != 0 {
-					row = append(row, fmt.Sprintf("%d", count))
+					row = append(row, strconv.Itoa(count))
 				} else {
 					row = append(row, "-")
 				}
+
 				t.AddRow(row...)
 				numRows++
 			}
 		}
 	}
+
 	return numRows
 }
 
 func wlMetricsToTable(t *table.Table, stats map[string]map[string]map[string]int, noUnit bool) (int, error) {
 	if t == nil {
-		return 0, fmt.Errorf("nil table")
+		return 0, ErrNilTable
 	}
 
 	numRows := 0
 
 	for _, name := range maptools.SortedKeys(stats) {
 		for _, reason := range maptools.SortedKeys(stats[name]) {
-			row := make([]string, 4)
-			row[0] = name
-			row[1] = reason
-			row[2] = "-"
-			row[3] = "-"
+			row := []string{
+				name,
+				reason,
+				"-",
+				"-",
+			}
 
 			for _, action := range maptools.SortedKeys(stats[name][reason]) {
 				value := stats[name][reason][action]
-				if action == "whitelisted" {
-					row[3] = fmt.Sprintf("%d", value)
-				} else if action == "hits" {
-					row[2] = fmt.Sprintf("%d", value)
-				} else {
+
+				switch action {
+				case "whitelisted":
+					row[3] = strconv.Itoa(value)
+				case "hits":
+					row[2] = strconv.Itoa(value)
+				default:
 					log.Debugf("unexpected counter '%s' for whitelists = %d", action, value)
 				}
 			}
+
 			t.AddRow(row...)
 			numRows++
 		}
 	}
+
 	return numRows, nil
 }
 
 func metricsToTable(t *table.Table, stats map[string]map[string]int, keys []string, noUnit bool) (int, error) {
 	if t == nil {
-		return 0, fmt.Errorf("nil table")
+		return 0, ErrNilTable
 	}
 
 	numRows := 0
@@ -89,12 +101,14 @@ func metricsToTable(t *table.Table, stats map[string]map[string]int, keys []stri
 		if !ok {
 			continue
 		}
+
 		row := []string{
 			alabel,
 		}
+
 		for _, sl := range keys {
 			if v, ok := astats[sl]; ok && v != 0 {
-				numberToShow := fmt.Sprintf("%d", v)
+				numberToShow := strconv.Itoa(v)
 				if !noUnit {
 					numberToShow = formatNumber(v)
 				}
@@ -104,15 +118,26 @@ func metricsToTable(t *table.Table, stats map[string]map[string]int, keys []stri
 				row = append(row, "-")
 			}
 		}
+
 		t.AddRow(row...)
 		numRows++
 	}
+
 	return numRows, nil
 }
 
 func (s statBucket) Description() (string, string) {
 	return "Bucket Metrics",
-		`Measure events in different scenarios. Current count is the number of buckets during metrics collection. Overflows are past event-producing buckets, while Expired are the ones that didn’t receive enough events to Overflow.`
+		`Measure events in different scenarios. Current count is the number of buckets during metrics collection. ` +
+			`Overflows are past event-producing buckets, while Expired are the ones that didn’t receive enough events to Overflow.`
+}
+
+func (s statBucket) Process(bucket, metric string, val int) {
+	if _, ok := s[bucket]; !ok {
+		s[bucket] = make(map[string]int)
+	}
+
+	s[bucket][metric] += val
 }
 
 func (s statBucket) Table(out io.Writer, noUnit bool, showEmpty bool) {
@@ -134,7 +159,18 @@ func (s statBucket) Table(out io.Writer, noUnit bool, showEmpty bool) {
 
 func (s statAcquis) Description() (string, string) {
 	return "Acquisition Metrics",
-		`Measures the lines read, parsed, and unparsed per datasource. Zero read lines indicate a misconfigured or inactive datasource. Zero parsed lines mean the parser(s) failed. Non-zero parsed lines are fine as crowdsec selects relevant lines.`
+		`Measures the lines read, parsed, and unparsed per datasource. ` +
+			`Zero read lines indicate a misconfigured or inactive datasource. ` +
+			`Zero parsed lines mean the parser(s) failed. ` +
+			`Non-zero parsed lines are fine as crowdsec selects relevant lines.`
+}
+
+func (s statAcquis) Process(source, metric string, val int) {
+	if _, ok := s[source]; !ok {
+		s[source] = make(map[string]int)
+	}
+
+	s[source][metric] += val
 }
 
 func (s statAcquis) Table(out io.Writer, noUnit bool, showEmpty bool) {
@@ -159,12 +195,22 @@ func (s statAppsecEngine) Description() (string, string) {
 		`Measures the number of parsed and blocked requests by the AppSec Component.`
 }
 
+func (s statAppsecEngine) Process(appsecEngine, metric string, val int) {
+	if _, ok := s[appsecEngine]; !ok {
+		s[appsecEngine] = make(map[string]int)
+	}
+
+	s[appsecEngine][metric] += val
+}
+
 func (s statAppsecEngine) Table(out io.Writer, noUnit bool, showEmpty bool) {
 	t := newTable(out)
 	t.SetRowLines(false)
 	t.SetHeaders("Appsec Engine", "Processed", "Blocked")
 	t.SetAlignment(table.AlignLeft, table.AlignLeft)
+
 	keys := []string{"processed", "blocked"}
+
 	if numRows, err := metricsToTable(t, s, keys, noUnit); err != nil {
 		log.Warningf("while collecting appsec stats: %s", err)
 	} else if numRows > 0 || showEmpty {
@@ -179,13 +225,27 @@ func (s statAppsecRule) Description() (string, string) {
 		`Provides “per AppSec Component” information about the number of matches for loaded AppSec Rules.`
 }
 
+func (s statAppsecRule) Process(appsecEngine, appsecRule string, metric string, val int) {
+	if _, ok := s[appsecEngine]; !ok {
+		s[appsecEngine] = make(map[string]map[string]int)
+	}
+
+	if _, ok := s[appsecEngine][appsecRule]; !ok {
+		s[appsecEngine][appsecRule] = make(map[string]int)
+	}
+
+	s[appsecEngine][appsecRule][metric] += val
+}
+
 func (s statAppsecRule) Table(out io.Writer, noUnit bool, showEmpty bool) {
 	for appsecEngine, appsecEngineRulesStats := range s {
 		t := newTable(out)
 		t.SetRowLines(false)
 		t.SetHeaders("Rule ID", "Triggered")
 		t.SetAlignment(table.AlignLeft, table.AlignLeft)
+
 		keys := []string{"triggered"}
+
 		if numRows, err := metricsToTable(t, appsecEngineRulesStats, keys, noUnit); err != nil {
 			log.Warningf("while collecting appsec rules stats: %s", err)
 		} else if numRows > 0 || showEmpty {
@@ -193,7 +253,6 @@ func (s statAppsecRule) Table(out io.Writer, noUnit bool, showEmpty bool) {
 			t.Render()
 		}
 	}
-
 }
 
 func (s statWhitelist) Description() (string, string) {
@@ -201,6 +260,18 @@ func (s statWhitelist) Description() (string, string) {
 		`Tracks the number of events processed and possibly whitelisted by each parser whitelist.`
 }
 
+func (s statWhitelist) Process(whitelist, reason, metric string, val int) {
+	if _, ok := s[whitelist]; !ok {
+		s[whitelist] = make(map[string]map[string]int)
+	}
+
+	if _, ok := s[whitelist][reason]; !ok {
+		s[whitelist][reason] = make(map[string]int)
+	}
+
+	s[whitelist][reason][metric] += val
+}
+
 func (s statWhitelist) Table(out io.Writer, noUnit bool, showEmpty bool) {
 	t := newTable(out)
 	t.SetRowLines(false)
@@ -218,7 +289,17 @@ func (s statWhitelist) Table(out io.Writer, noUnit bool, showEmpty bool) {
 
 func (s statParser) Description() (string, string) {
 	return "Parser Metrics",
-		`Tracks the number of events processed by each parser and indicates success of failure. Zero parsed lines means the parer(s) failed. Non-zero unparsed lines are fine as crowdsec select relevant lines.`
+		`Tracks the number of events processed by each parser and indicates success of failure. ` +
+			`Zero parsed lines means the parer(s) failed. ` +
+			`Non-zero unparsed lines are fine as crowdsec select relevant lines.`
+}
+
+func (s statParser) Process(parser, metric string, val int) {
+	if _, ok := s[parser]; !ok {
+		s[parser] = make(map[string]int)
+	}
+
+	s[parser][metric] += val
 }
 
 func (s statParser) Table(out io.Writer, noUnit bool, showEmpty bool) {
@@ -243,6 +324,16 @@ func (s statStash) Description() (string, string) {
 		`Tracks the status of stashes that might be created by various parsers and scenarios.`
 }
 
+func (s statStash) Process(name, mtype string, val int) {
+	s[name] = struct {
+		Type  string
+		Count int
+	}{
+		Type:  mtype,
+		Count: val,
+	}
+}
+
 func (s statStash) Table(out io.Writer, noUnit bool, showEmpty bool) {
 	t := newTable(out)
 	t.SetRowLines(false)
@@ -258,11 +349,12 @@ func (s statStash) Table(out io.Writer, noUnit bool, showEmpty bool) {
 		row := []string{
 			alabel,
 			astats.Type,
-			fmt.Sprintf("%d", astats.Count),
+			strconv.Itoa(astats.Count),
 		}
 		t.AddRow(row...)
 		numRows++
 	}
+
 	if numRows > 0 || showEmpty {
 		title, _ := s.Description()
 		renderTableTitle(out, "\n"+title+":")
@@ -275,6 +367,14 @@ func (s statLapi) Description() (string, string) {
 		`Monitors the requests made to local API routes.`
 }
 
+func (s statLapi) Process(route, method string, val int) {
+	if _, ok := s[route]; !ok {
+		s[route] = make(map[string]int)
+	}
+
+	s[route][method] += val
+}
+
 func (s statLapi) Table(out io.Writer, noUnit bool, showEmpty bool) {
 	t := newTable(out)
 	t.SetRowLines(false)
@@ -291,13 +391,14 @@ func (s statLapi) Table(out io.Writer, noUnit bool, showEmpty bool) {
 		for skey := range astats {
 			subKeys = append(subKeys, skey)
 		}
+
 		sort.Strings(subKeys)
 
 		for _, sl := range subKeys {
 			row := []string{
 				alabel,
 				sl,
-				fmt.Sprintf("%d", astats[sl]),
+				strconv.Itoa(astats[sl]),
 			}
 			t.AddRow(row...)
 			numRows++
@@ -316,6 +417,18 @@ func (s statLapiMachine) Description() (string, string) {
 		`Tracks the number of calls to the local API from each registered machine.`
 }
 
+func (s statLapiMachine) Process(machine, route, method string, val int) {
+	if _, ok := s[machine]; !ok {
+		s[machine] = make(map[string]map[string]int)
+	}
+
+	if _, ok := s[machine][route]; !ok {
+		s[machine][route] = make(map[string]int)
+	}
+
+	s[machine][route][method] += val
+}
+
 func (s statLapiMachine) Table(out io.Writer, noUnit bool, showEmpty bool) {
 	t := newTable(out)
 	t.SetRowLines(false)
@@ -336,6 +449,18 @@ func (s statLapiBouncer) Description() (string, string) {
 		`Tracks total hits to remediation component related API routes.`
 }
 
+func (s statLapiBouncer) Process(bouncer, route, method string, val int) {
+	if _, ok := s[bouncer]; !ok {
+		s[bouncer] = make(map[string]map[string]int)
+	}
+
+	if _, ok := s[bouncer][route]; !ok {
+		s[bouncer][route] = make(map[string]int)
+	}
+
+	s[bouncer][route][method] += val
+}
+
 func (s statLapiBouncer) Table(out io.Writer, noUnit bool, showEmpty bool) {
 	t := newTable(out)
 	t.SetRowLines(false)
@@ -356,6 +481,26 @@ func (s statLapiDecision) Description() (string, string) {
 		`Tracks the number of empty/non-empty answers from LAPI to bouncers that are working in "live" mode.`
 }
 
+func (s statLapiDecision) Process(bouncer, fam string, val int) {
+	if _, ok := s[bouncer]; !ok {
+		s[bouncer] = struct {
+			NonEmpty int
+			Empty    int
+		}{}
+	}
+
+	x := s[bouncer]
+
+	switch fam {
+	case "cs_lapi_decisions_ko_total":
+		x.Empty += val
+	case "cs_lapi_decisions_ok_total":
+		x.NonEmpty += val
+	}
+
+	s[bouncer] = x
+}
+
 func (s statLapiDecision) Table(out io.Writer, noUnit bool, showEmpty bool) {
 	t := newTable(out)
 	t.SetRowLines(false)
@@ -363,11 +508,12 @@ func (s statLapiDecision) Table(out io.Writer, noUnit bool, showEmpty bool) {
 	t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft)
 
 	numRows := 0
+
 	for bouncer, hits := range s {
 		t.AddRow(
 			bouncer,
-			fmt.Sprintf("%d", hits.Empty),
-			fmt.Sprintf("%d", hits.NonEmpty),
+			strconv.Itoa(hits.Empty),
+			strconv.Itoa(hits.NonEmpty),
 		)
 		numRows++
 	}
@@ -381,7 +527,20 @@ func (s statLapiDecision) Table(out io.Writer, noUnit bool, showEmpty bool) {
 
 func (s statDecision) Description() (string, string) {
 	return "Local API Decisions",
-		`Provides information about all currently active decisions. Includes both local (crowdsec) and global decisions (CAPI), and lists subscriptions (lists).`
+		`Provides information about all currently active decisions. ` +
+			`Includes both local (crowdsec) and global decisions (CAPI), and lists subscriptions (lists).`
+}
+
+func (s statDecision) Process(reason, origin, action string, val int) {
+	if _, ok := s[reason]; !ok {
+		s[reason] = make(map[string]map[string]int)
+	}
+
+	if _, ok := s[reason][origin]; !ok {
+		s[reason][origin] = make(map[string]int)
+	}
+
+	s[reason][origin][action] += val
 }
 
 func (s statDecision) Table(out io.Writer, noUnit bool, showEmpty bool) {
@@ -391,6 +550,7 @@ func (s statDecision) Table(out io.Writer, noUnit bool, showEmpty bool) {
 	t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft)
 
 	numRows := 0
+
 	for reason, origins := range s {
 		for origin, actions := range origins {
 			for action, hits := range actions {
@@ -398,7 +558,7 @@ func (s statDecision) Table(out io.Writer, noUnit bool, showEmpty bool) {
 					reason,
 					origin,
 					action,
-					fmt.Sprintf("%d", hits),
+					strconv.Itoa(hits),
 				)
 				numRows++
 			}
@@ -417,6 +577,10 @@ func (s statAlert) Description() (string, string) {
 		`Tracks the total number of past and present alerts for the installed scenarios.`
 }
 
+func (s statAlert) Process(reason string, val int) {
+	s[reason] += val
+}
+
 func (s statAlert) Table(out io.Writer, noUnit bool, showEmpty bool) {
 	t := newTable(out)
 	t.SetRowLines(false)
@@ -424,10 +588,11 @@ func (s statAlert) Table(out io.Writer, noUnit bool, showEmpty bool) {
 	t.SetAlignment(table.AlignLeft, table.AlignLeft)
 
 	numRows := 0
+
 	for scenario, hits := range s {
 		t.AddRow(
 			scenario,
-			fmt.Sprintf("%d", hits),
+			strconv.Itoa(hits),
 		)
 		numRows++
 	}