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