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 }