metrics.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. package main
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net/http"
  6. "os"
  7. "sort"
  8. "strconv"
  9. "strings"
  10. "time"
  11. "github.com/crowdsecurity/crowdsec/pkg/types"
  12. log "github.com/sirupsen/logrus"
  13. "gopkg.in/yaml.v2"
  14. "github.com/olekukonko/tablewriter"
  15. dto "github.com/prometheus/client_model/go"
  16. "github.com/prometheus/prom2json"
  17. "github.com/spf13/cobra"
  18. )
  19. func lapiMetricsToTable(table *tablewriter.Table, stats map[string]map[string]map[string]int) error {
  20. //stats : machine -> route -> method -> count
  21. /*we want consistent display order*/
  22. machineKeys := []string{}
  23. for k := range stats {
  24. machineKeys = append(machineKeys, k)
  25. }
  26. sort.Strings(machineKeys)
  27. for _, machine := range machineKeys {
  28. //oneRow : route -> method -> count
  29. machineRow := stats[machine]
  30. for routeName, route := range machineRow {
  31. for methodName, count := range route {
  32. row := []string{}
  33. row = append(row, machine)
  34. row = append(row, routeName)
  35. row = append(row, methodName)
  36. if count != 0 {
  37. row = append(row, fmt.Sprintf("%d", count))
  38. } else {
  39. row = append(row, "-")
  40. }
  41. table.Append(row)
  42. }
  43. }
  44. }
  45. return nil
  46. }
  47. func metricsToTable(table *tablewriter.Table, stats map[string]map[string]int, keys []string) error {
  48. var sortedKeys []string
  49. if table == nil {
  50. return fmt.Errorf("nil table")
  51. }
  52. //sort keys to keep consistent order when printing
  53. sortedKeys = []string{}
  54. for akey := range stats {
  55. sortedKeys = append(sortedKeys, akey)
  56. }
  57. sort.Strings(sortedKeys)
  58. //
  59. for _, alabel := range sortedKeys {
  60. astats, ok := stats[alabel]
  61. if !ok {
  62. continue
  63. }
  64. row := []string{}
  65. row = append(row, alabel) //name
  66. for _, sl := range keys {
  67. if v, ok := astats[sl]; ok && v != 0 {
  68. numberToShow := fmt.Sprintf("%d", v)
  69. if !noUnit {
  70. numberToShow = formatNumber(v)
  71. }
  72. row = append(row, numberToShow)
  73. } else {
  74. row = append(row, "-")
  75. }
  76. }
  77. table.Append(row)
  78. }
  79. return nil
  80. }
  81. /*This is a complete rip from prom2json*/
  82. func ShowPrometheus(url string) {
  83. mfChan := make(chan *dto.MetricFamily, 1024)
  84. // Start with the DefaultTransport for sane defaults.
  85. transport := http.DefaultTransport.(*http.Transport).Clone()
  86. // Conservatively disable HTTP keep-alives as this program will only
  87. // ever need a single HTTP request.
  88. transport.DisableKeepAlives = true
  89. // Timeout early if the server doesn't even return the headers.
  90. transport.ResponseHeaderTimeout = time.Minute
  91. go func() {
  92. defer types.CatchPanic("crowdsec/ShowPrometheus")
  93. err := prom2json.FetchMetricFamilies(url, mfChan, transport)
  94. if err != nil {
  95. log.Fatalf("failed to fetch prometheus metrics : %v", err)
  96. }
  97. }()
  98. result := []*prom2json.Family{}
  99. for mf := range mfChan {
  100. result = append(result, prom2json.NewFamily(mf))
  101. }
  102. log.Debugf("Finished reading prometheus output, %d entries", len(result))
  103. /*walk*/
  104. lapi_decisions_stats := map[string]struct {
  105. NonEmpty int
  106. Empty int
  107. }{}
  108. acquis_stats := map[string]map[string]int{}
  109. parsers_stats := map[string]map[string]int{}
  110. buckets_stats := map[string]map[string]int{}
  111. lapi_stats := map[string]map[string]int{}
  112. lapi_machine_stats := map[string]map[string]map[string]int{}
  113. lapi_bouncer_stats := map[string]map[string]map[string]int{}
  114. decisions_stats := map[string]map[string]map[string]int{}
  115. alerts_stats := map[string]int{}
  116. for idx, fam := range result {
  117. if !strings.HasPrefix(fam.Name, "cs_") {
  118. continue
  119. }
  120. log.Tracef("round %d", idx)
  121. for _, m := range fam.Metrics {
  122. metric, ok := m.(prom2json.Metric)
  123. if !ok {
  124. log.Debugf("failed to convert metric to prom2json.Metric")
  125. continue
  126. }
  127. name, ok := metric.Labels["name"]
  128. if !ok {
  129. log.Debugf("no name in Metric %v", metric.Labels)
  130. }
  131. source, ok := metric.Labels["source"]
  132. if !ok {
  133. log.Debugf("no source in Metric %v for %s", metric.Labels, fam.Name)
  134. } else {
  135. if srctype, ok := metric.Labels["type"]; ok {
  136. source = srctype + ":" + source
  137. }
  138. }
  139. value := m.(prom2json.Metric).Value
  140. machine := metric.Labels["machine"]
  141. bouncer := metric.Labels["bouncer"]
  142. route := metric.Labels["route"]
  143. method := metric.Labels["method"]
  144. reason := metric.Labels["reason"]
  145. origin := metric.Labels["origin"]
  146. action := metric.Labels["action"]
  147. fval, err := strconv.ParseFloat(value, 32)
  148. if err != nil {
  149. log.Errorf("Unexpected int value %s : %s", value, err)
  150. }
  151. ival := int(fval)
  152. switch fam.Name {
  153. /*buckets*/
  154. case "cs_bucket_created_total":
  155. if _, ok := buckets_stats[name]; !ok {
  156. buckets_stats[name] = make(map[string]int)
  157. }
  158. buckets_stats[name]["instanciation"] += ival
  159. case "cs_buckets":
  160. if _, ok := buckets_stats[name]; !ok {
  161. buckets_stats[name] = make(map[string]int)
  162. }
  163. buckets_stats[name]["curr_count"] += ival
  164. case "cs_bucket_overflowed_total":
  165. if _, ok := buckets_stats[name]; !ok {
  166. buckets_stats[name] = make(map[string]int)
  167. }
  168. buckets_stats[name]["overflow"] += ival
  169. case "cs_bucket_poured_total":
  170. if _, ok := buckets_stats[name]; !ok {
  171. buckets_stats[name] = make(map[string]int)
  172. }
  173. if _, ok := acquis_stats[source]; !ok {
  174. acquis_stats[source] = make(map[string]int)
  175. }
  176. buckets_stats[name]["pour"] += ival
  177. acquis_stats[source]["pour"] += ival
  178. case "cs_bucket_underflowed_total":
  179. if _, ok := buckets_stats[name]; !ok {
  180. buckets_stats[name] = make(map[string]int)
  181. }
  182. buckets_stats[name]["underflow"] += ival
  183. /*acquis*/
  184. case "cs_parser_hits_total":
  185. if _, ok := acquis_stats[source]; !ok {
  186. acquis_stats[source] = make(map[string]int)
  187. }
  188. acquis_stats[source]["reads"] += ival
  189. case "cs_parser_hits_ok_total":
  190. if _, ok := acquis_stats[source]; !ok {
  191. acquis_stats[source] = make(map[string]int)
  192. }
  193. acquis_stats[source]["parsed"] += ival
  194. case "cs_parser_hits_ko_total":
  195. if _, ok := acquis_stats[source]; !ok {
  196. acquis_stats[source] = make(map[string]int)
  197. }
  198. acquis_stats[source]["unparsed"] += ival
  199. case "cs_node_hits_total":
  200. if _, ok := parsers_stats[name]; !ok {
  201. parsers_stats[name] = make(map[string]int)
  202. }
  203. parsers_stats[name]["hits"] += ival
  204. case "cs_node_hits_ok_total":
  205. if _, ok := parsers_stats[name]; !ok {
  206. parsers_stats[name] = make(map[string]int)
  207. }
  208. parsers_stats[name]["parsed"] += ival
  209. case "cs_node_hits_ko_total":
  210. if _, ok := parsers_stats[name]; !ok {
  211. parsers_stats[name] = make(map[string]int)
  212. }
  213. parsers_stats[name]["unparsed"] += ival
  214. case "cs_lapi_route_requests_total":
  215. if _, ok := lapi_stats[route]; !ok {
  216. lapi_stats[route] = make(map[string]int)
  217. }
  218. lapi_stats[route][method] += ival
  219. case "cs_lapi_machine_requests_total":
  220. if _, ok := lapi_machine_stats[machine]; !ok {
  221. lapi_machine_stats[machine] = make(map[string]map[string]int)
  222. }
  223. if _, ok := lapi_machine_stats[machine][route]; !ok {
  224. lapi_machine_stats[machine][route] = make(map[string]int)
  225. }
  226. lapi_machine_stats[machine][route][method] += ival
  227. case "cs_lapi_bouncer_requests_total":
  228. if _, ok := lapi_bouncer_stats[bouncer]; !ok {
  229. lapi_bouncer_stats[bouncer] = make(map[string]map[string]int)
  230. }
  231. if _, ok := lapi_bouncer_stats[bouncer][route]; !ok {
  232. lapi_bouncer_stats[bouncer][route] = make(map[string]int)
  233. }
  234. lapi_bouncer_stats[bouncer][route][method] += ival
  235. case "cs_lapi_decisions_ko_total", "cs_lapi_decisions_ok_total":
  236. if _, ok := lapi_decisions_stats[bouncer]; !ok {
  237. lapi_decisions_stats[bouncer] = struct {
  238. NonEmpty int
  239. Empty int
  240. }{}
  241. }
  242. x := lapi_decisions_stats[bouncer]
  243. if fam.Name == "cs_lapi_decisions_ko_total" {
  244. x.Empty += ival
  245. } else if fam.Name == "cs_lapi_decisions_ok_total" {
  246. x.NonEmpty += ival
  247. }
  248. lapi_decisions_stats[bouncer] = x
  249. case "cs_active_decisions":
  250. if _, ok := decisions_stats[reason]; !ok {
  251. decisions_stats[reason] = make(map[string]map[string]int)
  252. }
  253. if _, ok := decisions_stats[reason][origin]; !ok {
  254. decisions_stats[reason][origin] = make(map[string]int)
  255. }
  256. decisions_stats[reason][origin][action] += ival
  257. case "cs_alerts":
  258. /*if _, ok := alerts_stats[scenario]; !ok {
  259. alerts_stats[scenario] = make(map[string]int)
  260. }*/
  261. alerts_stats[reason] += ival
  262. default:
  263. continue
  264. }
  265. }
  266. }
  267. if csConfig.Cscli.Output == "human" {
  268. acquisTable := tablewriter.NewWriter(os.Stdout)
  269. acquisTable.SetHeader([]string{"Source", "Lines read", "Lines parsed", "Lines unparsed", "Lines poured to bucket"})
  270. keys := []string{"reads", "parsed", "unparsed", "pour"}
  271. if err := metricsToTable(acquisTable, acquis_stats, keys); err != nil {
  272. log.Warningf("while collecting acquis stats : %s", err)
  273. }
  274. bucketsTable := tablewriter.NewWriter(os.Stdout)
  275. bucketsTable.SetHeader([]string{"Bucket", "Current Count", "Overflows", "Instantiated", "Poured", "Expired"})
  276. keys = []string{"curr_count", "overflow", "instanciation", "pour", "underflow"}
  277. if err := metricsToTable(bucketsTable, buckets_stats, keys); err != nil {
  278. log.Warningf("while collecting acquis stats : %s", err)
  279. }
  280. parsersTable := tablewriter.NewWriter(os.Stdout)
  281. parsersTable.SetHeader([]string{"Parsers", "Hits", "Parsed", "Unparsed"})
  282. keys = []string{"hits", "parsed", "unparsed"}
  283. if err := metricsToTable(parsersTable, parsers_stats, keys); err != nil {
  284. log.Warningf("while collecting acquis stats : %s", err)
  285. }
  286. lapiMachinesTable := tablewriter.NewWriter(os.Stdout)
  287. lapiMachinesTable.SetHeader([]string{"Machine", "Route", "Method", "Hits"})
  288. if err := lapiMetricsToTable(lapiMachinesTable, lapi_machine_stats); err != nil {
  289. log.Warningf("while collecting machine lapi stats : %s", err)
  290. }
  291. //lapiMetricsToTable
  292. lapiBouncersTable := tablewriter.NewWriter(os.Stdout)
  293. lapiBouncersTable.SetHeader([]string{"Bouncer", "Route", "Method", "Hits"})
  294. if err := lapiMetricsToTable(lapiBouncersTable, lapi_bouncer_stats); err != nil {
  295. log.Warningf("while collecting bouncer lapi stats : %s", err)
  296. }
  297. lapiDecisionsTable := tablewriter.NewWriter(os.Stdout)
  298. lapiDecisionsTable.SetHeader([]string{"Bouncer", "Empty answers", "Non-empty answers"})
  299. for bouncer, hits := range lapi_decisions_stats {
  300. row := []string{}
  301. row = append(row, bouncer)
  302. row = append(row, fmt.Sprintf("%d", hits.Empty))
  303. row = append(row, fmt.Sprintf("%d", hits.NonEmpty))
  304. lapiDecisionsTable.Append(row)
  305. }
  306. /*unfortunately, we can't reuse metricsToTable as the structure is too different :/*/
  307. lapiTable := tablewriter.NewWriter(os.Stdout)
  308. lapiTable.SetHeader([]string{"Route", "Method", "Hits"})
  309. sortedKeys := []string{}
  310. for akey := range lapi_stats {
  311. sortedKeys = append(sortedKeys, akey)
  312. }
  313. sort.Strings(sortedKeys)
  314. for _, alabel := range sortedKeys {
  315. astats := lapi_stats[alabel]
  316. subKeys := []string{}
  317. for skey := range astats {
  318. subKeys = append(subKeys, skey)
  319. }
  320. sort.Strings(subKeys)
  321. for _, sl := range subKeys {
  322. row := []string{}
  323. row = append(row, alabel)
  324. row = append(row, sl)
  325. row = append(row, fmt.Sprintf("%d", astats[sl]))
  326. lapiTable.Append(row)
  327. }
  328. }
  329. decisionsTable := tablewriter.NewWriter(os.Stdout)
  330. decisionsTable.SetHeader([]string{"Reason", "Origin", "Action", "Count"})
  331. for reason, origins := range decisions_stats {
  332. for origin, actions := range origins {
  333. for action, hits := range actions {
  334. row := []string{}
  335. row = append(row, reason)
  336. row = append(row, origin)
  337. row = append(row, action)
  338. row = append(row, fmt.Sprintf("%d", hits))
  339. decisionsTable.Append(row)
  340. }
  341. }
  342. }
  343. alertsTable := tablewriter.NewWriter(os.Stdout)
  344. alertsTable.SetHeader([]string{"Reason", "Count"})
  345. for scenario, hits := range alerts_stats {
  346. row := []string{}
  347. row = append(row, scenario)
  348. row = append(row, fmt.Sprintf("%d", hits))
  349. alertsTable.Append(row)
  350. }
  351. if bucketsTable.NumLines() > 0 {
  352. log.Printf("Buckets Metrics:")
  353. bucketsTable.SetAlignment(tablewriter.ALIGN_LEFT)
  354. bucketsTable.Render()
  355. }
  356. if acquisTable.NumLines() > 0 {
  357. log.Printf("Acquisition Metrics:")
  358. acquisTable.SetAlignment(tablewriter.ALIGN_LEFT)
  359. acquisTable.Render()
  360. }
  361. if parsersTable.NumLines() > 0 {
  362. log.Printf("Parser Metrics:")
  363. parsersTable.SetAlignment(tablewriter.ALIGN_LEFT)
  364. parsersTable.Render()
  365. }
  366. if lapiTable.NumLines() > 0 {
  367. log.Printf("Local Api Metrics:")
  368. lapiTable.SetAlignment(tablewriter.ALIGN_LEFT)
  369. lapiTable.Render()
  370. }
  371. if lapiMachinesTable.NumLines() > 0 {
  372. log.Printf("Local Api Machines Metrics:")
  373. lapiMachinesTable.SetAlignment(tablewriter.ALIGN_LEFT)
  374. lapiMachinesTable.Render()
  375. }
  376. if lapiBouncersTable.NumLines() > 0 {
  377. log.Printf("Local Api Bouncers Metrics:")
  378. lapiBouncersTable.SetAlignment(tablewriter.ALIGN_LEFT)
  379. lapiBouncersTable.Render()
  380. }
  381. if lapiDecisionsTable.NumLines() > 0 {
  382. log.Printf("Local Api Bouncers Decisions:")
  383. lapiDecisionsTable.SetAlignment(tablewriter.ALIGN_LEFT)
  384. lapiDecisionsTable.Render()
  385. }
  386. if decisionsTable.NumLines() > 0 {
  387. log.Printf("Local Api Decisions:")
  388. decisionsTable.SetAlignment(tablewriter.ALIGN_LEFT)
  389. decisionsTable.Render()
  390. }
  391. if alertsTable.NumLines() > 0 {
  392. log.Printf("Local Api Alerts:")
  393. alertsTable.SetAlignment(tablewriter.ALIGN_LEFT)
  394. alertsTable.Render()
  395. }
  396. } else if csConfig.Cscli.Output == "json" {
  397. for _, val := range []interface{}{acquis_stats, parsers_stats, buckets_stats, lapi_stats, lapi_bouncer_stats, lapi_machine_stats, lapi_decisions_stats, decisions_stats, alerts_stats} {
  398. x, err := json.MarshalIndent(val, "", " ")
  399. if err != nil {
  400. log.Fatalf("failed to unmarshal metrics : %v", err)
  401. }
  402. fmt.Printf("%s\n", string(x))
  403. }
  404. } else if csConfig.Cscli.Output == "raw" {
  405. for _, val := range []interface{}{acquis_stats, parsers_stats, buckets_stats, lapi_stats, lapi_bouncer_stats, lapi_machine_stats, lapi_decisions_stats, decisions_stats, alerts_stats} {
  406. x, err := yaml.Marshal(val)
  407. if err != nil {
  408. log.Fatalf("failed to unmarshal metrics : %v", err)
  409. }
  410. fmt.Printf("%s\n", string(x))
  411. }
  412. }
  413. }
  414. var noUnit bool
  415. func NewMetricsCmd() *cobra.Command {
  416. /* ---- UPDATE COMMAND */
  417. var cmdMetrics = &cobra.Command{
  418. Use: "metrics",
  419. Short: "Display crowdsec prometheus metrics.",
  420. Long: `Fetch metrics from the prometheus server and display them in a human-friendly way`,
  421. Args: cobra.ExactArgs(0),
  422. DisableAutoGenTag: true,
  423. Run: func(cmd *cobra.Command, args []string) {
  424. if err := csConfig.LoadPrometheus(); err != nil {
  425. log.Fatalf(err.Error())
  426. }
  427. if !csConfig.Prometheus.Enabled {
  428. log.Warning("Prometheus is not enabled, can't show metrics")
  429. os.Exit(1)
  430. }
  431. if prometheusURL == "" {
  432. prometheusURL = csConfig.Cscli.PrometheusUrl
  433. }
  434. if prometheusURL == "" {
  435. log.Errorf("No prometheus url, please specify in %s or via -u", *csConfig.FilePath)
  436. os.Exit(1)
  437. }
  438. ShowPrometheus(prometheusURL + "/metrics")
  439. },
  440. }
  441. cmdMetrics.PersistentFlags().StringVarP(&prometheusURL, "url", "u", "", "Prometheus url (http://<ip>:<port>/metrics)")
  442. cmdMetrics.PersistentFlags().BoolVar(&noUnit, "no-unit", false, "Show the real number instead of formatted with units")
  443. return cmdMetrics
  444. }