linkerd2/cli/cmd/alpha_stat.go

291 lines
7.8 KiB
Go

package cmd
import (
"errors"
"fmt"
"io"
"time"
"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"
"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
}
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
}
target, err := util.BuildResource(options.namespace, args[0])
if err != nil {
return err
}
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.namespace, 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)
} else {
metrics, err := smimetrics.GetTrafficMetrics(k8sAPI, target.GetNamespace(), kind, name, nil)
if err != nil {
return err
}
renderTrafficMetrics(metrics, 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, 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")
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 *smimetrics.TrafficMetrics, w io.Writer) {
t := buildTable(false)
t.Data = []table.Row{metricsToRow(metrics, false)}
t.Render(w)
}
func renderTrafficMetricsList(metrics *smimetrics.TrafficMetricsList, w io.Writer) {
t := buildTable(false)
t.Data = []table.Row{}
for _, row := range metrics.Items {
row := row // Copy to satisfy golint.
t.Data = append(t.Data, metricsToRow(&row, false))
}
t.Render(w)
}
func renderTrafficMetricsEdgesList(metrics *smimetrics.TrafficMetricsList, w io.Writer, toResource *public.Resource) {
outbound := toResource != nil
t := buildTable(outbound)
t.Data = []table.Row{}
for _, row := range metrics.Items {
row := row // Copy to satisfy golint.
if row.Edge.Direction != "to" {
continue
}
if toResource != nil && toResource.GetName() != "" &&
(row.Edge.Resource.Name != toResource.GetName() || row.Edge.Resource.Namespace != toResource.GetNamespace()) {
log.Debugf("Skipping edge %v", row.Edge.Resource)
continue
}
t.Data = append(t.Data, metricsToRow(&row, outbound))
}
t.Render(w)
}
func getNumericMetric(metrics *smimetrics.TrafficMetrics, name string) *resource.Quantity {
for _, m := range metrics.Metrics {
if m.Name == name {
quantity, err := resource.ParseQuantity(m.Value)
if err != nil {
return resource.NewQuantity(0, resource.DecimalSI)
}
return &quantity
}
}
return resource.NewQuantity(0, resource.DecimalSI)
}
func getNumericMetricWithUnit(metrics *smimetrics.TrafficMetrics, name string) string {
for _, m := range metrics.Metrics {
if m.Name == name {
quantity, err := resource.ParseQuantity(m.Value)
if err != nil {
return ""
}
value := quantity.Value()
return fmt.Sprintf("%d%s", value, m.Unit)
}
}
return ""
}
func metricsToRow(metrics *smimetrics.TrafficMetrics, outbound bool) []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)
}
rps := "-"
window, err := time.ParseDuration(metrics.Window)
if err == nil {
rate := float64(success+failure) / 1000.0 / window.Seconds()
rps = fmt.Sprintf("%.1frps", rate)
}
var to string
if outbound {
to = metrics.Edge.Resource.Name
}
return []string{
metrics.Resource.Name, // Name
metrics.Resource.Name, // From
to, // To
sr,
rps,
getNumericMetricWithUnit(metrics, "p50_response_latency"),
getNumericMetricWithUnit(metrics, "p90_response_latency"),
getNumericMetricWithUnit(metrics, "p99_response_latency"),
}
}
func buildTable(outbound bool) table.Table {
columns := []table.Column{
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,
},
}
return table.NewTable(columns, []table.Row{})
}