github.com/crowdsecurity/crowdsec@v1.6.1/cmd/crowdsec-cli/item_metrics.go (about)

     1  package main
     2  
     3  import (
     4  	"fmt"
     5  	"math"
     6  	"net/http"
     7  	"strconv"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/fatih/color"
    12  	dto "github.com/prometheus/client_model/go"
    13  	"github.com/prometheus/prom2json"
    14  	log "github.com/sirupsen/logrus"
    15  
    16  	"github.com/crowdsecurity/go-cs-lib/trace"
    17  
    18  	"github.com/crowdsecurity/crowdsec/pkg/cwhub"
    19  )
    20  
    21  func ShowMetrics(hubItem *cwhub.Item) error {
    22  	switch hubItem.Type {
    23  	case cwhub.PARSERS:
    24  		metrics := GetParserMetric(csConfig.Cscli.PrometheusUrl, hubItem.Name)
    25  		parserMetricsTable(color.Output, hubItem.Name, metrics)
    26  	case cwhub.SCENARIOS:
    27  		metrics := GetScenarioMetric(csConfig.Cscli.PrometheusUrl, hubItem.Name)
    28  		scenarioMetricsTable(color.Output, hubItem.Name, metrics)
    29  	case cwhub.COLLECTIONS:
    30  		for _, sub := range hubItem.SubItems() {
    31  			if err := ShowMetrics(sub); err != nil {
    32  				return err
    33  			}
    34  		}
    35  	case cwhub.APPSEC_RULES:
    36  		metrics := GetAppsecRuleMetric(csConfig.Cscli.PrometheusUrl, hubItem.Name)
    37  		appsecMetricsTable(color.Output, hubItem.Name, metrics)
    38  	default: // no metrics for this item type
    39  	}
    40  	return nil
    41  }
    42  
    43  // GetParserMetric is a complete rip from prom2json
    44  func GetParserMetric(url string, itemName string) map[string]map[string]int {
    45  	stats := make(map[string]map[string]int)
    46  
    47  	result := GetPrometheusMetric(url)
    48  	for idx, fam := range result {
    49  		if !strings.HasPrefix(fam.Name, "cs_") {
    50  			continue
    51  		}
    52  		log.Tracef("round %d", idx)
    53  		for _, m := range fam.Metrics {
    54  			metric, ok := m.(prom2json.Metric)
    55  			if !ok {
    56  				log.Debugf("failed to convert metric to prom2json.Metric")
    57  				continue
    58  			}
    59  			name, ok := metric.Labels["name"]
    60  			if !ok {
    61  				log.Debugf("no name in Metric %v", metric.Labels)
    62  			}
    63  			if name != itemName {
    64  				continue
    65  			}
    66  			source, ok := metric.Labels["source"]
    67  			if !ok {
    68  				log.Debugf("no source in Metric %v", metric.Labels)
    69  			} else {
    70  				if srctype, ok := metric.Labels["type"]; ok {
    71  					source = srctype + ":" + source
    72  				}
    73  			}
    74  			value := m.(prom2json.Metric).Value
    75  			fval, err := strconv.ParseFloat(value, 32)
    76  			if err != nil {
    77  				log.Errorf("Unexpected int value %s : %s", value, err)
    78  				continue
    79  			}
    80  			ival := int(fval)
    81  
    82  			switch fam.Name {
    83  			case "cs_reader_hits_total":
    84  				if _, ok := stats[source]; !ok {
    85  					stats[source] = make(map[string]int)
    86  					stats[source]["parsed"] = 0
    87  					stats[source]["reads"] = 0
    88  					stats[source]["unparsed"] = 0
    89  					stats[source]["hits"] = 0
    90  				}
    91  				stats[source]["reads"] += ival
    92  			case "cs_parser_hits_ok_total":
    93  				if _, ok := stats[source]; !ok {
    94  					stats[source] = make(map[string]int)
    95  				}
    96  				stats[source]["parsed"] += ival
    97  			case "cs_parser_hits_ko_total":
    98  				if _, ok := stats[source]; !ok {
    99  					stats[source] = make(map[string]int)
   100  				}
   101  				stats[source]["unparsed"] += ival
   102  			case "cs_node_hits_total":
   103  				if _, ok := stats[source]; !ok {
   104  					stats[source] = make(map[string]int)
   105  				}
   106  				stats[source]["hits"] += ival
   107  			case "cs_node_hits_ok_total":
   108  				if _, ok := stats[source]; !ok {
   109  					stats[source] = make(map[string]int)
   110  				}
   111  				stats[source]["parsed"] += ival
   112  			case "cs_node_hits_ko_total":
   113  				if _, ok := stats[source]; !ok {
   114  					stats[source] = make(map[string]int)
   115  				}
   116  				stats[source]["unparsed"] += ival
   117  			default:
   118  				continue
   119  			}
   120  		}
   121  	}
   122  	return stats
   123  }
   124  
   125  func GetScenarioMetric(url string, itemName string) map[string]int {
   126  	stats := make(map[string]int)
   127  
   128  	stats["instantiation"] = 0
   129  	stats["curr_count"] = 0
   130  	stats["overflow"] = 0
   131  	stats["pour"] = 0
   132  	stats["underflow"] = 0
   133  
   134  	result := GetPrometheusMetric(url)
   135  	for idx, fam := range result {
   136  		if !strings.HasPrefix(fam.Name, "cs_") {
   137  			continue
   138  		}
   139  		log.Tracef("round %d", idx)
   140  		for _, m := range fam.Metrics {
   141  			metric, ok := m.(prom2json.Metric)
   142  			if !ok {
   143  				log.Debugf("failed to convert metric to prom2json.Metric")
   144  				continue
   145  			}
   146  			name, ok := metric.Labels["name"]
   147  			if !ok {
   148  				log.Debugf("no name in Metric %v", metric.Labels)
   149  			}
   150  			if name != itemName {
   151  				continue
   152  			}
   153  			value := m.(prom2json.Metric).Value
   154  			fval, err := strconv.ParseFloat(value, 32)
   155  			if err != nil {
   156  				log.Errorf("Unexpected int value %s : %s", value, err)
   157  				continue
   158  			}
   159  			ival := int(fval)
   160  
   161  			switch fam.Name {
   162  			case "cs_bucket_created_total":
   163  				stats["instantiation"] += ival
   164  			case "cs_buckets":
   165  				stats["curr_count"] += ival
   166  			case "cs_bucket_overflowed_total":
   167  				stats["overflow"] += ival
   168  			case "cs_bucket_poured_total":
   169  				stats["pour"] += ival
   170  			case "cs_bucket_underflowed_total":
   171  				stats["underflow"] += ival
   172  			default:
   173  				continue
   174  			}
   175  		}
   176  	}
   177  	return stats
   178  }
   179  
   180  func GetAppsecRuleMetric(url string, itemName string) map[string]int {
   181  	stats := make(map[string]int)
   182  
   183  	stats["inband_hits"] = 0
   184  	stats["outband_hits"] = 0
   185  
   186  	results := GetPrometheusMetric(url)
   187  	for idx, fam := range results {
   188  		if !strings.HasPrefix(fam.Name, "cs_") {
   189  			continue
   190  		}
   191  		log.Tracef("round %d", idx)
   192  		for _, m := range fam.Metrics {
   193  			metric, ok := m.(prom2json.Metric)
   194  			if !ok {
   195  				log.Debugf("failed to convert metric to prom2json.Metric")
   196  				continue
   197  			}
   198  			name, ok := metric.Labels["rule_name"]
   199  			if !ok {
   200  				log.Debugf("no rule_name in Metric %v", metric.Labels)
   201  			}
   202  			if name != itemName {
   203  				continue
   204  			}
   205  
   206  			band, ok := metric.Labels["type"]
   207  			if !ok {
   208  				log.Debugf("no type in Metric %v", metric.Labels)
   209  			}
   210  
   211  			value := m.(prom2json.Metric).Value
   212  			fval, err := strconv.ParseFloat(value, 32)
   213  			if err != nil {
   214  				log.Errorf("Unexpected int value %s : %s", value, err)
   215  				continue
   216  			}
   217  			ival := int(fval)
   218  
   219  			switch fam.Name {
   220  			case "cs_appsec_rule_hits":
   221  				switch band {
   222  				case "inband":
   223  					stats["inband_hits"] += ival
   224  				case "outband":
   225  					stats["outband_hits"] += ival
   226  				default:
   227  					continue
   228  				}
   229  			default:
   230  				continue
   231  			}
   232  		}
   233  	}
   234  	return stats
   235  }
   236  
   237  func GetPrometheusMetric(url string) []*prom2json.Family {
   238  	mfChan := make(chan *dto.MetricFamily, 1024)
   239  
   240  	// Start with the DefaultTransport for sane defaults.
   241  	transport := http.DefaultTransport.(*http.Transport).Clone()
   242  	// Conservatively disable HTTP keep-alives as this program will only
   243  	// ever need a single HTTP request.
   244  	transport.DisableKeepAlives = true
   245  	// Timeout early if the server doesn't even return the headers.
   246  	transport.ResponseHeaderTimeout = time.Minute
   247  
   248  	go func() {
   249  		defer trace.CatchPanic("crowdsec/GetPrometheusMetric")
   250  		err := prom2json.FetchMetricFamilies(url, mfChan, transport)
   251  		if err != nil {
   252  			log.Fatalf("failed to fetch prometheus metrics : %v", err)
   253  		}
   254  	}()
   255  
   256  	result := []*prom2json.Family{}
   257  	for mf := range mfChan {
   258  		result = append(result, prom2json.NewFamily(mf))
   259  	}
   260  	log.Debugf("Finished reading prometheus output, %d entries", len(result))
   261  
   262  	return result
   263  }
   264  
   265  type unit struct {
   266  	value  int64
   267  	symbol string
   268  }
   269  
   270  var ranges = []unit{
   271  	{value: 1e18, symbol: "E"},
   272  	{value: 1e15, symbol: "P"},
   273  	{value: 1e12, symbol: "T"},
   274  	{value: 1e9, symbol: "G"},
   275  	{value: 1e6, symbol: "M"},
   276  	{value: 1e3, symbol: "k"},
   277  	{value: 1, symbol: ""},
   278  }
   279  
   280  func formatNumber(num int) string {
   281  	goodUnit := unit{}
   282  
   283  	for _, u := range ranges {
   284  		if int64(num) >= u.value {
   285  			goodUnit = u
   286  			break
   287  		}
   288  	}
   289  
   290  	if goodUnit.value == 1 {
   291  		return fmt.Sprintf("%d%s", num, goodUnit.symbol)
   292  	}
   293  
   294  	res := math.Round(float64(num)/float64(goodUnit.value)*100) / 100
   295  
   296  	return fmt.Sprintf("%.2f%s", res, goodUnit.symbol)
   297  }