diff --git a/base/collect_grpc.go b/base/collect_grpc.go index 1bb89c2fc..9b1ddd6bc 100644 --- a/base/collect_grpc.go +++ b/base/collect_grpc.go @@ -2,6 +2,7 @@ package base import ( "fmt" + "os" "time" "github.com/bojand/ghz/runner" @@ -38,6 +39,11 @@ type collectGRPCInputs struct { // Endpoints is used to define multiple endpoints to test Endpoints map[string]runner.Config `json:"endpoints" yaml:"endpoints"` + + // Determines if Grafana dashboard should be created + // dasboard vs report/assess tasks + // TODO: remove + Grafana bool `json:"grafana" yaml:"grafana"` } // collectGRPCTask enables load testing of gRPC services. @@ -49,6 +55,15 @@ type collectGRPCTask struct { With collectGRPCInputs `json:"with" yaml:"with"` } +// GHZResult is the raw data sent to the metrics server +// This data will be transformed into httpDashboard when getGHZGrafana is called +type GHZResult struct { + // key is the endpoint + EndpointResults map[string]runner.Report + + Summary Insights +} + // initializeDefaults sets default values for the collect task func (t *collectGRPCTask) initializeDefaults() { // set defaults @@ -71,11 +86,11 @@ func (t *collectGRPCTask) validateInputs() error { } // resultForVersion collects gRPC test result for a given version -func (t *collectGRPCTask) resultForVersion() (map[string]*runner.Report, error) { +func (t *collectGRPCTask) resultForVersion() (map[string]runner.Report, error) { // the main idea is to run ghz with proper options var err error - results := map[string]*runner.Report{} + results := map[string]runner.Report{} if len(t.With.Endpoints) > 0 { log.Logger.Trace("multiple endpoints") @@ -108,7 +123,11 @@ func (t *collectGRPCTask) resultForVersion() (map[string]*runner.Report, error) continue } - results[gRPCMetricPrefix+"-"+endpointID] = igr + resultsKey := gRPCMetricPrefix + "-" + endpointID + if t.With.Grafana { + resultsKey = endpoint.Call + } + results[resultsKey] = *igr } } else { // TODO: supply all the allowed options @@ -121,7 +140,11 @@ func (t *collectGRPCTask) resultForVersion() (map[string]*runner.Report, error) return results, err } - results[gRPCMetricPrefix] = igr + resultsKey := gRPCMetricPrefix + if t.With.Grafana { + resultsKey = t.With.Call + } + results[resultsKey] = *igr } return results, err @@ -170,60 +193,80 @@ func (t *collectGRPCTask) run(exp *Experiment) error { } in := exp.Result.Insights - // 4. Populate all metrics collected by this task - for provider, data := range data { - // populate grpc request count - // todo: this logic breaks for looped experiments. Fix when we get to loops. - m := provider + "/" + gRPCRequestCountMetricName - mm := MetricMeta{ - Description: "number of gRPC requests sent", - Type: CounterMetricType, - } - if err = in.updateMetric(m, mm, 0, float64(data.Count)); err != nil { - return err + if t.With.Grafana { + // push data to metrics service + ghzResult := GHZResult{ + EndpointResults: data, + Summary: *exp.Result.Insights, } - // populate error count & rate - ec := float64(0) - for _, count := range data.ErrorDist { - ec += float64(count) + // get URL of metrics server from environment variable + metricsServerURL, ok := os.LookupEnv(MetricsServerURL) + if !ok { + errorMessage := "could not look up METRICS_SERVER_URL environment variable" + log.Logger.Error(errorMessage) + return fmt.Errorf(errorMessage) } - // populate count - // todo: This logic breaks for looped experiments. Fix when we get to loops. - m = provider + "/" + gRPCErrorCountMetricName - mm = MetricMeta{ - Description: "number of responses that were errors", - Type: CounterMetricType, - } - if err = in.updateMetric(m, mm, 0, ec); err != nil { + if err = putPerformanceResultToMetricsService(metricsServerURL, exp.Metadata.Namespace, exp.Metadata.Name, ghzResult); err != nil { return err } + } else { + // 4. Populate all metrics collected by this task + for provider, data := range data { + // populate grpc request count + // todo: this logic breaks for looped experiments. Fix when we get to loops. + m := provider + "/" + gRPCRequestCountMetricName + mm := MetricMeta{ + Description: "number of gRPC requests sent", + Type: CounterMetricType, + } + if err = in.updateMetric(m, mm, 0, float64(data.Count)); err != nil { + return err + } + + // populate error count & rate + ec := float64(0) + for _, count := range data.ErrorDist { + ec += float64(count) + } - // populate rate - // todo: This logic breaks for looped experiments. Fix when we get to loops. - m = provider + "/" + gRPCErrorRateMetricName - rc := float64(data.Count) - if rc != 0 { + // populate count + // todo: This logic breaks for looped experiments. Fix when we get to loops. + m = provider + "/" + gRPCErrorCountMetricName mm = MetricMeta{ - Description: "fraction of responses that were errors", - Type: GaugeMetricType, + Description: "number of responses that were errors", + Type: CounterMetricType, } - if err = in.updateMetric(m, mm, 0, ec/rc); err != nil { + if err = in.updateMetric(m, mm, 0, ec); err != nil { return err } - } - // populate latency sample - m = provider + "/" + gRPCLatencySampleMetricName - mm = MetricMeta{ - Description: "gRPC Latency Sample", - Type: SampleMetricType, - Units: StringPointer("msec"), - } - lh := latencySample(data.Details) - if err = in.updateMetric(m, mm, 0, lh); err != nil { - return err + // populate rate + // todo: This logic breaks for looped experiments. Fix when we get to loops. + m = provider + "/" + gRPCErrorRateMetricName + rc := float64(data.Count) + if rc != 0 { + mm = MetricMeta{ + Description: "fraction of responses that were errors", + Type: GaugeMetricType, + } + if err = in.updateMetric(m, mm, 0, ec/rc); err != nil { + return err + } + } + + // populate latency sample + m = provider + "/" + gRPCLatencySampleMetricName + mm = MetricMeta{ + Description: "gRPC Latency Sample", + Type: SampleMetricType, + Units: StringPointer("msec"), + } + lh := latencySample(data.Details) + if err = in.updateMetric(m, mm, 0, lh); err != nil { + return err + } } } diff --git a/metrics/server.go b/metrics/server.go index b70f17902..d9e668252 100644 --- a/metrics/server.go +++ b/metrics/server.go @@ -11,6 +11,7 @@ import ( "strconv" "time" + "github.com/bojand/ghz/runner" "github.com/iter8-tools/iter8/abn" util "github.com/iter8-tools/iter8/base" "github.com/iter8-tools/iter8/base/log" @@ -82,6 +83,24 @@ type httpDashboard struct { Summary util.Insights } +type ghzStatistics struct { + Count uint64 + ErrorCount float64 +} + +type ghzEndpointPanel struct { + Durations grafanaHistogram + Statistics ghzStatistics + StatusCodeDistribution map[string]int `json:"Status codes"` +} + +type ghzDashboard struct { + // key is the endpoint + Endpoints map[string]ghzEndpointPanel + + Summary util.Insights +} + var allRoutemaps controllers.AllRouteMapsInterface = &controllers.DefaultRoutemaps{} // Start starts the HTTP server @@ -102,6 +121,7 @@ func Start(stopCh <-chan struct{}) error { http.HandleFunc("/metrics", getMetrics) http.HandleFunc(util.PerformanceResultPath, putResult) http.HandleFunc("/httpDashboard", getHTTPDashboard) + http.HandleFunc("/ghzDashboard", getGHZDashboard) // configure HTTP server server := &http.Server{ @@ -423,24 +443,23 @@ func getHTTPStatistics(fortioHistogram *fstats.HistogramData, decimalPlace float } func getHTTPEndpointPanel(httpRunnerResults *fhttp.HTTPRunnerResults) httpEndpointPanel { - result := httpEndpointPanel{} + panel := httpEndpointPanel{} if httpRunnerResults.DurationHistogram != nil { - result.Durations = getHTTPHistogram(httpRunnerResults.DurationHistogram.Data, 1) - result.Statistics = getHTTPStatistics(httpRunnerResults.DurationHistogram, 1) + panel.Durations = getHTTPHistogram(httpRunnerResults.DurationHistogram.Data, 1) + panel.Statistics = getHTTPStatistics(httpRunnerResults.DurationHistogram, 1) } if httpRunnerResults.ErrorsDurationHistogram != nil { - result.ErrorDurations = getHTTPHistogram(httpRunnerResults.ErrorsDurationHistogram.Data, 1) - result.ErrorStatistics = getHTTPStatistics(httpRunnerResults.ErrorsDurationHistogram, 1) + panel.ErrorDurations = getHTTPHistogram(httpRunnerResults.ErrorsDurationHistogram.Data, 1) + panel.ErrorStatistics = getHTTPStatistics(httpRunnerResults.ErrorsDurationHistogram, 1) } - result.ReturnCodes = httpRunnerResults.RetCodes + panel.ReturnCodes = httpRunnerResults.RetCodes - return result + return panel } func getHTTPDashboardHelper(fortioResult util.FortioResult) httpDashboard { - // add endpoint results dashboard := httpDashboard{ Endpoints: map[string]httpEndpointPanel{}, } @@ -528,8 +547,8 @@ func getHTTPDashboard(w http.ResponseWriter, r *http.Request) { } // verify request (query parameter) + // required namespace and experiment name // Key: kt-result::my-namespace::my-experiment-name::my-endpoint - // Should namespace and experiment name come from application? namespace := r.URL.Query().Get("namespace") if namespace == "" { http.Error(w, "no namespace specified", http.StatusBadRequest) @@ -557,8 +576,6 @@ func getHTTPDashboard(w http.ResponseWriter, r *http.Request) { return } - // TODO: should these functions belong in collect_http.go? Or be somewhere closeby? - // These functions are only for the purpose of processing the results of collect_http.go fortioResult := util.FortioResult{} err = json.Unmarshal(result, &fortioResult) if err != nil { @@ -581,3 +598,121 @@ func getHTTPDashboard(w http.ResponseWriter, r *http.Request) { w.Header().Add("Content-Type", "application/json") _, _ = w.Write(dashboardBytes) } + +func getGHZHistogram(ghzHistogram []runner.Bucket, decimalPlace float64) grafanaHistogram { + grafanaHistogram := grafanaHistogram{} + + for _, bucket := range ghzHistogram { + grafanaHistogram = append(grafanaHistogram, grafanaHistogramBucket{ + Version: "0", + Bucket: fmt.Sprint(roundDecimal(bucket.Mark*1000, 3)), + Value: float64(bucket.Count), + }) + } + + return grafanaHistogram +} + +func getGHZStatistics(ghzRunnerReport runner.Report) ghzStatistics { + // populate error count & rate + ec := float64(0) + for _, count := range ghzRunnerReport.ErrorDist { + ec += float64(count) + } + + return ghzStatistics{ + Count: ghzRunnerReport.Count, + ErrorCount: ec, + } +} + +func getGHZEndpointPanel(ghzRunnerReport runner.Report) ghzEndpointPanel { + panel := ghzEndpointPanel{} + + if ghzRunnerReport.Histogram != nil { + panel.Durations = getGHZHistogram(ghzRunnerReport.Histogram, 3) + panel.Statistics = getGHZStatistics(ghzRunnerReport) + } + + panel.StatusCodeDistribution = ghzRunnerReport.StatusCodeDist + + return panel +} + +func getGHZDashboardHelper(ghzResult util.GHZResult) ghzDashboard { + dashboard := ghzDashboard{ + Endpoints: map[string]ghzEndpointPanel{}, + } + + for endpoint, endpointResult := range ghzResult.EndpointResults { + endpointResult := endpointResult + dashboard.Endpoints[endpoint] = getGHZEndpointPanel(endpointResult) + } + + dashboard.Summary = ghzResult.Summary + + return dashboard +} + +func getGHZDashboard(w http.ResponseWriter, r *http.Request) { + log.Logger.Trace("getGHZDashboard called") + defer log.Logger.Trace("getGHZDashboard completed") + + // verify method + if r.Method != http.MethodGet { + http.Error(w, "expected GET", http.StatusMethodNotAllowed) + return + } + + // verify request (query parameter) + // required namespace and experiment name + // Key: kt-result::my-namespace::my-experiment-name::my-endpoint + namespace := r.URL.Query().Get("namespace") + if namespace == "" { + http.Error(w, "no namespace specified", http.StatusBadRequest) + return + } + + experiment := r.URL.Query().Get("experiment") + if experiment == "" { + http.Error(w, "no experiment specified", http.StatusBadRequest) + return + } + + log.Logger.Tracef("getGHZDashboard called for namespace %s and experiment %s", namespace, experiment) + + // get result from metrics client + if abn.MetricsClient == nil { + http.Error(w, "no metrics client", http.StatusInternalServerError) + return + } + result, err := abn.MetricsClient.GetResult(namespace, experiment) + if err != nil { + errorMessage := fmt.Sprintf("cannot get result with namespace %s, experiment %s", namespace, experiment) + log.Logger.Error(errorMessage) + http.Error(w, errorMessage, http.StatusBadRequest) + return + } + + ghzResult := util.GHZResult{} + err = json.Unmarshal(result, &ghzResult) + if err != nil { + errorMessage := fmt.Sprintf("cannot JSON unmarshal result into GHZResult: \"%s\"", string(result)) + log.Logger.Error(errorMessage) + http.Error(w, errorMessage, http.StatusInternalServerError) + return + } + + // JSON marshal the dashboard + dashboardBytes, err := json.Marshal(getGHZDashboardHelper(ghzResult)) + if err != nil { + errorMessage := "cannot JSON marshal ghz dashboard" + log.Logger.Error(errorMessage) + http.Error(w, errorMessage, http.StatusInternalServerError) + return + } + + // finally, send response + w.Header().Add("Content-Type", "application/json") + _, _ = w.Write(dashboardBytes) +} diff --git a/metrics/server_test.go b/metrics/server_test.go index b784b8751..d2c3385a2 100644 --- a/metrics/server_test.go +++ b/metrics/server_test.go @@ -16,7 +16,6 @@ import ( "testing" "time" - fstats "fortio.org/fortio/stats" "github.com/dgraph-io/badger/v4" "github.com/iter8-tools/iter8/abn" util "github.com/iter8-tools/iter8/base" @@ -341,157 +340,9 @@ func getTestRM(namespace, name string) *testroutemap { } -func TestGetHTTPHistogram(t *testing.T) { - data := []fstats.Bucket{ - { - Interval: fstats.Interval{ - Start: 0.005229875, - End: 0.006, - }, - Percent: 2, - Count: 2, - }, - { - Interval: fstats.Interval{ - Start: 0.006, - End: 0.007, - }, - Percent: 5, - Count: 3, - }, - { - Interval: fstats.Interval{ - Start: 0.007, - End: 0.008, - }, - Percent: 6, - Count: 1, - }, - { - Interval: fstats.Interval{ - Start: 0.009000000000000001, - End: 0.01, - }, - Percent: 7, - Count: 1, - }, - { - Interval: fstats.Interval{ - Start: 0.01, - End: 0.011, - }, - Percent: 12, - Count: 5, - }, - { - Interval: fstats.Interval{ - Start: 0.011, - End: 0.012, - }, - Percent: 15, - Count: 3, - }, - { - Interval: fstats.Interval{ - Start: 0.012, - End: 0.014, - }, - Percent: 22, - Count: 7, - }, - { - Interval: fstats.Interval{ - Start: 0.014, - End: 0.016, - }, - Percent: 26, - Count: 4, - }, - { - Interval: fstats.Interval{ - Start: 0.016, - End: 0.018000000000000002, - }, - Percent: 37, - Count: 11, - }, - { - Interval: fstats.Interval{ - Start: 0.018000000000000002, - End: 0.02, - }, - Percent: 42, - Count: 5, - }, - { - Interval: fstats.Interval{ - Start: 0.02, - End: 0.025, - }, - Percent: 57, - Count: 15, - }, - { - Interval: fstats.Interval{ - Start: 0.025, - End: 0.03, - }, - Percent: 70, - Count: 13, - }, - { - Interval: fstats.Interval{ - Start: 0.03, - End: 0.035, - }, - Percent: 79, - Count: 9, - }, - { - Interval: fstats.Interval{ - Start: 0.035, - End: 0.04, - }, - Percent: 86, - Count: 7, - }, - { - Interval: fstats.Interval{ - Start: 0.04, - End: 0.045, - }, - Percent: 95, - Count: 9, - }, - { - Interval: fstats.Interval{ - Start: 0.045, - End: 0.05, - }, - Percent: 97, - Count: 2, - }, - { - Interval: fstats.Interval{ - Start: 0.05, - End: 0.051404375, - }, - Percent: 100, - Count: 3, - }, - } - - histogram := getHTTPHistogram(data, 1) - - histogramJSON, _ := json.Marshal(histogram) - fmt.Println(string(histogramJSON)) -} - func TestGetHTTPDashboardHelper(t *testing.T) { - result := "{\"EndpointResults\":{\"http://httpbin.default/get\":{\"RunType\":\"HTTP\",\"Labels\":\"\",\"StartTime\":\"2023-07-21T14:00:40.134434969Z\",\"RequestedQPS\":\"8\",\"RequestedDuration\":\"exactly 100 calls\",\"ActualQPS\":7.975606391552989,\"ActualDuration\":12538231589,\"NumThreads\":4,\"Version\":\"1.57.3\",\"DurationHistogram\":{\"Count\":100,\"Min\":0.004223875,\"Max\":0.040490042,\"Sum\":1.5977100850000001,\"Avg\":0.015977100850000002,\"StdDev\":0.008340658047253256,\"Data\":[{\"Start\":0.004223875,\"End\":0.005,\"Percent\":5,\"Count\":5},{\"Start\":0.005,\"End\":0.006,\"Percent\":10,\"Count\":5},{\"Start\":0.006,\"End\":0.007,\"Percent\":14,\"Count\":4},{\"Start\":0.007,\"End\":0.008,\"Percent\":19,\"Count\":5},{\"Start\":0.008,\"End\":0.009000000000000001,\"Percent\":24,\"Count\":5},{\"Start\":0.009000000000000001,\"End\":0.01,\"Percent\":28,\"Count\":4},{\"Start\":0.01,\"End\":0.011,\"Percent\":33,\"Count\":5},{\"Start\":0.011,\"End\":0.012,\"Percent\":36,\"Count\":3},{\"Start\":0.012,\"End\":0.014,\"Percent\":48,\"Count\":12},{\"Start\":0.014,\"End\":0.016,\"Percent\":55,\"Count\":7},{\"Start\":0.016,\"End\":0.018000000000000002,\"Percent\":65,\"Count\":10},{\"Start\":0.018000000000000002,\"End\":0.02,\"Percent\":74,\"Count\":9},{\"Start\":0.02,\"End\":0.025,\"Percent\":85,\"Count\":11},{\"Start\":0.025,\"End\":0.03,\"Percent\":93,\"Count\":8},{\"Start\":0.03,\"End\":0.035,\"Percent\":98,\"Count\":5},{\"Start\":0.035,\"End\":0.04,\"Percent\":99,\"Count\":1},{\"Start\":0.04,\"End\":0.040490042,\"Percent\":100,\"Count\":1}],\"Percentiles\":[{\"Percentile\":50,\"Value\":0.014571428571428572},{\"Percentile\":75,\"Value\":0.020454545454545454},{\"Percentile\":90,\"Value\":0.028125},{\"Percentile\":95,\"Value\":0.032},{\"Percentile\":99,\"Value\":0.04},{\"Percentile\":99.9,\"Value\":0.0404410378}]},\"ErrorsDurationHistogram\":{\"Count\":0,\"Min\":0,\"Max\":0,\"Sum\":0,\"Avg\":0,\"StdDev\":0,\"Data\":null},\"Exactly\":100,\"Jitter\":false,\"Uniform\":false,\"NoCatchUp\":false,\"RunID\":0,\"AccessLoggerInfo\":\"\",\"ID\":\"2023-07-21-140040\",\"RetCodes\":{\"200\":100},\"IPCountMap\":{\"10.96.108.76:80\":4},\"Insecure\":false,\"MTLS\":false,\"CACert\":\"\",\"Cert\":\"\",\"Key\":\"\",\"UnixDomainSocket\":\"\",\"URL\":\"http://httpbin.default/get\",\"NumConnections\":1,\"Compression\":false,\"DisableFastClient\":false,\"HTTP10\":false,\"H2\":false,\"DisableKeepAlive\":false,\"AllowHalfClose\":false,\"FollowRedirects\":false,\"Resolve\":\"\",\"HTTPReqTimeOut\":3000000000,\"UserCredentials\":\"\",\"ContentType\":\"\",\"Payload\":null,\"MethodOverride\":\"\",\"LogErrors\":false,\"SequentialWarmup\":false,\"ConnReuseRange\":[0,0],\"NoResolveEachConn\":false,\"Offset\":0,\"Resolution\":0.001,\"Sizes\":{\"Count\":100,\"Min\":413,\"Max\":413,\"Sum\":41300,\"Avg\":413,\"StdDev\":0,\"Data\":[{\"Start\":413,\"End\":413,\"Percent\":100,\"Count\":100}]},\"HeaderSizes\":{\"Count\":100,\"Min\":230,\"Max\":230,\"Sum\":23000,\"Avg\":230,\"StdDev\":0,\"Data\":[{\"Start\":230,\"End\":230,\"Percent\":100,\"Count\":100}]},\"Sockets\":[1,1,1,1],\"SocketCount\":4,\"ConnectionStats\":{\"Count\":4,\"Min\":0.001385875,\"Max\":0.001724375,\"Sum\":0.006404583,\"Avg\":0.00160114575,\"StdDev\":0.00013101857565508474,\"Data\":[{\"Start\":0.001385875,\"End\":0.001724375,\"Percent\":100,\"Count\":4}],\"Percentiles\":[{\"Percentile\":50,\"Value\":0.0014987083333333332},{\"Percentile\":75,\"Value\":0.0016115416666666667},{\"Percentile\":90,\"Value\":0.0016792416666666667},{\"Percentile\":95,\"Value\":0.0017018083333333333},{\"Percentile\":99,\"Value\":0.0017198616666666668},{\"Percentile\":99.9,\"Value\":0.0017239236666666668}]},\"AbortOn\":0}},\"Summary\":{\"numVersions\":1,\"versionNames\":null,\"metricsInfo\":{\"http/latency\":{\"description\":\"Latency Histogram\",\"units\":\"msec\",\"type\":\"Histogram\"},\"http://httpbin.default/get/error-count\":{\"description\":\"number of responses that were errors\",\"type\":\"Counter\"},\"http://httpbin.default/get/error-rate\":{\"description\":\"fraction of responses that were errors\",\"type\":\"Gauge\"},\"http://httpbin.default/get/latency-max\":{\"description\":\"maximum of observed latency values\",\"units\":\"msec\",\"type\":\"Gauge\"},\"http://httpbin.default/get/latency-mean\":{\"description\":\"mean of observed latency values\",\"units\":\"msec\",\"type\":\"Gauge\"},\"http://httpbin.default/get/latency-min\":{\"description\":\"minimum of observed latency values\",\"units\":\"msec\",\"type\":\"Gauge\"},\"http://httpbin.default/get/latency-p50\":{\"description\":\"50-th percentile of observed latency values\",\"units\":\"msec\",\"type\":\"Gauge\"},\"http://httpbin.default/get/latency-p75\":{\"description\":\"75-th percentile of observed latency values\",\"units\":\"msec\",\"type\":\"Gauge\"},\"http://httpbin.default/get/latency-p90\":{\"description\":\"90-th percentile of observed latency values\",\"units\":\"msec\",\"type\":\"Gauge\"},\"http://httpbin.default/get/latency-p95\":{\"description\":\"95-th percentile of observed latency values\",\"units\":\"msec\",\"type\":\"Gauge\"},\"http://httpbin.default/get/latency-p99\":{\"description\":\"99-th percentile of observed latency values\",\"units\":\"msec\",\"type\":\"Gauge\"},\"http://httpbin.default/get/latency-p99.9\":{\"description\":\"99.9-th percentile of observed latency values\",\"units\":\"msec\",\"type\":\"Gauge\"},\"http://httpbin.default/get/latency-stddev\":{\"description\":\"standard deviation of observed latency values\",\"units\":\"msec\",\"type\":\"Gauge\"},\"http://httpbin.default/get/request-count\":{\"description\":\"number of requests sent\",\"type\":\"Counter\"}},\"nonHistMetricValues\":[{\"http://httpbin.default/get/error-count\":[0],\"http://httpbin.default/get/error-rate\":[0],\"http://httpbin.default/get/latency-max\":[40.490041999999995],\"http://httpbin.default/get/latency-mean\":[15.977100850000001],\"http://httpbin.default/get/latency-min\":[4.2238750000000005],\"http://httpbin.default/get/latency-p50\":[14.571428571428571],\"http://httpbin.default/get/latency-p75\":[20.454545454545453],\"http://httpbin.default/get/latency-p90\":[28.125],\"http://httpbin.default/get/latency-p95\":[32],\"http://httpbin.default/get/latency-p99\":[40],\"http://httpbin.default/get/latency-p99.9\":[40.441037800000004],\"http://httpbin.default/get/latency-stddev\":[8.340658047253257],\"http://httpbin.default/get/request-count\":[100]}],\"histMetricValues\":[{\"http/latency\":[{\"lower\":4.2238750000000005,\"upper\":5,\"count\":5},{\"lower\":5,\"upper\":6,\"count\":5},{\"lower\":6,\"upper\":7,\"count\":4},{\"lower\":7,\"upper\":8,\"count\":5},{\"lower\":8,\"upper\":9.000000000000002,\"count\":5},{\"lower\":9.000000000000002,\"upper\":10,\"count\":4},{\"lower\":10,\"upper\":11,\"count\":5},{\"lower\":11,\"upper\":12,\"count\":3},{\"lower\":12,\"upper\":14,\"count\":12},{\"lower\":14,\"upper\":16,\"count\":7},{\"lower\":16,\"upper\":18.000000000000004,\"count\":10},{\"lower\":18.000000000000004,\"upper\":20,\"count\":9},{\"lower\":20,\"upper\":25,\"count\":11},{\"lower\":25,\"upper\":30,\"count\":8},{\"lower\":30,\"upper\":35,\"count\":5},{\"lower\":35,\"upper\":40,\"count\":1},{\"lower\":40,\"upper\":40.490041999999995,\"count\":1}]}],\"SummaryMetricValues\":[{}]}}" - fortioResult := util.FortioResult{} - err := json.Unmarshal([]byte(result), &fortioResult) + err := json.Unmarshal([]byte(fortioResultJSON), &fortioResult) assert.NoError(t, err) dashboard := getHTTPDashboardHelper(fortioResult) @@ -501,7 +352,24 @@ func TestGetHTTPDashboardHelper(t *testing.T) { assert.Equal( t, - "{\"Endpoints\":{\"http://httpbin.default/get\":{\"Durations\":[{\"Version\":\"0\",\"Bucket\":\"4.2 - 5\",\"Value\":5},{\"Version\":\"0\",\"Bucket\":\"5 - 6\",\"Value\":5},{\"Version\":\"0\",\"Bucket\":\"6 - 7\",\"Value\":4},{\"Version\":\"0\",\"Bucket\":\"7 - 8\",\"Value\":5},{\"Version\":\"0\",\"Bucket\":\"8 - 9\",\"Value\":5},{\"Version\":\"0\",\"Bucket\":\"9 - 10\",\"Value\":4},{\"Version\":\"0\",\"Bucket\":\"10 - 11\",\"Value\":5},{\"Version\":\"0\",\"Bucket\":\"11 - 12\",\"Value\":3},{\"Version\":\"0\",\"Bucket\":\"12 - 14\",\"Value\":12},{\"Version\":\"0\",\"Bucket\":\"14 - 16\",\"Value\":7},{\"Version\":\"0\",\"Bucket\":\"16 - 18\",\"Value\":10},{\"Version\":\"0\",\"Bucket\":\"18 - 20\",\"Value\":9},{\"Version\":\"0\",\"Bucket\":\"20 - 25\",\"Value\":11},{\"Version\":\"0\",\"Bucket\":\"25 - 30\",\"Value\":8},{\"Version\":\"0\",\"Bucket\":\"30 - 35\",\"Value\":5},{\"Version\":\"0\",\"Bucket\":\"35 - 40\",\"Value\":1},{\"Version\":\"0\",\"Bucket\":\"40 - 40.4\",\"Value\":1}],\"Statistics\":{\"Count\":100,\"Mean\":15.977100850000001,\"StdDev\":8.340658047253257,\"Min\":4.2238750000000005,\"Max\":40.490041999999995},\"Error durations\":[],\"Error statistics\":{\"Count\":0,\"Mean\":0,\"StdDev\":0,\"Min\":0,\"Max\":0},\"Return codes\":{\"200\":100}}},\"Summary\":{\"numVersions\":1,\"versionNames\":null,\"metricsInfo\":{\"http/latency\":{\"description\":\"Latency Histogram\",\"units\":\"msec\",\"type\":\"Histogram\"},\"http://httpbin.default/get/error-count\":{\"description\":\"number of responses that were errors\",\"type\":\"Counter\"},\"http://httpbin.default/get/error-rate\":{\"description\":\"fraction of responses that were errors\",\"type\":\"Gauge\"},\"http://httpbin.default/get/latency-max\":{\"description\":\"maximum of observed latency values\",\"units\":\"msec\",\"type\":\"Gauge\"},\"http://httpbin.default/get/latency-mean\":{\"description\":\"mean of observed latency values\",\"units\":\"msec\",\"type\":\"Gauge\"},\"http://httpbin.default/get/latency-min\":{\"description\":\"minimum of observed latency values\",\"units\":\"msec\",\"type\":\"Gauge\"},\"http://httpbin.default/get/latency-p50\":{\"description\":\"50-th percentile of observed latency values\",\"units\":\"msec\",\"type\":\"Gauge\"},\"http://httpbin.default/get/latency-p75\":{\"description\":\"75-th percentile of observed latency values\",\"units\":\"msec\",\"type\":\"Gauge\"},\"http://httpbin.default/get/latency-p90\":{\"description\":\"90-th percentile of observed latency values\",\"units\":\"msec\",\"type\":\"Gauge\"},\"http://httpbin.default/get/latency-p95\":{\"description\":\"95-th percentile of observed latency values\",\"units\":\"msec\",\"type\":\"Gauge\"},\"http://httpbin.default/get/latency-p99\":{\"description\":\"99-th percentile of observed latency values\",\"units\":\"msec\",\"type\":\"Gauge\"},\"http://httpbin.default/get/latency-p99.9\":{\"description\":\"99.9-th percentile of observed latency values\",\"units\":\"msec\",\"type\":\"Gauge\"},\"http://httpbin.default/get/latency-stddev\":{\"description\":\"standard deviation of observed latency values\",\"units\":\"msec\",\"type\":\"Gauge\"},\"http://httpbin.default/get/request-count\":{\"description\":\"number of requests sent\",\"type\":\"Counter\"}},\"nonHistMetricValues\":[{\"http://httpbin.default/get/error-count\":[0],\"http://httpbin.default/get/error-rate\":[0],\"http://httpbin.default/get/latency-max\":[40.490041999999995],\"http://httpbin.default/get/latency-mean\":[15.977100850000001],\"http://httpbin.default/get/latency-min\":[4.2238750000000005],\"http://httpbin.default/get/latency-p50\":[14.571428571428571],\"http://httpbin.default/get/latency-p75\":[20.454545454545453],\"http://httpbin.default/get/latency-p90\":[28.125],\"http://httpbin.default/get/latency-p95\":[32],\"http://httpbin.default/get/latency-p99\":[40],\"http://httpbin.default/get/latency-p99.9\":[40.441037800000004],\"http://httpbin.default/get/latency-stddev\":[8.340658047253257],\"http://httpbin.default/get/request-count\":[100]}],\"histMetricValues\":[{\"http/latency\":[{\"lower\":4.2238750000000005,\"upper\":5,\"count\":5},{\"lower\":5,\"upper\":6,\"count\":5},{\"lower\":6,\"upper\":7,\"count\":4},{\"lower\":7,\"upper\":8,\"count\":5},{\"lower\":8,\"upper\":9.000000000000002,\"count\":5},{\"lower\":9.000000000000002,\"upper\":10,\"count\":4},{\"lower\":10,\"upper\":11,\"count\":5},{\"lower\":11,\"upper\":12,\"count\":3},{\"lower\":12,\"upper\":14,\"count\":12},{\"lower\":14,\"upper\":16,\"count\":7},{\"lower\":16,\"upper\":18.000000000000004,\"count\":10},{\"lower\":18.000000000000004,\"upper\":20,\"count\":9},{\"lower\":20,\"upper\":25,\"count\":11},{\"lower\":25,\"upper\":30,\"count\":8},{\"lower\":30,\"upper\":35,\"count\":5},{\"lower\":35,\"upper\":40,\"count\":1},{\"lower\":40,\"upper\":40.490041999999995,\"count\":1}]}],\"SummaryMetricValues\":[{}]}}", + fortioDashboardJSON, + string(dashboardBytes), + ) +} + +func TestGetGHZDashboardHelper(t *testing.T) { + ghzResult := util.GHZResult{} + err := json.Unmarshal([]byte(ghzResultJSON), &ghzResult) + assert.NoError(t, err) + + dashboard := getGHZDashboardHelper(ghzResult) + + assert.NotNil(t, dashboard) + dashboardBytes, err := json.Marshal(dashboard) + assert.NoError(t, err) + assert.Equal( + t, + ghzDashboardJSON, string(dashboardBytes), ) } @@ -651,6 +519,506 @@ func TestGetHTTPDashboardMissingParameter(t *testing.T) { } } +const fortioResultJSON = `{ + "EndpointResults": { + "http://httpbin.default/get": { + "RunType": "HTTP", + "Labels": "", + "StartTime": "2023-07-21T14:00:40.134434969Z", + "RequestedQPS": "8", + "RequestedDuration": "exactly 100 calls", + "ActualQPS": 7.975606391552989, + "ActualDuration": 12538231589, + "NumThreads": 4, + "Version": "1.57.3", + "DurationHistogram": { + "Count": 100, + "Min": 0.004223875, + "Max": 0.040490042, + "Sum": 1.5977100850000001, + "Avg": 0.015977100850000002, + "StdDev": 0.008340658047253256, + "Data": [ + { + "Start": 0.004223875, + "End": 0.005, + "Percent": 5, + "Count": 5 + }, + { + "Start": 0.005, + "End": 0.006, + "Percent": 10, + "Count": 5 + }, + { + "Start": 0.006, + "End": 0.007, + "Percent": 14, + "Count": 4 + }, + { + "Start": 0.007, + "End": 0.008, + "Percent": 19, + "Count": 5 + }, + { + "Start": 0.008, + "End": 0.009000000000000001, + "Percent": 24, + "Count": 5 + }, + { + "Start": 0.009000000000000001, + "End": 0.01, + "Percent": 28, + "Count": 4 + }, + { + "Start": 0.01, + "End": 0.011, + "Percent": 33, + "Count": 5 + }, + { + "Start": 0.011, + "End": 0.012, + "Percent": 36, + "Count": 3 + }, + { + "Start": 0.012, + "End": 0.014, + "Percent": 48, + "Count": 12 + }, + { + "Start": 0.014, + "End": 0.016, + "Percent": 55, + "Count": 7 + }, + { + "Start": 0.016, + "End": 0.018000000000000002, + "Percent": 65, + "Count": 10 + }, + { + "Start": 0.018000000000000002, + "End": 0.02, + "Percent": 74, + "Count": 9 + }, + { + "Start": 0.02, + "End": 0.025, + "Percent": 85, + "Count": 11 + }, + { + "Start": 0.025, + "End": 0.03, + "Percent": 93, + "Count": 8 + }, + { + "Start": 0.03, + "End": 0.035, + "Percent": 98, + "Count": 5 + }, + { + "Start": 0.035, + "End": 0.04, + "Percent": 99, + "Count": 1 + }, + { + "Start": 0.04, + "End": 0.040490042, + "Percent": 100, + "Count": 1 + } + ], + "Percentiles": [ + { + "Percentile": 50, + "Value": 0.014571428571428572 + }, + { + "Percentile": 75, + "Value": 0.020454545454545454 + }, + { + "Percentile": 90, + "Value": 0.028125 + }, + { + "Percentile": 95, + "Value": 0.032 + }, + { + "Percentile": 99, + "Value": 0.04 + }, + { + "Percentile": 99.9, + "Value": 0.0404410378 + } + ] + }, + "ErrorsDurationHistogram": { + "Count": 0, + "Min": 0, + "Max": 0, + "Sum": 0, + "Avg": 0, + "StdDev": 0, + "Data": null + }, + "Exactly": 100, + "Jitter": false, + "Uniform": false, + "NoCatchUp": false, + "RunID": 0, + "AccessLoggerInfo": "", + "ID": "2023-07-21-140040", + "RetCodes": { + "200": 100 + }, + "IPCountMap": { + "10.96.108.76:80": 4 + }, + "Insecure": false, + "MTLS": false, + "CACert": "", + "Cert": "", + "Key": "", + "UnixDomainSocket": "", + "URL": "http://httpbin.default/get", + "NumConnections": 1, + "Compression": false, + "DisableFastClient": false, + "HTTP10": false, + "H2": false, + "DisableKeepAlive": false, + "AllowHalfClose": false, + "FollowRedirects": false, + "Resolve": "", + "HTTPReqTimeOut": 3000000000, + "UserCredentials": "", + "ContentType": "", + "Payload": null, + "MethodOverride": "", + "LogErrors": false, + "SequentialWarmup": false, + "ConnReuseRange": [ + 0, + 0 + ], + "NoResolveEachConn": false, + "Offset": 0, + "Resolution": 0.001, + "Sizes": { + "Count": 100, + "Min": 413, + "Max": 413, + "Sum": 41300, + "Avg": 413, + "StdDev": 0, + "Data": [ + { + "Start": 413, + "End": 413, + "Percent": 100, + "Count": 100 + } + ] + }, + "HeaderSizes": { + "Count": 100, + "Min": 230, + "Max": 230, + "Sum": 23000, + "Avg": 230, + "StdDev": 0, + "Data": [ + { + "Start": 230, + "End": 230, + "Percent": 100, + "Count": 100 + } + ] + }, + "Sockets": [ + 1, + 1, + 1, + 1 + ], + "SocketCount": 4, + "ConnectionStats": { + "Count": 4, + "Min": 0.001385875, + "Max": 0.001724375, + "Sum": 0.006404583, + "Avg": 0.00160114575, + "StdDev": 0.00013101857565508474, + "Data": [ + { + "Start": 0.001385875, + "End": 0.001724375, + "Percent": 100, + "Count": 4 + } + ], + "Percentiles": [ + { + "Percentile": 50, + "Value": 0.0014987083333333332 + }, + { + "Percentile": 75, + "Value": 0.0016115416666666667 + }, + { + "Percentile": 90, + "Value": 0.0016792416666666667 + }, + { + "Percentile": 95, + "Value": 0.0017018083333333333 + }, + { + "Percentile": 99, + "Value": 0.0017198616666666668 + }, + { + "Percentile": 99.9, + "Value": 0.0017239236666666668 + } + ] + }, + "AbortOn": 0 + } + }, + "Summary": { + "numVersions": 1, + "versionNames": null, + "metricsInfo": { + "http/latency": { + "description": "Latency Histogram", + "units": "msec", + "type": "Histogram" + }, + "http://httpbin.default/get/error-count": { + "description": "number of responses that were errors", + "type": "Counter" + }, + "http://httpbin.default/get/error-rate": { + "description": "fraction of responses that were errors", + "type": "Gauge" + }, + "http://httpbin.default/get/latency-max": { + "description": "maximum of observed latency values", + "units": "msec", + "type": "Gauge" + }, + "http://httpbin.default/get/latency-mean": { + "description": "mean of observed latency values", + "units": "msec", + "type": "Gauge" + }, + "http://httpbin.default/get/latency-min": { + "description": "minimum of observed latency values", + "units": "msec", + "type": "Gauge" + }, + "http://httpbin.default/get/latency-p50": { + "description": "50-th percentile of observed latency values", + "units": "msec", + "type": "Gauge" + }, + "http://httpbin.default/get/latency-p75": { + "description": "75-th percentile of observed latency values", + "units": "msec", + "type": "Gauge" + }, + "http://httpbin.default/get/latency-p90": { + "description": "90-th percentile of observed latency values", + "units": "msec", + "type": "Gauge" + }, + "http://httpbin.default/get/latency-p95": { + "description": "95-th percentile of observed latency values", + "units": "msec", + "type": "Gauge" + }, + "http://httpbin.default/get/latency-p99": { + "description": "99-th percentile of observed latency values", + "units": "msec", + "type": "Gauge" + }, + "http://httpbin.default/get/latency-p99.9": { + "description": "99.9-th percentile of observed latency values", + "units": "msec", + "type": "Gauge" + }, + "http://httpbin.default/get/latency-stddev": { + "description": "standard deviation of observed latency values", + "units": "msec", + "type": "Gauge" + }, + "http://httpbin.default/get/request-count": { + "description": "number of requests sent", + "type": "Counter" + } + }, + "nonHistMetricValues": [ + { + "http://httpbin.default/get/error-count": [ + 0 + ], + "http://httpbin.default/get/error-rate": [ + 0 + ], + "http://httpbin.default/get/latency-max": [ + 40.490041999999995 + ], + "http://httpbin.default/get/latency-mean": [ + 15.977100850000001 + ], + "http://httpbin.default/get/latency-min": [ + 4.2238750000000005 + ], + "http://httpbin.default/get/latency-p50": [ + 14.571428571428571 + ], + "http://httpbin.default/get/latency-p75": [ + 20.454545454545453 + ], + "http://httpbin.default/get/latency-p90": [ + 28.125 + ], + "http://httpbin.default/get/latency-p95": [ + 32 + ], + "http://httpbin.default/get/latency-p99": [ + 40 + ], + "http://httpbin.default/get/latency-p99.9": [ + 40.441037800000004 + ], + "http://httpbin.default/get/latency-stddev": [ + 8.340658047253257 + ], + "http://httpbin.default/get/request-count": [ + 100 + ] + } + ], + "histMetricValues": [ + { + "http/latency": [ + { + "lower": 4.2238750000000005, + "upper": 5, + "count": 5 + }, + { + "lower": 5, + "upper": 6, + "count": 5 + }, + { + "lower": 6, + "upper": 7, + "count": 4 + }, + { + "lower": 7, + "upper": 8, + "count": 5 + }, + { + "lower": 8, + "upper": 9.000000000000002, + "count": 5 + }, + { + "lower": 9.000000000000002, + "upper": 10, + "count": 4 + }, + { + "lower": 10, + "upper": 11, + "count": 5 + }, + { + "lower": 11, + "upper": 12, + "count": 3 + }, + { + "lower": 12, + "upper": 14, + "count": 12 + }, + { + "lower": 14, + "upper": 16, + "count": 7 + }, + { + "lower": 16, + "upper": 18.000000000000004, + "count": 10 + }, + { + "lower": 18.000000000000004, + "upper": 20, + "count": 9 + }, + { + "lower": 20, + "upper": 25, + "count": 11 + }, + { + "lower": 25, + "upper": 30, + "count": 8 + }, + { + "lower": 30, + "upper": 35, + "count": 5 + }, + { + "lower": 35, + "upper": 40, + "count": 1 + }, + { + "lower": 40, + "upper": 40.490041999999995, + "count": 1 + } + ] + } + ], + "SummaryMetricValues": [ + {} + ] + } +}` + +const fortioDashboardJSON = `{"Endpoints":{"http://httpbin.default/get":{"Durations":[{"Version":"0","Bucket":"4.2 - 5","Value":5},{"Version":"0","Bucket":"5 - 6","Value":5},{"Version":"0","Bucket":"6 - 7","Value":4},{"Version":"0","Bucket":"7 - 8","Value":5},{"Version":"0","Bucket":"8 - 9","Value":5},{"Version":"0","Bucket":"9 - 10","Value":4},{"Version":"0","Bucket":"10 - 11","Value":5},{"Version":"0","Bucket":"11 - 12","Value":3},{"Version":"0","Bucket":"12 - 14","Value":12},{"Version":"0","Bucket":"14 - 16","Value":7},{"Version":"0","Bucket":"16 - 18","Value":10},{"Version":"0","Bucket":"18 - 20","Value":9},{"Version":"0","Bucket":"20 - 25","Value":11},{"Version":"0","Bucket":"25 - 30","Value":8},{"Version":"0","Bucket":"30 - 35","Value":5},{"Version":"0","Bucket":"35 - 40","Value":1},{"Version":"0","Bucket":"40 - 40.4","Value":1}],"Statistics":{"Count":100,"Mean":15.977100850000001,"StdDev":8.340658047253257,"Min":4.2238750000000005,"Max":40.490041999999995},"Error durations":[],"Error statistics":{"Count":0,"Mean":0,"StdDev":0,"Min":0,"Max":0},"Return codes":{"200":100}}},"Summary":{"numVersions":1,"versionNames":null,"metricsInfo":{"http/latency":{"description":"Latency Histogram","units":"msec","type":"Histogram"},"http://httpbin.default/get/error-count":{"description":"number of responses that were errors","type":"Counter"},"http://httpbin.default/get/error-rate":{"description":"fraction of responses that were errors","type":"Gauge"},"http://httpbin.default/get/latency-max":{"description":"maximum of observed latency values","units":"msec","type":"Gauge"},"http://httpbin.default/get/latency-mean":{"description":"mean of observed latency values","units":"msec","type":"Gauge"},"http://httpbin.default/get/latency-min":{"description":"minimum of observed latency values","units":"msec","type":"Gauge"},"http://httpbin.default/get/latency-p50":{"description":"50-th percentile of observed latency values","units":"msec","type":"Gauge"},"http://httpbin.default/get/latency-p75":{"description":"75-th percentile of observed latency values","units":"msec","type":"Gauge"},"http://httpbin.default/get/latency-p90":{"description":"90-th percentile of observed latency values","units":"msec","type":"Gauge"},"http://httpbin.default/get/latency-p95":{"description":"95-th percentile of observed latency values","units":"msec","type":"Gauge"},"http://httpbin.default/get/latency-p99":{"description":"99-th percentile of observed latency values","units":"msec","type":"Gauge"},"http://httpbin.default/get/latency-p99.9":{"description":"99.9-th percentile of observed latency values","units":"msec","type":"Gauge"},"http://httpbin.default/get/latency-stddev":{"description":"standard deviation of observed latency values","units":"msec","type":"Gauge"},"http://httpbin.default/get/request-count":{"description":"number of requests sent","type":"Counter"}},"nonHistMetricValues":[{"http://httpbin.default/get/error-count":[0],"http://httpbin.default/get/error-rate":[0],"http://httpbin.default/get/latency-max":[40.490041999999995],"http://httpbin.default/get/latency-mean":[15.977100850000001],"http://httpbin.default/get/latency-min":[4.2238750000000005],"http://httpbin.default/get/latency-p50":[14.571428571428571],"http://httpbin.default/get/latency-p75":[20.454545454545453],"http://httpbin.default/get/latency-p90":[28.125],"http://httpbin.default/get/latency-p95":[32],"http://httpbin.default/get/latency-p99":[40],"http://httpbin.default/get/latency-p99.9":[40.441037800000004],"http://httpbin.default/get/latency-stddev":[8.340658047253257],"http://httpbin.default/get/request-count":[100]}],"histMetricValues":[{"http/latency":[{"lower":4.2238750000000005,"upper":5,"count":5},{"lower":5,"upper":6,"count":5},{"lower":6,"upper":7,"count":4},{"lower":7,"upper":8,"count":5},{"lower":8,"upper":9.000000000000002,"count":5},{"lower":9.000000000000002,"upper":10,"count":4},{"lower":10,"upper":11,"count":5},{"lower":11,"upper":12,"count":3},{"lower":12,"upper":14,"count":12},{"lower":14,"upper":16,"count":7},{"lower":16,"upper":18.000000000000004,"count":10},{"lower":18.000000000000004,"upper":20,"count":9},{"lower":20,"upper":25,"count":11},{"lower":25,"upper":30,"count":8},{"lower":30,"upper":35,"count":5},{"lower":35,"upper":40,"count":1},{"lower":40,"upper":40.490041999999995,"count":1}]}],"SummaryMetricValues":[{}]}}` + func TestGetHTTPDashboard(t *testing.T) { // instantiate metrics client tempDirPath := t.TempDir() @@ -659,8 +1027,7 @@ func TestGetHTTPDashboard(t *testing.T) { abn.MetricsClient = client // preload metric client with result - result := "{\"EndpointResults\":{\"http://httpbin.default/get\":{\"RunType\":\"HTTP\",\"Labels\":\"\",\"StartTime\":\"2023-07-21T14:00:40.134434969Z\",\"RequestedQPS\":\"8\",\"RequestedDuration\":\"exactly 100 calls\",\"ActualQPS\":7.975606391552989,\"ActualDuration\":12538231589,\"NumThreads\":4,\"Version\":\"1.57.3\",\"DurationHistogram\":{\"Count\":100,\"Min\":0.004223875,\"Max\":0.040490042,\"Sum\":1.5977100850000001,\"Avg\":0.015977100850000002,\"StdDev\":0.008340658047253256,\"Data\":[{\"Start\":0.004223875,\"End\":0.005,\"Percent\":5,\"Count\":5},{\"Start\":0.005,\"End\":0.006,\"Percent\":10,\"Count\":5},{\"Start\":0.006,\"End\":0.007,\"Percent\":14,\"Count\":4},{\"Start\":0.007,\"End\":0.008,\"Percent\":19,\"Count\":5},{\"Start\":0.008,\"End\":0.009000000000000001,\"Percent\":24,\"Count\":5},{\"Start\":0.009000000000000001,\"End\":0.01,\"Percent\":28,\"Count\":4},{\"Start\":0.01,\"End\":0.011,\"Percent\":33,\"Count\":5},{\"Start\":0.011,\"End\":0.012,\"Percent\":36,\"Count\":3},{\"Start\":0.012,\"End\":0.014,\"Percent\":48,\"Count\":12},{\"Start\":0.014,\"End\":0.016,\"Percent\":55,\"Count\":7},{\"Start\":0.016,\"End\":0.018000000000000002,\"Percent\":65,\"Count\":10},{\"Start\":0.018000000000000002,\"End\":0.02,\"Percent\":74,\"Count\":9},{\"Start\":0.02,\"End\":0.025,\"Percent\":85,\"Count\":11},{\"Start\":0.025,\"End\":0.03,\"Percent\":93,\"Count\":8},{\"Start\":0.03,\"End\":0.035,\"Percent\":98,\"Count\":5},{\"Start\":0.035,\"End\":0.04,\"Percent\":99,\"Count\":1},{\"Start\":0.04,\"End\":0.040490042,\"Percent\":100,\"Count\":1}],\"Percentiles\":[{\"Percentile\":50,\"Value\":0.014571428571428572},{\"Percentile\":75,\"Value\":0.020454545454545454},{\"Percentile\":90,\"Value\":0.028125},{\"Percentile\":95,\"Value\":0.032},{\"Percentile\":99,\"Value\":0.04},{\"Percentile\":99.9,\"Value\":0.0404410378}]},\"ErrorsDurationHistogram\":{\"Count\":0,\"Min\":0,\"Max\":0,\"Sum\":0,\"Avg\":0,\"StdDev\":0,\"Data\":null},\"Exactly\":100,\"Jitter\":false,\"Uniform\":false,\"NoCatchUp\":false,\"RunID\":0,\"AccessLoggerInfo\":\"\",\"ID\":\"2023-07-21-140040\",\"RetCodes\":{\"200\":100},\"IPCountMap\":{\"10.96.108.76:80\":4},\"Insecure\":false,\"MTLS\":false,\"CACert\":\"\",\"Cert\":\"\",\"Key\":\"\",\"UnixDomainSocket\":\"\",\"URL\":\"http://httpbin.default/get\",\"NumConnections\":1,\"Compression\":false,\"DisableFastClient\":false,\"HTTP10\":false,\"H2\":false,\"DisableKeepAlive\":false,\"AllowHalfClose\":false,\"FollowRedirects\":false,\"Resolve\":\"\",\"HTTPReqTimeOut\":3000000000,\"UserCredentials\":\"\",\"ContentType\":\"\",\"Payload\":null,\"MethodOverride\":\"\",\"LogErrors\":false,\"SequentialWarmup\":false,\"ConnReuseRange\":[0,0],\"NoResolveEachConn\":false,\"Offset\":0,\"Resolution\":0.001,\"Sizes\":{\"Count\":100,\"Min\":413,\"Max\":413,\"Sum\":41300,\"Avg\":413,\"StdDev\":0,\"Data\":[{\"Start\":413,\"End\":413,\"Percent\":100,\"Count\":100}]},\"HeaderSizes\":{\"Count\":100,\"Min\":230,\"Max\":230,\"Sum\":23000,\"Avg\":230,\"StdDev\":0,\"Data\":[{\"Start\":230,\"End\":230,\"Percent\":100,\"Count\":100}]},\"Sockets\":[1,1,1,1],\"SocketCount\":4,\"ConnectionStats\":{\"Count\":4,\"Min\":0.001385875,\"Max\":0.001724375,\"Sum\":0.006404583,\"Avg\":0.00160114575,\"StdDev\":0.00013101857565508474,\"Data\":[{\"Start\":0.001385875,\"End\":0.001724375,\"Percent\":100,\"Count\":4}],\"Percentiles\":[{\"Percentile\":50,\"Value\":0.0014987083333333332},{\"Percentile\":75,\"Value\":0.0016115416666666667},{\"Percentile\":90,\"Value\":0.0016792416666666667},{\"Percentile\":95,\"Value\":0.0017018083333333333},{\"Percentile\":99,\"Value\":0.0017198616666666668},{\"Percentile\":99.9,\"Value\":0.0017239236666666668}]},\"AbortOn\":0}},\"Summary\":{\"numVersions\":1,\"versionNames\":null,\"metricsInfo\":{\"http/latency\":{\"description\":\"Latency Histogram\",\"units\":\"msec\",\"type\":\"Histogram\"},\"http://httpbin.default/get/error-count\":{\"description\":\"number of responses that were errors\",\"type\":\"Counter\"},\"http://httpbin.default/get/error-rate\":{\"description\":\"fraction of responses that were errors\",\"type\":\"Gauge\"},\"http://httpbin.default/get/latency-max\":{\"description\":\"maximum of observed latency values\",\"units\":\"msec\",\"type\":\"Gauge\"},\"http://httpbin.default/get/latency-mean\":{\"description\":\"mean of observed latency values\",\"units\":\"msec\",\"type\":\"Gauge\"},\"http://httpbin.default/get/latency-min\":{\"description\":\"minimum of observed latency values\",\"units\":\"msec\",\"type\":\"Gauge\"},\"http://httpbin.default/get/latency-p50\":{\"description\":\"50-th percentile of observed latency values\",\"units\":\"msec\",\"type\":\"Gauge\"},\"http://httpbin.default/get/latency-p75\":{\"description\":\"75-th percentile of observed latency values\",\"units\":\"msec\",\"type\":\"Gauge\"},\"http://httpbin.default/get/latency-p90\":{\"description\":\"90-th percentile of observed latency values\",\"units\":\"msec\",\"type\":\"Gauge\"},\"http://httpbin.default/get/latency-p95\":{\"description\":\"95-th percentile of observed latency values\",\"units\":\"msec\",\"type\":\"Gauge\"},\"http://httpbin.default/get/latency-p99\":{\"description\":\"99-th percentile of observed latency values\",\"units\":\"msec\",\"type\":\"Gauge\"},\"http://httpbin.default/get/latency-p99.9\":{\"description\":\"99.9-th percentile of observed latency values\",\"units\":\"msec\",\"type\":\"Gauge\"},\"http://httpbin.default/get/latency-stddev\":{\"description\":\"standard deviation of observed latency values\",\"units\":\"msec\",\"type\":\"Gauge\"},\"http://httpbin.default/get/request-count\":{\"description\":\"number of requests sent\",\"type\":\"Counter\"}},\"nonHistMetricValues\":[{\"http://httpbin.default/get/error-count\":[0],\"http://httpbin.default/get/error-rate\":[0],\"http://httpbin.default/get/latency-max\":[40.490041999999995],\"http://httpbin.default/get/latency-mean\":[15.977100850000001],\"http://httpbin.default/get/latency-min\":[4.2238750000000005],\"http://httpbin.default/get/latency-p50\":[14.571428571428571],\"http://httpbin.default/get/latency-p75\":[20.454545454545453],\"http://httpbin.default/get/latency-p90\":[28.125],\"http://httpbin.default/get/latency-p95\":[32],\"http://httpbin.default/get/latency-p99\":[40],\"http://httpbin.default/get/latency-p99.9\":[40.441037800000004],\"http://httpbin.default/get/latency-stddev\":[8.340658047253257],\"http://httpbin.default/get/request-count\":[100]}],\"histMetricValues\":[{\"http/latency\":[{\"lower\":4.2238750000000005,\"upper\":5,\"count\":5},{\"lower\":5,\"upper\":6,\"count\":5},{\"lower\":6,\"upper\":7,\"count\":4},{\"lower\":7,\"upper\":8,\"count\":5},{\"lower\":8,\"upper\":9.000000000000002,\"count\":5},{\"lower\":9.000000000000002,\"upper\":10,\"count\":4},{\"lower\":10,\"upper\":11,\"count\":5},{\"lower\":11,\"upper\":12,\"count\":3},{\"lower\":12,\"upper\":14,\"count\":12},{\"lower\":14,\"upper\":16,\"count\":7},{\"lower\":16,\"upper\":18.000000000000004,\"count\":10},{\"lower\":18.000000000000004,\"upper\":20,\"count\":9},{\"lower\":20,\"upper\":25,\"count\":11},{\"lower\":25,\"upper\":30,\"count\":8},{\"lower\":30,\"upper\":35,\"count\":5},{\"lower\":35,\"upper\":40,\"count\":1},{\"lower\":40,\"upper\":40.490041999999995,\"count\":1}]}],\"SummaryMetricValues\":[{}]}}" - err = abn.MetricsClient.SetResult("default", "default", []byte(result)) + err = abn.MetricsClient.SetResult("default", "default", []byte(fortioResultJSON)) assert.NoError(t, err) w := httptest.NewRecorder() @@ -690,7 +1057,298 @@ func TestGetHTTPDashboard(t *testing.T) { assert.NoError(t, err) assert.Equal( t, - `{"Endpoints":{"http://httpbin.default/get":{"Durations":[{"Version":"0","Bucket":"4.2 - 5","Value":5},{"Version":"0","Bucket":"5 - 6","Value":5},{"Version":"0","Bucket":"6 - 7","Value":4},{"Version":"0","Bucket":"7 - 8","Value":5},{"Version":"0","Bucket":"8 - 9","Value":5},{"Version":"0","Bucket":"9 - 10","Value":4},{"Version":"0","Bucket":"10 - 11","Value":5},{"Version":"0","Bucket":"11 - 12","Value":3},{"Version":"0","Bucket":"12 - 14","Value":12},{"Version":"0","Bucket":"14 - 16","Value":7},{"Version":"0","Bucket":"16 - 18","Value":10},{"Version":"0","Bucket":"18 - 20","Value":9},{"Version":"0","Bucket":"20 - 25","Value":11},{"Version":"0","Bucket":"25 - 30","Value":8},{"Version":"0","Bucket":"30 - 35","Value":5},{"Version":"0","Bucket":"35 - 40","Value":1},{"Version":"0","Bucket":"40 - 40.4","Value":1}],"Statistics":{"Count":100,"Mean":15.977100850000001,"StdDev":8.340658047253257,"Min":4.2238750000000005,"Max":40.490041999999995},"Error durations":[],"Error statistics":{"Count":0,"Mean":0,"StdDev":0,"Min":0,"Max":0},"Return codes":{"200":100}}},"Summary":{"numVersions":1,"versionNames":null,"metricsInfo":{"http/latency":{"description":"Latency Histogram","units":"msec","type":"Histogram"},"http://httpbin.default/get/error-count":{"description":"number of responses that were errors","type":"Counter"},"http://httpbin.default/get/error-rate":{"description":"fraction of responses that were errors","type":"Gauge"},"http://httpbin.default/get/latency-max":{"description":"maximum of observed latency values","units":"msec","type":"Gauge"},"http://httpbin.default/get/latency-mean":{"description":"mean of observed latency values","units":"msec","type":"Gauge"},"http://httpbin.default/get/latency-min":{"description":"minimum of observed latency values","units":"msec","type":"Gauge"},"http://httpbin.default/get/latency-p50":{"description":"50-th percentile of observed latency values","units":"msec","type":"Gauge"},"http://httpbin.default/get/latency-p75":{"description":"75-th percentile of observed latency values","units":"msec","type":"Gauge"},"http://httpbin.default/get/latency-p90":{"description":"90-th percentile of observed latency values","units":"msec","type":"Gauge"},"http://httpbin.default/get/latency-p95":{"description":"95-th percentile of observed latency values","units":"msec","type":"Gauge"},"http://httpbin.default/get/latency-p99":{"description":"99-th percentile of observed latency values","units":"msec","type":"Gauge"},"http://httpbin.default/get/latency-p99.9":{"description":"99.9-th percentile of observed latency values","units":"msec","type":"Gauge"},"http://httpbin.default/get/latency-stddev":{"description":"standard deviation of observed latency values","units":"msec","type":"Gauge"},"http://httpbin.default/get/request-count":{"description":"number of requests sent","type":"Counter"}},"nonHistMetricValues":[{"http://httpbin.default/get/error-count":[0],"http://httpbin.default/get/error-rate":[0],"http://httpbin.default/get/latency-max":[40.490041999999995],"http://httpbin.default/get/latency-mean":[15.977100850000001],"http://httpbin.default/get/latency-min":[4.2238750000000005],"http://httpbin.default/get/latency-p50":[14.571428571428571],"http://httpbin.default/get/latency-p75":[20.454545454545453],"http://httpbin.default/get/latency-p90":[28.125],"http://httpbin.default/get/latency-p95":[32],"http://httpbin.default/get/latency-p99":[40],"http://httpbin.default/get/latency-p99.9":[40.441037800000004],"http://httpbin.default/get/latency-stddev":[8.340658047253257],"http://httpbin.default/get/request-count":[100]}],"histMetricValues":[{"http/latency":[{"lower":4.2238750000000005,"upper":5,"count":5},{"lower":5,"upper":6,"count":5},{"lower":6,"upper":7,"count":4},{"lower":7,"upper":8,"count":5},{"lower":8,"upper":9.000000000000002,"count":5},{"lower":9.000000000000002,"upper":10,"count":4},{"lower":10,"upper":11,"count":5},{"lower":11,"upper":12,"count":3},{"lower":12,"upper":14,"count":12},{"lower":14,"upper":16,"count":7},{"lower":16,"upper":18.000000000000004,"count":10},{"lower":18.000000000000004,"upper":20,"count":9},{"lower":20,"upper":25,"count":11},{"lower":25,"upper":30,"count":8},{"lower":30,"upper":35,"count":5},{"lower":35,"upper":40,"count":1},{"lower":40,"upper":40.490041999999995,"count":1}]}],"SummaryMetricValues":[{}]}}`, + fortioDashboardJSON, + string(body), + ) +} + +func TestGetGHZDashboardInvalidMethod(t *testing.T) { + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, util.PerformanceResultPath, nil) + putResult(w, req) + res := w.Result() + defer func() { + err := res.Body.Close() + assert.NoError(t, err) + }() + assert.Equal(t, http.StatusMethodNotAllowed, res.StatusCode) +} + +func TestGetGHZDashboardMissingParameter(t *testing.T) { + tests := []struct { + queryParams url.Values + expectedStatusCode int + }{ + { + expectedStatusCode: http.StatusBadRequest, + }, + { + queryParams: url.Values{ + "namespace": {"default"}, + }, + expectedStatusCode: http.StatusBadRequest, + }, + { + queryParams: url.Values{ + "experiment": {"default"}, + }, + expectedStatusCode: http.StatusBadRequest, + }, + } + + for _, test := range tests { + w := httptest.NewRecorder() + + u, err := url.ParseRequestURI(util.PerformanceResultPath) + assert.NoError(t, err) + u.RawQuery = test.queryParams.Encode() + urlStr := fmt.Sprintf("%v", u) + + req := httptest.NewRequest(http.MethodPut, urlStr, nil) + + putResult(w, req) + res := w.Result() + defer func() { + err := res.Body.Close() + assert.NoError(t, err) + }() + + assert.Equal(t, test.expectedStatusCode, res.StatusCode) + } +} + +const ghzResultJSON = `{ + "EndpointResults": { + "routeguide.RouteGuide.GetFeature": { + "date": "2023-07-17T12:23:56Z", + "endReason": "normal", + "options": { + "call": "routeguide.RouteGuide.GetFeature", + "host": "routeguide.default:50051", + "proto": "/tmp/ghz.proto", + "import-paths": [ + "/tmp", + "." + ], + "insecure": true, + "load-schedule": "const", + "load-start": 0, + "load-end": 0, + "load-step": 0, + "load-step-duration": 0, + "load-max-duration": 0, + "concurrency": 50, + "concurrency-schedule": "const", + "concurrency-start": 1, + "concurrency-end": 0, + "concurrency-step": 0, + "concurrency-step-duration": 0, + "concurrency-max-duration": 0, + "total": 200, + "connections": 1, + "dial-timeout": 10000000000, + "data": { + "latitude": 407838351, + "longitude": -746143763 + }, + "binary": false, + "CPUs": 5, + "count-errors": true + }, + "count": 200, + "total": 592907667, + "average": 25208185, + "fastest": 32375, + "slowest": 195740917, + "rps": 337.3206506368217, + "errorDistribution": { + "rpc error: code = Unavailable desc = connection error: desc = \"transport: Error while dialing: dial tcp 10.96.20.53:50051: connect: connection refused\"": 200 + }, + "statusCodeDistribution": { + "Unavailable": 200 + }, + "latencyDistribution": [ + { + "percentage": 10, + "latency": 35584 + }, + { + "percentage": 25, + "latency": 39958 + }, + { + "percentage": 50, + "latency": 86208 + }, + { + "percentage": 75, + "latency": 12777625 + }, + { + "percentage": 90, + "latency": 106714334 + }, + { + "percentage": 95, + "latency": 189847000 + }, + { + "percentage": 99, + "latency": 195400792 + } + ], + "histogram": [ + { + "mark": 0.000032375, + "count": 1, + "frequency": 0.005 + }, + { + "mark": 0.0196032292, + "count": 167, + "frequency": 0.835 + }, + { + "mark": 0.0391740834, + "count": 0, + "frequency": 0 + }, + { + "mark": 0.05874493759999999, + "count": 0, + "frequency": 0 + }, + { + "mark": 0.07831579179999999, + "count": 0, + "frequency": 0 + }, + { + "mark": 0.097886646, + "count": 3, + "frequency": 0.015 + }, + { + "mark": 0.11745750019999998, + "count": 13, + "frequency": 0.065 + }, + { + "mark": 0.1370283544, + "count": 0, + "frequency": 0 + }, + { + "mark": 0.15659920859999998, + "count": 0, + "frequency": 0 + }, + { + "mark": 0.17617006279999997, + "count": 0, + "frequency": 0 + }, + { + "mark": 0.195740917, + "count": 16, + "frequency": 0.08 + } + ], + "details": [ + { + "timestamp": "2023-07-17T12:23:56.089998719Z", + "latency": 14490041, + "error": "rpc error: code = Unavailable desc = connection error: desc = \"transport: Error while dialing: dial tcp 10.96.20.53:50051: connect: connection refused\"", + "status": "Unavailable" + }, + { + "timestamp": "2023-07-17T12:23:56.090471886Z", + "latency": 13759125, + "error": "rpc error: code = Unavailable desc = connection error: desc = \"transport: Error while dialing: dial tcp 10.96.20.53:50051: connect: connection refused\"", + "status": "Unavailable" + }, + { + "timestamp": "2023-07-17T12:23:56.090528678Z", + "latency": 194468542, + "error": "rpc error: code = Unavailable desc = connection error: desc = \"transport: Error while dialing: dial tcp 10.96.20.53:50051: connect: connection refused\"", + "status": "Unavailable" + }, + { + "timestamp": "2023-07-17T12:23:56.090079886Z", + "latency": 105031291, + "error": "rpc error: code = Unavailable desc = connection error: desc = \"transport: Error while dialing: dial tcp 10.96.20.53:50051: connect: connection refused\"", + "status": "Unavailable" + }, + { + "timestamp": "2023-07-17T12:23:56.090224928Z", + "latency": 100337083, + "error": "rpc error: code = Unavailable desc = connection error: desc = \"transport: Error while dialing: dial tcp 10.96.20.53:50051: connect: connection refused\"", + "status": "Unavailable" + }, + { + "timestamp": "2023-07-17T12:23:56.091097053Z", + "latency": 12463750, + "error": "rpc error: code = Unavailable desc = connection error: desc = \"transport: Error while dialing: dial tcp 10.96.20.53:50051: connect: connection refused\"", + "status": "Unavailable" + }, + { + "timestamp": "2023-07-17T12:23:56.091135844Z", + "latency": 12603875, + "error": "rpc error: code = Unavailable desc = connection error: desc = \"transport: Error while dialing: dial tcp 10.96.20.53:50051: connect: connection refused\"", + "status": "Unavailable" + }, + { + "timestamp": "2023-07-17T12:23:56.478469636Z", + "latency": 86208, + "error": "rpc error: code = Unavailable desc = connection error: desc = \"transport: Error while dialing: dial tcp 10.96.20.53:50051: connect: connection refused\"", + "status": "Unavailable" + } + ] + } + } +}` + +const ghzDashboardJSON = `{"Endpoints":{"routeguide.RouteGuide.GetFeature":{"Durations":[{"Version":"0","Bucket":"0.032","Value":1},{"Version":"0","Bucket":"19.603","Value":167},{"Version":"0","Bucket":"39.174","Value":0},{"Version":"0","Bucket":"58.744","Value":0},{"Version":"0","Bucket":"78.315","Value":0},{"Version":"0","Bucket":"97.886","Value":3},{"Version":"0","Bucket":"117.457","Value":13},{"Version":"0","Bucket":"137.028","Value":0},{"Version":"0","Bucket":"156.599","Value":0},{"Version":"0","Bucket":"176.17","Value":0},{"Version":"0","Bucket":"195.74","Value":16}],"Statistics":{"Count":200,"ErrorCount":200},"Status codes":{"Unavailable":200}}},"Summary":{"numVersions":0,"versionNames":null,"SummaryMetricValues":null}}` + +func TestGetGHZDashboard(t *testing.T) { + // instantiate metrics client + tempDirPath := t.TempDir() + client, err := badgerdb.GetClient(badger.DefaultOptions(tempDirPath), badgerdb.AdditionalOptions{}) + assert.NoError(t, err) + abn.MetricsClient = client + + // preload metric client with result + err = abn.MetricsClient.SetResult("default", "default", []byte(ghzResultJSON)) + assert.NoError(t, err) + + w := httptest.NewRecorder() + + // construct inputs to getGHZDashboard + u, err := url.ParseRequestURI(util.PerformanceResultPath) + assert.NoError(t, err) + params := url.Values{ + "namespace": {"default"}, + "experiment": {"default"}, + } + u.RawQuery = params.Encode() + urlStr := fmt.Sprintf("%v", u) + + req := httptest.NewRequest(http.MethodGet, urlStr, nil) + + // get ghz dashboard based on result in metrics client + getGHZDashboard(w, req) + res := w.Result() + defer func() { + err := res.Body.Close() + assert.NoError(t, err) + }() + + // check the ghz dashboard + body, err := io.ReadAll(res.Body) + assert.NoError(t, err) + assert.Equal( + t, + ghzDashboardJSON, string(body), ) }