linkerd2/cli/cmd/alpha_stat.go

306 lines
8.5 KiB
Go

package cmd
import (
"errors"
"fmt"
"io"
"github.com/linkerd/linkerd2/cli/table"
"github.com/linkerd/linkerd2/controller/api/util"
"github.com/linkerd/linkerd2/controller/gen/public"
"github.com/linkerd/linkerd2/pkg/k8s"
"github.com/linkerd/linkerd2/pkg/smimetrics"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/servicemeshinterface/smi-sdk-go/pkg/apis/metrics/v1alpha1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
)
var allowedKinds = map[string]struct{}{
k8s.CronJob: struct{}{},
k8s.DaemonSet: struct{}{},
k8s.Deployment: struct{}{},
k8s.Job: struct{}{},
k8s.Namespace: struct{}{},
k8s.Pod: struct{}{},
k8s.ReplicaSet: struct{}{},
k8s.ReplicationController: struct{}{},
k8s.StatefulSet: struct{}{},
}
type alphaStatOptions struct {
namespace string
toResource string
allNamespaces bool
}
func newCmdAlphaStat() *cobra.Command {
options := alphaStatOptions{
namespace: "default",
}
statCmd := &cobra.Command{
Use: "stat [flags] (RESOURCE)",
Short: "Display traffic stats about one or many resources",
Long: `Display traffic stats about one or many resources
(RESOURCE) can be a resource kind; one of:
* cronjobs
* daemonsets
* deployments
* jobs
* namespaces
* pods
* replicasets
* replicationcontrollers
* statefulsets
or it may be a specific named resource of one of the above kinds.
linkerd alpha stat will return a table of the requested resource or resources
showing the top-line metrics for those resources such as request rate, success
rate, and latency percentiles. These values are measured on the server-side
unless the --to flag is specified.
The --to flag accepts a resource kind or a specific resource and instead
displays the metrics measured on the client side from the root resource to
the to-resource. The root resource must be a specific named resource.
Examples:
# Topline Resource Metrics
linkerd alpha stat -n emojivoto deploy/web
# Topline Resource Metrics for a whole Kind
linkerd alpha stat -n emojivoto deploy
# Outbound edges
linkerd alpha stat -n emojivoto deploy/web --to=deploy
# Outbound to a specific destination
linkerd alpha stat -n emojivoto deploy/web --to=deploy/emoji`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
k8sAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
if err != nil {
return err
}
namespace := options.namespace
if options.allNamespaces {
namespace = corev1.NamespaceAll
}
target, err := util.BuildResource(namespace, args[0])
if err != nil {
return err
}
if target.GetName() != "" && options.allNamespaces {
// Getting a named resource from all-namespaces is not supported.
return errors.New("cannot use --all-namespaces flag with a named target resource")
}
if _, ok := allowedKinds[target.GetType()]; !ok {
return fmt.Errorf("%s is not a supported resource type", target.GetType())
}
kind, err := k8s.PluralResourceNameFromFriendlyName(target.GetType())
if err != nil {
return err
}
name := target.GetName()
toResource := buildToResource("", options.toResource)
// TODO: Lift this requirement once the API supports it.
if toResource != nil && toResource.GetType() != target.GetType() {
return errors.New("the --to resource must have the same kind as the target resource")
}
if name != "" {
if toResource != nil {
metrics, err := smimetrics.GetTrafficMetricsEdgesList(k8sAPI, target.GetNamespace(), kind, name, nil)
if err != nil {
return err
}
renderTrafficMetricsEdgesList(metrics, stdout, toResource, "to")
} else {
metrics, err := smimetrics.GetTrafficMetrics(k8sAPI, target.GetNamespace(), kind, name, nil)
if err != nil {
return err
}
renderTrafficMetrics(metrics, options.allNamespaces, stdout)
}
} else {
if toResource != nil {
return errors.New("the --to flag requires that the target resource name be specified")
}
metrics, err := smimetrics.GetTrafficMetricsList(k8sAPI, target.GetNamespace(), kind, nil)
if err != nil {
return err
}
renderTrafficMetricsList(metrics, options.allNamespaces, stdout)
}
return nil
},
}
statCmd.PersistentFlags().StringVarP(&options.namespace, "namespace", "n", options.namespace, "Namespace of the specified resource")
statCmd.PersistentFlags().StringVar(&options.toResource, "to", options.toResource, "If present, restricts outbound stats to the specified resource name")
statCmd.PersistentFlags().BoolVarP(&options.allNamespaces, "all-namespaces", "A", options.allNamespaces, "Ignore the --namespace flag and fetches data from all namespaces")
return statCmd
}
func buildToResource(namespace, to string) *public.Resource {
toResource, err := util.BuildResource(namespace, to)
if err != nil {
log.Debugf("Invalid to resource: %s", err)
return nil
}
log.Debugf("Using to resource: %v", toResource)
return &toResource
}
func renderTrafficMetrics(metrics *v1alpha1.TrafficMetrics, allNamespaces bool, w io.Writer) {
t := buildTable(false, allNamespaces)
t.Data = []table.Row{metricsToRow(metrics, "")}
t.Render(w)
}
func renderTrafficMetricsList(metrics *v1alpha1.TrafficMetricsList, allNamespaces bool, w io.Writer) {
t := buildTable(false, allNamespaces)
t.Data = []table.Row{}
for _, row := range metrics.Items {
row := row // Copy to satisfy golint.
t.Data = append(t.Data, metricsToRow(row, ""))
}
t.Render(w)
}
func renderTrafficMetricsEdgesList(metrics *v1alpha1.TrafficMetricsList, w io.Writer, toResource *public.Resource, direction string) {
t := buildTable(true, false)
t.Data = []table.Row{}
for _, row := range metrics.Items {
row := row // Copy to satisfy golint.
if string(row.Edge.Direction) != direction {
continue
}
if toResource != nil {
if toResource.GetName() != "" && row.Edge.Resource.Name != toResource.GetName() {
log.Debugf("Skipping edge %v", row.Edge.Resource)
continue
}
}
t.Data = append(t.Data, metricsToRow(row, direction))
}
t.Render(w)
}
func getNumericMetric(metrics *v1alpha1.TrafficMetrics, name string) *resource.Quantity {
for _, m := range metrics.Metrics {
if m.Name == name {
return m.Value
}
}
return resource.NewQuantity(0, resource.DecimalSI)
}
func getNumericMetricWithUnit(metrics *v1alpha1.TrafficMetrics, name string) string {
for _, m := range metrics.Metrics {
if m.Name == name {
value := m.Value.Value()
return fmt.Sprintf("%d%s", value, m.Unit)
}
}
return ""
}
func metricsToRow(metrics *v1alpha1.TrafficMetrics, direction string) []string {
success := getNumericMetric(metrics, "success_count").MilliValue()
failure := getNumericMetric(metrics, "failure_count").MilliValue()
sr := "-"
if success+failure > 0 {
rate := float32(success) / float32(success+failure) * 100
sr = fmt.Sprintf("%.2f%%", rate)
}
rate := float64(success+failure) / 1000.0 / metrics.Window.Seconds()
rps := fmt.Sprintf("%.1frps", rate)
var to string
var from string
if direction == "to" {
to = metrics.Edge.Resource.String()
from = metrics.Resource.Name
} else if direction == "from" {
to = metrics.Resource.Name
from = metrics.Edge.Resource.String()
}
return []string{
metrics.Resource.Namespace,
metrics.Resource.Name,
from,
to,
sr,
rps,
getNumericMetricWithUnit(metrics, "p50_response_latency"),
getNumericMetricWithUnit(metrics, "p90_response_latency"),
getNumericMetricWithUnit(metrics, "p99_response_latency"),
}
}
func buildTable(outbound, allNamespaces bool) table.Table {
columns := []table.Column{
table.Column{
Header: "NAMESPACE",
Width: 9,
Hide: !allNamespaces,
Flexible: true,
LeftAlign: true,
},
table.Column{
Header: "NAME",
Width: 4,
Hide: outbound,
Flexible: true,
LeftAlign: true,
},
table.Column{
Header: "FROM",
Width: 4,
Hide: !outbound,
Flexible: true,
LeftAlign: true,
},
table.Column{
Header: "TO",
Width: 2,
Hide: !outbound,
Flexible: true,
LeftAlign: true,
},
table.Column{
Header: "SUCCESS",
Width: 7,
},
table.Column{
Header: "RPS",
Width: 9,
},
table.Column{
Header: "LATENCY_P50",
Width: 11,
},
table.Column{
Header: "LATENCY_P90",
Width: 11,
},
table.Column{
Header: "LATENCY_P99",
Width: 11,
},
}
t := table.NewTable(columns, []table.Row{})
t.Sort = []int{0, 1} // Sort by namespace, then name.
return t
}