diff --git a/.golangci.yml b/.golangci.yml index a3aacccb817..f69bf66eaa5 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -22,7 +22,7 @@ linters-settings: gocognit: # lower this after refactoring - min-complexity: 150 + min-complexity: 145 gocyclo: # lower this after refactoring diff --git a/cmd/crowdsec-cli/metrics.go b/cmd/crowdsec-cli/metrics.go index 6b3155e5549..6e23bcf12e4 100644 --- a/cmd/crowdsec-cli/metrics.go +++ b/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() }, } diff --git a/cmd/crowdsec-cli/metrics_table.go b/cmd/crowdsec-cli/metrics_table.go index f11ee11bf88..da6ea3d9f1d 100644 --- a/cmd/crowdsec-cli/metrics_table.go +++ b/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++ }