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