linkerd2/cli/cmd/stat_summary.go

226 lines
6.5 KiB
Go

package cmd
import (
"bytes"
"context"
"errors"
"fmt"
"sort"
"strings"
"text/tabwriter"
"time"
"github.com/prometheus/common/log"
"github.com/runconduit/conduit/controller/api/util"
pb "github.com/runconduit/conduit/controller/gen/public"
"github.com/spf13/cobra"
)
var namespace, resourceType, resourceName string
var outToNamespace, outToType, outToName string
var outFromNamespace, outFromType, outFromName string
var statSummaryCommand = &cobra.Command{
Use: "statsummary [flags] RESOURCETYPE [RESOURCENAME]",
Short: "Display traffic stats about one or many resources",
Long: `Display traffic stats about one or many resources.
Valid resource types include:
* deployment
This command will hide resources that have completed, such as pods that are in the Succeeded or Failed phases.
If no resource name is specified, displays stats about all resources of the specified RESOURCETYPE`,
Example: ` # Get all deployments in the test namespace.
conduit statsummary deployments -a test
# Get the hello1 deployment in the test namespace.
conduit statsummary deployments hello1 -a test`,
Args: cobra.RangeArgs(1, 2),
ValidArgs: []string{"deployment"},
RunE: func(cmd *cobra.Command, args []string) error {
switch len(args) {
case 1:
resourceType = args[0]
case 2:
resourceType = args[0]
resourceName = args[1]
default:
return errors.New("please specify one resource only")
}
client, err := newPublicAPIClient()
if err != nil {
return fmt.Errorf("error creating api client while making stats request: %v", err)
}
output, err := requestStatSummaryFromAPI(client)
if err != nil {
return err
}
_, err = fmt.Print(output)
return err
},
}
func init() {
RootCmd.AddCommand(statSummaryCommand)
statSummaryCommand.PersistentFlags().StringVarP(&namespace, "namespace", "n", "default", "Namespace of the specified resource")
statSummaryCommand.PersistentFlags().StringVarP(&timeWindow, "time-window", "t", "1m", "Stat window (one of: \"10s\", \"1m\", \"10m\", \"1h\")")
statSummaryCommand.PersistentFlags().StringVar(&outToName, "out-to", "", "If present, restricts outbound stats to the specified resource name")
statSummaryCommand.PersistentFlags().StringVar(&outToNamespace, "out-to-namespace", "", "Sets the namespace used to lookup the \"--out-to\" resource; by default the current \"--namespace\" is used")
statSummaryCommand.PersistentFlags().StringVar(&outToType, "out-to-resource", "", "If present, restricts outbound stats to the specified resource type")
statSummaryCommand.PersistentFlags().StringVar(&outFromName, "out-from", "", "If present, restricts outbound stats to the specified resource name")
statSummaryCommand.PersistentFlags().StringVar(&outFromNamespace, "out-from-namespace", "", "Sets the namespace used to lookup the \"--out-from\" resource; by default the current \"--namespace\" is used")
statSummaryCommand.PersistentFlags().StringVar(&outFromType, "out-from-resource", "", "If present, restricts outbound stats to the specified resource type")
}
func requestStatSummaryFromAPI(client pb.ApiClient) (string, error) {
req, err := buildStatSummaryRequest()
if err != nil {
return "", fmt.Errorf("error creating metrics request while making stats request: %v", err)
}
resp, err := client.StatSummary(context.Background(), req)
if err != nil {
return "", fmt.Errorf("error calling stat with request: %v", err)
}
return renderStatSummary(resp), nil
}
func renderStatSummary(resp *pb.StatSummaryResponse) string {
var buffer bytes.Buffer
w := tabwriter.NewWriter(&buffer, 0, 0, padding, ' ', tabwriter.AlignRight)
writeStatTableToBuffer(resp, w)
w.Flush()
// strip left padding on the first column
out := string(buffer.Bytes()[padding:])
out = strings.Replace(out, "\n"+strings.Repeat(" ", padding), "\n", -1)
return out
}
type summaryRow struct {
meshed string
requestRate float64
successRate float64
latencyP50 int64
latencyP99 int64
}
func writeStatTableToBuffer(resp *pb.StatSummaryResponse, w *tabwriter.Writer) {
nameHeader := "NAME"
maxNameLength := len(nameHeader)
stats := make(map[string]*summaryRow)
for _, statTable := range resp.GetOk().StatTables {
table := statTable.GetPodGroup()
for _, r := range table.Rows {
name := r.Resource.Name
if name == "" {
continue
}
if len(name) > maxNameLength {
maxNameLength = len(name)
}
stats[name] = &summaryRow{
meshed: fmt.Sprintf("%d/%d", r.MeshedPodCount, r.TotalPodCount),
requestRate: getRequestRate(*r),
successRate: getSuccessRate(*r),
}
}
}
fmt.Fprintln(w, strings.Join([]string{
nameHeader + strings.Repeat(" ", maxNameLength-len(nameHeader)),
"MESHED",
"IN_RPS",
"IN_SUCCESS",
"IN_LATENCY_P50",
"IN_LATENCY_P99\t", // trailing \t is required to format last column
}, "\t"))
sortedNames := sortStatSummaryKeys(stats)
for _, name := range sortedNames {
fmt.Fprintf(
w,
"%s\t%s\t%.1frps\t%.2f%%\t%dms\t%dms\t\n",
name+strings.Repeat(" ", maxNameLength-len(name)),
stats[name].meshed,
stats[name].requestRate,
stats[name].successRate*100,
stats[name].latencyP50,
stats[name].latencyP99,
)
}
}
func buildStatSummaryRequest() (*pb.StatSummaryRequest, error) {
requestParams := util.StatSummaryRequestParams{
TimeWindow: timeWindow,
ResourceName: resourceName,
ResourceType: resourceType,
Namespace: namespace,
OutToName: outToName,
OutToType: outToType,
OutToNamespace: outToNamespace,
OutFromName: outFromName,
OutFromType: outFromType,
OutFromNamespace: outFromNamespace,
}
return util.BuildStatSummaryRequest(requestParams)
}
func getRequestRate(r pb.StatTable_PodGroup_Row) float64 {
if r.Stats == nil {
return 0.0
}
success := r.Stats.SuccessCount
failure := r.Stats.FailureCount
window, err := util.GetWindowString(r.TimeWindow)
if err != nil {
log.Error(err.Error())
return 0.0
}
windowLength, err := time.ParseDuration(window)
if err != nil {
log.Error(err.Error())
return 0.0
}
return float64(success+failure) / windowLength.Seconds()
}
func getSuccessRate(r pb.StatTable_PodGroup_Row) float64 {
if r.Stats == nil {
return 0.0
}
success := r.Stats.SuccessCount
failure := r.Stats.FailureCount
if success+failure == 0 {
return 0.0
}
return float64(success) / float64(success+failure)
}
func sortStatSummaryKeys(stats map[string]*summaryRow) []string {
var sortedKeys []string
for key := range stats {
sortedKeys = append(sortedKeys, key)
}
sort.Strings(sortedKeys)
return sortedKeys
}