github.com/crowdsecurity/crowdsec@v1.6.1/cmd/crowdsec-cli/metrics_table.go (about)

     1  package main
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"sort"
     8  	"strconv"
     9  
    10  	"github.com/aquasecurity/table"
    11  	log "github.com/sirupsen/logrus"
    12  
    13  	"github.com/crowdsecurity/go-cs-lib/maptools"
    14  )
    15  
    16  // ErrNilTable means a nil pointer was passed instead of a table instance. This is a programming error.
    17  var ErrNilTable = errors.New("nil table")
    18  
    19  func lapiMetricsToTable(t *table.Table, stats map[string]map[string]map[string]int) int {
    20  	// stats: machine -> route -> method -> count
    21  	// sort keys to keep consistent order when printing
    22  	machineKeys := []string{}
    23  	for k := range stats {
    24  		machineKeys = append(machineKeys, k)
    25  	}
    26  
    27  	sort.Strings(machineKeys)
    28  
    29  	numRows := 0
    30  
    31  	for _, machine := range machineKeys {
    32  		// oneRow: route -> method -> count
    33  		machineRow := stats[machine]
    34  		for routeName, route := range machineRow {
    35  			for methodName, count := range route {
    36  				row := []string{
    37  					machine,
    38  					routeName,
    39  					methodName,
    40  				}
    41  				if count != 0 {
    42  					row = append(row, strconv.Itoa(count))
    43  				} else {
    44  					row = append(row, "-")
    45  				}
    46  
    47  				t.AddRow(row...)
    48  
    49  				numRows++
    50  			}
    51  		}
    52  	}
    53  
    54  	return numRows
    55  }
    56  
    57  func wlMetricsToTable(t *table.Table, stats map[string]map[string]map[string]int, noUnit bool) (int, error) {
    58  	if t == nil {
    59  		return 0, ErrNilTable
    60  	}
    61  
    62  	numRows := 0
    63  
    64  	for _, name := range maptools.SortedKeys(stats) {
    65  		for _, reason := range maptools.SortedKeys(stats[name]) {
    66  			row := []string{
    67  				name,
    68  				reason,
    69  				"-",
    70  				"-",
    71  			}
    72  
    73  			for _, action := range maptools.SortedKeys(stats[name][reason]) {
    74  				value := stats[name][reason][action]
    75  
    76  				switch action {
    77  				case "whitelisted":
    78  					row[3] = strconv.Itoa(value)
    79  				case "hits":
    80  					row[2] = strconv.Itoa(value)
    81  				default:
    82  					log.Debugf("unexpected counter '%s' for whitelists = %d", action, value)
    83  				}
    84  			}
    85  
    86  			t.AddRow(row...)
    87  
    88  			numRows++
    89  		}
    90  	}
    91  
    92  	return numRows, nil
    93  }
    94  
    95  func metricsToTable(t *table.Table, stats map[string]map[string]int, keys []string, noUnit bool) (int, error) {
    96  	if t == nil {
    97  		return 0, ErrNilTable
    98  	}
    99  
   100  	numRows := 0
   101  
   102  	for _, alabel := range maptools.SortedKeys(stats) {
   103  		astats, ok := stats[alabel]
   104  		if !ok {
   105  			continue
   106  		}
   107  
   108  		row := []string{
   109  			alabel,
   110  		}
   111  
   112  		for _, sl := range keys {
   113  			if v, ok := astats[sl]; ok && v != 0 {
   114  				numberToShow := strconv.Itoa(v)
   115  				if !noUnit {
   116  					numberToShow = formatNumber(v)
   117  				}
   118  
   119  				row = append(row, numberToShow)
   120  			} else {
   121  				row = append(row, "-")
   122  			}
   123  		}
   124  
   125  		t.AddRow(row...)
   126  
   127  		numRows++
   128  	}
   129  
   130  	return numRows, nil
   131  }
   132  
   133  func (s statBucket) Description() (string, string) {
   134  	return "Scenario Metrics",
   135  		`Measure events in different scenarios. Current count is the number of buckets during metrics collection. ` +
   136  			`Overflows are past event-producing buckets, while Expired are the ones that didn’t receive enough events to Overflow.`
   137  }
   138  
   139  func (s statBucket) Process(bucket, metric string, val int) {
   140  	if _, ok := s[bucket]; !ok {
   141  		s[bucket] = make(map[string]int)
   142  	}
   143  
   144  	s[bucket][metric] += val
   145  }
   146  
   147  func (s statBucket) Table(out io.Writer, noUnit bool, showEmpty bool) {
   148  	t := newTable(out)
   149  	t.SetRowLines(false)
   150  	t.SetHeaders("Scenario", "Current Count", "Overflows", "Instantiated", "Poured", "Expired")
   151  	t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft)
   152  
   153  	keys := []string{"curr_count", "overflow", "instantiation", "pour", "underflow"}
   154  
   155  	if numRows, err := metricsToTable(t, s, keys, noUnit); err != nil {
   156  		log.Warningf("while collecting scenario stats: %s", err)
   157  	} else if numRows > 0 || showEmpty {
   158  		title, _ := s.Description()
   159  		renderTableTitle(out, "\n"+title+":")
   160  		t.Render()
   161  	}
   162  }
   163  
   164  func (s statAcquis) Description() (string, string) {
   165  	return "Acquisition Metrics",
   166  		`Measures the lines read, parsed, and unparsed per datasource. ` +
   167  			`Zero read lines indicate a misconfigured or inactive datasource. ` +
   168  			`Zero parsed lines mean the parser(s) failed. ` +
   169  			`Non-zero parsed lines are fine as crowdsec selects relevant lines.`
   170  }
   171  
   172  func (s statAcquis) Process(source, metric string, val int) {
   173  	if _, ok := s[source]; !ok {
   174  		s[source] = make(map[string]int)
   175  	}
   176  
   177  	s[source][metric] += val
   178  }
   179  
   180  func (s statAcquis) Table(out io.Writer, noUnit bool, showEmpty bool) {
   181  	t := newTable(out)
   182  	t.SetRowLines(false)
   183  	t.SetHeaders("Source", "Lines read", "Lines parsed", "Lines unparsed", "Lines poured to bucket", "Lines whitelisted")
   184  	t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft)
   185  
   186  	keys := []string{"reads", "parsed", "unparsed", "pour", "whitelisted"}
   187  
   188  	if numRows, err := metricsToTable(t, s, keys, noUnit); err != nil {
   189  		log.Warningf("while collecting acquis stats: %s", err)
   190  	} else if numRows > 0 || showEmpty {
   191  		title, _ := s.Description()
   192  		renderTableTitle(out, "\n"+title+":")
   193  		t.Render()
   194  	}
   195  }
   196  
   197  func (s statAppsecEngine) Description() (string, string) {
   198  	return "Appsec Metrics",
   199  		`Measures the number of parsed and blocked requests by the AppSec Component.`
   200  }
   201  
   202  func (s statAppsecEngine) Process(appsecEngine, metric string, val int) {
   203  	if _, ok := s[appsecEngine]; !ok {
   204  		s[appsecEngine] = make(map[string]int)
   205  	}
   206  
   207  	s[appsecEngine][metric] += val
   208  }
   209  
   210  func (s statAppsecEngine) Table(out io.Writer, noUnit bool, showEmpty bool) {
   211  	t := newTable(out)
   212  	t.SetRowLines(false)
   213  	t.SetHeaders("Appsec Engine", "Processed", "Blocked")
   214  	t.SetAlignment(table.AlignLeft, table.AlignLeft)
   215  
   216  	keys := []string{"processed", "blocked"}
   217  
   218  	if numRows, err := metricsToTable(t, s, keys, noUnit); err != nil {
   219  		log.Warningf("while collecting appsec stats: %s", err)
   220  	} else if numRows > 0 || showEmpty {
   221  		title, _ := s.Description()
   222  		renderTableTitle(out, "\n"+title+":")
   223  		t.Render()
   224  	}
   225  }
   226  
   227  func (s statAppsecRule) Description() (string, string) {
   228  	return "Appsec Rule Metrics",
   229  		`Provides “per AppSec Component” information about the number of matches for loaded AppSec Rules.`
   230  }
   231  
   232  func (s statAppsecRule) Process(appsecEngine, appsecRule string, metric string, val int) {
   233  	if _, ok := s[appsecEngine]; !ok {
   234  		s[appsecEngine] = make(map[string]map[string]int)
   235  	}
   236  
   237  	if _, ok := s[appsecEngine][appsecRule]; !ok {
   238  		s[appsecEngine][appsecRule] = make(map[string]int)
   239  	}
   240  
   241  	s[appsecEngine][appsecRule][metric] += val
   242  }
   243  
   244  func (s statAppsecRule) Table(out io.Writer, noUnit bool, showEmpty bool) {
   245  	for appsecEngine, appsecEngineRulesStats := range s {
   246  		t := newTable(out)
   247  		t.SetRowLines(false)
   248  		t.SetHeaders("Rule ID", "Triggered")
   249  		t.SetAlignment(table.AlignLeft, table.AlignLeft)
   250  
   251  		keys := []string{"triggered"}
   252  
   253  		if numRows, err := metricsToTable(t, appsecEngineRulesStats, keys, noUnit); err != nil {
   254  			log.Warningf("while collecting appsec rules stats: %s", err)
   255  		} else if numRows > 0 || showEmpty {
   256  			renderTableTitle(out, fmt.Sprintf("\nAppsec '%s' Rules Metrics:", appsecEngine))
   257  			t.Render()
   258  		}
   259  	}
   260  }
   261  
   262  func (s statWhitelist) Description() (string, string) {
   263  	return "Whitelist Metrics",
   264  		`Tracks the number of events processed and possibly whitelisted by each parser whitelist.`
   265  }
   266  
   267  func (s statWhitelist) Process(whitelist, reason, metric string, val int) {
   268  	if _, ok := s[whitelist]; !ok {
   269  		s[whitelist] = make(map[string]map[string]int)
   270  	}
   271  
   272  	if _, ok := s[whitelist][reason]; !ok {
   273  		s[whitelist][reason] = make(map[string]int)
   274  	}
   275  
   276  	s[whitelist][reason][metric] += val
   277  }
   278  
   279  func (s statWhitelist) Table(out io.Writer, noUnit bool, showEmpty bool) {
   280  	t := newTable(out)
   281  	t.SetRowLines(false)
   282  	t.SetHeaders("Whitelist", "Reason", "Hits", "Whitelisted")
   283  	t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft)
   284  
   285  	if numRows, err := wlMetricsToTable(t, s, noUnit); err != nil {
   286  		log.Warningf("while collecting parsers stats: %s", err)
   287  	} else if numRows > 0 || showEmpty {
   288  		title, _ := s.Description()
   289  		renderTableTitle(out, "\n"+title+":")
   290  		t.Render()
   291  	}
   292  }
   293  
   294  func (s statParser) Description() (string, string) {
   295  	return "Parser Metrics",
   296  		`Tracks the number of events processed by each parser and indicates success of failure. ` +
   297  			`Zero parsed lines means the parer(s) failed. ` +
   298  			`Non-zero unparsed lines are fine as crowdsec select relevant lines.`
   299  }
   300  
   301  func (s statParser) Process(parser, metric string, val int) {
   302  	if _, ok := s[parser]; !ok {
   303  		s[parser] = make(map[string]int)
   304  	}
   305  
   306  	s[parser][metric] += val
   307  }
   308  
   309  func (s statParser) Table(out io.Writer, noUnit bool, showEmpty bool) {
   310  	t := newTable(out)
   311  	t.SetRowLines(false)
   312  	t.SetHeaders("Parsers", "Hits", "Parsed", "Unparsed")
   313  	t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft)
   314  
   315  	keys := []string{"hits", "parsed", "unparsed"}
   316  
   317  	if numRows, err := metricsToTable(t, s, keys, noUnit); err != nil {
   318  		log.Warningf("while collecting parsers stats: %s", err)
   319  	} else if numRows > 0 || showEmpty {
   320  		title, _ := s.Description()
   321  		renderTableTitle(out, "\n"+title+":")
   322  		t.Render()
   323  	}
   324  }
   325  
   326  func (s statStash) Description() (string, string) {
   327  	return "Parser Stash Metrics",
   328  		`Tracks the status of stashes that might be created by various parsers and scenarios.`
   329  }
   330  
   331  func (s statStash) Process(name, mtype string, val int) {
   332  	s[name] = struct {
   333  		Type  string
   334  		Count int
   335  	}{
   336  		Type:  mtype,
   337  		Count: val,
   338  	}
   339  }
   340  
   341  func (s statStash) Table(out io.Writer, noUnit bool, showEmpty bool) {
   342  	t := newTable(out)
   343  	t.SetRowLines(false)
   344  	t.SetHeaders("Name", "Type", "Items")
   345  	t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft)
   346  
   347  	// unfortunately, we can't reuse metricsToTable as the structure is too different :/
   348  	numRows := 0
   349  
   350  	for _, alabel := range maptools.SortedKeys(s) {
   351  		astats := s[alabel]
   352  
   353  		row := []string{
   354  			alabel,
   355  			astats.Type,
   356  			strconv.Itoa(astats.Count),
   357  		}
   358  		t.AddRow(row...)
   359  
   360  		numRows++
   361  	}
   362  
   363  	if numRows > 0 || showEmpty {
   364  		title, _ := s.Description()
   365  		renderTableTitle(out, "\n"+title+":")
   366  		t.Render()
   367  	}
   368  }
   369  
   370  func (s statLapi) Description() (string, string) {
   371  	return "Local API Metrics",
   372  		`Monitors the requests made to local API routes.`
   373  }
   374  
   375  func (s statLapi) Process(route, method string, val int) {
   376  	if _, ok := s[route]; !ok {
   377  		s[route] = make(map[string]int)
   378  	}
   379  
   380  	s[route][method] += val
   381  }
   382  
   383  func (s statLapi) Table(out io.Writer, noUnit bool, showEmpty bool) {
   384  	t := newTable(out)
   385  	t.SetRowLines(false)
   386  	t.SetHeaders("Route", "Method", "Hits")
   387  	t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft)
   388  
   389  	// unfortunately, we can't reuse metricsToTable as the structure is too different :/
   390  	numRows := 0
   391  
   392  	for _, alabel := range maptools.SortedKeys(s) {
   393  		astats := s[alabel]
   394  
   395  		subKeys := []string{}
   396  		for skey := range astats {
   397  			subKeys = append(subKeys, skey)
   398  		}
   399  
   400  		sort.Strings(subKeys)
   401  
   402  		for _, sl := range subKeys {
   403  			row := []string{
   404  				alabel,
   405  				sl,
   406  				strconv.Itoa(astats[sl]),
   407  			}
   408  
   409  			t.AddRow(row...)
   410  
   411  			numRows++
   412  		}
   413  	}
   414  
   415  	if numRows > 0 || showEmpty {
   416  		title, _ := s.Description()
   417  		renderTableTitle(out, "\n"+title+":")
   418  		t.Render()
   419  	}
   420  }
   421  
   422  func (s statLapiMachine) Description() (string, string) {
   423  	return "Local API Machines Metrics",
   424  		`Tracks the number of calls to the local API from each registered machine.`
   425  }
   426  
   427  func (s statLapiMachine) Process(machine, route, method string, val int) {
   428  	if _, ok := s[machine]; !ok {
   429  		s[machine] = make(map[string]map[string]int)
   430  	}
   431  
   432  	if _, ok := s[machine][route]; !ok {
   433  		s[machine][route] = make(map[string]int)
   434  	}
   435  
   436  	s[machine][route][method] += val
   437  }
   438  
   439  func (s statLapiMachine) Table(out io.Writer, noUnit bool, showEmpty bool) {
   440  	t := newTable(out)
   441  	t.SetRowLines(false)
   442  	t.SetHeaders("Machine", "Route", "Method", "Hits")
   443  	t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft)
   444  
   445  	numRows := lapiMetricsToTable(t, s)
   446  
   447  	if numRows > 0 || showEmpty {
   448  		title, _ := s.Description()
   449  		renderTableTitle(out, "\n"+title+":")
   450  		t.Render()
   451  	}
   452  }
   453  
   454  func (s statLapiBouncer) Description() (string, string) {
   455  	return "Local API Bouncers Metrics",
   456  		`Tracks total hits to remediation component related API routes.`
   457  }
   458  
   459  func (s statLapiBouncer) Process(bouncer, route, method string, val int) {
   460  	if _, ok := s[bouncer]; !ok {
   461  		s[bouncer] = make(map[string]map[string]int)
   462  	}
   463  
   464  	if _, ok := s[bouncer][route]; !ok {
   465  		s[bouncer][route] = make(map[string]int)
   466  	}
   467  
   468  	s[bouncer][route][method] += val
   469  }
   470  
   471  func (s statLapiBouncer) Table(out io.Writer, noUnit bool, showEmpty bool) {
   472  	t := newTable(out)
   473  	t.SetRowLines(false)
   474  	t.SetHeaders("Bouncer", "Route", "Method", "Hits")
   475  	t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft)
   476  
   477  	numRows := lapiMetricsToTable(t, s)
   478  
   479  	if numRows > 0 || showEmpty {
   480  		title, _ := s.Description()
   481  		renderTableTitle(out, "\n"+title+":")
   482  		t.Render()
   483  	}
   484  }
   485  
   486  func (s statLapiDecision) Description() (string, string) {
   487  	return "Local API Bouncers Decisions",
   488  		`Tracks the number of empty/non-empty answers from LAPI to bouncers that are working in "live" mode.`
   489  }
   490  
   491  func (s statLapiDecision) Process(bouncer, fam string, val int) {
   492  	if _, ok := s[bouncer]; !ok {
   493  		s[bouncer] = struct {
   494  			NonEmpty int
   495  			Empty    int
   496  		}{}
   497  	}
   498  
   499  	x := s[bouncer]
   500  
   501  	switch fam {
   502  	case "cs_lapi_decisions_ko_total":
   503  		x.Empty += val
   504  	case "cs_lapi_decisions_ok_total":
   505  		x.NonEmpty += val
   506  	}
   507  
   508  	s[bouncer] = x
   509  }
   510  
   511  func (s statLapiDecision) Table(out io.Writer, noUnit bool, showEmpty bool) {
   512  	t := newTable(out)
   513  	t.SetRowLines(false)
   514  	t.SetHeaders("Bouncer", "Empty answers", "Non-empty answers")
   515  	t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft)
   516  
   517  	numRows := 0
   518  
   519  	for bouncer, hits := range s {
   520  		t.AddRow(
   521  			bouncer,
   522  			strconv.Itoa(hits.Empty),
   523  			strconv.Itoa(hits.NonEmpty),
   524  		)
   525  
   526  		numRows++
   527  	}
   528  
   529  	if numRows > 0 || showEmpty {
   530  		title, _ := s.Description()
   531  		renderTableTitle(out, "\n"+title+":")
   532  		t.Render()
   533  	}
   534  }
   535  
   536  func (s statDecision) Description() (string, string) {
   537  	return "Local API Decisions",
   538  		`Provides information about all currently active decisions. ` +
   539  			`Includes both local (crowdsec) and global decisions (CAPI), and lists subscriptions (lists).`
   540  }
   541  
   542  func (s statDecision) Process(reason, origin, action string, val int) {
   543  	if _, ok := s[reason]; !ok {
   544  		s[reason] = make(map[string]map[string]int)
   545  	}
   546  
   547  	if _, ok := s[reason][origin]; !ok {
   548  		s[reason][origin] = make(map[string]int)
   549  	}
   550  
   551  	s[reason][origin][action] += val
   552  }
   553  
   554  func (s statDecision) Table(out io.Writer, noUnit bool, showEmpty bool) {
   555  	t := newTable(out)
   556  	t.SetRowLines(false)
   557  	t.SetHeaders("Reason", "Origin", "Action", "Count")
   558  	t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft)
   559  
   560  	numRows := 0
   561  
   562  	for reason, origins := range s {
   563  		for origin, actions := range origins {
   564  			for action, hits := range actions {
   565  				t.AddRow(
   566  					reason,
   567  					origin,
   568  					action,
   569  					strconv.Itoa(hits),
   570  				)
   571  
   572  				numRows++
   573  			}
   574  		}
   575  	}
   576  
   577  	if numRows > 0 || showEmpty {
   578  		title, _ := s.Description()
   579  		renderTableTitle(out, "\n"+title+":")
   580  		t.Render()
   581  	}
   582  }
   583  
   584  func (s statAlert) Description() (string, string) {
   585  	return "Local API Alerts",
   586  		`Tracks the total number of past and present alerts for the installed scenarios.`
   587  }
   588  
   589  func (s statAlert) Process(reason string, val int) {
   590  	s[reason] += val
   591  }
   592  
   593  func (s statAlert) Table(out io.Writer, noUnit bool, showEmpty bool) {
   594  	t := newTable(out)
   595  	t.SetRowLines(false)
   596  	t.SetHeaders("Reason", "Count")
   597  	t.SetAlignment(table.AlignLeft, table.AlignLeft)
   598  
   599  	numRows := 0
   600  
   601  	for scenario, hits := range s {
   602  		t.AddRow(
   603  			scenario,
   604  			strconv.Itoa(hits),
   605  		)
   606  
   607  		numRows++
   608  	}
   609  
   610  	if numRows > 0 || showEmpty {
   611  		title, _ := s.Description()
   612  		renderTableTitle(out, "\n"+title+":")
   613  		t.Render()
   614  	}
   615  }