mirror of https://github.com/linkerd/linkerd2.git
226 lines
6.5 KiB
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
|
|
}
|