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 }