-
Notifications
You must be signed in to change notification settings - Fork 34
/
server.go
799 lines (666 loc) · 23.7 KB
/
server.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
package metrics
import (
"context"
"encoding/json"
"fmt"
"io"
"math"
"net/http"
"reflect"
"strconv"
"time"
"github.com/bojand/ghz/runner"
util "github.com/iter8-tools/iter8/base"
"github.com/iter8-tools/iter8/base/log"
"github.com/iter8-tools/iter8/controllers"
"github.com/iter8-tools/iter8/storage"
storageclient "github.com/iter8-tools/iter8/storage/client"
"github.com/montanaflynn/stats"
"gonum.org/v1/plot/plotter"
"fortio.org/fortio/fhttp"
fstats "fortio.org/fortio/stats"
)
const (
// MetricsConfigFileEnv is name of environment variable containing the name metrics config file
MetricsConfigFileEnv = "METRICS_CONFIG_FILE"
defaultPortNumber = 8080
timeFormat = "02 Jan 06 15:04 MST"
)
// versionSummarizedMetric adds version to summary data
type versionSummarizedMetric struct {
Version int
storage.SummarizedMetric
}
// grafanaHistogram represents the histogram in the Grafana Iter8 dashboard
type grafanaHistogram []grafanaHistogramBucket
// grafanaHistogramBucket represents a bucket in the histogram in the Grafana Iter8 dashboard
type grafanaHistogramBucket struct {
// Version is the version of the application
Version string
// Bucket is the bucket of the histogram
// For example: 8-12
Bucket string
// Value is the number of points in this bucket
Value float64
}
// metricSummary is result for a metric
type metricSummary struct {
HistogramsOverTransactions *grafanaHistogram
HistogramsOverUsers *grafanaHistogram
SummaryOverTransactions []*versionSummarizedMetric
SummaryOverUsers []*versionSummarizedMetric
}
// dashboardExperimentResult is a capitalized version of ExperimentResult used to display data in Grafana
type dashboardExperimentResult struct {
// Name is the name of this experiment
Name string
// Namespace is the namespace of this experiment
Namespace string
// Revision of this experiment
Revision int
// StartTime is the time when the experiment run started
StartTime string `json:"Start time"`
// NumCompletedTasks is the number of completed tasks
NumCompletedTasks int `json:"Completed tasks"`
// Failure is true if any of its tasks failed
Failure bool
// Insights produced in this experiment
Insights *util.Insights
// Iter8Version is the version of Iter8 CLI that created this result object
Iter8Version string `json:"Iter8 version"`
}
// httpEndpointRow is the data needed to produce a single row for an HTTP experiment in the Iter8 Grafana dashboard
type httpEndpointRow struct {
Durations grafanaHistogram
Statistics storage.SummarizedMetric
ErrorDurations grafanaHistogram `json:"Error durations"`
ErrorStatistics storage.SummarizedMetric `json:"Error statistics"`
ReturnCodes map[int]int64 `json:"Return codes"`
}
type httpDashboard struct {
// key is the endpoint
Endpoints map[string]httpEndpointRow
ExperimentResult dashboardExperimentResult
}
type ghzStatistics struct {
Count uint64
ErrorCount float64
}
// ghzEndpointRow is the data needed to produce a single row for an gRPC experiment in the Iter8 Grafana dashboard
type ghzEndpointRow struct {
Durations grafanaHistogram
Statistics ghzStatistics
StatusCodeDistribution map[string]int `json:"Status codes"`
}
type ghzDashboard struct {
// key is the endpoint
Endpoints map[string]ghzEndpointRow
ExperimentResult dashboardExperimentResult
}
var allRoutemaps controllers.AllRouteMapsInterface = &controllers.DefaultRoutemaps{}
// metricsConfig is configuration of metrics service
type metricsServiceConfig struct {
// Port is port number on which the metrics service should listen
Port *int `json:"port,omitempty"`
}
// Start starts the HTTP server
func Start(stopCh <-chan struct{}) error {
// read configutation for metrics service
conf := &metricsServiceConfig{}
err := util.ReadConfig(MetricsConfigFileEnv, conf, func() {
if nil == conf.Port {
conf.Port = util.IntPointer(defaultPortNumber)
}
})
if err != nil {
log.Logger.Errorf("unable to read metrics configuration: %s", err.Error())
return err
}
// configure endpoints
http.HandleFunc(util.TestResultPath, putExperimentResult)
http.HandleFunc(util.AbnDashboard, getAbnDashboard)
http.HandleFunc(util.HTTPDashboardPath, getHTTPDashboard)
http.HandleFunc(util.GRPCDashboardPath, getGRPCDashboard)
// configure HTTP server
server := &http.Server{
Addr: fmt.Sprintf(":%d", *conf.Port),
ReadHeaderTimeout: 3 * time.Second,
}
go func() {
<-stopCh
log.Logger.Warnf("stop channel closed, shutting down")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = server.Shutdown(ctx)
}()
// start HTTP server
err = server.ListenAndServe()
if err != nil {
log.Logger.Errorf("unable to start metrics service: %s", err.Error())
return err
}
return nil
}
// getAbnDashboard handles GET /abnDashboard with query parameter application=name and namespace=namespace
func getAbnDashboard(w http.ResponseWriter, r *http.Request) {
log.Logger.Trace("getAbnDashboard called")
defer log.Logger.Trace("getAbnDashboard completed")
// verify method
if r.Method != http.MethodGet {
http.Error(w, "expected GET", http.StatusMethodNotAllowed)
return
}
// verify request (query parameters)
application := r.URL.Query().Get("application")
if application == "" {
http.Error(w, "no application specified", http.StatusBadRequest)
return
}
namespace := r.URL.Query().Get("namespace")
if namespace == "" {
http.Error(w, "no namespace specified", http.StatusBadRequest)
return
}
namespaceApplication := fmt.Sprintf("%s/%s", namespace, application)
log.Logger.Tracef("getAbnDashboard called for application %s", namespaceApplication)
// identify the routemap for the application
rm := allRoutemaps.GetAllRoutemaps().GetRoutemapFromNamespaceName(namespace, application)
if rm == nil || reflect.ValueOf(rm).IsNil() {
http.Error(w, fmt.Sprintf("unknown application %s", namespaceApplication), http.StatusBadRequest)
return
}
log.Logger.Tracef("getAbnDashboard found routemap %v", rm)
// initialize result
result := make(map[string]*metricSummary, 0)
byMetricOverTransactions := make(map[string](map[string][]float64), 0)
byMetricOverUsers := make(map[string](map[string][]float64), 0)
// for each version:
// get metrics
// for each metric, compute summary for metric, version
// prepare for histogram computation
for v, version := range rm.GetVersions() {
signature := version.GetSignature()
if signature == nil {
log.Logger.Debugf("no signature for application %s (version %d)", namespaceApplication, v)
continue
}
if storageclient.MetricsClient == nil {
log.Logger.Error("no metrics client")
continue
}
versionmetrics, err := storageclient.MetricsClient.GetMetrics(namespaceApplication, v, *signature)
if err != nil {
log.Logger.Debugf("no metrics found for application %s (version %d; signature %s)", namespaceApplication, v, *signature)
continue
}
for metric, metrics := range *versionmetrics {
_, ok := result[metric]
if !ok {
// no entry for metric result; create empty entry
result[metric] = &metricSummary{
HistogramsOverTransactions: nil,
HistogramsOverUsers: nil,
SummaryOverTransactions: []*versionSummarizedMetric{},
SummaryOverUsers: []*versionSummarizedMetric{},
}
}
entry := result[metric]
smT, err := calculateSummarizedMetric(metrics.MetricsOverTransactions)
if err != nil {
log.Logger.Debugf("unable to compute summaried metrics over transactions for application %s (version %d; signature %s)", namespaceApplication, v, *signature)
continue
}
entry.SummaryOverTransactions = append(entry.SummaryOverTransactions, &versionSummarizedMetric{
Version: v,
SummarizedMetric: smT,
})
smU, err := calculateSummarizedMetric(metrics.MetricsOverUsers)
if err != nil {
log.Logger.Debugf("unable to compute summaried metrics over users for application %s (version %d; signature %s)", namespaceApplication, v, *signature)
continue
}
entry.SummaryOverUsers = append(entry.SummaryOverUsers, &versionSummarizedMetric{
Version: v,
SummarizedMetric: smU,
})
result[metric] = entry
// copy data into structure for histogram calculation (to be done later)
vStr := fmt.Sprintf("%d", v)
// over transaction data
_, ok = byMetricOverTransactions[metric]
if !ok {
byMetricOverTransactions[metric] = make(map[string][]float64, 0)
}
(byMetricOverTransactions[metric])[vStr] = metrics.MetricsOverTransactions
// over user data
_, ok = byMetricOverUsers[metric]
if !ok {
byMetricOverUsers[metric] = make(map[string][]float64, 0)
}
(byMetricOverUsers[metric])[vStr] = metrics.MetricsOverUsers
}
}
// compute histograms
for metric, byVersion := range byMetricOverTransactions {
hT, err := calculateHistogram(byVersion, 0, 0)
if err != nil {
log.Logger.Debugf("unable to compute histogram over transactions for application %s (metric %s)", namespaceApplication, metric)
continue
}
resultEntry := result[metric]
resultEntry.HistogramsOverTransactions = &hT
result[metric] = resultEntry
}
for metric, byVersion := range byMetricOverUsers {
hT, err := calculateHistogram(byVersion, 0, 0)
if err != nil {
log.Logger.Debugf("unable to compute histogram over users for application %s (metric %s)", namespaceApplication, metric)
continue
}
resultEntry := result[metric]
resultEntry.HistogramsOverUsers = &hT
result[metric] = resultEntry
}
// convert to JSON
b, err := json.MarshalIndent(result, "", " ")
if err != nil {
http.Error(w, fmt.Sprintf("unable to create JSON response %s", string(b)), http.StatusInternalServerError)
return
}
// finally, send response
w.Header().Add("Content-Type", "application/json")
_, _ = w.Write(b)
}
// calculateSummarizedMetric calculates a metric summary for a particular collection of data
func calculateSummarizedMetric(data []float64) (storage.SummarizedMetric, error) {
if len(data) == 0 {
return storage.SummarizedMetric{}, nil
}
// NOTE: len() does not produce a uint64
count := uint64(len(data))
min, err := stats.Min(data)
if err != nil {
return storage.SummarizedMetric{}, err
}
max, err := stats.Max(data)
if err != nil {
return storage.SummarizedMetric{}, err
}
mean, err := stats.Mean(data)
if err != nil {
return storage.SummarizedMetric{}, err
}
stdDev, err := stats.StandardDeviation(data)
if err != nil {
return storage.SummarizedMetric{}, err
}
return storage.SummarizedMetric{
Count: count,
Mean: mean,
StdDev: stdDev,
Min: min,
Max: max,
}, nil
}
// calculateHistogram creates histograms for multiple versions
// the histograms have the same buckets so they can be displayed together
// numBuckets is the number of buckets in the histogram
// decimalPlace is the number of decimal places that the histogram labels should be rounded to
//
// For example: "-0.24178488465151116 - 0.24782423875427073" -> "-0.242 - 0.248"
//
// TODO: defaults for numBuckets/decimalPlace?
func calculateHistogram(versionMetrics map[string][]float64, numBuckets int, decimalPlace float64) (grafanaHistogram, error) {
if numBuckets == 0 {
numBuckets = 10
}
if decimalPlace == 0 {
decimalPlace = 1
}
mins := []float64{}
maxs := []float64{}
for _, metrics := range versionMetrics {
summary, err := calculateSummarizedMetric(metrics)
if err != nil {
return nil, fmt.Errorf("cannot calculate summarized metric: %e", err)
}
mins = append(mins, summary.Min)
maxs = append(maxs, summary.Max)
}
// versionMin is the minimum across all versions
// versionMax is the maximum across all versions
// added to the metrics of each version in order to ensure consistent bins across all versions
versionMin, err := stats.Min(mins)
if err != nil {
return nil, fmt.Errorf("cannot calculate version minimum: %e", err)
}
versionMax, err := stats.Max(maxs)
if err != nil {
return nil, fmt.Errorf("cannot create version maximum: %e", err)
}
grafanaHistogram := grafanaHistogram{}
for version, metrics := range versionMetrics {
// convert the raw values to the gonum plot values
values := make(plotter.Values, len(metrics))
copy(values, metrics)
// append the minimum and maximum across all versions
// allows all the buckets to be the same across versions
values = append(values, versionMin, versionMax)
h, err := plotter.NewHist(values, numBuckets)
if err != nil {
return nil, fmt.Errorf("cannot create Grafana historgram: %e", err)
}
for i, bin := range h.Bins {
count := bin.Weight
// reduce the count for the starting and ending bins to compensate for versionMin and versionMax
// bins are sorted by bucket
// TODO: verify bins are sorted
if i == 0 || i == len(h.Bins)-1 {
count--
}
grafanaHistogram = append(grafanaHistogram, grafanaHistogramBucket{
Version: version,
Bucket: bucketLabel(bin.Min, bin.Max, decimalPlace),
Value: count,
})
}
}
return grafanaHistogram, nil
}
// roundDecimal rounds a given number to the given decimal place
// For example: if x = 2270424855658346, decimalPlace = 3, then return 1.227
func roundDecimal(x float64, decimalPlace float64) float64 {
y := math.Pow(10, decimalPlace)
return math.Floor(x*y) / y
}
// bucketLabel return a label for a histogram bucket
func bucketLabel(min, max float64, decimalPlace float64) string {
return fmt.Sprintf("%s - %s", strconv.FormatFloat(roundDecimal(min, decimalPlace), 'f', -1, 64), strconv.FormatFloat(roundDecimal(max, decimalPlace), 'f', -1, 64))
}
func getHTTPHistogram(fortioHistogram []fstats.Bucket, decimalPlace float64) grafanaHistogram {
grafanaHistogram := grafanaHistogram{}
for _, bucket := range fortioHistogram {
grafanaHistogram = append(grafanaHistogram, grafanaHistogramBucket{
Version: "0",
Bucket: bucketLabel(bucket.Start*1000, bucket.End*1000, decimalPlace),
Value: float64(bucket.Count),
})
}
return grafanaHistogram
}
func getHTTPStatistics(fortioHistogram *fstats.HistogramData, _ float64) storage.SummarizedMetric {
return storage.SummarizedMetric{
Count: uint64(fortioHistogram.Count),
Mean: fortioHistogram.Avg * 1000,
StdDev: fortioHistogram.StdDev * 1000,
Min: fortioHistogram.Min * 1000,
Max: fortioHistogram.Max * 1000,
}
}
func getHTTPEndpointRow(httpRunnerResults *fhttp.HTTPRunnerResults) httpEndpointRow {
row := httpEndpointRow{}
if httpRunnerResults.DurationHistogram != nil {
row.Durations = getHTTPHistogram(httpRunnerResults.DurationHistogram.Data, 1)
row.Statistics = getHTTPStatistics(httpRunnerResults.DurationHistogram, 1)
}
if httpRunnerResults.ErrorsDurationHistogram != nil {
row.ErrorDurations = getHTTPHistogram(httpRunnerResults.ErrorsDurationHistogram.Data, 1)
row.ErrorStatistics = getHTTPStatistics(httpRunnerResults.ErrorsDurationHistogram, 1)
}
row.ReturnCodes = httpRunnerResults.RetCodes
return row
}
func getHTTPDashboardHelper(experimentResult *util.ExperimentResult) httpDashboard {
dashboard := httpDashboard{
Endpoints: map[string]httpEndpointRow{},
ExperimentResult: dashboardExperimentResult{
Name: experimentResult.Name,
Namespace: experimentResult.Namespace,
Revision: experimentResult.Revision,
StartTime: experimentResult.StartTime.Time.Format(timeFormat),
NumCompletedTasks: experimentResult.NumCompletedTasks,
Failure: experimentResult.Failure,
Iter8Version: experimentResult.Iter8Version,
},
}
// get raw data from ExperimentResult
httpTaskData := experimentResult.Insights.TaskData[util.CollectHTTPTaskName]
if httpTaskData == nil {
log.Logger.Error("cannot get http task data from Insights")
return dashboard
}
httpTaskDataBytes, err := json.Marshal(httpTaskData)
if err != nil {
log.Logger.Error("cannot marshal http task data")
return dashboard
}
httpResult := util.HTTPResult{}
err = json.Unmarshal(httpTaskDataBytes, &httpResult)
if err != nil {
log.Logger.Error("cannot unmarshal http task data into HTTPResult")
return dashboard
}
// form rows of dashboard
for endpoint, endpointResult := range httpResult {
endpointResult := endpointResult
dashboard.Endpoints[endpoint] = getHTTPEndpointRow(endpointResult)
}
return dashboard
}
// getHTTPDashboard handles GET /getHTTPDashboard with query parameter test=name and namespace=namespace
func getHTTPDashboard(w http.ResponseWriter, r *http.Request) {
log.Logger.Trace("getHTTPGrafana called")
defer log.Logger.Trace("getHTTPGrafana completed")
// verify method
if r.Method != http.MethodGet {
http.Error(w, "expected GET", http.StatusMethodNotAllowed)
return
}
// verify request (query parameters)
namespace := r.URL.Query().Get("namespace")
if namespace == "" {
http.Error(w, "no namespace specified", http.StatusBadRequest)
return
}
test := r.URL.Query().Get("test")
if test == "" {
http.Error(w, "no test specified", http.StatusBadRequest)
return
}
log.Logger.Tracef("getHTTPGrafana called for namespace %s and test %s", namespace, test)
// get fortioResult from metrics client
if storageclient.MetricsClient == nil {
http.Error(w, "no metrics client", http.StatusInternalServerError)
return
}
// get testResult from metrics client
testResult, err := storageclient.MetricsClient.GetExperimentResult(namespace, test)
if err != nil {
errorMessage := fmt.Sprintf("cannot get experiment result with namespace %s, test %s", namespace, test)
log.Logger.Error(errorMessage)
http.Error(w, errorMessage, http.StatusBadRequest)
return
}
// JSON marshal the dashboard
dashboardBytes, err := json.Marshal(getHTTPDashboardHelper(testResult))
if err != nil {
errorMessage := "cannot JSON marshal HTTP 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)
}
func getGRPCHistogram(ghzHistogram []runner.Bucket, _ 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 getGRPCStatistics(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 getGRPCEndpointRow(ghzRunnerReport *runner.Report) ghzEndpointRow {
row := ghzEndpointRow{}
if ghzRunnerReport.Histogram != nil {
row.Durations = getGRPCHistogram(ghzRunnerReport.Histogram, 3)
row.Statistics = getGRPCStatistics(ghzRunnerReport)
}
row.StatusCodeDistribution = ghzRunnerReport.StatusCodeDist
return row
}
func getGRPCDashboardHelper(experimentResult *util.ExperimentResult) ghzDashboard {
dashboard := ghzDashboard{
Endpoints: map[string]ghzEndpointRow{},
ExperimentResult: dashboardExperimentResult{
Name: experimentResult.Name,
Namespace: experimentResult.Namespace,
Revision: experimentResult.Revision,
StartTime: experimentResult.StartTime.Time.Format(timeFormat),
NumCompletedTasks: experimentResult.NumCompletedTasks,
Failure: experimentResult.Failure,
Iter8Version: experimentResult.Iter8Version,
},
}
// get raw data from ExperimentResult
ghzTaskData := experimentResult.Insights.TaskData[util.CollectGRPCTaskName]
if ghzTaskData == nil {
return dashboard
}
ghzTaskDataBytes, err := json.Marshal(ghzTaskData)
if err != nil {
log.Logger.Error("cannot marshal ghz task data")
return dashboard
}
ghzResult := util.GHZResult{}
err = json.Unmarshal(ghzTaskDataBytes, &ghzResult)
if err != nil {
log.Logger.Error("cannot unmarshal ghz task data into GHZResult")
return dashboard
}
// form rows of dashboard
for endpoint, endpointResult := range ghzResult {
endpointResult := endpointResult
dashboard.Endpoints[endpoint] = getGRPCEndpointRow(endpointResult)
}
return dashboard
}
// getGRPCDashboard handles GET /getGRPCDashboard with query parameter test=name and namespace=namespace
func getGRPCDashboard(w http.ResponseWriter, r *http.Request) {
log.Logger.Trace("getGRPCDashboard called")
defer log.Logger.Trace("getGRPCDashboard completed")
// verify method
if r.Method != http.MethodGet {
http.Error(w, "expected GET", http.StatusMethodNotAllowed)
return
}
// verify request (query parameters)
namespace := r.URL.Query().Get("namespace")
if namespace == "" {
http.Error(w, "no namespace specified", http.StatusBadRequest)
return
}
test := r.URL.Query().Get("test")
if test == "" {
http.Error(w, "no test specified", http.StatusBadRequest)
return
}
log.Logger.Tracef("getGRPCDashboard called for namespace %s and test %s", namespace, test)
// get ghz result from metrics client
if storageclient.MetricsClient == nil {
http.Error(w, "no metrics client", http.StatusInternalServerError)
return
}
// get testResult from metrics client
testResult, err := storageclient.MetricsClient.GetExperimentResult(namespace, test)
if err != nil {
errorMessage := fmt.Sprintf("cannot get experiment result with namespace %s, test %s", namespace, test)
log.Logger.Error(errorMessage)
http.Error(w, errorMessage, http.StatusBadRequest)
return
}
// JSON marshal the dashboard
dashboardBytes, err := json.Marshal(getGRPCDashboardHelper(testResult))
if err != nil {
errorMessage := "cannot JSON marshal gRPC 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)
}
// putExperimentResult handles PUT /testResult with query parameter test=name and namespace=namespace
func putExperimentResult(w http.ResponseWriter, r *http.Request) {
log.Logger.Trace("putExperimentResult called")
defer log.Logger.Trace("putExperimentResult completed")
// verify method
if r.Method != http.MethodPut {
http.Error(w, "expected PUT", http.StatusMethodNotAllowed)
return
}
// verify request (query parameters)
namespace := r.URL.Query().Get("namespace")
if namespace == "" {
http.Error(w, "no namespace specified", http.StatusBadRequest)
return
}
experiment := r.URL.Query().Get("test")
if experiment == "" {
http.Error(w, "no test specified", http.StatusBadRequest)
return
}
log.Logger.Tracef("putExperimentResult called for namespace %s and test %s", namespace, experiment)
defer func() {
err := r.Body.Close()
if err != nil {
errorMessage := fmt.Sprintf("cannot close request body: %e", err)
log.Logger.Error(errorMessage)
http.Error(w, errorMessage, http.StatusBadRequest)
return
}
}()
body, err := io.ReadAll(r.Body)
if err != nil {
errorMessage := fmt.Sprintf("cannot read request body: %e", err)
log.Logger.Error(errorMessage)
http.Error(w, errorMessage, http.StatusBadRequest)
return
}
experimentResult := util.ExperimentResult{}
err = json.Unmarshal(body, &experimentResult)
if err != nil {
errorMessage := fmt.Sprintf("cannot unmarshal body into ExperimentResult: %s: %e", string(body), err)
log.Logger.Error(errorMessage)
http.Error(w, errorMessage, http.StatusBadRequest)
return
}
if storageclient.MetricsClient == nil {
http.Error(w, "no metrics client", http.StatusInternalServerError)
return
}
err = storageclient.MetricsClient.SetExperimentResult(namespace, experiment, &experimentResult)
if err != nil {
errorMessage := fmt.Sprintf("cannot store result in storage client: %s: %e", string(body), err)
log.Logger.Error(errorMessage)
http.Error(w, errorMessage, http.StatusInternalServerError)
return
}
// TODO: 201 for new resource, 200 for update
}