metrics_table.go 15 KB

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