metrics/utils.go (164 lines of code) (raw):
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package metrics
import (
"errors"
"fmt"
"io"
"math"
"net"
"net/http"
"sort"
"strings"
"syscall"
"github.com/Azure/kperf/api/types"
"golang.org/x/net/http2"
apierrors "k8s.io/apimachinery/pkg/api/errors"
)
// BuildPercentileLatencies builds percentile latencies.
func BuildPercentileLatencies(latencies []float64) [][2]float64 {
if len(latencies) == 0 {
return nil
}
var percentiles = []float64{0, 0.5, 0.90, 0.95, 0.99, 1}
res := make([][2]float64, len(percentiles))
n := len(latencies)
sort.Float64s(latencies)
for pi, pv := range percentiles {
idx := int(math.Ceil(float64(n) * pv))
if idx > 0 {
idx--
}
res[pi] = [2]float64{pv, latencies[idx]}
}
return res
}
// BuildErrorStatsGroupByType summaries total count for each type of errors.
func BuildErrorStatsGroupByType(errors []types.ResponseError) map[string]int32 {
res := map[string]int32{}
for _, err := range errors {
var key string
switch err.Type {
case types.ResponseErrorTypeHTTP:
key = fmt.Sprintf("%s/%d", err.Type, err.Code)
default:
key = fmt.Sprintf("%s/%s", err.Type, err.Message)
}
res[key]++
}
return res
}
var (
// errHTTP2ClientConnectionLost is used to track unexported http2 error.
errHTTP2ClientConnectionLost = errors.New("http2: client connection lost")
// errTLSHandshakeTimeout is used to track unexported tlsHandshakeTimeoutError from net/http.
errTLSHandshakeTimeout = errors.New("net/http: TLS handshake timeout")
)
// codeFromHTTP parses error to get http code.
func codeFromHTTP(err error) int {
if err == nil {
return 0
}
switch {
case apierrors.IsBadRequest(err):
return http.StatusBadRequest // 400
case apierrors.IsUnauthorized(err):
return http.StatusUnauthorized // 401
case apierrors.IsForbidden(err):
return http.StatusForbidden // 403
case apierrors.IsNotFound(err):
return http.StatusNotFound // 404
case apierrors.IsMethodNotSupported(err):
return http.StatusMethodNotAllowed // 405
case apierrors.IsNotAcceptable(err):
return http.StatusNotAcceptable // 406
case apierrors.IsAlreadyExists(err):
return http.StatusConflict // 409
case apierrors.IsGone(err):
return http.StatusGone // 410
case apierrors.IsRequestEntityTooLargeError(err):
return http.StatusRequestEntityTooLarge // 413
case apierrors.IsUnsupportedMediaType(err):
return http.StatusUnsupportedMediaType // 415
case apierrors.IsInvalid(err):
return http.StatusUnprocessableEntity // 422
case apierrors.IsTooManyRequests(err):
return http.StatusTooManyRequests // 429
case apierrors.IsInternalError(err):
return http.StatusInternalServerError // 500
case apierrors.IsServiceUnavailable(err):
return http.StatusServiceUnavailable // 503
case apierrors.IsTimeout(err):
return http.StatusGatewayTimeout // 504
default:
if status, ok := err.(apierrors.APIStatus); ok || errors.As(err, &status) {
return int(status.Status().Code)
}
return 0
}
}
// isHTTP2Error returns true if it's related to http2 error.
func isHTTP2Error(err error) (string, bool) {
if err == nil {
return "", false
}
if connErr, ok := err.(http2.ConnectionError); ok || errors.As(err, &connErr) {
return (http2.ErrCode(connErr)).String(), true
}
if streamErr, ok := err.(http2.StreamError); ok || errors.As(err, &streamErr) {
return streamErr.Code.String(), true
}
if connErr, ok := err.(http2.GoAwayError); ok || errors.As(err, &connErr) {
return fmt.Sprintf("http2: server sent GOAWAY and closed the connection; ErrCode=%v, debug=%s",
connErr.ErrCode, connErr.DebugData), true
}
if strings.Contains(err.Error(), errHTTP2ClientConnectionLost.Error()) {
return errHTTP2ClientConnectionLost.Error(), true
}
return "", false
}
// isConnectionError returns true if it's related to connection error.
func isConnectionError(err error) (string, bool) {
if err == nil {
return "", false
}
switch {
case isTimeoutError(err):
return err.Error(), true
case isConnectionRefused(err):
return syscall.ECONNREFUSED.Error(), true
case isConnectionResetByPeer(err):
return syscall.ECONNRESET.Error(), true
case errors.Is(err, io.ErrUnexpectedEOF):
return io.ErrUnexpectedEOF.Error(), true
case errors.Is(err, io.EOF):
return io.EOF.Error(), true
case strings.Contains(err.Error(), errTLSHandshakeTimeout.Error()):
return errTLSHandshakeTimeout.Error(), true
default:
return "", false
}
}
// isTimeoutError returns true if it's related to golang standard library
// net's timeout error.
func isTimeoutError(err error) bool {
if err == nil {
return false
}
terr, ok := err.(net.Error)
if !ok {
if !errors.As(err, &terr) {
return false
}
}
return terr.Timeout()
}
// isConnectionRefused returns true if the error is connection refused
func isConnectionRefused(err error) bool {
if err == nil {
return false
}
var errno syscall.Errno
if errors.As(err, &errno) {
return errno == syscall.ECONNREFUSED
}
return false
}
// isConnectionResetByPeer returns true if the error is "connection reset by peer".
func isConnectionResetByPeer(err error) bool {
if err == nil {
return false
}
var errno syscall.Errno
if errors.As(err, &errno) {
return errno == syscall.ECONNRESET
}
return false
}