metrics.go 15 KB

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