mirror of https://github.com/linkerd/linkerd2.git
384 lines
11 KiB
Go
384 lines
11 KiB
Go
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"text/tabwriter"
|
|
"time"
|
|
|
|
"github.com/runconduit/conduit/controller/api/util"
|
|
pb "github.com/runconduit/conduit/controller/gen/public"
|
|
"github.com/runconduit/conduit/pkg/k8s"
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
type statOptions struct {
|
|
namespace string
|
|
timeWindow string
|
|
toNamespace string
|
|
toResource string
|
|
fromNamespace string
|
|
fromResource string
|
|
allNamespaces bool
|
|
}
|
|
|
|
func newStatOptions() *statOptions {
|
|
return &statOptions{
|
|
namespace: "default",
|
|
timeWindow: "1m",
|
|
toNamespace: "",
|
|
toResource: "",
|
|
fromNamespace: "",
|
|
fromResource: "",
|
|
allNamespaces: false,
|
|
}
|
|
}
|
|
|
|
func newCmdStat() *cobra.Command {
|
|
options := newStatOptions()
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "stat [flags] (RESOURCE)",
|
|
Short: "Display traffic stats about one or many resources",
|
|
Long: `Display traffic stats about one or many resources.
|
|
|
|
The RESOURCE argument specifies the target resource(s) to aggregate stats over:
|
|
(TYPE [NAME] | TYPE/NAME)
|
|
|
|
Examples:
|
|
* deploy
|
|
* deploy/my-deploy
|
|
* deploy my-deploy
|
|
* ns/my-ns
|
|
* all
|
|
|
|
Valid resource types include:
|
|
|
|
* deployments
|
|
* namespaces
|
|
* pods
|
|
* replicationcontrollers
|
|
* services (only supported if a "--from" is also specified, or as a "--to")
|
|
* all (all resource types, not supported in --from or --to)
|
|
|
|
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 stat deployments -n test
|
|
|
|
# Get the hello1 replication controller in the test namespace.
|
|
conduit stat replicationcontrollers hello1 -n test
|
|
|
|
# Get all namespaces.
|
|
conduit stat namespaces
|
|
|
|
# Get all inbound stats to the web deployment.
|
|
conduit stat deploy/web
|
|
|
|
# Get all pods in all namespaces that call the hello1 deployment in the test namesapce.
|
|
conduit stat pods --to deploy/hello1 --to-namespace test --all-namespaces
|
|
|
|
# Get all pods in all namespaces that call the hello1 service in the test namesapce.
|
|
conduit stat pods --to svc/hello1 --to-namespace test --all-namespaces
|
|
|
|
# Get all services in all namespaces that receive calls from hello1 deployment in the test namesapce.
|
|
conduit stat services --from deploy/hello1 --from-namespace test --all-namespaces`,
|
|
Args: cobra.RangeArgs(1, 2),
|
|
ValidArgs: util.ValidTargets,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
client, err := newPublicAPIClient()
|
|
if err != nil {
|
|
return fmt.Errorf("error creating api client while making stats request: %v", err)
|
|
}
|
|
|
|
req, err := buildStatSummaryRequest(args, options)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating metrics request while making stats request: %v", err)
|
|
}
|
|
|
|
output, err := requestStatsFromAPI(client, req, options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = fmt.Print(output)
|
|
|
|
return err
|
|
},
|
|
}
|
|
|
|
cmd.PersistentFlags().StringVarP(&options.namespace, "namespace", "n", options.namespace, "Namespace of the specified resource")
|
|
cmd.PersistentFlags().StringVarP(&options.timeWindow, "time-window", "t", options.timeWindow, "Stat window (for example: \"10s\", \"1m\", \"10m\", \"1h\")")
|
|
cmd.PersistentFlags().StringVar(&options.toResource, "to", options.toResource, "If present, restricts outbound stats to the specified resource name")
|
|
cmd.PersistentFlags().StringVar(&options.toNamespace, "to-namespace", options.toNamespace, "Sets the namespace used to lookup the \"--to\" resource; by default the current \"--namespace\" is used")
|
|
cmd.PersistentFlags().StringVar(&options.fromResource, "from", options.fromResource, "If present, restricts outbound stats from the specified resource name")
|
|
cmd.PersistentFlags().StringVar(&options.fromNamespace, "from-namespace", options.fromNamespace, "Sets the namespace used from lookup the \"--from\" resource; by default the current \"--namespace\" is used")
|
|
cmd.PersistentFlags().BoolVar(&options.allNamespaces, "all-namespaces", options.allNamespaces, "If present, returns stats across all namespaces, ignoring the \"--namespace\" flag")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func requestStatsFromAPI(client pb.ApiClient, req *pb.StatSummaryRequest, options *statOptions) (string, error) {
|
|
resp, err := client.StatSummary(context.Background(), req)
|
|
if err != nil {
|
|
return "", fmt.Errorf("StatSummary API error: %v", err)
|
|
}
|
|
if e := resp.GetError(); e != nil {
|
|
return "", fmt.Errorf("StatSummary API response error: %v", e.Error)
|
|
}
|
|
|
|
return renderStats(resp, req.Selector.Resource.Type, options), nil
|
|
}
|
|
|
|
func renderStats(resp *pb.StatSummaryResponse, resourceType string, options *statOptions) string {
|
|
var buffer bytes.Buffer
|
|
w := tabwriter.NewWriter(&buffer, 0, 0, padding, ' ', tabwriter.AlignRight)
|
|
writeStatsToBuffer(resp, resourceType, w, options)
|
|
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
|
|
}
|
|
|
|
const padding = 3
|
|
|
|
type rowStats struct {
|
|
requestRate float64
|
|
successRate float64
|
|
secured float64
|
|
latencyP50 uint64
|
|
latencyP95 uint64
|
|
latencyP99 uint64
|
|
}
|
|
|
|
type row struct {
|
|
meshed string
|
|
*rowStats
|
|
}
|
|
|
|
var (
|
|
nameHeader = "NAME"
|
|
namespaceHeader = "NAMESPACE"
|
|
)
|
|
|
|
func writeStatsToBuffer(resp *pb.StatSummaryResponse, reqResourceType string, w *tabwriter.Writer, options *statOptions) {
|
|
maxNameLength := len(nameHeader)
|
|
maxNamespaceLength := len(namespaceHeader)
|
|
statTables := make(map[string]map[string]*row)
|
|
|
|
for _, statTable := range resp.GetOk().StatTables {
|
|
table := statTable.GetPodGroup()
|
|
|
|
for _, r := range table.Rows {
|
|
name := r.Resource.Name
|
|
nameWithPrefix := name
|
|
if reqResourceType == k8s.All {
|
|
nameWithPrefix = getNamePrefix(r.Resource.Type) + nameWithPrefix
|
|
}
|
|
|
|
namespace := r.Resource.Namespace
|
|
key := fmt.Sprintf("%s/%s", namespace, name)
|
|
resourceKey := r.Resource.Type
|
|
|
|
if _, ok := statTables[resourceKey]; !ok {
|
|
statTables[resourceKey] = make(map[string]*row)
|
|
}
|
|
|
|
if len(nameWithPrefix) > maxNameLength {
|
|
maxNameLength = len(nameWithPrefix)
|
|
}
|
|
|
|
if len(namespace) > maxNamespaceLength {
|
|
maxNamespaceLength = len(namespace)
|
|
}
|
|
|
|
statTables[resourceKey][key] = &row{
|
|
meshed: fmt.Sprintf("%d/%d", r.MeshedPodCount, r.RunningPodCount),
|
|
}
|
|
|
|
if r.Stats != nil {
|
|
statTables[resourceKey][key].rowStats = &rowStats{
|
|
requestRate: getRequestRate(*r),
|
|
successRate: getSuccessRate(*r),
|
|
secured: getPercentSecured(*r),
|
|
latencyP50: r.Stats.LatencyMsP50,
|
|
latencyP95: r.Stats.LatencyMsP95,
|
|
latencyP99: r.Stats.LatencyMsP99,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(statTables) == 0 {
|
|
fmt.Fprintln(os.Stderr, "No traffic found.")
|
|
os.Exit(0)
|
|
}
|
|
|
|
switch reqResourceType {
|
|
case k8s.All:
|
|
firstDisplayedStat := true // don't print a newline before the first stat
|
|
for _, resourceType := range k8s.StatAllResourceTypes {
|
|
if stats, ok := statTables[resourceType]; ok {
|
|
if !firstDisplayedStat {
|
|
fmt.Fprint(w, "\n")
|
|
}
|
|
firstDisplayedStat = false
|
|
printStatTable(stats, resourceType, w, maxNameLength, maxNamespaceLength, options)
|
|
}
|
|
}
|
|
default:
|
|
if stats, ok := statTables[reqResourceType]; ok {
|
|
printStatTable(stats, "", w, maxNameLength, maxNamespaceLength, options)
|
|
}
|
|
}
|
|
}
|
|
|
|
func printStatTable(stats map[string]*row, resourceType string, w *tabwriter.Writer, maxNameLength int, maxNamespaceLength int, options *statOptions) {
|
|
headers := make([]string, 0)
|
|
if options.allNamespaces {
|
|
headers = append(headers,
|
|
namespaceHeader+strings.Repeat(" ", maxNamespaceLength-len(namespaceHeader)))
|
|
}
|
|
headers = append(headers, []string{
|
|
nameHeader + strings.Repeat(" ", maxNameLength-len(nameHeader)),
|
|
"MESHED",
|
|
"SUCCESS",
|
|
"RPS",
|
|
"LATENCY_P50",
|
|
"LATENCY_P95",
|
|
"LATENCY_P99",
|
|
"SECURED\t", // trailing \t is required to format last column
|
|
}...)
|
|
|
|
fmt.Fprintln(w, strings.Join(headers, "\t"))
|
|
|
|
namePrefix := getNamePrefix(resourceType)
|
|
|
|
sortedKeys := sortStatsKeys(stats)
|
|
for _, key := range sortedKeys {
|
|
parts := strings.Split(key, "/")
|
|
namespace := parts[0]
|
|
name := namePrefix + parts[1]
|
|
values := make([]interface{}, 0)
|
|
templateString := "%s\t%s\t%.2f%%\t%.1frps\t%dms\t%dms\t%dms\t%.f%%\t\n"
|
|
templateStringEmpty := "%s\t%s\t-\t-\t-\t-\t-\t-\t\n"
|
|
|
|
if options.allNamespaces {
|
|
values = append(values,
|
|
namespace+strings.Repeat(" ", maxNamespaceLength-len(namespace)))
|
|
templateString = "%s\t" + templateString
|
|
templateStringEmpty = "%s\t" + templateStringEmpty
|
|
}
|
|
values = append(values, []interface{}{
|
|
name + strings.Repeat(" ", maxNameLength-len(name)),
|
|
stats[key].meshed,
|
|
}...)
|
|
|
|
if stats[key].rowStats != nil {
|
|
values = append(values, []interface{}{
|
|
stats[key].successRate * 100,
|
|
stats[key].requestRate,
|
|
stats[key].latencyP50,
|
|
stats[key].latencyP95,
|
|
stats[key].latencyP99,
|
|
stats[key].secured * 100,
|
|
}...)
|
|
|
|
fmt.Fprintf(w, templateString, values...)
|
|
} else {
|
|
fmt.Fprintf(w, templateStringEmpty, values...)
|
|
}
|
|
}
|
|
}
|
|
|
|
func getNamePrefix(resourceType string) string {
|
|
if resourceType == "" {
|
|
return ""
|
|
} else {
|
|
return k8s.ShortNameFromCanonicalKubernetesName(resourceType) + "/"
|
|
}
|
|
}
|
|
|
|
func buildStatSummaryRequest(resource []string, options *statOptions) (*pb.StatSummaryRequest, error) {
|
|
target, err := util.BuildResource(options.namespace, resource...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var toRes, fromRes pb.Resource
|
|
if options.toResource != "" {
|
|
toRes, err = util.BuildResource(options.toNamespace, options.toResource)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if options.fromResource != "" {
|
|
fromRes, err = util.BuildResource(options.fromNamespace, options.fromResource)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
requestParams := util.StatSummaryRequestParams{
|
|
TimeWindow: options.timeWindow,
|
|
ResourceName: target.Name,
|
|
ResourceType: target.Type,
|
|
Namespace: options.namespace,
|
|
ToName: toRes.Name,
|
|
ToType: toRes.Type,
|
|
ToNamespace: options.toNamespace,
|
|
FromName: fromRes.Name,
|
|
FromType: fromRes.Type,
|
|
FromNamespace: options.fromNamespace,
|
|
AllNamespaces: options.allNamespaces,
|
|
}
|
|
|
|
return util.BuildStatSummaryRequest(requestParams)
|
|
}
|
|
|
|
func getRequestRate(r pb.StatTable_PodGroup_Row) float64 {
|
|
success := r.Stats.SuccessCount
|
|
failure := r.Stats.FailureCount
|
|
windowLength, err := time.ParseDuration(r.TimeWindow)
|
|
if err != nil {
|
|
log.Error(err.Error())
|
|
return 0.0
|
|
}
|
|
return float64(success+failure) / windowLength.Seconds()
|
|
}
|
|
|
|
func getSuccessRate(r pb.StatTable_PodGroup_Row) float64 {
|
|
success := r.Stats.SuccessCount
|
|
failure := r.Stats.FailureCount
|
|
|
|
if success+failure == 0 {
|
|
return 0.0
|
|
}
|
|
return float64(success) / float64(success+failure)
|
|
}
|
|
|
|
func getPercentSecured(r pb.StatTable_PodGroup_Row) float64 {
|
|
reqTotal := r.Stats.SuccessCount + r.Stats.FailureCount
|
|
if reqTotal == 0 {
|
|
return 0.0
|
|
}
|
|
return float64(r.Stats.TlsRequestCount) / float64(reqTotal)
|
|
}
|
|
|
|
func sortStatsKeys(stats map[string]*row) []string {
|
|
var sortedKeys []string
|
|
for key := range stats {
|
|
sortedKeys = append(sortedKeys, key)
|
|
}
|
|
sort.Strings(sortedKeys)
|
|
return sortedKeys
|
|
}
|