metrics.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. package main
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "io"
  6. "net/http"
  7. "strconv"
  8. "strings"
  9. "time"
  10. "github.com/fatih/color"
  11. dto "github.com/prometheus/client_model/go"
  12. "github.com/prometheus/prom2json"
  13. log "github.com/sirupsen/logrus"
  14. "github.com/spf13/cobra"
  15. "gopkg.in/yaml.v3"
  16. "github.com/crowdsecurity/go-cs-lib/maptools"
  17. "github.com/crowdsecurity/go-cs-lib/trace"
  18. )
  19. type (
  20. statAcquis map[string]map[string]int
  21. statParser map[string]map[string]int
  22. statBucket map[string]map[string]int
  23. statLapi map[string]map[string]int
  24. statLapiMachine map[string]map[string]map[string]int
  25. statLapiBouncer map[string]map[string]map[string]int
  26. statLapiDecision map[string]struct {
  27. NonEmpty int
  28. Empty int
  29. }
  30. statDecision map[string]map[string]map[string]int
  31. statAppsecEngine map[string]map[string]int
  32. statAppsecRule map[string]map[string]map[string]int
  33. statAlert map[string]int
  34. statStash map[string]struct {
  35. Type string
  36. Count int
  37. }
  38. )
  39. type metricSection interface {
  40. Table(io.Writer, bool, bool)
  41. Description() (string, string)
  42. }
  43. type metricStore map[string]metricSection
  44. func NewMetricStore() metricStore {
  45. return metricStore{
  46. "acquisition": statAcquis{},
  47. "buckets": statBucket{},
  48. "parsers": statParser{},
  49. "lapi": statLapi{},
  50. "lapi-machine": statLapiMachine{},
  51. "lapi-bouncer": statLapiBouncer{},
  52. "lapi-decisions": statLapiDecision{},
  53. "decisions": statDecision{},
  54. "alerts": statAlert{},
  55. "stash": statStash{},
  56. "appsec-engine": statAppsecEngine{},
  57. "appsec-rule": statAppsecRule{},
  58. }
  59. }
  60. func (ms metricStore) Fetch(url string) error {
  61. mfChan := make(chan *dto.MetricFamily, 1024)
  62. errChan := make(chan error, 1)
  63. // Start with the DefaultTransport for sane defaults.
  64. transport := http.DefaultTransport.(*http.Transport).Clone()
  65. // Conservatively disable HTTP keep-alives as this program will only
  66. // ever need a single HTTP request.
  67. transport.DisableKeepAlives = true
  68. // Timeout early if the server doesn't even return the headers.
  69. transport.ResponseHeaderTimeout = time.Minute
  70. go func() {
  71. defer trace.CatchPanic("crowdsec/ShowPrometheus")
  72. err := prom2json.FetchMetricFamilies(url, mfChan, transport)
  73. if err != nil {
  74. errChan <- fmt.Errorf("failed to fetch metrics: %w", err)
  75. return
  76. }
  77. errChan <- nil
  78. }()
  79. result := []*prom2json.Family{}
  80. for mf := range mfChan {
  81. result = append(result, prom2json.NewFamily(mf))
  82. }
  83. if err := <-errChan; err != nil {
  84. return err
  85. }
  86. log.Debugf("Finished reading metrics output, %d entries", len(result))
  87. /*walk*/
  88. mAcquis := ms["acquisition"].(statAcquis)
  89. mParser := ms["parsers"].(statParser)
  90. mBucket := ms["buckets"].(statBucket)
  91. mLapi := ms["lapi"].(statLapi)
  92. mLapiMachine := ms["lapi-machine"].(statLapiMachine)
  93. mLapiBouncer := ms["lapi-bouncer"].(statLapiBouncer)
  94. mLapiDecision := ms["lapi-decisions"].(statLapiDecision)
  95. mDecision := ms["decisions"].(statDecision)
  96. mAppsecEngine := ms["appsec-engine"].(statAppsecEngine)
  97. mAppsecRule := ms["appsec-rule"].(statAppsecRule)
  98. mAlert := ms["alerts"].(statAlert)
  99. mStash := ms["stash"].(statStash)
  100. for idx, fam := range result {
  101. if !strings.HasPrefix(fam.Name, "cs_") {
  102. continue
  103. }
  104. log.Tracef("round %d", idx)
  105. for _, m := range fam.Metrics {
  106. metric, ok := m.(prom2json.Metric)
  107. if !ok {
  108. log.Debugf("failed to convert metric to prom2json.Metric")
  109. continue
  110. }
  111. name, ok := metric.Labels["name"]
  112. if !ok {
  113. log.Debugf("no name in Metric %v", metric.Labels)
  114. }
  115. source, ok := metric.Labels["source"]
  116. if !ok {
  117. log.Debugf("no source in Metric %v for %s", metric.Labels, fam.Name)
  118. } else {
  119. if srctype, ok := metric.Labels["type"]; ok {
  120. source = srctype + ":" + source
  121. }
  122. }
  123. value := m.(prom2json.Metric).Value
  124. machine := metric.Labels["machine"]
  125. bouncer := metric.Labels["bouncer"]
  126. route := metric.Labels["route"]
  127. method := metric.Labels["method"]
  128. reason := metric.Labels["reason"]
  129. origin := metric.Labels["origin"]
  130. action := metric.Labels["action"]
  131. mtype := metric.Labels["type"]
  132. fval, err := strconv.ParseFloat(value, 32)
  133. if err != nil {
  134. log.Errorf("Unexpected int value %s : %s", value, err)
  135. }
  136. ival := int(fval)
  137. switch fam.Name {
  138. /*buckets*/
  139. case "cs_bucket_created_total":
  140. if _, ok := mBucket[name]; !ok {
  141. mBucket[name] = make(map[string]int)
  142. }
  143. mBucket[name]["instantiation"] += ival
  144. case "cs_buckets":
  145. if _, ok := mBucket[name]; !ok {
  146. mBucket[name] = make(map[string]int)
  147. }
  148. mBucket[name]["curr_count"] += ival
  149. case "cs_bucket_overflowed_total":
  150. if _, ok := mBucket[name]; !ok {
  151. mBucket[name] = make(map[string]int)
  152. }
  153. mBucket[name]["overflow"] += ival
  154. case "cs_bucket_poured_total":
  155. if _, ok := mBucket[name]; !ok {
  156. mBucket[name] = make(map[string]int)
  157. }
  158. if _, ok := mAcquis[source]; !ok {
  159. mAcquis[source] = make(map[string]int)
  160. }
  161. mBucket[name]["pour"] += ival
  162. mAcquis[source]["pour"] += ival
  163. case "cs_bucket_underflowed_total":
  164. if _, ok := mBucket[name]; !ok {
  165. mBucket[name] = make(map[string]int)
  166. }
  167. mBucket[name]["underflow"] += ival
  168. /*acquis*/
  169. case "cs_parser_hits_total":
  170. if _, ok := mAcquis[source]; !ok {
  171. mAcquis[source] = make(map[string]int)
  172. }
  173. mAcquis[source]["reads"] += ival
  174. case "cs_parser_hits_ok_total":
  175. if _, ok := mAcquis[source]; !ok {
  176. mAcquis[source] = make(map[string]int)
  177. }
  178. mAcquis[source]["parsed"] += ival
  179. case "cs_parser_hits_ko_total":
  180. if _, ok := mAcquis[source]; !ok {
  181. mAcquis[source] = make(map[string]int)
  182. }
  183. mAcquis[source]["unparsed"] += ival
  184. case "cs_node_hits_total":
  185. if _, ok := mParser[name]; !ok {
  186. mParser[name] = make(map[string]int)
  187. }
  188. mParser[name]["hits"] += ival
  189. case "cs_node_hits_ok_total":
  190. if _, ok := mParser[name]; !ok {
  191. mParser[name] = make(map[string]int)
  192. }
  193. mParser[name]["parsed"] += ival
  194. case "cs_node_hits_ko_total":
  195. if _, ok := mParser[name]; !ok {
  196. mParser[name] = make(map[string]int)
  197. }
  198. mParser[name]["unparsed"] += ival
  199. case "cs_lapi_route_requests_total":
  200. if _, ok := mLapi[route]; !ok {
  201. mLapi[route] = make(map[string]int)
  202. }
  203. mLapi[route][method] += ival
  204. case "cs_lapi_machine_requests_total":
  205. if _, ok := mLapiMachine[machine]; !ok {
  206. mLapiMachine[machine] = make(map[string]map[string]int)
  207. }
  208. if _, ok := mLapiMachine[machine][route]; !ok {
  209. mLapiMachine[machine][route] = make(map[string]int)
  210. }
  211. mLapiMachine[machine][route][method] += ival
  212. case "cs_lapi_bouncer_requests_total":
  213. if _, ok := mLapiBouncer[bouncer]; !ok {
  214. mLapiBouncer[bouncer] = make(map[string]map[string]int)
  215. }
  216. if _, ok := mLapiBouncer[bouncer][route]; !ok {
  217. mLapiBouncer[bouncer][route] = make(map[string]int)
  218. }
  219. mLapiBouncer[bouncer][route][method] += ival
  220. case "cs_lapi_decisions_ko_total", "cs_lapi_decisions_ok_total":
  221. if _, ok := mLapiDecision[bouncer]; !ok {
  222. mLapiDecision[bouncer] = struct {
  223. NonEmpty int
  224. Empty int
  225. }{}
  226. }
  227. x := mLapiDecision[bouncer]
  228. if fam.Name == "cs_lapi_decisions_ko_total" {
  229. x.Empty += ival
  230. } else if fam.Name == "cs_lapi_decisions_ok_total" {
  231. x.NonEmpty += ival
  232. }
  233. mLapiDecision[bouncer] = x
  234. case "cs_active_decisions":
  235. if _, ok := mDecision[reason]; !ok {
  236. mDecision[reason] = make(map[string]map[string]int)
  237. }
  238. if _, ok := mDecision[reason][origin]; !ok {
  239. mDecision[reason][origin] = make(map[string]int)
  240. }
  241. mDecision[reason][origin][action] += ival
  242. case "cs_alerts":
  243. /*if _, ok := mAlert[scenario]; !ok {
  244. mAlert[scenario] = make(map[string]int)
  245. }*/
  246. mAlert[reason] += ival
  247. case "cs_cache_size":
  248. mStash[name] = struct {
  249. Type string
  250. Count int
  251. }{Type: mtype, Count: ival}
  252. case "cs_appsec_reqs_total":
  253. if _, ok := mAppsecEngine[metric.Labels["appsec_engine"]]; !ok {
  254. mAppsecEngine[metric.Labels["appsec_engine"]] = make(map[string]int, 0)
  255. }
  256. mAppsecEngine[metric.Labels["appsec_engine"]]["processed"] = ival
  257. case "cs_appsec_block_total":
  258. if _, ok := mAppsecEngine[metric.Labels["appsec_engine"]]; !ok {
  259. mAppsecEngine[metric.Labels["appsec_engine"]] = make(map[string]int, 0)
  260. }
  261. mAppsecEngine[metric.Labels["appsec_engine"]]["blocked"] = ival
  262. case "cs_appsec_rule_hits":
  263. appsecEngine := metric.Labels["appsec_engine"]
  264. ruleID := metric.Labels["rule_name"]
  265. if _, ok := mAppsecRule[appsecEngine]; !ok {
  266. mAppsecRule[appsecEngine] = make(map[string]map[string]int, 0)
  267. }
  268. if _, ok := mAppsecRule[appsecEngine][ruleID]; !ok {
  269. mAppsecRule[appsecEngine][ruleID] = make(map[string]int, 0)
  270. }
  271. mAppsecRule[appsecEngine][ruleID]["triggered"] = ival
  272. default:
  273. log.Debugf("unknown: %+v", fam.Name)
  274. continue
  275. }
  276. }
  277. }
  278. return nil
  279. }
  280. type cliMetrics struct {
  281. cfg configGetter
  282. }
  283. func NewCLIMetrics(cfg configGetter) *cliMetrics {
  284. return &cliMetrics{
  285. cfg: cfg,
  286. }
  287. }
  288. func (ms metricStore) Format(out io.Writer, sections []string, formatType string, noUnit bool) error {
  289. // copy only the sections we want
  290. want := map[string]metricSection{}
  291. // if explicitly asking for sections, we want to show empty tables
  292. showEmpty := len(sections) > 0
  293. // if no sections are specified, we want all of them
  294. if len(sections) == 0 {
  295. for section := range ms {
  296. sections = append(sections, section)
  297. }
  298. }
  299. for _, section := range sections {
  300. want[section] = ms[section]
  301. }
  302. switch formatType {
  303. case "human":
  304. for section := range want {
  305. want[section].Table(out, noUnit, showEmpty)
  306. }
  307. case "json":
  308. x, err := json.MarshalIndent(want, "", " ")
  309. if err != nil {
  310. return fmt.Errorf("failed to unmarshal metrics : %v", err)
  311. }
  312. out.Write(x)
  313. case "raw":
  314. x, err := yaml.Marshal(want)
  315. if err != nil {
  316. return fmt.Errorf("failed to unmarshal metrics : %v", err)
  317. }
  318. out.Write(x)
  319. default:
  320. return fmt.Errorf("unknown format type %s", formatType)
  321. }
  322. return nil
  323. }
  324. func (cli *cliMetrics) show(sections []string, url string, noUnit bool) error {
  325. cfg := cli.cfg()
  326. if url != "" {
  327. cfg.Cscli.PrometheusUrl = url
  328. }
  329. if cfg.Prometheus == nil {
  330. return fmt.Errorf("prometheus section missing, can't show metrics")
  331. }
  332. if !cfg.Prometheus.Enabled {
  333. return fmt.Errorf("prometheus is not enabled, can't show metrics")
  334. }
  335. ms := NewMetricStore()
  336. if err := ms.Fetch(cfg.Cscli.PrometheusUrl); err != nil {
  337. return err
  338. }
  339. // any section that we don't have in the store is an error
  340. for _, section := range sections {
  341. if _, ok := ms[section]; !ok {
  342. return fmt.Errorf("unknown metrics type: %s", section)
  343. }
  344. }
  345. if err := ms.Format(color.Output, sections, cfg.Cscli.Output, noUnit); err != nil {
  346. return err
  347. }
  348. return nil
  349. }
  350. func (cli *cliMetrics) NewCommand() *cobra.Command {
  351. var (
  352. url string
  353. noUnit bool
  354. )
  355. cmd := &cobra.Command{
  356. Use: "metrics",
  357. Short: "Display crowdsec prometheus metrics.",
  358. Long: `Fetch metrics from a Local API server and display them`,
  359. Example: `# Show all Metrics, skip empty tables (same as "cecli metrics show")
  360. cscli metrics
  361. # Show only some metrics, connect to a different url
  362. cscli metrics --url http://lapi.local:6060/metrics show acquisition parsers
  363. # List available metric types
  364. cscli metrics list`,
  365. Args: cobra.ExactArgs(0),
  366. DisableAutoGenTag: true,
  367. RunE: func(cmd *cobra.Command, args []string) error {
  368. return cli.show(nil, url, noUnit)
  369. },
  370. }
  371. flags := cmd.Flags()
  372. flags.StringVarP(&url, "url", "u", "", "Prometheus url (http://<ip>:<port>/metrics)")
  373. flags.BoolVar(&noUnit, "no-unit", false, "Show the real number instead of formatted with units")
  374. cmd.AddCommand(cli.newShowCmd())
  375. cmd.AddCommand(cli.newListCmd())
  376. return cmd
  377. }
  378. // expandAlias returns a list of sections. The input can be a list of sections or alias.
  379. func (cli *cliMetrics) expandSectionGroups(args []string) []string {
  380. ret := []string{}
  381. for _, section := range args {
  382. switch section {
  383. case "engine":
  384. ret = append(ret, "acquisition", "parsers", "buckets", "stash")
  385. case "lapi":
  386. ret = append(ret, "alerts", "decisions", "lapi", "lapi-bouncer", "lapi-decisions", "lapi-machine")
  387. case "appsec":
  388. ret = append(ret, "appsec-engine", "appsec-rule")
  389. default:
  390. ret = append(ret, section)
  391. }
  392. }
  393. return ret
  394. }
  395. func (cli *cliMetrics) newShowCmd() *cobra.Command {
  396. var (
  397. url string
  398. noUnit bool
  399. )
  400. cmd := &cobra.Command{
  401. Use: "show [type]...",
  402. Short: "Display all or part of the available metrics.",
  403. Long: `Fetch metrics from a Local API server and display them, optionally filtering on specific types.`,
  404. Example: `# Show all Metrics, skip empty tables
  405. cscli metrics show
  406. # Use an alias: "engine", "lapi" or "appsec" to show a group of metrics
  407. cscli metrics show engine
  408. # Show some specific metrics, show empty tables, connect to a different url
  409. cscli metrics show acquisition parsers buckets stash --url http://lapi.local:6060/metrics
  410. # Show metrics in json format
  411. cscli metrics show acquisition parsers buckets stash -o json`,
  412. // Positional args are optional
  413. DisableAutoGenTag: true,
  414. RunE: func(_ *cobra.Command, args []string) error {
  415. args = cli.expandSectionGroups(args)
  416. return cli.show(args, url, noUnit)
  417. },
  418. }
  419. flags := cmd.Flags()
  420. flags.StringVarP(&url, "url", "u", "", "Metrics url (http://<ip>:<port>/metrics)")
  421. flags.BoolVar(&noUnit, "no-unit", false, "Show the real number instead of formatted with units")
  422. return cmd
  423. }
  424. func (cli *cliMetrics) list() error {
  425. type metricType struct {
  426. Type string `json:"type" yaml:"type"`
  427. Title string `json:"title" yaml:"title"`
  428. Description string `json:"description" yaml:"description"`
  429. }
  430. var allMetrics []metricType
  431. ms := NewMetricStore()
  432. for _, section := range maptools.SortedKeys(ms) {
  433. title, description := ms[section].Description()
  434. allMetrics = append(allMetrics, metricType{
  435. Type: section,
  436. Title: title,
  437. Description: description,
  438. })
  439. }
  440. switch cli.cfg().Cscli.Output {
  441. case "human":
  442. t := newTable(color.Output)
  443. t.SetRowLines(true)
  444. t.SetHeaders("Type", "Title", "Description")
  445. for _, metric := range allMetrics {
  446. t.AddRow(metric.Type, metric.Title, metric.Description)
  447. }
  448. t.Render()
  449. case "json":
  450. x, err := json.MarshalIndent(allMetrics, "", " ")
  451. if err != nil {
  452. return fmt.Errorf("failed to unmarshal metrics: %w", err)
  453. }
  454. fmt.Println(string(x))
  455. case "raw":
  456. x, err := yaml.Marshal(allMetrics)
  457. if err != nil {
  458. return fmt.Errorf("failed to unmarshal metrics: %w", err)
  459. }
  460. fmt.Println(string(x))
  461. }
  462. return nil
  463. }
  464. func (cli *cliMetrics) newListCmd() *cobra.Command {
  465. cmd := &cobra.Command{
  466. Use: "list",
  467. Short: "List available types of metrics.",
  468. Long: `List available types of metrics.`,
  469. Args: cobra.ExactArgs(0),
  470. DisableAutoGenTag: true,
  471. RunE: func(_ *cobra.Command, _ []string) error {
  472. cli.list()
  473. return nil
  474. },
  475. }
  476. return cmd
  477. }