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  }