-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for retries #2038
Add support for retries #2038
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -162,17 +162,17 @@ func newStatOptionsBase() *statOptionsBase { | |
|
||
func (o *statOptionsBase) validateOutputFormat() error { | ||
switch o.outputFormat { | ||
case "table", "json", "": | ||
case "table", "json", "wide", "": | ||
return nil | ||
default: | ||
return fmt.Errorf("--output currently only supports table and json") | ||
return fmt.Errorf("--output currently only supports table, wide, and json") | ||
} | ||
} | ||
|
||
func renderStats(buffer bytes.Buffer, options *statOptionsBase) string { | ||
var out string | ||
switch options.outputFormat { | ||
case "table", "": | ||
case "table", "wide", "": | ||
// strip left padding on the first column | ||
out = string(buffer.Bytes()[padding:]) | ||
out = strings.Replace(out, "\n"+strings.Repeat(" ", padding), "\n", -1) | ||
|
@@ -184,9 +184,7 @@ func renderStats(buffer bytes.Buffer, options *statOptionsBase) string { | |
} | ||
|
||
// getRequestRate calculates request rate from Public API BasicStats. | ||
func getRequestRate(stats *pb.BasicStats, timeWindow string) float64 { | ||
success := stats.SuccessCount | ||
failure := stats.FailureCount | ||
func getRequestRate(success, failure uint64, timeWindow string) float64 { | ||
windowLength, err := time.ParseDuration(timeWindow) | ||
if err != nil { | ||
log.Error(err.Error()) | ||
|
@@ -196,10 +194,7 @@ func getRequestRate(stats *pb.BasicStats, timeWindow string) float64 { | |
} | ||
|
||
// getSuccessRate calculates success rate from Public API BasicStats. | ||
func getSuccessRate(stats *pb.BasicStats) float64 { | ||
success := stats.SuccessCount | ||
failure := stats.FailureCount | ||
|
||
func getSuccessRate(success, failure uint64) float64 { | ||
if success+failure == 0 { | ||
return 0.0 | ||
} | ||
|
@@ -246,25 +241,25 @@ const ( | |
|
||
func newProxyConfigOptions() *proxyConfigOptions { | ||
return &proxyConfigOptions{ | ||
linkerdVersion: version.Version, | ||
proxyImage: defaultDockerRegistry + "/proxy", | ||
initImage: defaultDockerRegistry + "/proxy-init", | ||
dockerRegistry: defaultDockerRegistry, | ||
imagePullPolicy: "IfNotPresent", | ||
inboundPort: 4143, | ||
outboundPort: 4140, | ||
ignoreInboundPorts: nil, | ||
ignoreOutboundPorts: nil, | ||
proxyUID: 2102, | ||
proxyLogLevel: "warn,linkerd2_proxy=info", | ||
proxyBindTimeout: "10s", | ||
proxyAPIPort: 8086, | ||
proxyControlPort: 4190, | ||
proxyMetricsPort: 4191, | ||
proxyOutboundCapacity: map[string]uint{}, | ||
proxyCPURequest: "", | ||
proxyMemoryRequest: "", | ||
tls: "", | ||
linkerdVersion: version.Version, | ||
proxyImage: defaultDockerRegistry + "/proxy", | ||
initImage: defaultDockerRegistry + "/proxy-init", | ||
dockerRegistry: defaultDockerRegistry, | ||
imagePullPolicy: "IfNotPresent", | ||
inboundPort: 4143, | ||
outboundPort: 4140, | ||
ignoreInboundPorts: nil, | ||
ignoreOutboundPorts: nil, | ||
proxyUID: 2102, | ||
proxyLogLevel: "warn,linkerd2_proxy=info", | ||
proxyBindTimeout: "10s", | ||
proxyAPIPort: 8086, | ||
proxyControlPort: 4190, | ||
proxyMetricsPort: 4191, | ||
proxyOutboundCapacity: map[string]uint{}, | ||
proxyCPURequest: "", | ||
proxyMemoryRequest: "", | ||
tls: "", | ||
disableExternalProfiles: false, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think this is a go-fmt issue? when i save this file in vscode it re-aligns the field assignments. |
||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,6 +25,12 @@ type routesOptions struct { | |
dstIsService bool | ||
} | ||
|
||
type routeRowStats struct { | ||
rowStats | ||
actualRequestRate float64 | ||
actualSuccessRate float64 | ||
} | ||
|
||
const defaultRoute = "[UNKNOWN]" | ||
|
||
func newRoutesOptions() *routesOptions { | ||
|
@@ -72,7 +78,7 @@ This command will only display traffic which is sent to a service that has a Ser | |
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, shows outbound stats to the specified resource") | ||
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().StringVarP(&options.outputFormat, "output", "o", options.outputFormat, "Output format; currently only \"table\" (default) and \"json\" are supported") | ||
cmd.PersistentFlags().StringVarP(&options.outputFormat, "output", "o", options.outputFormat, "Output format; currently only \"table\" (default), \"wide\", and \"json\" are supported") | ||
|
||
return cmd | ||
} | ||
|
@@ -99,22 +105,26 @@ func renderRouteStats(resp *pb.TopRoutesResponse, options *routesOptions) string | |
} | ||
|
||
func writeRouteStatsToBuffer(resp *pb.TopRoutesResponse, w *tabwriter.Writer, options *routesOptions) { | ||
table := make([]*rowStats, 0) | ||
table := make([]*routeRowStats, 0) | ||
|
||
for _, r := range resp.GetRoutes().Rows { | ||
if r.Stats != nil { | ||
route := r.GetRoute() | ||
if route == "" { | ||
route = defaultRoute | ||
} | ||
table = append(table, &rowStats{ | ||
route: route, | ||
dst: r.GetAuthority(), | ||
requestRate: getRequestRate(r.Stats, r.TimeWindow), | ||
successRate: getSuccessRate(r.Stats), | ||
latencyP50: r.Stats.LatencyMsP50, | ||
latencyP95: r.Stats.LatencyMsP95, | ||
latencyP99: r.Stats.LatencyMsP99, | ||
table = append(table, &routeRowStats{ | ||
rowStats: rowStats{ | ||
route: route, | ||
dst: r.GetAuthority(), | ||
requestRate: getRequestRate(r.Stats.GetSuccessCount(), r.Stats.GetFailureCount(), r.TimeWindow), | ||
successRate: getSuccessRate(r.Stats.GetSuccessCount(), r.Stats.GetFailureCount()), | ||
latencyP50: r.Stats.LatencyMsP50, | ||
latencyP95: r.Stats.LatencyMsP95, | ||
latencyP99: r.Stats.LatencyMsP99, | ||
}, | ||
actualRequestRate: getRequestRate(r.Stats.GetActualSuccessCount(), r.Stats.GetActualFailureCount(), r.TimeWindow), | ||
actualSuccessRate: getSuccessRate(r.Stats.GetActualSuccessCount(), r.Stats.GetActualFailureCount()), | ||
}) | ||
} | ||
} | ||
|
@@ -124,18 +134,18 @@ func writeRouteStatsToBuffer(resp *pb.TopRoutesResponse, w *tabwriter.Writer, op | |
}) | ||
|
||
switch options.outputFormat { | ||
case "table", "": | ||
case "table", "wide", "": | ||
if len(table) == 0 { | ||
fmt.Fprintln(os.Stderr, "No traffic found. Does the service have a service profile? You can create one with the `linkerd profile` command.") | ||
os.Exit(0) | ||
} | ||
printRouteTable(table, w, options) | ||
case "json": | ||
printRouteJSON(table, w) | ||
printRouteJSON(table, w, options) | ||
} | ||
} | ||
|
||
func printRouteTable(stats []*rowStats, w *tabwriter.Writer, options *routesOptions) { | ||
func printRouteTable(stats []*routeRowStats, w *tabwriter.Writer, options *routesOptions) { | ||
// template for left-aligning the route column | ||
routeTemplate := fmt.Sprintf("%%-%ds", routeWidth(stats)) | ||
|
||
|
@@ -147,16 +157,37 @@ func printRouteTable(stats []*rowStats, w *tabwriter.Writer, options *routesOpti | |
headers := []string{ | ||
fmt.Sprintf(routeTemplate, "ROUTE"), | ||
authorityColumn, | ||
"SUCCESS", | ||
"RPS", | ||
} | ||
if options.toResource == "" || options.outputFormat != "wide" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. consider: outputActual := options.toResource != "" && options.outputFormat == "wide" ... and then use |
||
headers = append(headers, []string{ | ||
"SUCCESS", | ||
"RPS", | ||
}...) | ||
} else { | ||
headers = append(headers, []string{ | ||
"EFFECTIVE_SUCCESS", | ||
"EFFECTIVE_RPS", | ||
"ACTUAL_SUCCESS", | ||
"ACTUAL_RPS", | ||
}...) | ||
} | ||
|
||
headers = append(headers, []string{ | ||
"LATENCY_P50", | ||
"LATENCY_P95", | ||
"LATENCY_P99\t", // trailing \t is required to format last column | ||
} | ||
}...) | ||
|
||
fmt.Fprintln(w, strings.Join(headers, "\t")) | ||
|
||
templateString := routeTemplate + "\t%s\t%.2f%%\t%.1frps\t%dms\t%dms\t%dms\t\n" | ||
// route, success rate, rps | ||
templateString := routeTemplate + "\t%s\t%.2f%%\t%.1frps\t" | ||
if options.toResource != "" && options.outputFormat == "wide" { | ||
// actual success rate, actual rps | ||
templateString = templateString + "%.2f%%\t%.1frps\t" | ||
} | ||
// p50, p95, p99 | ||
templateString = templateString + "%dms\t%dms\t%dms\t\n" | ||
|
||
for _, row := range stats { | ||
|
||
|
@@ -166,30 +197,44 @@ func printRouteTable(stats []*rowStats, w *tabwriter.Writer, options *routesOpti | |
authorityValue = segments[0] | ||
} | ||
|
||
fmt.Fprintf(w, templateString, | ||
values := []interface{}{ | ||
row.route, | ||
authorityValue, | ||
row.successRate*100, | ||
row.successRate * 100, | ||
row.requestRate, | ||
} | ||
if options.toResource != "" && options.outputFormat == "wide" { | ||
values = append(values, []interface{}{ | ||
row.actualSuccessRate * 100, | ||
row.actualRequestRate, | ||
}...) | ||
} | ||
values = append(values, []interface{}{ | ||
row.latencyP50, | ||
row.latencyP95, | ||
row.latencyP99, | ||
) | ||
}...) | ||
|
||
fmt.Fprintf(w, templateString, values...) | ||
} | ||
} | ||
|
||
// Using pointers there where the value is NA and the corresponding json is null | ||
type jsonRouteStats struct { | ||
Route string `json:"route"` | ||
Authority string `json:"authority"` | ||
Success *float64 `json:"success"` | ||
Rps *float64 `json:"rps"` | ||
LatencyMSp50 *uint64 `json:"latency_ms_p50"` | ||
LatencyMSp95 *uint64 `json:"latency_ms_p95"` | ||
LatencyMSp99 *uint64 `json:"latency_ms_p99"` | ||
Route string `json:"route"` | ||
Authority string `json:"authority"` | ||
Success *float64 `json:"success,omitempty"` | ||
Rps *float64 `json:"rps,omitempty"` | ||
EffectiveSuccess *float64 `json:"effective_success,omitempty"` | ||
EffectiveRps *float64 `json:"effective_rps,omitempty"` | ||
ActualSuccess *float64 `json:"actual_success,omitempty"` | ||
ActualRps *float64 `json:"actual_rps,omitempty"` | ||
LatencyMSp50 *uint64 `json:"latency_ms_p50"` | ||
LatencyMSp95 *uint64 `json:"latency_ms_p95"` | ||
LatencyMSp99 *uint64 `json:"latency_ms_p99"` | ||
} | ||
|
||
func printRouteJSON(stats []*rowStats, w *tabwriter.Writer) { | ||
func printRouteJSON(stats []*routeRowStats, w *tabwriter.Writer, options *routesOptions) { | ||
// avoid nil initialization so that if there are not stats it gets marshalled as an empty array vs null | ||
entries := []*jsonRouteStats{} | ||
for _, row := range stats { | ||
|
@@ -199,8 +244,15 @@ func printRouteJSON(stats []*rowStats, w *tabwriter.Writer) { | |
} | ||
|
||
entry.Authority = row.dst | ||
entry.Success = &row.successRate | ||
entry.Rps = &row.requestRate | ||
if options.toResource != "" { | ||
entry.EffectiveSuccess = &row.successRate | ||
entry.EffectiveRps = &row.requestRate | ||
entry.ActualSuccess = &row.actualSuccessRate | ||
entry.ActualRps = &row.actualRequestRate | ||
} else { | ||
entry.Success = &row.successRate | ||
entry.Rps = &row.requestRate | ||
} | ||
entry.LatencyMSp50 = &row.latencyP50 | ||
entry.LatencyMSp95 = &row.latencyP95 | ||
entry.LatencyMSp99 = &row.latencyP99 | ||
|
@@ -277,7 +329,7 @@ func buildTopRoutesTo(toResource pb.Resource) (string, error) { | |
} | ||
|
||
// returns the length of the longest route name | ||
func routeWidth(stats []*rowStats) int { | ||
func routeWidth(stats []*routeRowStats) int { | ||
maxLength := len(defaultRoute) | ||
for _, row := range stats { | ||
if len(row.route) > maxLength { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it possible to implement the EFFECTIVE/ACTUAL columns for the
linkerd stat
command? if not, it's a bit confusing thatstat -o wide
works but is a no-op.related, if
routes -o wide
only works with--to
, consider validating that, or maybe documenting it in the help.