refact "cscli metrics" part 3 (#2807)
This commit is contained in:
parent
81acad0d66
commit
fdc525164a
|
@ -146,6 +146,8 @@ It is meant to allow you to manage bans, parsers/scenarios/etc, api and generall
|
||||||
FlagsDataType: cc.White,
|
FlagsDataType: cc.White,
|
||||||
Flags: cc.Green,
|
Flags: cc.Green,
|
||||||
FlagsDescr: cc.Cyan,
|
FlagsDescr: cc.Cyan,
|
||||||
|
NoExtraNewlines: true,
|
||||||
|
NoBottomNewline: true,
|
||||||
})
|
})
|
||||||
cmd.SetOut(color.Output)
|
cmd.SetOut(color.Output)
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"github.com/crowdsecurity/go-cs-lib/maptools"
|
||||||
"github.com/crowdsecurity/go-cs-lib/trace"
|
"github.com/crowdsecurity/go-cs-lib/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -40,18 +41,31 @@ type (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
type cliMetrics struct {
|
type metricSection interface {
|
||||||
cfg configGetter
|
Table(io.Writer, bool, bool)
|
||||||
|
Description() (string, string)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCLIMetrics(getconfig configGetter) *cliMetrics {
|
type metricStore map[string]metricSection
|
||||||
return &cliMetrics{
|
|
||||||
cfg: getconfig,
|
func NewMetricStore() metricStore {
|
||||||
|
return metricStore{
|
||||||
|
"acquisition": statAcquis{},
|
||||||
|
"buckets": statBucket{},
|
||||||
|
"parsers": statParser{},
|
||||||
|
"lapi": statLapi{},
|
||||||
|
"lapi-machine": statLapiMachine{},
|
||||||
|
"lapi-bouncer": statLapiBouncer{},
|
||||||
|
"lapi-decisions": statLapiDecision{},
|
||||||
|
"decisions": statDecision{},
|
||||||
|
"alerts": statAlert{},
|
||||||
|
"stash": statStash{},
|
||||||
|
"appsec-engine": statAppsecEngine{},
|
||||||
|
"appsec-rule": statAppsecRule{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FormatPrometheusMetrics is a complete rip from prom2json
|
func (ms metricStore) Fetch(url string) error {
|
||||||
func FormatPrometheusMetrics(out io.Writer, url string, formatType string, noUnit bool) error {
|
|
||||||
mfChan := make(chan *dto.MetricFamily, 1024)
|
mfChan := make(chan *dto.MetricFamily, 1024)
|
||||||
errChan := make(chan error, 1)
|
errChan := make(chan error, 1)
|
||||||
|
|
||||||
|
@ -64,9 +78,10 @@ func FormatPrometheusMetrics(out io.Writer, url string, formatType string, noUni
|
||||||
transport.ResponseHeaderTimeout = time.Minute
|
transport.ResponseHeaderTimeout = time.Minute
|
||||||
go func() {
|
go func() {
|
||||||
defer trace.CatchPanic("crowdsec/ShowPrometheus")
|
defer trace.CatchPanic("crowdsec/ShowPrometheus")
|
||||||
|
|
||||||
err := prom2json.FetchMetricFamilies(url, mfChan, transport)
|
err := prom2json.FetchMetricFamilies(url, mfChan, transport)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errChan <- fmt.Errorf("failed to fetch prometheus metrics: %w", err)
|
errChan <- fmt.Errorf("failed to fetch metrics: %w", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
errChan <- nil
|
errChan <- nil
|
||||||
|
@ -81,21 +96,21 @@ func FormatPrometheusMetrics(out io.Writer, url string, formatType string, noUni
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("Finished reading prometheus output, %d entries", len(result))
|
log.Debugf("Finished reading metrics output, %d entries", len(result))
|
||||||
/*walk*/
|
/*walk*/
|
||||||
|
|
||||||
mAcquis := statAcquis{}
|
mAcquis := ms["acquisition"].(statAcquis)
|
||||||
mParser := statParser{}
|
mParser := ms["parsers"].(statParser)
|
||||||
mBucket := statBucket{}
|
mBucket := ms["buckets"].(statBucket)
|
||||||
mLapi := statLapi{}
|
mLapi := ms["lapi"].(statLapi)
|
||||||
mLapiMachine := statLapiMachine{}
|
mLapiMachine := ms["lapi-machine"].(statLapiMachine)
|
||||||
mLapiBouncer := statLapiBouncer{}
|
mLapiBouncer := ms["lapi-bouncer"].(statLapiBouncer)
|
||||||
mLapiDecision := statLapiDecision{}
|
mLapiDecision := ms["lapi-decisions"].(statLapiDecision)
|
||||||
mDecision := statDecision{}
|
mDecision := ms["decisions"].(statDecision)
|
||||||
mAppsecEngine := statAppsecEngine{}
|
mAppsecEngine := ms["appsec-engine"].(statAppsecEngine)
|
||||||
mAppsecRule := statAppsecRule{}
|
mAppsecRule := ms["appsec-rule"].(statAppsecRule)
|
||||||
mAlert := statAlert{}
|
mAlert := ms["alerts"].(statAlert)
|
||||||
mStash := statStash{}
|
mStash := ms["stash"].(statStash)
|
||||||
|
|
||||||
for idx, fam := range result {
|
for idx, fam := range result {
|
||||||
if !strings.HasPrefix(fam.Name, "cs_") {
|
if !strings.HasPrefix(fam.Name, "cs_") {
|
||||||
|
@ -281,44 +296,50 @@ func FormatPrometheusMetrics(out io.Writer, url string, formatType string, noUni
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if formatType == "human" {
|
|
||||||
mAcquis.table(out, noUnit)
|
|
||||||
mBucket.table(out, noUnit)
|
|
||||||
mParser.table(out, noUnit)
|
|
||||||
mLapi.table(out)
|
|
||||||
mLapiMachine.table(out)
|
|
||||||
mLapiBouncer.table(out)
|
|
||||||
mLapiDecision.table(out)
|
|
||||||
mDecision.table(out)
|
|
||||||
mAlert.table(out)
|
|
||||||
mStash.table(out)
|
|
||||||
mAppsecEngine.table(out, noUnit)
|
|
||||||
mAppsecRule.table(out, noUnit)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
stats := make(map[string]any)
|
type cliMetrics struct {
|
||||||
|
cfg configGetter
|
||||||
|
}
|
||||||
|
|
||||||
stats["acquisition"] = mAcquis
|
func NewCLIMetrics(getconfig configGetter) *cliMetrics {
|
||||||
stats["buckets"] = mBucket
|
return &cliMetrics{
|
||||||
stats["parsers"] = mParser
|
cfg: getconfig,
|
||||||
stats["lapi"] = mLapi
|
}
|
||||||
stats["lapi_machine"] = mLapiMachine
|
}
|
||||||
stats["lapi_bouncer"] = mLapiBouncer
|
|
||||||
stats["lapi_decisions"] = mLapiDecision
|
func (ms metricStore) Format(out io.Writer, sections []string, formatType string, noUnit bool) error {
|
||||||
stats["decisions"] = mDecision
|
// copy only the sections we want
|
||||||
stats["alerts"] = mAlert
|
want := map[string]metricSection{}
|
||||||
stats["stash"] = mStash
|
|
||||||
|
// if explicitly asking for sections, we want to show empty tables
|
||||||
|
showEmpty := len(sections) > 0
|
||||||
|
|
||||||
|
// if no sections are specified, we want all of them
|
||||||
|
if len(sections) == 0 {
|
||||||
|
for section := range ms {
|
||||||
|
sections = append(sections, section)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, section := range sections {
|
||||||
|
want[section] = ms[section]
|
||||||
|
}
|
||||||
|
|
||||||
switch formatType {
|
switch formatType {
|
||||||
|
case "human":
|
||||||
|
for section := range want {
|
||||||
|
want[section].Table(out, noUnit, showEmpty)
|
||||||
|
}
|
||||||
case "json":
|
case "json":
|
||||||
x, err := json.MarshalIndent(stats, "", " ")
|
x, err := json.MarshalIndent(want, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to unmarshal metrics : %v", err)
|
return fmt.Errorf("failed to unmarshal metrics : %v", err)
|
||||||
}
|
}
|
||||||
out.Write(x)
|
out.Write(x)
|
||||||
case "raw":
|
case "raw":
|
||||||
x, err := yaml.Marshal(stats)
|
x, err := yaml.Marshal(want)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to unmarshal metrics : %v", err)
|
return fmt.Errorf("failed to unmarshal metrics : %v", err)
|
||||||
}
|
}
|
||||||
|
@ -330,7 +351,7 @@ func FormatPrometheusMetrics(out io.Writer, url string, formatType string, noUni
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *cliMetrics) run(url string, noUnit bool) error {
|
func (cli *cliMetrics) show(sections []string, url string, noUnit bool) error {
|
||||||
cfg := cli.cfg()
|
cfg := cli.cfg()
|
||||||
|
|
||||||
if url != "" {
|
if url != "" {
|
||||||
|
@ -345,7 +366,20 @@ func (cli *cliMetrics) run(url string, noUnit bool) error {
|
||||||
return fmt.Errorf("prometheus is not enabled, can't show metrics")
|
return fmt.Errorf("prometheus is not enabled, can't show metrics")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := FormatPrometheusMetrics(color.Output, cfg.Cscli.PrometheusUrl, cfg.Cscli.Output, noUnit); err != nil {
|
ms := NewMetricStore()
|
||||||
|
|
||||||
|
if err := ms.Fetch(cfg.Cscli.PrometheusUrl); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// any section that we don't have in the store is an error
|
||||||
|
for _, section := range sections {
|
||||||
|
if _, ok := ms[section]; !ok {
|
||||||
|
return fmt.Errorf("unknown metrics type: %s", section)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ms.Format(color.Output, sections, cfg.Cscli.Output, noUnit); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -360,11 +394,19 @@ func (cli *cliMetrics) NewCommand() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "metrics",
|
Use: "metrics",
|
||||||
Short: "Display crowdsec prometheus metrics.",
|
Short: "Display crowdsec prometheus metrics.",
|
||||||
Long: `Fetch metrics from the prometheus server and display them in a human-friendly way`,
|
Long: `Fetch metrics from a Local API server and display them`,
|
||||||
|
Example: `# Show all Metrics, skip empty tables (same as "cecli metrics show")
|
||||||
|
cscli metrics
|
||||||
|
|
||||||
|
# Show only some metrics, connect to a different url
|
||||||
|
cscli metrics --url http://lapi.local:6060/metrics show acquisition parsers
|
||||||
|
|
||||||
|
# List available metric types
|
||||||
|
cscli metrics list`,
|
||||||
Args: cobra.ExactArgs(0),
|
Args: cobra.ExactArgs(0),
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return cli.run(url, noUnit)
|
return cli.show(nil, url, noUnit)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -372,5 +414,126 @@ func (cli *cliMetrics) NewCommand() *cobra.Command {
|
||||||
flags.StringVarP(&url, "url", "u", "", "Prometheus url (http://<ip>:<port>/metrics)")
|
flags.StringVarP(&url, "url", "u", "", "Prometheus url (http://<ip>:<port>/metrics)")
|
||||||
flags.BoolVar(&noUnit, "no-unit", false, "Show the real number instead of formatted with units")
|
flags.BoolVar(&noUnit, "no-unit", false, "Show the real number instead of formatted with units")
|
||||||
|
|
||||||
|
cmd.AddCommand(cli.newShowCmd())
|
||||||
|
cmd.AddCommand(cli.newListCmd())
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// expandAlias returns a list of sections. The input can be a list of sections or alias.
|
||||||
|
func (cli *cliMetrics) expandSectionGroups(args []string) []string {
|
||||||
|
ret := []string{}
|
||||||
|
for _, section := range args {
|
||||||
|
switch section {
|
||||||
|
case "engine":
|
||||||
|
ret = append(ret, "acquisition", "parsers", "buckets", "stash")
|
||||||
|
case "lapi":
|
||||||
|
ret = append(ret, "alerts", "decisions", "lapi", "lapi-bouncer", "lapi-decisions", "lapi-machine")
|
||||||
|
case "appsec":
|
||||||
|
ret = append(ret, "appsec-engine", "appsec-rule")
|
||||||
|
default:
|
||||||
|
ret = append(ret, section)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *cliMetrics) newShowCmd() *cobra.Command {
|
||||||
|
var (
|
||||||
|
url string
|
||||||
|
noUnit bool
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "show [type]...",
|
||||||
|
Short: "Display all or part of the available metrics.",
|
||||||
|
Long: `Fetch metrics from a Local API server and display them, optionally filtering on specific types.`,
|
||||||
|
Example: `# Show all Metrics, skip empty tables
|
||||||
|
cscli metrics show
|
||||||
|
|
||||||
|
# Use an alias: "engine", "lapi" or "appsec" to show a group of metrics
|
||||||
|
cscli metrics show engine
|
||||||
|
|
||||||
|
# Show some specific metrics, show empty tables, connect to a different url
|
||||||
|
cscli metrics show acquisition parsers buckets stash --url http://lapi.local:6060/metrics
|
||||||
|
|
||||||
|
# Show metrics in json format
|
||||||
|
cscli metrics show acquisition parsers buckets stash -o json`,
|
||||||
|
// Positional args are optional
|
||||||
|
DisableAutoGenTag: true,
|
||||||
|
RunE: func(_ *cobra.Command, args []string) error {
|
||||||
|
args = cli.expandSectionGroups(args)
|
||||||
|
return cli.show(args, url, noUnit)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.StringVarP(&url, "url", "u", "", "Metrics url (http://<ip>:<port>/metrics)")
|
||||||
|
flags.BoolVar(&noUnit, "no-unit", false, "Show the real number instead of formatted with units")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *cliMetrics) list() error {
|
||||||
|
type metricType struct {
|
||||||
|
Type string `json:"type" yaml:"type"`
|
||||||
|
Title string `json:"title" yaml:"title"`
|
||||||
|
Description string `json:"description" yaml:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var allMetrics []metricType
|
||||||
|
|
||||||
|
ms := NewMetricStore()
|
||||||
|
for _, section := range maptools.SortedKeys(ms) {
|
||||||
|
title, description := ms[section].Description()
|
||||||
|
allMetrics = append(allMetrics, metricType{
|
||||||
|
Type: section,
|
||||||
|
Title: title,
|
||||||
|
Description: description,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
switch cli.cfg().Cscli.Output {
|
||||||
|
case "human":
|
||||||
|
t := newTable(color.Output)
|
||||||
|
t.SetRowLines(true)
|
||||||
|
t.SetHeaders("Type", "Title", "Description")
|
||||||
|
|
||||||
|
for _, metric := range allMetrics {
|
||||||
|
t.AddRow(metric.Type, metric.Title, metric.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Render()
|
||||||
|
case "json":
|
||||||
|
x, err := json.MarshalIndent(allMetrics, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal metrics: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(x))
|
||||||
|
case "raw":
|
||||||
|
x, err := yaml.Marshal(allMetrics)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal metrics: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *cliMetrics) newListCmd() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "List available types of metrics.",
|
||||||
|
Long: `List available types of metrics.`,
|
||||||
|
Args: cobra.ExactArgs(0),
|
||||||
|
DisableAutoGenTag: true,
|
||||||
|
RunE: func(_ *cobra.Command, _ []string) error {
|
||||||
|
cli.list()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@ import (
|
||||||
|
|
||||||
"github.com/aquasecurity/table"
|
"github.com/aquasecurity/table"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/crowdsecurity/go-cs-lib/maptools"
|
||||||
)
|
)
|
||||||
|
|
||||||
func lapiMetricsToTable(t *table.Table, stats map[string]map[string]map[string]int) int {
|
func lapiMetricsToTable(t *table.Table, stats map[string]map[string]map[string]int) int {
|
||||||
|
@ -47,15 +49,10 @@ func metricsToTable(t *table.Table, stats map[string]map[string]int, keys []stri
|
||||||
if t == nil {
|
if t == nil {
|
||||||
return 0, fmt.Errorf("nil table")
|
return 0, fmt.Errorf("nil table")
|
||||||
}
|
}
|
||||||
// sort keys to keep consistent order when printing
|
|
||||||
sortedKeys := []string{}
|
|
||||||
for k := range stats {
|
|
||||||
sortedKeys = append(sortedKeys, k)
|
|
||||||
}
|
|
||||||
sort.Strings(sortedKeys)
|
|
||||||
|
|
||||||
numRows := 0
|
numRows := 0
|
||||||
for _, alabel := range sortedKeys {
|
|
||||||
|
for _, alabel := range maptools.SortedKeys(stats) {
|
||||||
astats, ok := stats[alabel]
|
astats, ok := stats[alabel]
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
|
@ -81,7 +78,12 @@ func metricsToTable(t *table.Table, stats map[string]map[string]int, keys []stri
|
||||||
return numRows, nil
|
return numRows, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s statBucket) table(out io.Writer, noUnit bool) {
|
func (s statBucket) Description() (string, string) {
|
||||||
|
return "Bucket Metrics",
|
||||||
|
`Measure events in different scenarios. Current count is the number of buckets during metrics collection. Overflows are past event-producing buckets, while Expired are the ones that didn’t receive enough events to Overflow.`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s statBucket) Table(out io.Writer, noUnit bool, showEmpty bool) {
|
||||||
t := newTable(out)
|
t := newTable(out)
|
||||||
t.SetRowLines(false)
|
t.SetRowLines(false)
|
||||||
t.SetHeaders("Bucket", "Current Count", "Overflows", "Instantiated", "Poured", "Expired")
|
t.SetHeaders("Bucket", "Current Count", "Overflows", "Instantiated", "Poured", "Expired")
|
||||||
|
@ -91,13 +93,19 @@ func (s statBucket) table(out io.Writer, noUnit bool) {
|
||||||
|
|
||||||
if numRows, err := metricsToTable(t, s, keys, noUnit); err != nil {
|
if numRows, err := metricsToTable(t, s, keys, noUnit); err != nil {
|
||||||
log.Warningf("while collecting bucket stats: %s", err)
|
log.Warningf("while collecting bucket stats: %s", err)
|
||||||
} else if numRows > 0 {
|
} else if numRows > 0 || showEmpty {
|
||||||
renderTableTitle(out, "\nBucket Metrics:")
|
title, _ := s.Description()
|
||||||
|
renderTableTitle(out, "\n" + title + ":")
|
||||||
t.Render()
|
t.Render()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s statAcquis) table(out io.Writer, noUnit bool) {
|
func (s statAcquis) Description() (string, string) {
|
||||||
|
return "Acquisition Metrics",
|
||||||
|
`Measures the lines read, parsed, and unparsed per datasource. Zero read lines indicate a misconfigured or inactive datasource. Zero parsed lines mean the parser(s) failed. Non-zero parsed lines are fine as crowdsec selects relevant lines.`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s statAcquis) Table(out io.Writer, noUnit bool, showEmpty bool) {
|
||||||
t := newTable(out)
|
t := newTable(out)
|
||||||
t.SetRowLines(false)
|
t.SetRowLines(false)
|
||||||
t.SetHeaders("Source", "Lines read", "Lines parsed", "Lines unparsed", "Lines poured to bucket")
|
t.SetHeaders("Source", "Lines read", "Lines parsed", "Lines unparsed", "Lines poured to bucket")
|
||||||
|
@ -107,13 +115,19 @@ func (s statAcquis) table(out io.Writer, noUnit bool) {
|
||||||
|
|
||||||
if numRows, err := metricsToTable(t, s, keys, noUnit); err != nil {
|
if numRows, err := metricsToTable(t, s, keys, noUnit); err != nil {
|
||||||
log.Warningf("while collecting acquis stats: %s", err)
|
log.Warningf("while collecting acquis stats: %s", err)
|
||||||
} else if numRows > 0 {
|
} else if numRows > 0 || showEmpty {
|
||||||
renderTableTitle(out, "\nAcquisition Metrics:")
|
title, _ := s.Description()
|
||||||
|
renderTableTitle(out, "\n" + title + ":")
|
||||||
t.Render()
|
t.Render()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s statAppsecEngine) table(out io.Writer, noUnit bool) {
|
func (s statAppsecEngine) Description() (string, string) {
|
||||||
|
return "Appsec Metrics",
|
||||||
|
`Measures the number of parsed and blocked requests by the AppSec Component.`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s statAppsecEngine) Table(out io.Writer, noUnit bool, showEmpty bool) {
|
||||||
t := newTable(out)
|
t := newTable(out)
|
||||||
t.SetRowLines(false)
|
t.SetRowLines(false)
|
||||||
t.SetHeaders("Appsec Engine", "Processed", "Blocked")
|
t.SetHeaders("Appsec Engine", "Processed", "Blocked")
|
||||||
|
@ -121,13 +135,19 @@ func (s statAppsecEngine) table(out io.Writer, noUnit bool) {
|
||||||
keys := []string{"processed", "blocked"}
|
keys := []string{"processed", "blocked"}
|
||||||
if numRows, err := metricsToTable(t, s, keys, noUnit); err != nil {
|
if numRows, err := metricsToTable(t, s, keys, noUnit); err != nil {
|
||||||
log.Warningf("while collecting appsec stats: %s", err)
|
log.Warningf("while collecting appsec stats: %s", err)
|
||||||
} else if numRows > 0 {
|
} else if numRows > 0 || showEmpty {
|
||||||
renderTableTitle(out, "\nAppsec Metrics:")
|
title, _ := s.Description()
|
||||||
|
renderTableTitle(out, "\n" + title + ":")
|
||||||
t.Render()
|
t.Render()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s statAppsecRule) table(out io.Writer, noUnit bool) {
|
func (s statAppsecRule) Description() (string, string) {
|
||||||
|
return "Appsec Rule Metrics",
|
||||||
|
`Provides “per AppSec Component” information about the number of matches for loaded AppSec Rules.`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s statAppsecRule) Table(out io.Writer, noUnit bool, showEmpty bool) {
|
||||||
for appsecEngine, appsecEngineRulesStats := range s {
|
for appsecEngine, appsecEngineRulesStats := range s {
|
||||||
t := newTable(out)
|
t := newTable(out)
|
||||||
t.SetRowLines(false)
|
t.SetRowLines(false)
|
||||||
|
@ -136,7 +156,7 @@ func (s statAppsecRule) table(out io.Writer, noUnit bool) {
|
||||||
keys := []string{"triggered"}
|
keys := []string{"triggered"}
|
||||||
if numRows, err := metricsToTable(t, appsecEngineRulesStats, keys, noUnit); err != nil {
|
if numRows, err := metricsToTable(t, appsecEngineRulesStats, keys, noUnit); err != nil {
|
||||||
log.Warningf("while collecting appsec rules stats: %s", err)
|
log.Warningf("while collecting appsec rules stats: %s", err)
|
||||||
} else if numRows > 0 {
|
} else if numRows > 0 || showEmpty{
|
||||||
renderTableTitle(out, fmt.Sprintf("\nAppsec '%s' Rules Metrics:", appsecEngine))
|
renderTableTitle(out, fmt.Sprintf("\nAppsec '%s' Rules Metrics:", appsecEngine))
|
||||||
t.Render()
|
t.Render()
|
||||||
}
|
}
|
||||||
|
@ -144,7 +164,12 @@ func (s statAppsecRule) table(out io.Writer, noUnit bool) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s statParser) table(out io.Writer, noUnit bool) {
|
func (s statParser) Description() (string, string) {
|
||||||
|
return "Parser Metrics",
|
||||||
|
`Tracks the number of events processed by each parser and indicates success of failure. Zero parsed lines means the parer(s) failed. Non-zero unparsed lines are fine as crowdsec select relevant lines.`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s statParser) Table(out io.Writer, noUnit bool, showEmpty bool) {
|
||||||
t := newTable(out)
|
t := newTable(out)
|
||||||
t.SetRowLines(false)
|
t.SetRowLines(false)
|
||||||
t.SetHeaders("Parsers", "Hits", "Parsed", "Unparsed")
|
t.SetHeaders("Parsers", "Hits", "Parsed", "Unparsed")
|
||||||
|
@ -154,27 +179,28 @@ func (s statParser) table(out io.Writer, noUnit bool) {
|
||||||
|
|
||||||
if numRows, err := metricsToTable(t, s, keys, noUnit); err != nil {
|
if numRows, err := metricsToTable(t, s, keys, noUnit); err != nil {
|
||||||
log.Warningf("while collecting parsers stats: %s", err)
|
log.Warningf("while collecting parsers stats: %s", err)
|
||||||
} else if numRows > 0 {
|
} else if numRows > 0 || showEmpty {
|
||||||
renderTableTitle(out, "\nParser Metrics:")
|
title, _ := s.Description()
|
||||||
|
renderTableTitle(out, "\n" + title + ":")
|
||||||
t.Render()
|
t.Render()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s statStash) table(out io.Writer) {
|
func (s statStash) Description() (string, string) {
|
||||||
|
return "Parser Stash Metrics",
|
||||||
|
`Tracks the status of stashes that might be created by various parsers and scenarios.`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s statStash) Table(out io.Writer, noUnit bool, showEmpty bool) {
|
||||||
t := newTable(out)
|
t := newTable(out)
|
||||||
t.SetRowLines(false)
|
t.SetRowLines(false)
|
||||||
t.SetHeaders("Name", "Type", "Items")
|
t.SetHeaders("Name", "Type", "Items")
|
||||||
t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft)
|
t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft)
|
||||||
|
|
||||||
// unfortunately, we can't reuse metricsToTable as the structure is too different :/
|
// unfortunately, we can't reuse metricsToTable as the structure is too different :/
|
||||||
sortedKeys := []string{}
|
|
||||||
for k := range s {
|
|
||||||
sortedKeys = append(sortedKeys, k)
|
|
||||||
}
|
|
||||||
sort.Strings(sortedKeys)
|
|
||||||
|
|
||||||
numRows := 0
|
numRows := 0
|
||||||
for _, alabel := range sortedKeys {
|
|
||||||
|
for _, alabel := range maptools.SortedKeys(s) {
|
||||||
astats := s[alabel]
|
astats := s[alabel]
|
||||||
|
|
||||||
row := []string{
|
row := []string{
|
||||||
|
@ -185,27 +211,28 @@ func (s statStash) table(out io.Writer) {
|
||||||
t.AddRow(row...)
|
t.AddRow(row...)
|
||||||
numRows++
|
numRows++
|
||||||
}
|
}
|
||||||
if numRows > 0 {
|
if numRows > 0 || showEmpty {
|
||||||
renderTableTitle(out, "\nParser Stash Metrics:")
|
title, _ := s.Description()
|
||||||
|
renderTableTitle(out, "\n" + title + ":")
|
||||||
t.Render()
|
t.Render()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s statLapi) table(out io.Writer) {
|
func (s statLapi) Description() (string, string) {
|
||||||
|
return "Local API Metrics",
|
||||||
|
`Monitors the requests made to local API routes.`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s statLapi) Table(out io.Writer, noUnit bool, showEmpty bool) {
|
||||||
t := newTable(out)
|
t := newTable(out)
|
||||||
t.SetRowLines(false)
|
t.SetRowLines(false)
|
||||||
t.SetHeaders("Route", "Method", "Hits")
|
t.SetHeaders("Route", "Method", "Hits")
|
||||||
t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft)
|
t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft)
|
||||||
|
|
||||||
// unfortunately, we can't reuse metricsToTable as the structure is too different :/
|
// unfortunately, we can't reuse metricsToTable as the structure is too different :/
|
||||||
sortedKeys := []string{}
|
|
||||||
for k := range s {
|
|
||||||
sortedKeys = append(sortedKeys, k)
|
|
||||||
}
|
|
||||||
sort.Strings(sortedKeys)
|
|
||||||
|
|
||||||
numRows := 0
|
numRows := 0
|
||||||
for _, alabel := range sortedKeys {
|
|
||||||
|
for _, alabel := range maptools.SortedKeys(s) {
|
||||||
astats := s[alabel]
|
astats := s[alabel]
|
||||||
|
|
||||||
subKeys := []string{}
|
subKeys := []string{}
|
||||||
|
@ -225,13 +252,19 @@ func (s statLapi) table(out io.Writer) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if numRows > 0 {
|
if numRows > 0 || showEmpty {
|
||||||
renderTableTitle(out, "\nLocal API Metrics:")
|
title, _ := s.Description()
|
||||||
|
renderTableTitle(out, "\n" + title + ":")
|
||||||
t.Render()
|
t.Render()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s statLapiMachine) table(out io.Writer) {
|
func (s statLapiMachine) Description() (string, string) {
|
||||||
|
return "Local API Machines Metrics",
|
||||||
|
`Tracks the number of calls to the local API from each registered machine.`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s statLapiMachine) Table(out io.Writer, noUnit bool, showEmpty bool) {
|
||||||
t := newTable(out)
|
t := newTable(out)
|
||||||
t.SetRowLines(false)
|
t.SetRowLines(false)
|
||||||
t.SetHeaders("Machine", "Route", "Method", "Hits")
|
t.SetHeaders("Machine", "Route", "Method", "Hits")
|
||||||
|
@ -239,13 +272,19 @@ func (s statLapiMachine) table(out io.Writer) {
|
||||||
|
|
||||||
numRows := lapiMetricsToTable(t, s)
|
numRows := lapiMetricsToTable(t, s)
|
||||||
|
|
||||||
if numRows > 0 {
|
if numRows > 0 || showEmpty{
|
||||||
renderTableTitle(out, "\nLocal API Machines Metrics:")
|
title, _ := s.Description()
|
||||||
|
renderTableTitle(out, "\n" + title + ":")
|
||||||
t.Render()
|
t.Render()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s statLapiBouncer) table(out io.Writer) {
|
func (s statLapiBouncer) Description() (string, string) {
|
||||||
|
return "Local API Bouncers Metrics",
|
||||||
|
`Tracks total hits to remediation component related API routes.`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s statLapiBouncer) Table(out io.Writer, noUnit bool, showEmpty bool) {
|
||||||
t := newTable(out)
|
t := newTable(out)
|
||||||
t.SetRowLines(false)
|
t.SetRowLines(false)
|
||||||
t.SetHeaders("Bouncer", "Route", "Method", "Hits")
|
t.SetHeaders("Bouncer", "Route", "Method", "Hits")
|
||||||
|
@ -253,13 +292,19 @@ func (s statLapiBouncer) table(out io.Writer) {
|
||||||
|
|
||||||
numRows := lapiMetricsToTable(t, s)
|
numRows := lapiMetricsToTable(t, s)
|
||||||
|
|
||||||
if numRows > 0 {
|
if numRows > 0 || showEmpty {
|
||||||
renderTableTitle(out, "\nLocal API Bouncers Metrics:")
|
title, _ := s.Description()
|
||||||
|
renderTableTitle(out, "\n" + title + ":")
|
||||||
t.Render()
|
t.Render()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s statLapiDecision) table(out io.Writer) {
|
func (s statLapiDecision) Description() (string, string) {
|
||||||
|
return "Local API Bouncers Decisions",
|
||||||
|
`Tracks the number of empty/non-empty answers from LAPI to bouncers that are working in "live" mode.`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s statLapiDecision) Table(out io.Writer, noUnit bool, showEmpty bool) {
|
||||||
t := newTable(out)
|
t := newTable(out)
|
||||||
t.SetRowLines(false)
|
t.SetRowLines(false)
|
||||||
t.SetHeaders("Bouncer", "Empty answers", "Non-empty answers")
|
t.SetHeaders("Bouncer", "Empty answers", "Non-empty answers")
|
||||||
|
@ -275,13 +320,19 @@ func (s statLapiDecision) table(out io.Writer) {
|
||||||
numRows++
|
numRows++
|
||||||
}
|
}
|
||||||
|
|
||||||
if numRows > 0 {
|
if numRows > 0 || showEmpty{
|
||||||
renderTableTitle(out, "\nLocal API Bouncers Decisions:")
|
title, _ := s.Description()
|
||||||
|
renderTableTitle(out, "\n" + title + ":")
|
||||||
t.Render()
|
t.Render()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s statDecision) table(out io.Writer) {
|
func (s statDecision) Description() (string, string) {
|
||||||
|
return "Local API Decisions",
|
||||||
|
`Provides information about all currently active decisions. Includes both local (crowdsec) and global decisions (CAPI), and lists subscriptions (lists).`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s statDecision) Table(out io.Writer, noUnit bool, showEmpty bool) {
|
||||||
t := newTable(out)
|
t := newTable(out)
|
||||||
t.SetRowLines(false)
|
t.SetRowLines(false)
|
||||||
t.SetHeaders("Reason", "Origin", "Action", "Count")
|
t.SetHeaders("Reason", "Origin", "Action", "Count")
|
||||||
|
@ -302,13 +353,19 @@ func (s statDecision) table(out io.Writer) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if numRows > 0 {
|
if numRows > 0 || showEmpty{
|
||||||
renderTableTitle(out, "\nLocal API Decisions:")
|
title, _ := s.Description()
|
||||||
|
renderTableTitle(out, "\n" + title + ":")
|
||||||
t.Render()
|
t.Render()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s statAlert) table(out io.Writer) {
|
func (s statAlert) Description() (string, string) {
|
||||||
|
return "Local API Alerts",
|
||||||
|
`Tracks the total number of past and present alerts for the installed scenarios.`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s statAlert) Table(out io.Writer, noUnit bool, showEmpty bool) {
|
||||||
t := newTable(out)
|
t := newTable(out)
|
||||||
t.SetRowLines(false)
|
t.SetRowLines(false)
|
||||||
t.SetHeaders("Reason", "Count")
|
t.SetHeaders("Reason", "Count")
|
||||||
|
@ -323,8 +380,9 @@ func (s statAlert) table(out io.Writer) {
|
||||||
numRows++
|
numRows++
|
||||||
}
|
}
|
||||||
|
|
||||||
if numRows > 0 {
|
if numRows > 0 || showEmpty{
|
||||||
renderTableTitle(out, "\nLocal API Alerts:")
|
title, _ := s.Description()
|
||||||
|
renderTableTitle(out, "\n" + title + ":")
|
||||||
t.Render()
|
t.Render()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,10 +66,15 @@ func collectMetrics() ([]byte, []byte, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
humanMetrics := bytes.NewBuffer(nil)
|
humanMetrics := bytes.NewBuffer(nil)
|
||||||
err := FormatPrometheusMetrics(humanMetrics, csConfig.Cscli.PrometheusUrl, "human", false)
|
|
||||||
|
|
||||||
if err != nil {
|
ms := NewMetricStore()
|
||||||
return nil, nil, fmt.Errorf("could not fetch promtheus metrics: %s", err)
|
|
||||||
|
if err := ms.Fetch(csConfig.Cscli.PrometheusUrl); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("could not fetch prometheus metrics: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ms.Format(humanMetrics, nil, "human", false); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodGet, csConfig.Cscli.PrometheusUrl, nil)
|
req, err := http.NewRequest(http.MethodGet, csConfig.Cscli.PrometheusUrl, nil)
|
||||||
|
|
|
@ -273,15 +273,6 @@ teardown() {
|
||||||
assert_output 'failed to authenticate to Local API (LAPI): API error: incorrect Username or Password'
|
assert_output 'failed to authenticate to Local API (LAPI): API error: incorrect Username or Password'
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "cscli metrics" {
|
|
||||||
rune -0 ./instance-crowdsec start
|
|
||||||
rune -0 cscli lapi status
|
|
||||||
rune -0 cscli metrics
|
|
||||||
assert_output --partial "Route"
|
|
||||||
assert_output --partial '/v1/watchers/login'
|
|
||||||
assert_output --partial "Local API Metrics:"
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "'cscli completion' with or without configuration file" {
|
@test "'cscli completion' with or without configuration file" {
|
||||||
rune -0 cscli completion bash
|
rune -0 cscli completion bash
|
||||||
assert_output --partial "# bash completion for cscli"
|
assert_output --partial "# bash completion for cscli"
|
||||||
|
|
|
@ -25,7 +25,7 @@ teardown() {
|
||||||
@test "cscli metrics (crowdsec not running)" {
|
@test "cscli metrics (crowdsec not running)" {
|
||||||
rune -1 cscli metrics
|
rune -1 cscli metrics
|
||||||
# crowdsec is down
|
# crowdsec is down
|
||||||
assert_stderr --partial 'failed to fetch prometheus metrics: executing GET request for URL \"http://127.0.0.1:6060/metrics\" failed: Get \"http://127.0.0.1:6060/metrics\": dial tcp 127.0.0.1:6060: connect: connection refused'
|
assert_stderr --partial 'failed to fetch metrics: executing GET request for URL \"http://127.0.0.1:6060/metrics\" failed: Get \"http://127.0.0.1:6060/metrics\": dial tcp 127.0.0.1:6060: connect: connection refused'
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "cscli metrics (bad configuration)" {
|
@test "cscli metrics (bad configuration)" {
|
||||||
|
@ -59,3 +59,57 @@ teardown() {
|
||||||
rune -1 cscli metrics
|
rune -1 cscli metrics
|
||||||
assert_stderr --partial "prometheus is not enabled, can't show metrics"
|
assert_stderr --partial "prometheus is not enabled, can't show metrics"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@test "cscli metrics" {
|
||||||
|
rune -0 ./instance-crowdsec start
|
||||||
|
rune -0 cscli lapi status
|
||||||
|
rune -0 cscli metrics
|
||||||
|
assert_output --partial "Route"
|
||||||
|
assert_output --partial '/v1/watchers/login'
|
||||||
|
assert_output --partial "Local API Metrics:"
|
||||||
|
|
||||||
|
rune -0 cscli metrics -o json
|
||||||
|
rune -0 jq 'keys' <(output)
|
||||||
|
assert_output --partial '"alerts",'
|
||||||
|
assert_output --partial '"parsers",'
|
||||||
|
|
||||||
|
rune -0 cscli metrics -o raw
|
||||||
|
assert_output --partial 'alerts: {}'
|
||||||
|
assert_output --partial 'parsers: {}'
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "cscli metrics list" {
|
||||||
|
rune -0 cscli metrics list
|
||||||
|
assert_output --regexp "Type.*Title.*Description"
|
||||||
|
|
||||||
|
rune -0 cscli metrics list -o json
|
||||||
|
rune -0 jq -c '.[] | [.type,.title]' <(output)
|
||||||
|
assert_line '["acquisition","Acquisition Metrics"]'
|
||||||
|
|
||||||
|
rune -0 cscli metrics list -o raw
|
||||||
|
assert_line "- type: acquisition"
|
||||||
|
assert_line " title: Acquisition Metrics"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "cscli metrics show" {
|
||||||
|
rune -0 ./instance-crowdsec start
|
||||||
|
rune -0 cscli lapi status
|
||||||
|
|
||||||
|
assert_equal "$(cscli metrics)" "$(cscli metrics show)"
|
||||||
|
|
||||||
|
rune -1 cscli metrics show foobar
|
||||||
|
assert_stderr --partial "unknown metrics type: foobar"
|
||||||
|
|
||||||
|
rune -0 cscli metrics show lapi
|
||||||
|
assert_output --partial "Local API Metrics:"
|
||||||
|
assert_output --regexp "Route.*Method.*Hits"
|
||||||
|
assert_output --regexp "/v1/watchers/login.*POST"
|
||||||
|
|
||||||
|
rune -0 cscli metrics show lapi -o json
|
||||||
|
rune -0 jq -c '.lapi."/v1/watchers/login" | keys' <(output)
|
||||||
|
assert_json '["POST"]'
|
||||||
|
|
||||||
|
rune -0 cscli metrics show lapi -o raw
|
||||||
|
assert_line 'lapi:'
|
||||||
|
assert_line ' /v1/watchers/login:'
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue