mirror of https://github.com/linkerd/linkerd2.git
686 lines
18 KiB
Go
686 lines
18 KiB
Go
package util
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
pb "github.com/linkerd/linkerd2/controller/gen/public"
|
|
"github.com/linkerd/linkerd2/pkg/addr"
|
|
"github.com/linkerd/linkerd2/pkg/k8s"
|
|
log "github.com/sirupsen/logrus"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
"k8s.io/api/core/v1"
|
|
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
)
|
|
|
|
/*
|
|
Shared utilities for interacting with the controller public api
|
|
*/
|
|
|
|
var (
|
|
defaultMetricTimeWindow = "1m"
|
|
|
|
// ValidTargets specifies resource types allowed as a target:
|
|
// target resource on an inbound query
|
|
// target resource on an outbound 'to' query
|
|
// destination resource on an outbound 'from' query
|
|
ValidTargets = []string{
|
|
k8s.Authority,
|
|
k8s.Deployment,
|
|
k8s.Namespace,
|
|
k8s.Pod,
|
|
k8s.ReplicationController,
|
|
}
|
|
|
|
// ValidTapDestinations specifies resource types allowed as a tap destination:
|
|
// destination resource on an outbound 'to' query
|
|
ValidTapDestinations = []string{
|
|
k8s.Deployment,
|
|
k8s.Job,
|
|
k8s.Namespace,
|
|
k8s.Pod,
|
|
k8s.ReplicationController,
|
|
k8s.Service,
|
|
}
|
|
)
|
|
|
|
// StatsBaseRequestParams contains parameters that are used to build requests
|
|
// for metrics data. This includes requests to StatSummary and TopRoutes.
|
|
type StatsBaseRequestParams struct {
|
|
TimeWindow string
|
|
Namespace string
|
|
ResourceType string
|
|
ResourceName string
|
|
AllNamespaces bool
|
|
}
|
|
|
|
// StatsSummaryRequestParams contains parameters that are used to build
|
|
// StatSummary requests.
|
|
type StatsSummaryRequestParams struct {
|
|
StatsBaseRequestParams
|
|
ToNamespace string
|
|
ToType string
|
|
ToName string
|
|
FromNamespace string
|
|
FromType string
|
|
FromName string
|
|
SkipStats bool
|
|
}
|
|
|
|
// TopRoutesRequestParams contains parameters that are used to build TopRoutes
|
|
// requests.
|
|
type TopRoutesRequestParams struct {
|
|
StatsBaseRequestParams
|
|
To string
|
|
ToAll bool
|
|
}
|
|
|
|
// TapRequestParams contains parameters that are used to build a
|
|
// TapByResourceRequest.
|
|
type TapRequestParams struct {
|
|
Resource string
|
|
Namespace string
|
|
ToResource string
|
|
ToNamespace string
|
|
MaxRps float32
|
|
Scheme string
|
|
Method string
|
|
Authority string
|
|
Path string
|
|
}
|
|
|
|
// GRPCError generates a gRPC error code, as defined in
|
|
// google.golang.org/grpc/status.
|
|
// If the error is nil or already a gRPC error, return the error.
|
|
// If the error is of type k8s.io/apimachinery/pkg/apis/meta/v1#StatusReason,
|
|
// attempt to map the reason to a gRPC error.
|
|
func GRPCError(err error) error {
|
|
if err != nil && status.Code(err) == codes.Unknown {
|
|
code := codes.Internal
|
|
|
|
switch k8sErrors.ReasonForError(err) {
|
|
case metav1.StatusReasonUnknown:
|
|
code = codes.Unknown
|
|
case metav1.StatusReasonUnauthorized, metav1.StatusReasonForbidden:
|
|
code = codes.PermissionDenied
|
|
case metav1.StatusReasonNotFound:
|
|
code = codes.NotFound
|
|
case metav1.StatusReasonAlreadyExists:
|
|
code = codes.AlreadyExists
|
|
case metav1.StatusReasonInvalid:
|
|
code = codes.InvalidArgument
|
|
case metav1.StatusReasonExpired:
|
|
code = codes.DeadlineExceeded
|
|
case metav1.StatusReasonServiceUnavailable:
|
|
code = codes.Unavailable
|
|
}
|
|
|
|
err = status.Error(code, err.Error())
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// BuildStatSummaryRequest builds a Public API StatSummaryRequest from a
|
|
// StatsSummaryRequestParams.
|
|
func BuildStatSummaryRequest(p StatsSummaryRequestParams) (*pb.StatSummaryRequest, error) {
|
|
window := defaultMetricTimeWindow
|
|
if p.TimeWindow != "" {
|
|
_, err := time.ParseDuration(p.TimeWindow)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
window = p.TimeWindow
|
|
}
|
|
|
|
if p.AllNamespaces && p.ResourceName != "" {
|
|
return nil, errors.New("stats for a resource cannot be retrieved by name across all namespaces")
|
|
}
|
|
|
|
targetNamespace := p.Namespace
|
|
if p.AllNamespaces {
|
|
targetNamespace = ""
|
|
} else if p.Namespace == "" {
|
|
targetNamespace = v1.NamespaceDefault
|
|
}
|
|
|
|
resourceType, err := k8s.CanonicalResourceNameFromFriendlyName(p.ResourceType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
statRequest := &pb.StatSummaryRequest{
|
|
Selector: &pb.ResourceSelection{
|
|
Resource: &pb.Resource{
|
|
Namespace: targetNamespace,
|
|
Name: p.ResourceName,
|
|
Type: resourceType,
|
|
},
|
|
},
|
|
TimeWindow: window,
|
|
SkipStats: p.SkipStats,
|
|
}
|
|
|
|
if p.ToName != "" || p.ToType != "" || p.ToNamespace != "" {
|
|
if p.ToNamespace == "" {
|
|
p.ToNamespace = targetNamespace
|
|
}
|
|
if p.ToType == "" {
|
|
p.ToType = resourceType
|
|
}
|
|
|
|
toType, err := k8s.CanonicalResourceNameFromFriendlyName(p.ToType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
toResource := pb.StatSummaryRequest_ToResource{
|
|
ToResource: &pb.Resource{
|
|
Namespace: p.ToNamespace,
|
|
Type: toType,
|
|
Name: p.ToName,
|
|
},
|
|
}
|
|
statRequest.Outbound = &toResource
|
|
}
|
|
|
|
if p.FromName != "" || p.FromType != "" || p.FromNamespace != "" {
|
|
if p.FromNamespace == "" {
|
|
p.FromNamespace = targetNamespace
|
|
}
|
|
if p.FromType == "" {
|
|
p.FromType = resourceType
|
|
}
|
|
|
|
fromType, err := validateFromResourceType(p.FromType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fromResource := pb.StatSummaryRequest_FromResource{
|
|
FromResource: &pb.Resource{
|
|
Namespace: p.FromNamespace,
|
|
Type: fromType,
|
|
Name: p.FromName,
|
|
},
|
|
}
|
|
statRequest.Outbound = &fromResource
|
|
}
|
|
|
|
return statRequest, nil
|
|
}
|
|
|
|
// BuildTopRoutesRequest builds a Public API TopRoutesRequest from a
|
|
// TopRoutesRequestParams.
|
|
func BuildTopRoutesRequest(p TopRoutesRequestParams) (*pb.TopRoutesRequest, error) {
|
|
window := defaultMetricTimeWindow
|
|
if p.TimeWindow != "" {
|
|
_, err := time.ParseDuration(p.TimeWindow)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
window = p.TimeWindow
|
|
}
|
|
|
|
if p.AllNamespaces && p.ResourceName != "" {
|
|
return nil, errors.New("routes for a resource cannot be retrieved by name across all namespaces")
|
|
}
|
|
|
|
targetNamespace := p.Namespace
|
|
if p.AllNamespaces {
|
|
targetNamespace = ""
|
|
} else if p.Namespace == "" {
|
|
targetNamespace = v1.NamespaceDefault
|
|
}
|
|
|
|
resourceType, err := k8s.CanonicalResourceNameFromFriendlyName(p.ResourceType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
topRoutesRequest := &pb.TopRoutesRequest{
|
|
Selector: &pb.ResourceSelection{
|
|
Resource: &pb.Resource{
|
|
Namespace: targetNamespace,
|
|
Name: p.ResourceName,
|
|
Type: resourceType,
|
|
},
|
|
},
|
|
TimeWindow: window,
|
|
}
|
|
|
|
if p.To != "" && p.ToAll {
|
|
return nil, errors.New("ToService and ToAll are mutually exclusive")
|
|
}
|
|
|
|
if p.To != "" {
|
|
topRoutesRequest.Outbound = &pb.TopRoutesRequest_ToAuthority{
|
|
ToAuthority: p.To,
|
|
}
|
|
}
|
|
|
|
if p.ToAll {
|
|
topRoutesRequest.Outbound = &pb.TopRoutesRequest_ToAll{
|
|
ToAll: &pb.Empty{},
|
|
}
|
|
}
|
|
|
|
return topRoutesRequest, nil
|
|
}
|
|
|
|
// An authority can only receive traffic, not send it, so it can't be a --from
|
|
func validateFromResourceType(resourceType string) (string, error) {
|
|
name, err := k8s.CanonicalResourceNameFromFriendlyName(resourceType)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if name == k8s.Authority {
|
|
return "", errors.New("cannot query traffic --from an authority")
|
|
}
|
|
return name, nil
|
|
}
|
|
|
|
// BuildResource parses input strings, typically from CLI flags, to build a
|
|
// Resource object for use in the protobuf API.
|
|
// It's the same as BuildResources but only admits one arg and only returns one resource
|
|
func BuildResource(namespace, arg string) (pb.Resource, error) {
|
|
res, err := BuildResources(namespace, []string{arg})
|
|
if err != nil {
|
|
return pb.Resource{}, err
|
|
}
|
|
|
|
return res[0], err
|
|
}
|
|
|
|
// BuildResources parses input strings, typically from CLI flags, to build a
|
|
// slice of Resource objects for use in the protobuf API.
|
|
// It's the same as BuildResource but it admits any number of args and returns multiple resources
|
|
func BuildResources(namespace string, args []string) ([]pb.Resource, error) {
|
|
switch len(args) {
|
|
case 0:
|
|
return nil, errors.New("No resource arguments provided")
|
|
case 1:
|
|
return parseResources(namespace, "", args)
|
|
default:
|
|
if res, err := k8s.CanonicalResourceNameFromFriendlyName(args[0]); err == nil && res != k8s.All {
|
|
// --namespace my-ns deploy foo1 foo2 ...
|
|
return parseResources(namespace, args[0], args[1:])
|
|
}
|
|
|
|
return parseResources(namespace, "", args)
|
|
}
|
|
}
|
|
|
|
func parseResources(namespace string, resType string, args []string) ([]pb.Resource, error) {
|
|
if err := validateResources(resType, args); err != nil {
|
|
return nil, err
|
|
}
|
|
resources := make([]pb.Resource, 0)
|
|
for _, arg := range args {
|
|
res, err := parseResource(namespace, resType, arg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resources = append(resources, res)
|
|
}
|
|
return resources, nil
|
|
}
|
|
|
|
func validateResources(resType string, args []string) error {
|
|
set := make(map[string]bool)
|
|
all := false
|
|
for _, arg := range args {
|
|
set[arg] = true
|
|
if arg == k8s.All {
|
|
all = true
|
|
}
|
|
}
|
|
if len(set) < len(args) {
|
|
return errors.New("cannot supply duplicate resources")
|
|
}
|
|
if all && len(args) > 1 {
|
|
return errors.New("'all' can't be supplied alongside other resources")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseResource(namespace, resType string, arg string) (pb.Resource, error) {
|
|
if resType != "" {
|
|
return buildResource(namespace, resType, arg)
|
|
}
|
|
elems := strings.Split(arg, "/")
|
|
switch len(elems) {
|
|
case 1:
|
|
// --namespace my-ns deploy
|
|
return buildResource(namespace, elems[0], "")
|
|
case 2:
|
|
// --namespace my-ns deploy/foo
|
|
return buildResource(namespace, elems[0], elems[1])
|
|
default:
|
|
return pb.Resource{}, errors.New("Invalid resource string: " + arg)
|
|
}
|
|
}
|
|
|
|
func buildResource(namespace string, resType string, name string) (pb.Resource, error) {
|
|
canonicalType, err := k8s.CanonicalResourceNameFromFriendlyName(resType)
|
|
if err != nil {
|
|
return pb.Resource{}, err
|
|
}
|
|
if canonicalType == k8s.Namespace {
|
|
// ignore --namespace flags if type is namespace
|
|
namespace = ""
|
|
}
|
|
|
|
return pb.Resource{
|
|
Namespace: namespace,
|
|
Type: canonicalType,
|
|
Name: name,
|
|
}, nil
|
|
}
|
|
|
|
// BuildTapByResourceRequest builds a Public API TapByResourceRequest from a
|
|
// TapRequestParams.
|
|
func BuildTapByResourceRequest(params TapRequestParams) (*pb.TapByResourceRequest, error) {
|
|
target, err := BuildResource(params.Namespace, params.Resource)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("target resource invalid: %s", err)
|
|
}
|
|
if !contains(ValidTargets, target.Type) {
|
|
return nil, fmt.Errorf("unsupported resource type [%s]", target.Type)
|
|
}
|
|
|
|
matches := []*pb.TapByResourceRequest_Match{}
|
|
|
|
if params.ToResource != "" {
|
|
destination, err := BuildResource(params.ToNamespace, params.ToResource)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("destination resource invalid: %s", err)
|
|
}
|
|
if !contains(ValidTapDestinations, destination.Type) {
|
|
return nil, fmt.Errorf("unsupported resource type [%s]", destination.Type)
|
|
}
|
|
|
|
match := pb.TapByResourceRequest_Match{
|
|
Match: &pb.TapByResourceRequest_Match_Destinations{
|
|
Destinations: &pb.ResourceSelection{
|
|
Resource: &destination,
|
|
},
|
|
},
|
|
}
|
|
matches = append(matches, &match)
|
|
}
|
|
|
|
if params.Scheme != "" {
|
|
match := buildMatchHTTP(&pb.TapByResourceRequest_Match_Http{
|
|
Match: &pb.TapByResourceRequest_Match_Http_Scheme{Scheme: params.Scheme},
|
|
})
|
|
matches = append(matches, &match)
|
|
}
|
|
if params.Method != "" {
|
|
match := buildMatchHTTP(&pb.TapByResourceRequest_Match_Http{
|
|
Match: &pb.TapByResourceRequest_Match_Http_Method{Method: params.Method},
|
|
})
|
|
matches = append(matches, &match)
|
|
}
|
|
if params.Authority != "" {
|
|
match := buildMatchHTTP(&pb.TapByResourceRequest_Match_Http{
|
|
Match: &pb.TapByResourceRequest_Match_Http_Authority{Authority: params.Authority},
|
|
})
|
|
matches = append(matches, &match)
|
|
}
|
|
if params.Path != "" {
|
|
match := buildMatchHTTP(&pb.TapByResourceRequest_Match_Http{
|
|
Match: &pb.TapByResourceRequest_Match_Http_Path{Path: params.Path},
|
|
})
|
|
matches = append(matches, &match)
|
|
}
|
|
|
|
return &pb.TapByResourceRequest{
|
|
Target: &pb.ResourceSelection{
|
|
Resource: &target,
|
|
},
|
|
MaxRps: params.MaxRps,
|
|
Match: &pb.TapByResourceRequest_Match{
|
|
Match: &pb.TapByResourceRequest_Match_All{
|
|
All: &pb.TapByResourceRequest_Match_Seq{
|
|
Matches: matches,
|
|
},
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func buildMatchHTTP(match *pb.TapByResourceRequest_Match_Http) pb.TapByResourceRequest_Match {
|
|
return pb.TapByResourceRequest_Match{
|
|
Match: &pb.TapByResourceRequest_Match_Http_{
|
|
Http: match,
|
|
},
|
|
}
|
|
}
|
|
|
|
func contains(list []string, s string) bool {
|
|
for _, elem := range list {
|
|
if s == elem {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
type peer struct {
|
|
address *pb.TcpAddress
|
|
labels map[string]string
|
|
direction string
|
|
}
|
|
|
|
// src returns the source peer of a `TapEvent`.
|
|
func src(event *pb.TapEvent) peer {
|
|
return peer{
|
|
address: event.GetSource(),
|
|
labels: event.GetSourceMeta().GetLabels(),
|
|
direction: "src",
|
|
}
|
|
}
|
|
|
|
// dst returns the destination peer of a `TapEvent`.
|
|
func dst(event *pb.TapEvent) peer {
|
|
return peer{
|
|
address: event.GetDestination(),
|
|
labels: event.GetDestinationMeta().GetLabels(),
|
|
direction: "dst",
|
|
}
|
|
}
|
|
|
|
// formatAddr formats the peer's TCP address for the `src` or `dst` element in
|
|
// the tap output corresponding to this peer.
|
|
func (p *peer) formatAddr() string {
|
|
return fmt.Sprintf(
|
|
"%s=%s",
|
|
p.direction,
|
|
addr.PublicAddressToString(p.address),
|
|
)
|
|
}
|
|
|
|
// formatResource returns a label describing what Kubernetes resources the peer
|
|
// belongs to. If the peer belongs to a resource of kind `resourceKind`, it will
|
|
// return a label for that resource; otherwise, it will fall back to the peer's
|
|
// pod name. Additionally, if the resource is not of type `namespace`, it will
|
|
// also add a label describing the peer's resource.
|
|
func (p *peer) formatResource(resourceKind string) string {
|
|
var s string
|
|
if resourceName, exists := p.labels[resourceKind]; exists {
|
|
kind := resourceKind
|
|
if short := k8s.ShortNameFromCanonicalResourceName(resourceKind); short != "" {
|
|
kind = short
|
|
}
|
|
s = fmt.Sprintf(
|
|
" %s_res=%s/%s",
|
|
p.direction,
|
|
kind,
|
|
resourceName,
|
|
)
|
|
} else if pod, hasPod := p.labels[k8s.Pod]; hasPod {
|
|
s = fmt.Sprintf(" %s_pod=%s", p.direction, pod)
|
|
}
|
|
if resourceKind != k8s.Namespace {
|
|
if ns, hasNs := p.labels[k8s.Namespace]; hasNs {
|
|
s += fmt.Sprintf(" %s_ns=%s", p.direction, ns)
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (p *peer) tlsStatus() string {
|
|
return p.labels["tls"]
|
|
}
|
|
|
|
func routeLabels(event *pb.TapEvent) string {
|
|
out := ""
|
|
for key, val := range event.GetRouteMeta().GetLabels() {
|
|
out = fmt.Sprintf("%s rt_%s=%s", out, key, val)
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
// RenderTapEvent renders a Public API TapEvent to a string.
|
|
// TODO: consider moving this into cli/cmd/tap.go.
|
|
func RenderTapEvent(event *pb.TapEvent, resource string) string {
|
|
dst := dst(event)
|
|
src := src(event)
|
|
|
|
proxy := "???"
|
|
tls := ""
|
|
switch event.GetProxyDirection() {
|
|
case pb.TapEvent_INBOUND:
|
|
proxy = "in " // A space is added so it aligns with `out`.
|
|
tls = src.tlsStatus()
|
|
case pb.TapEvent_OUTBOUND:
|
|
proxy = "out"
|
|
tls = dst.tlsStatus()
|
|
default:
|
|
// Too old for TLS.
|
|
}
|
|
|
|
flow := fmt.Sprintf("proxy=%s %s %s tls=%s",
|
|
proxy,
|
|
src.formatAddr(),
|
|
dst.formatAddr(),
|
|
tls,
|
|
)
|
|
|
|
// If `resource` is non-empty, then
|
|
resources := ""
|
|
if resource != "" {
|
|
resources = fmt.Sprintf(
|
|
"%s%s%s",
|
|
src.formatResource(resource),
|
|
dst.formatResource(resource),
|
|
routeLabels(event),
|
|
)
|
|
}
|
|
|
|
switch ev := event.GetHttp().GetEvent().(type) {
|
|
case *pb.TapEvent_Http_RequestInit_:
|
|
return fmt.Sprintf("req id=%d:%d %s :method=%s :authority=%s :path=%s%s",
|
|
ev.RequestInit.GetId().GetBase(),
|
|
ev.RequestInit.GetId().GetStream(),
|
|
flow,
|
|
ev.RequestInit.GetMethod().GetRegistered().String(),
|
|
ev.RequestInit.GetAuthority(),
|
|
ev.RequestInit.GetPath(),
|
|
resources,
|
|
)
|
|
|
|
case *pb.TapEvent_Http_ResponseInit_:
|
|
return fmt.Sprintf("rsp id=%d:%d %s :status=%d latency=%dµs%s",
|
|
ev.ResponseInit.GetId().GetBase(),
|
|
ev.ResponseInit.GetId().GetStream(),
|
|
flow,
|
|
ev.ResponseInit.GetHttpStatus(),
|
|
ev.ResponseInit.GetSinceRequestInit().GetNanos()/1000,
|
|
resources,
|
|
)
|
|
|
|
case *pb.TapEvent_Http_ResponseEnd_:
|
|
switch eos := ev.ResponseEnd.GetEos().GetEnd().(type) {
|
|
case *pb.Eos_GrpcStatusCode:
|
|
return fmt.Sprintf(
|
|
"end id=%d:%d %s grpc-status=%s duration=%dµs response-length=%dB%s",
|
|
ev.ResponseEnd.GetId().GetBase(),
|
|
ev.ResponseEnd.GetId().GetStream(),
|
|
flow,
|
|
codes.Code(eos.GrpcStatusCode),
|
|
ev.ResponseEnd.GetSinceResponseInit().GetNanos()/1000,
|
|
ev.ResponseEnd.GetResponseBytes(),
|
|
resources,
|
|
)
|
|
|
|
case *pb.Eos_ResetErrorCode:
|
|
return fmt.Sprintf(
|
|
"end id=%d:%d %s reset-error=%+v duration=%dµs response-length=%dB%s",
|
|
ev.ResponseEnd.GetId().GetBase(),
|
|
ev.ResponseEnd.GetId().GetStream(),
|
|
flow,
|
|
eos.ResetErrorCode,
|
|
ev.ResponseEnd.GetSinceResponseInit().GetNanos()/1000,
|
|
ev.ResponseEnd.GetResponseBytes(),
|
|
resources,
|
|
)
|
|
|
|
default:
|
|
return fmt.Sprintf("end id=%d:%d %s duration=%dµs response-length=%dB%s",
|
|
ev.ResponseEnd.GetId().GetBase(),
|
|
ev.ResponseEnd.GetId().GetStream(),
|
|
flow,
|
|
ev.ResponseEnd.GetSinceResponseInit().GetNanos()/1000,
|
|
ev.ResponseEnd.GetResponseBytes(),
|
|
resources,
|
|
)
|
|
}
|
|
|
|
default:
|
|
return fmt.Sprintf("unknown %s", flow)
|
|
}
|
|
}
|
|
|
|
// GetRequestRate calculates request rate from Public API BasicStats.
|
|
// TODO: consider moving this into `/cli/cmd`.
|
|
func GetRequestRate(stats *pb.BasicStats, timeWindow string) float64 {
|
|
success := stats.SuccessCount
|
|
failure := stats.FailureCount
|
|
windowLength, err := time.ParseDuration(timeWindow)
|
|
if err != nil {
|
|
log.Error(err.Error())
|
|
return 0.0
|
|
}
|
|
return float64(success+failure) / windowLength.Seconds()
|
|
}
|
|
|
|
// GetSuccessRate calculates success rate from Public API BasicStats.
|
|
// TODO: consider moving this into `/cli/cmd`.
|
|
func GetSuccessRate(stats *pb.BasicStats) float64 {
|
|
success := stats.SuccessCount
|
|
failure := stats.FailureCount
|
|
|
|
if success+failure == 0 {
|
|
return 0.0
|
|
}
|
|
return float64(success) / float64(success+failure)
|
|
}
|
|
|
|
// GetPercentTLS calculates the percent of traffic that is TLS, from Public API
|
|
// BasicStats.
|
|
// TODO: consider moving this into `/cli/cmd/stat.go`.
|
|
func GetPercentTLS(stats *pb.BasicStats) float64 {
|
|
reqTotal := stats.SuccessCount + stats.FailureCount
|
|
if reqTotal == 0 {
|
|
return 0.0
|
|
}
|
|
return float64(stats.TlsRequestCount) / float64(reqTotal)
|
|
}
|