Repository: iter8-tools/iter8
Pull request #1151 (2022-01-26 14:44:35)

gRPC load test and other enhancements
1. Bumping up code coverage to 69%.
2. Removing tag based validation `validate:"url"` -- this is turning out to be really complex beyond the most elementary type of validation
3. Removing insight types -- this was an overkill  and not required
4. Removing task specs (Tasks can be directly serialized and deserialized now)
5. GRPC metrics collection task (new task)
6. renaming the old collect task as collect_http, along with cleanup
7. Introducing the notion of a sample metric type, and aggregation operations on top of it
8. Removing interpolation from run task (this is an excellent example of needless user-facing complexity)
9. Improved reporting using text and html tpl files that are now better aligned, and also using text and html templates (latter is supposed to be more secure while generating html output)
10. Renaming to `gen-load-and-collect-metrics-http`


Patch:
diff --git a/.gitignore b/.gitignore
index ef7d7baf..c2ddd751 100644
--- a/.gitignore
+++ b/.gitignore
@@ -134,3 +134,7 @@ iter8
 clibase/clibase
 result.yaml
 report.html
+**/ghz.proto
+**/ghz-call-data.json
+**/ghz-call-data.bin
+**/ghz-call-metadata.json
diff --git a/Makefile b/Makefile
index 5e08a0c3..4e705d37 100644
--- a/Makefile
+++ b/Makefile
@@ -17,7 +17,7 @@ LDFLAGS    := -w -s
 GOFLAGS    :=
 
 # Rebuild the binary if any of these files change
-SRC := $(shell find . -type f -name '*.go' -print) go.mod go.sum
+SRC := $(shell find . -type f -name '*.(go|proto|tpl)' -print) go.mod go.sum
 
 # Required for globs to work correctly
 SHELL      = /usr/bin/env bash
@@ -121,7 +121,7 @@ staticcheck:
 
 .PHONY: test
 test: fmt vet ## Run tests.
-	go test ./... -race -coverprofile=coverage.out -covermode=atomic	
+	go test ./... -race -coverprofile=coverage.out -covermode=atomic
 
 .PHONY: coverage
 coverage: test
diff --git a/README.md b/README.md
index 4b5beede..8893ee11 100644
--- a/README.md
+++ b/README.md
@@ -93,16 +93,4 @@ Congratulations! :tada: You completed your first Iter8 experiment.
 Iter8 documentation is available at https://iter8.tools.
 
 ## Contributing
-We are delighted that you want to contribute to Iter8! 💖
-
-As you get started, you are in the best position to give us feedback on areas of
-our project that we need help with including:
-
-* Problems found during setup of Iter8
-* Gaps in our quick start tutorial and other documentation
-* Bugs in our test and automation scripts
-
-If anything doesn't make sense, or doesn't work when you run it, please open a
-bug report and let us know!
-
 See [here](https://iter8.tools/latest/contributing/overview/) for information about ways to contribute, Iter8 community meetings, finding an issue, asking for help, pull-request lifecycle, and more.
diff --git a/base/assess.go b/base/assess.go
index 71ecac80..5ccb4101 100644
--- a/base/assess.go
+++ b/base/assess.go
@@ -2,14 +2,8 @@
 package base
 
 import (
-	"encoding/json"
 	"errors"
-	"fmt"
-	"regexp"
-	"strconv"
-	"strings"
 
-	"github.com/go-playground/validator/v10"
 	"github.com/iter8-tools/iter8/base/log"
 )
 
@@ -21,7 +15,7 @@ type assessInputs struct {
 
 // assessTask enables assessment of versions
 type assessTask struct {
-	taskMeta
+	TaskMeta
 	// With contains the inputs for the assessTask
 	With assessInputs `json:"with" yaml:"with"`
 }
@@ -29,50 +23,25 @@ type assessTask struct {
 const (
 	// AssessTaskName is the name of the task this file implements
 	AssessTaskName = "assess-app-versions"
-
-	// percentilePrefix is the prefix for builtin percentile metrics
-	percentilePrefix = `built-in/p`
-
-	// the regular expression used to match builtin percentile metrics
-	builtinPrecentileRE = percentilePrefix + `.+`
 )
 
-// MakeAssess constructs an asessTask out of a task spec
-func MakeAssess(t *TaskSpec) (Task, error) {
-	if t == nil || t.Task == nil || *t.Task != AssessTaskName {
-		return nil, errors.New("task needs to be " + AssessTaskName)
-	}
-	var err error
-	var jsonBytes []byte
-	var bt Task
-	// convert t to jsonBytes
-	jsonBytes, _ = json.Marshal(t)
-	// convert jsonString to AssessTask
-	at := &assessTask{}
-	err = json.Unmarshal(jsonBytes, &at)
-	if err != nil {
-		log.Logger.WithStackTrace(err.Error()).Error("unable to unmarshal assess task")
-		return nil, err
-	}
+// initializeDefaults sets default values for task inputs
+func (t *assessTask) initializeDefaults() {}
 
-	validate := validator.New()
-	err = validate.Struct(at)
-	if err != nil {
-		log.Logger.WithStackTrace(err.Error()).Error("invalid assess task specification")
-		return nil, err
-	}
-
-	bt = at
-	return bt, nil
-}
-
-// GetName returns the name of the assess task
-func (t *assessTask) GetName() string {
-	return AssessTaskName
+//validateInputs for this task
+func (t *assessTask) validateInputs() error {
+	return nil
 }
 
 // Run executes the assess-app-versions task
 func (t *assessTask) Run(exp *Experiment) error {
+	err := t.validateInputs()
+	if err != nil {
+		return err
+	}
+
+	t.initializeDefaults()
+
 	if exp.Result.Insights == nil {
 		log.Logger.Error("uninitialized insights within experiment")
 		return errors.New("uninitialized insights within experiment")
@@ -86,11 +55,8 @@ func (t *assessTask) Run(exp *Experiment) error {
 		return nil
 	}
 
-	// set insight type (if needed)
-	exp.Result.Insights.setInsightType(InsightTypeSLO)
-
 	// set SLOs (if needed)
-	err := exp.Result.Insights.setSLOs(t.With.SLOs)
+	err = exp.Result.Insights.setSLOs(t.With.SLOs)
 	if err != nil {
 		return err
 	}
@@ -104,9 +70,6 @@ func (t *assessTask) Run(exp *Experiment) error {
 	// set SLOsSatisfied
 	exp.Result.Insights.SLOsSatisfied = evaluateSLOs(exp, t.With.SLOs)
 
-	// set SLOsSatisfiedBy
-	exp.Result.Insights.SLOsSatisfiedBy = computeSLOsSatisfiedBy(exp)
-
 	return err
 }
 
@@ -124,7 +87,7 @@ func evaluateSLOs(exp *Experiment, slos []SLO) [][]bool {
 
 // return true if SLO i satisfied by version j
 func sloSatisfied(e *Experiment, slos []SLO, i int, j int) bool {
-	val := getMetricValue(e, j, slos[i].Metric)
+	val := e.Result.Insights.ScalarMetricValue(j, slos[i].Metric)
 	// check if metric is available
 	if val == nil {
 		log.Logger.Warnf("unable to find value for version %v and metric %s", j, slos[i].Metric)
@@ -144,60 +107,3 @@ func sloSatisfied(e *Experiment, slos []SLO, i int, j int) bool {
 	}
 	return true
 }
-
-// computeSLOsSatisfiedBy computes the subset of versions that satisfy SLOs
-func computeSLOsSatisfiedBy(exp *Experiment) []int {
-	sats := []int{}
-	for j := 0; j < exp.Result.Insights.NumVersions; j++ {
-		sat := true
-		for i := range exp.Result.Insights.SLOs {
-			sat = sat && exp.Result.Insights.SLOsSatisfied[i][j]
-		}
-		if sat {
-			sats = append(sats, j)
-		}
-	}
-	return sats
-}
-
-// get the value of the given metric for the given version
-func getMetricValue(e *Experiment, i int, m string) *float64 {
-	if e == nil ||
-		e.Result == nil ||
-		e.Result.Insights == nil ||
-		len(e.Result.Insights.MetricValues) <= i {
-		log.Logger.Warnf("metric values uninitialized for version %v", i)
-		return nil
-	}
-	// rename builtin metric
-	bpre := regexp.MustCompile(builtinPrecentileRE)
-
-	// match has the builtin percentile value in m
-	match := bpre.FindString(m)
-	if len(match) > 0 {
-		log.Logger.Trace("percentile match: ", match, " for string: ", m)
-		pStr := strings.TrimPrefix(match, percentilePrefix)
-		pFloat, err := strconv.ParseFloat(pStr, 32)
-		if err != nil {
-			log.Logger.Error("unable to parse metric", m)
-			return nil
-		}
-		tmp := []interface{}{}
-		for _, f := range e.Result.Insights.BuiltinLatencyPercentiles {
-			tmp = append(tmp, f)
-		}
-		if ok, ind := inList(tmp, pFloat); !ok {
-			log.Logger.Error("unable to find percentile value in list of built-in percentiles")
-			log.Logger.Error("value: ", pFloat, " built-in percentiles: ", e.Result.Insights.BuiltinLatencyPercentiles)
-			return nil
-		} else {
-			m = percentilePrefix + fmt.Sprintf("%v", e.Result.Insights.BuiltinLatencyPercentiles[ind])
-		}
-	}
-
-	vals := e.Result.Insights.MetricValues[i][m]
-	if len(vals) == 0 {
-		return nil
-	}
-	return float64Pointer(vals[len(vals)-1])
-}
diff --git a/base/collect.go b/base/collect.go
deleted file mode 100644
index d6f4b589..00000000
--- a/base/collect.go
+++ /dev/null
@@ -1,517 +0,0 @@
-package base
-
-import (
-	"encoding/json"
-	"errors"
-	"fmt"
-	"io"
-	"io/ioutil"
-	"math"
-	"math/rand"
-	"net/http"
-	"time"
-
-	"fortio.org/fortio/fhttp"
-	fortioLog "fortio.org/fortio/log"
-	"fortio.org/fortio/periodic"
-	"fortio.org/fortio/stats"
-	"github.com/go-playground/validator/v10"
-	log "github.com/iter8-tools/iter8/base/log"
-)
-
-// version contains header and url information needed to send requests to each version.
-type version struct {
-	// HTTP headers to use in the query for this version; optional
-	Headers map[string]string `json:"headers" yaml:"headers"`
-	// URL to use for querying this version
-	URL string `json:"url" yaml:"url" validate:"required,url"`
-}
-
-// errorRange has lower and upper limits for HTTP status codes. HTTP status code within this range is considered an error
-type errorRange struct {
-	// Lower end of the range
-	Lower *int `json:"lower" yaml:"lower" validate:"required_without=Upper"`
-	// Upper end of the range
-	Upper *int `json:"upper" yaml:"upper" validate:"required_without=Lower"`
-}
-
-// collectInputs contain the inputs to the metrics collection task to be executed.
-type collectInputs struct {
-	// NumRequests is the number of requests to be sent to each version. Default value is 100.
-	NumRequests *int64 `json:"numRequests" yaml:"numRequests"`
-	// Duration of this task. Specified in the Go duration string format (example, 5s). If both duration and numQueries are specified, then duration is ignored.
-	Duration *string `json:"duration" yaml:"duration"`
-	// QPS is the number of queries per second sent to each version. Default value is 8.0.
-	QPS *float32 `json:"qps" yaml:"qps"`
-	// Connections is the number of number of parallel connections used to send load. Default value is 4.
-	Connections *int `json:"connections" yaml:"connections"`
-	// PayloadStr is the string data to be sent as payload. If this field is specified, Iter8 will send HTTP POST requests to versions using this string as the payload.
-	PayloadStr *string `json:"payloadStr" yaml:"payloadStr"`
-	// PayloadURL is the URL of payload. If this field is specified, Iter8 will send HTTP POST requests to versions using data downloaded from this URL as the payload. If both `payloadStr` and `payloadURL` is specified, the former is ignored.
-	PayloadURL *string `json:"payloadURL" yaml:"payloadURL"`
-	// ContentType is the type of the payload. Indicated using the Content-Type HTTP header value. This is intended to be used in conjunction with one of the `payload*` fields above. If this field is specified, Iter8 will send HTTP POST requests to versions using this content type header value.
-	ContentType *string `json:"contentType" yaml:"contentType" validate:"required_with=PayloadURL"`
-	// ErrorRanges is a list of errorRange values. Each range specifies an upper and/or lower limit on HTTP status codes. HTTP responses that fall within these error ranges are considered error. Default value is {{lower: 400},} - i.e., HTTP status codes >= 400 are considered as error.
-	ErrorRanges []errorRange `json:"errorRanges" yaml:"errorRanges"`
-	// Percentiles are the latency percentiles collected by this task. Percentile values have a single digit precision (i.e., rounded to one decimal place). Default value is {50.0, 75.0, 90.0, 95.0, 99.0, 99.9,}.
-	Percentiles []float64 `json:"percentiles" yaml:"percentiles" validate:"unique,dive,gte=0.0,lte=100.0"`
-	// VersionInfo is a non-empty list of version values.
-	VersionInfo []*version `json:"versionInfo" yaml:"versionInfo" validate:"required,notallnil"`
-}
-
-const (
-	// CollectTaskName is the name of this task which performs load generation and metrics collection.
-	CollectTaskName            = "gen-load-and-collect-metrics"
-	defaultQPS                 = float32(8)
-	defaultNumRequests         = int64(100)
-	defaultConnections         = 4
-	requestCountMetricName     = "request-count"
-	errorCountMetricName       = "error-count"
-	errorRateMetricName        = "error-rate"
-	meanLatencyMetricName      = "mean-latency"
-	stdDevMetricName           = "stddev-latency"
-	minLatencyMetricName       = "min-latency"
-	maxLatencyMetricName       = "max-latency"
-	latencyHistogramMetricName = "latency"
-)
-
-var (
-	defaultErrorRanges = []errorRange{{Lower: intPointer(400)}}
-	defaultPercentiles = [...]float64{50.0, 75.0, 90.0, 95.0, 99.0, 99.9}
-)
-
-// errorCode checks if a given code is an error code
-func (t *collectTask) errorCode(code int) bool {
-	for _, lims := range t.With.ErrorRanges {
-		// if no lower limit (check upper)
-		if lims.Lower == nil && code <= *lims.Upper {
-			return true
-		}
-		// if no upper limit (check lower)
-		if lims.Upper == nil && code >= *lims.Lower {
-			return true
-		}
-		// if both limits are present (check both)
-		if lims.Upper != nil && lims.Lower != nil && code <= *lims.Upper && code >= *lims.Lower {
-			return true
-		}
-	}
-	return false
-}
-
-// collectTask enables collection of Iter8's built-in metrics.
-type collectTask struct {
-	taskMeta
-	With collectInputs `json:"with" yaml:"with" validate:"required"`
-}
-
-// notAllNil validates that not all entries in a slice are nil
-func notAllNil(fl validator.FieldLevel) bool {
-	slice, ok := fl.Field().Interface().([]*version)
-	if !ok {
-		log.Logger.WithStackTrace(fl.Field().String()).Error("unable to convert field to slice of version pointers")
-		return false
-	}
-	allnil := true
-	for _, s := range slice {
-		if s != nil {
-			allnil = false
-			break
-		}
-	}
-	return !allnil
-}
-
-// MakeCollect constructs a CollectTask out of a collect task spec
-func MakeCollect(t *TaskSpec) (Task, error) {
-	if *t.Task != CollectTaskName {
-		return nil, errors.New("task need to be " + CollectTaskName)
-	}
-	var err error
-	var jsonBytes []byte
-	var bt Task
-	// convert t to jsonBytes
-	jsonBytes, err = json.Marshal(t)
-	// convert jsonString to CollectTask
-	if err == nil {
-		ct := &collectTask{}
-		err = json.Unmarshal(jsonBytes, &ct)
-		if err != nil {
-			log.Logger.WithStackTrace(err.Error()).Error("invalid collect task specification")
-			return nil, err
-		}
-
-		validate := validator.New()
-		// returns nil or ValidationErrors ( []FieldError )
-		validate.RegisterValidation("notallnil", notAllNil)
-
-		err = validate.Struct(ct)
-		if err != nil {
-			log.Logger.WithStackTrace(err.Error()).Error("invalid collect task specification")
-			return nil, err
-		}
-
-		bt = ct
-	}
-	return bt, err
-}
-
-// initializeDefaults sets default values for the collect task
-func (t *collectTask) initializeDefaults() {
-	if t.With.NumRequests == nil && t.With.Duration == nil {
-		t.With.NumRequests = int64Pointer(defaultNumRequests)
-	}
-	if t.With.QPS == nil {
-		t.With.QPS = float32Pointer(defaultQPS)
-	}
-	if t.With.Connections == nil {
-		t.With.Connections = intPointer(defaultConnections)
-	}
-	if t.With.ErrorRanges == nil {
-		t.With.ErrorRanges = defaultErrorRanges
-	}
-	// default percentiles are always collected
-	// if other percentiles are specified, they are collected as well
-	for _, p := range defaultPercentiles {
-		t.With.Percentiles = append(t.With.Percentiles, p)
-	}
-	tmp := uniq(t.With.Percentiles)
-	t.With.Percentiles = []float64{}
-	for _, val := range tmp {
-		t.With.Percentiles = append(t.With.Percentiles, val.(float64))
-	}
-}
-
-// getFortioOptions constructs Fortio's HTTP runner options based on collect task inputs
-func (t *collectTask) getFortioOptions(j int) (*fhttp.HTTPRunnerOptions, error) {
-	fortioLog.SetOutput(io.Discard)
-	// basic runner
-	fo := &fhttp.HTTPRunnerOptions{
-		RunnerOptions: periodic.RunnerOptions{
-			RunType:     "Iter8 load test",
-			QPS:         float64(*t.With.QPS),
-			NumThreads:  *t.With.Connections,
-			Percentiles: t.With.Percentiles,
-			Out:         io.Discard,
-		},
-		HTTPOptions: fhttp.HTTPOptions{
-			URL: t.With.VersionInfo[j].URL,
-		},
-	}
-
-	//num requests
-	if t.With.NumRequests != nil {
-		fo.RunnerOptions.Exactly = *t.With.NumRequests
-	}
-
-	// add duration
-	var duration time.Duration
-	var err error
-	if t.With.Duration != nil {
-		duration, err = time.ParseDuration(*t.With.Duration)
-		if err == nil {
-			fo.RunnerOptions.Duration = duration
-		} else {
-			log.Logger.WithStackTrace(err.Error()).Error("unable to parse duration")
-			return nil, err
-		}
-	}
-
-	// content type & payload
-	if t.With.ContentType != nil {
-		fo.ContentType = *t.With.ContentType
-	}
-	if t.With.PayloadStr != nil {
-		fo.Payload = []byte(*t.With.PayloadStr)
-	}
-	if t.With.PayloadURL != nil {
-		b, err := GetPayloadBytes(*t.With.PayloadURL)
-		if err != nil {
-			return nil, err
-		} else {
-			fo.Payload = b
-		}
-	}
-
-	// headers
-	for key, value := range t.With.VersionInfo[j].Headers {
-		fo.AddAndValidateExtraHeader(key + ":" + value)
-	}
-
-	return fo, nil
-}
-
-// resultForVersion collects Fortio result for a given version
-func (t *collectTask) resultForVersion(j int) (*fhttp.HTTPRunnerResults, error) {
-	// the main idea is to run Fortio shell command with proper args
-	// collect Fortio output as a file
-	// and extract the result from the file, and return the result
-
-	fo, err := t.getFortioOptions(j)
-	if err != nil {
-		return nil, err
-	}
-	log.Logger.Trace("got fortio options")
-	ifr, err := fhttp.RunHTTPTest(fo)
-	if err != nil {
-		log.Logger.WithStackTrace(err.Error()).Error("fortio failed")
-		if ifr == nil {
-			log.Logger.Error("failed to get results since fortio run was aborted")
-		}
-	}
-	log.Logger.Trace("ran fortio http test")
-	return ifr, err
-}
-
-// GetName returns the name of the collect task
-func (t *collectTask) GetName() string {
-	return CollectTaskName
-}
-
-// Run executes this task
-func (t *collectTask) Run(exp *Experiment) error {
-	var err error
-	t.initializeDefaults()
-
-	if len(t.With.VersionInfo) == 0 {
-		log.Logger.Error("collect task must specify info for at least one version")
-		return errors.New("collect task must specify info for at least one version")
-	}
-
-	fm := make([]*fhttp.HTTPRunnerResults, len(t.With.VersionInfo))
-
-	// run fortio queries for each version sequentially
-	for j := range t.With.VersionInfo {
-		log.Logger.Trace("initiating fortio for version ", j)
-		var data *fhttp.HTTPRunnerResults
-		var err error
-		if t.With.VersionInfo[j] == nil {
-			data = nil
-		} else {
-			data, err = t.resultForVersion(j)
-			if err == nil {
-				fm[j] = data
-			} else {
-				return err
-			}
-		}
-	}
-
-	// initialize insights if required
-	if exp.Result.Insights == nil {
-		exp.Result.InitInsights(len(t.With.VersionInfo), []InsightType{InsightTypeMetrics})
-	}
-
-	in := exp.Result.Insights
-
-	// set builtinLatencyPercentiles (if needed)
-	err = in.setBuiltinLatencyPercentiles(t.With.Percentiles)
-	if err != nil {
-		return err
-	}
-
-	// set hist metrics insight type (if needed)
-	in.setInsightType(InsightTypeHistMetrics)
-
-	// initialize metric values (if needed)
-	err = in.initMetricValues(len(t.With.VersionInfo))
-	if err != nil {
-		return err
-	}
-
-	// compute min latency, max latency and bucket sizes needed for latency histograms
-	m := iter8BuiltInPrefix + "/" + latencyHistogramMetricName
-	var xMin float64 = 0   // msec
-	var xMax float64 = 200 // msec
-	if mi, ok := in.MetricsInfo[m]; ok {
-		xMin = *mi.XMin
-		xMax = *mi.XMax
-	}
-	xMin = aggregateLatencyBound(fm, xMin, true)
-	xMax = aggregateLatencyBound(fm, xMax, false)
-
-	for i := range t.With.VersionInfo {
-		if fm[i] != nil {
-			// request count
-			m := iter8BuiltInPrefix + "/" + requestCountMetricName
-			in.MetricsInfo[m] = MetricMeta{
-				Description: "number of requests sent",
-				Type:        CounterMetricType,
-			}
-			in.MetricValues[i][m] = append(in.MetricValues[i][m], float64(fm[i].DurationHistogram.Count))
-
-			// error count & rate
-			val := float64(0)
-			for code, count := range fm[i].RetCodes {
-				if t.errorCode(code) {
-					val += float64(count)
-				}
-			}
-			// error count
-			m = iter8BuiltInPrefix + "/" + errorCountMetricName
-			in.MetricsInfo[m] = MetricMeta{
-				Description: "number of responses that were errors",
-				Type:        CounterMetricType,
-			}
-			in.MetricValues[i][m] = append(in.MetricValues[i][m], val)
-
-			// error-rate
-			m = iter8BuiltInPrefix + "/" + errorRateMetricName
-			rc := float64(fm[i].DurationHistogram.Count)
-			if rc != 0 {
-				in.MetricsInfo[m] = MetricMeta{
-					Description: "fraction of responses that were errors",
-					Type:        GaugeMetricType,
-				}
-				in.MetricValues[i][m] = append(in.MetricValues[i][m], val/rc)
-			}
-
-			// mean-latency
-			m = iter8BuiltInPrefix + "/" + meanLatencyMetricName
-			in.MetricsInfo[m] = MetricMeta{
-				Description: "mean of observed latency values",
-				Type:        GaugeMetricType,
-				Units:       StringPointer("msec"),
-			}
-			in.MetricValues[i][m] = append(in.MetricValues[i][m], 1000.0*fm[i].DurationHistogram.Avg)
-
-			// stddev-latency
-			m = iter8BuiltInPrefix + "/" + stdDevMetricName
-			in.MetricsInfo[m] = MetricMeta{
-				Description: "standard deviation of observed latency values",
-				Type:        GaugeMetricType,
-				Units:       StringPointer("msec"),
-			}
-			in.MetricValues[i][m] = append(in.MetricValues[i][m], 1000.0*fm[i].DurationHistogram.StdDev)
-
-			// min-latency
-			m = iter8BuiltInPrefix + "/" + minLatencyMetricName
-			in.MetricsInfo[m] = MetricMeta{
-				Description: "minimum of observed latency values",
-				Type:        GaugeMetricType,
-				Units:       StringPointer("msec"),
-			}
-			in.MetricValues[i][m] = append(in.MetricValues[i][m], 1000.0*fm[i].DurationHistogram.Min)
-
-			// max-latency
-			m = iter8BuiltInPrefix + "/" + maxLatencyMetricName
-			in.MetricsInfo[m] = MetricMeta{
-				Description: "maximum of observed latency values",
-				Type:        GaugeMetricType,
-				Units:       StringPointer("msec"),
-			}
-			in.MetricValues[i][m] = append(in.MetricValues[i][m], 1000.0*fm[i].DurationHistogram.Max)
-
-			// percentiles
-			for _, p := range fm[i].DurationHistogram.Percentiles {
-				m = iter8BuiltInPrefix + "/" + fmt.Sprintf("p%v", p.Percentile)
-				in.MetricsInfo[m] = MetricMeta{
-					Description: fmt.Sprintf("%v-th percentile of observed latency values", p.Percentile),
-					Type:        GaugeMetricType,
-					Units:       StringPointer("msec"),
-				}
-				in.MetricValues[i][m] = append(in.MetricValues[i][m], 1000.0*p.Value)
-			}
-
-			// latency histogram
-			m = iter8BuiltInPrefix + "/" + latencyHistogramMetricName
-			in.MetricsInfo[m] = MetricMeta{
-				Description: "Latency Histogram",
-				Type:        HistogramMetricType,
-				Units:       StringPointer("msec"),
-				XMin:        float64Pointer(xMin),
-				XMax:        float64Pointer(xMax),
-				NumBuckets:  intPointer(20),
-			}
-			lh := latencyHist(in.MetricsInfo[m], fm[i].DurationHistogram)
-			if in.MetricValues[i][m] == nil {
-				in.MetricValues[i][m] = lh
-			} else {
-				in.MetricValues[i][m], err = vectorSum(in.MetricValues[i][m], lh)
-				if err != nil {
-					return err
-				}
-			}
-		}
-	}
-	return nil
-}
-
-// vectorSum produces the sum of two equi-length vectors
-func vectorSum(a []float64, b []float64) ([]float64, error) {
-	if len(a) != len(b) {
-		log.Logger.Error("vector lengths do not match: ", len(a), " != ", len(b))
-		return nil, fmt.Errorf("vector lengths do not match: %v != %v", len(a), len(b))
-	}
-	sum := make([]float64, len(a))
-	for i := 0; i < len(a); i++ {
-		sum[i] = a[i] + b[i]
-	}
-	return sum, nil
-}
-
-// GetPayloadBytes downloads payload from URL and returns a byte slice
-func GetPayloadBytes(url string) ([]byte, error) {
-	var myClient = &http.Client{Timeout: 10 * time.Second}
-	r, err := myClient.Get(url)
-	if err != nil || r.StatusCode >= 400 {
-		return nil, errors.New("error while fetching payload")
-	}
-	defer r.Body.Close()
-	body, err := ioutil.ReadAll(r.Body)
-	return body, err
-}
-
-// aggregateLatencyBound returns min or max latency value
-func aggregateLatencyBound(fm []*fhttp.HTTPRunnerResults, val float64, min bool) float64 {
-	extremeVal := val
-	for i := 0; i < len(fm); i++ {
-		if fm[i] != nil {
-			if fm[i].DurationHistogram != nil {
-				if min {
-					extremeVal = math.Min(extremeVal, 1000.0*fm[i].DurationHistogram.Min)
-				} else {
-					extremeVal = math.Max(extremeVal, 1000.0*fm[i].DurationHistogram.Max)
-				}
-			}
-		}
-	}
-	return extremeVal
-}
-
-// compute latency histogram by resampling
-func latencyHist(mm MetricMeta, hd *stats.HistogramData) []float64 {
-	rand.Seed(0) // we will make the random number generation deterministic
-	// initialize histogram
-	hist := make([]float64, *mm.NumBuckets)
-	if hd == nil {
-		return hist
-	}
-
-	// create sample
-	var sample []float64
-	for i := 0; i < len(hd.Data); i++ {
-		bucket := hd.Data[i]
-		for j := 0; j < int(bucket.Count); j++ {
-			// get a point; convert to msec
-			point := 1000.0 * (bucket.Start + (bucket.End-bucket.Start)*rand.Float64())
-			sample = append(sample, point)
-		}
-	}
-
-	// update hist
-	for i := 0; i < len(sample); i++ {
-		width := (*mm.XMax - *mm.XMin) / float64(*mm.NumBuckets)
-		ind := int((sample[i] - (*mm.XMin)) / width)
-		if ind < 0 {
-			log.Logger.Warn("sample index out of range: ", ind)
-			ind = 0
-		}
-		if ind >= len(hist) {
-			log.Logger.Warn("sample index out of range: ", ind)
-			ind = len(hist) - 1
-		}
-		hist[ind]++
-	}
-	return hist
-}
diff --git a/base/collect_grpc.go b/base/collect_grpc.go
new file mode 100644
index 00000000..f5c1d3f0
--- /dev/null
+++ b/base/collect_grpc.go
@@ -0,0 +1,273 @@
+package base
+
+import (
+	"errors"
+
+	"github.com/bojand/ghz/runner"
+	log "github.com/iter8-tools/iter8/base/log"
+)
+
+/*
+Credit: The inputs for this task, including their JSON/YAML tags and go doc comments are derived from https://github.com/bojand/ghz.
+*/
+
+// versionGRPC contains call and host information needed to send gRPC requests to each version.
+// this struct holds inputs that is specific to each version
+type versionGRPC struct {
+	// Call A fully-qualified method name in 'package.Service/method' or 'package.Service.Method' format.
+	Call string `json:"call" yaml:"call"`
+	// Host gRPC host in the hostname[:port] format.
+	Host string `json:"host" yaml:"host"`
+}
+
+// collectGRPCInputs holds all the inputs for this task
+type collectGRPCInputs struct {
+	// even though runner.Config is embedded,
+	// many of the fields in runner.Config will not be supported.
+	// Refer to iter8.tools documentation for supported fields.
+	runner.Config
+	// additional settings
+	// ProtoURL	URL pointing to the proto buf.
+	ProtoURL *string `json:"protoURL,omitempty" yaml:"protoURL,omitempty"`
+	// data and metadata settings
+	// DataURL	URL pointing to JSON data to be sent as part of the call.
+	// This takes precedence over Data.
+	DataURL *string `json:"JSONDataURL,omitempty" yaml:"JSONDataURL,omitempty"`
+	// BinaryDataURL	URL pointing to call data as serialized binary message or multiple count-prefixed messages.
+	BinaryDataURL *string `json:"BinaryDataURL,omitempty" yaml:"BinaryDataURL,omitempty"`
+	// MetadataURL	URL pointing to call metadata in JSON format.
+	// This takes precedence over Metadata.
+	MetadataURL *string `json:"MetadataURL,omitempty" yaml:"MetadataURL,omitempty"`
+
+	// VersionInfo is a non-empty list of versionGRPC values.
+	VersionInfo []*versionGRPC `json:"versionInfo" yaml:"versionInfo"`
+}
+
+const (
+	// CollectGPRCTaskName is the name of this task which performs load generation and metrics collection for gRPC services.
+	CollectGPRCTaskName = "gen-load-and-collect-metrics-grpc"
+	// protoFileName is the proto buf file
+	protoFileName = "ghz.proto"
+	// callDataJSONFileName is the JSON call data file
+	callDataJSONFileName = "ghz-call-data.json"
+	// callDataBinaryFileName is the binary call data file
+	callDataBinaryFileName = "ghz-call-data.bin"
+	// callMetadataJSONFileName is the JSON call metadata file
+	callMetadataJSONFileName = "ghz-call-metadata.json"
+	// gRPCRequestCountMetricName is name of the gRPC request count metric
+	gRPCRequestCountMetricName = "grpc-request-count"
+	// gRPCErrorCountMetricName is name of the gRPC error count metric
+	gRPCErrorCountMetricName = "grpc-error-count"
+	// gRPCErrorRateMetricName is name of the gRPC error rate metric
+	gRPCErrorRateMetricName = "grpc-error-rate"
+	// gRPCLatencySampleMetricName is name of the gRPC latency sample metric
+	gRPCLatencySampleMetricName = "grpc-latency"
+	// countErrorsDefault is the default value which indicates if errors are counted
+	countErrorsDefault = true
+)
+
+// collectGRPCTask enables load testing of gRPC services.
+type collectGRPCTask struct {
+	TaskMeta
+	With collectGRPCInputs `json:"with" yaml:"with"`
+}
+
+// initializeDefaults sets default values for the collect task
+func (t *collectGRPCTask) initializeDefaults() {
+	// always count errors
+	t.With.Config.CountErrors = countErrorsDefault
+	// always insecure
+	// ToDo: document security credentials
+	t.With.Config.Insecure = true
+}
+
+// validate task inputs
+func (t *collectGRPCTask) validateInputs() error {
+	return nil
+}
+
+// getGhzConfig constructs ghz's runner.Config for version j based on task inputs
+func (t *collectGRPCTask) getGhzConfig(j int) (*runner.Config, error) {
+	ghzc := t.With.Config
+	ghzc.Call = t.With.VersionInfo[j].Call
+	ghzc.Host = t.With.VersionInfo[j].Host
+
+	// get proto file
+	if t.With.ProtoURL != nil {
+		err := getFileFromURL(*t.With.ProtoURL, protoFileName)
+		if err != nil {
+			return nil, err
+		}
+		ghzc.Proto = protoFileName
+	}
+	// get JSON call data file
+	if t.With.DataURL != nil {
+		err := getFileFromURL(*t.With.ProtoURL, callDataJSONFileName)
+		if err != nil {
+			return nil, err
+		}
+		ghzc.DataPath = callDataJSONFileName
+	}
+	// get binary data file
+	if t.With.BinaryDataURL != nil {
+		err := getFileFromURL(*t.With.ProtoURL, callDataBinaryFileName)
+		if err != nil {
+			return nil, err
+		}
+		ghzc.BinDataPath = callDataBinaryFileName
+	}
+	// get metadata file
+	if t.With.MetadataURL != nil {
+		err := getFileFromURL(*t.With.ProtoURL, callMetadataJSONFileName)
+		if err != nil {
+			return nil, err
+		}
+		ghzc.MetadataPath = callMetadataJSONFileName
+	}
+
+	return &ghzc, nil
+}
+
+// getGhzOption constructs ghz's runner.Option based on task inputs
+func (t *collectGRPCTask) getGhzOption(j int) (runner.Option, error) {
+	ghzc, err := t.getGhzConfig(j)
+	if err != nil {
+		return nil, err
+	}
+	return runner.WithConfig(ghzc), nil
+}
+
+// resultForVersion collects gRPC test result for a given version
+func (t *collectGRPCTask) resultForVersion(j int) (*runner.Report, error) {
+	// the main idea is to run ghz with proper options
+
+	ghzo, err := t.getGhzOption(j)
+	if err != nil {
+		return nil, err
+	}
+	log.Logger.Trace("got ghz options")
+	igr, err := runner.Run(t.With.VersionInfo[j].Call, t.With.VersionInfo[j].Host, ghzo)
+	if err != nil {
+		log.Logger.WithStackTrace(err.Error()).Error("ghz failed")
+		if igr == nil {
+			log.Logger.Error("failed to get results since ghz run was aborted")
+		}
+	}
+	log.Logger.Trace("ran ghz gRPC test")
+	return igr, err
+}
+
+// latencySample extracts a latency sample from ghz result details
+func latencySample(rd []runner.ResultDetail) []float64 {
+	f := make([]float64, len(rd))
+	for i := 0; i < len(rd); i++ {
+		f[i] = float64(rd[i].Latency.Milliseconds())
+	}
+	return f
+}
+
+// Run executes this task
+func (t *collectGRPCTask) Run(exp *Experiment) error {
+	// 1. initialize defaults
+	var err error
+
+	err = t.validateInputs()
+	if err != nil {
+		return err
+	}
+
+	t.initializeDefaults()
+
+	// 2. validate input (without going crazy in terms of rules)
+	// idea is to check for the most obvious errors here
+	// validation also happens at the chart/values level
+	if len(t.With.VersionInfo) == 0 {
+		log.Logger.Error("collect task must specify info for at least one version")
+		return errors.New("collect task must specify info for at least one version")
+	}
+
+	// 3. collect raw results from ghz for each version
+
+	// initialize a slice to hold reports from the ghz tests
+	gr := make([]*runner.Report, len(t.With.VersionInfo))
+
+	// run ghz test for each version sequentially
+	// collect ghz report for each version
+	// ghz reports will be further processed to populate metrics
+	for j := range t.With.VersionInfo {
+		log.Logger.Trace("initiating ghz for version ", j)
+		var data *runner.Report
+		var err error
+		if t.With.VersionInfo[j] == nil { // nothing to do for this version
+			data = nil
+		} else {
+			// this is where we get the raw ghz data for version j
+			data, err = t.resultForVersion(j)
+			if err == nil {
+				gr[j] = data
+			} else {
+				return err
+			}
+		}
+	}
+
+	// 4. The inputs for this task determine the number of versions participating in the experiment.
+	// Hence, init insights with num versions
+	err = exp.Result.initInsightsWithNumVersions(len(t.With.VersionInfo))
+	if err != nil {
+		return err
+	}
+	in := exp.Result.Insights
+
+	// 5. Populate all metrics collected by this task
+	for i := range t.With.VersionInfo { // for each version
+		if gr[i] != nil { // assuming there is some raw ghz result to process for this version
+			// populate grpc request count
+			// ToDo: This logic breaks for looped experiments. Fix when we get to loops.
+			m := iter8BuiltInPrefix + "/" + gRPCRequestCountMetricName
+			mm := MetricMeta{
+				Description: "number of gRPC requests sent",
+				Type:        CounterMetricType,
+			}
+			in.updateMetric(m, mm, i, float64(gr[i].Count))
+
+			// populate error count & rate
+			ec := float64(0)
+			for _, count := range gr[i].ErrorDist {
+				ec += float64(count)
+			}
+
+			// populate count
+			// ToDo: This logic breaks for looped experiments. Fix when we get to loops.
+			m = iter8BuiltInPrefix + "/" + gRPCErrorCountMetricName
+			mm = MetricMeta{
+				Description: "number of responses that were errors",
+				Type:        CounterMetricType,
+			}
+			in.updateMetric(m, mm, i, ec)
+
+			// populate rate
+			// ToDo: This logic breaks for looped experiments. Fix when we get to loops.
+			m = iter8BuiltInPrefix + "/" + gRPCErrorRateMetricName
+			rc := float64(gr[i].Count)
+			if rc != 0 {
+				mm = MetricMeta{
+					Description: "fraction of responses that were errors",
+					Type:        GaugeMetricType,
+				}
+				in.updateMetric(m, mm, i, ec/rc)
+			}
+
+			// populate latency sample
+			m = iter8BuiltInPrefix + "/" + gRPCLatencySampleMetricName
+			mm = MetricMeta{
+				Description: "gRPC Latency Sample",
+				Type:        SampleMetricType,
+				Units:       StringPointer("msec"),
+			}
+			lh := latencySample(gr[i].Details)
+			in.updateMetric(m, mm, i, lh)
+		}
+	}
+	return nil
+}
diff --git a/base/collect_http.go b/base/collect_http.go
new file mode 100644
index 00000000..159d0271
--- /dev/null
+++ b/base/collect_http.go
@@ -0,0 +1,362 @@
+package base
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	"time"
+
+	"fortio.org/fortio/fhttp"
+	fortioLog "fortio.org/fortio/log"
+	"fortio.org/fortio/periodic"
+	"fortio.org/fortio/stats"
+	log "github.com/iter8-tools/iter8/base/log"
+)
+
+// versionHTTP contains header and url information needed to send requests to each version.
+type versionHTTP struct {
+	// HTTP headers to use in the query for this version; optional
+	Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"`
+	// URL to use for querying this version
+	URL string `json:"url" yaml:"url"`
+}
+
+// errorRange has lower and upper limits for HTTP status codes. HTTP status code within this range is considered an error
+type errorRange struct {
+	// Lower end of the range
+	Lower *int `json:"lower" yaml:"lower"`
+	// Upper end of the range
+	Upper *int `json:"upper" yaml:"upper"`
+}
+
+// collectHTTPInputs contain the inputs to the metrics collection task to be executed.
+type collectHTTPInputs struct {
+	// NumRequests is the number of requests to be sent to each version. Default value is 100.
+	NumRequests *int64 `json:"numRequests" yaml:"numRequests"`
+	// Duration of this task. Specified in the Go duration string format (example, 5s). If both duration and numQueries are specified, then duration is ignored.
+	Duration *string `json:"duration" yaml:"duration"`
+	// QPS is the number of requests per second sent to each version. Default value is 8.0.
+	RPS *float32 `json:"rps" yaml:"rps"`
+	// Connections is the number of number of parallel connections used to send load. Default value is 4.
+	Connections *int `json:"connections" yaml:"connections"`
+	// PayloadStr is the string data to be sent as payload. If this field is specified, Iter8 will send HTTP POST requests to versions using this string as the payload.
+	PayloadStr *string `json:"payloadStr" yaml:"payloadStr"`
+	// PayloadURL is the URL of payload. If this field is specified, Iter8 will send HTTP POST requests to versions using data downloaded from this URL as the payload. If both `payloadStr` and `payloadURL` is specified, the former is ignored.
+	PayloadURL *string `json:"payloadURL" yaml:"payloadURL"`
+	// ContentType is the type of the payload. Indicated using the Content-Type HTTP header value. This is intended to be used in conjunction with one of the `payload*` fields above. If this field is specified, Iter8 will send HTTP POST requests to versions using this content type header value.
+	ContentType *string `json:"contentType" yaml:"contentType"`
+	// ErrorRanges is a list of errorRange values. Each range specifies an upper and/or lower limit on HTTP status codes. HTTP responses that fall within these error ranges are considered error. Default value is {{lower: 400},} - i.e., HTTP status codes >= 400 are considered as error.
+	ErrorRanges []errorRange `json:"errorRanges" yaml:"errorRanges"`
+	// Percentiles are the latency percentiles collected by this task. Percentile values have a single digit precision (i.e., rounded to one decimal place). Default value is {50.0, 75.0, 90.0, 95.0, 99.0, 99.9,}.
+	Percentiles []float64 `json:"percentiles" yaml:"percentiles"`
+	// VersionInfo is a non-empty list of version values.
+	VersionInfo []*versionHTTP `json:"versionInfo" yaml:"versionInfo"`
+}
+
+const (
+	// CollectHTTPTaskName is the name of this task which performs load generation and metrics collection.
+	CollectHTTPTaskName                = "gen-load-and-collect-metrics-http"
+	defaultRPS                         = float32(8)
+	defaultHTTPNumRequests             = int64(100)
+	defaultHTTPConnections             = 4
+	builtInHTTPRequestCountId          = "http-request-count"
+	builtInHTTPErrorCountId            = "http-error-count"
+	builtInHTTPErrorRateId             = "http-error-rate"
+	builtInHTTPLatencyMeanId           = "http-latency-mean"
+	builtInHTTPLatencyStdDevId         = "http-latency-stddev"
+	builtInHTTPLatencyMinId            = "http-latency-min"
+	builtInHTTPLatencyMaxId            = "http-latency-max"
+	builtInHTTPLatencyHistId           = "http-latency"
+	builtInHTTPLatencyPercentilePrefix = "http-latency-p"
+)
+
+var (
+	defaultErrorRanges = []errorRange{{Lower: intPointer(400)}}
+	defaultPercentiles = [...]float64{50.0, 75.0, 90.0, 95.0, 99.0, 99.9}
+)
+
+// errorCode checks if a given code is an error code
+func (t *collectHTTPTask) errorCode(code int) bool {
+	for _, lims := range t.With.ErrorRanges {
+		// if no lower limit (check upper)
+		if lims.Lower == nil && code <= *lims.Upper {
+			return true
+		}
+		// if no upper limit (check lower)
+		if lims.Upper == nil && code >= *lims.Lower {
+			return true
+		}
+		// if both limits are present (check both)
+		if lims.Upper != nil && lims.Lower != nil && code <= *lims.Upper && code >= *lims.Lower {
+			return true
+		}
+	}
+	return false
+}
+
+// collectHTTPTask enables load testing of HTTP services.
+type collectHTTPTask struct {
+	TaskMeta
+	With collectHTTPInputs `json:"with" yaml:"with"`
+}
+
+// initializeDefaults sets default values for the collect task
+func (t *collectHTTPTask) initializeDefaults() {
+	if t.With.NumRequests == nil && t.With.Duration == nil {
+		t.With.NumRequests = int64Pointer(defaultHTTPNumRequests)
+	}
+	if t.With.RPS == nil {
+		t.With.RPS = float32Pointer(defaultRPS)
+	}
+	if t.With.Connections == nil {
+		t.With.Connections = intPointer(defaultHTTPConnections)
+	}
+	if t.With.ErrorRanges == nil {
+		t.With.ErrorRanges = defaultErrorRanges
+	}
+	// default percentiles are always collected
+	// if other percentiles are specified, they are collected as well
+	for _, p := range defaultPercentiles {
+		t.With.Percentiles = append(t.With.Percentiles, p)
+	}
+	tmp := uniq(t.With.Percentiles)
+	t.With.Percentiles = []float64{}
+	for _, val := range tmp {
+		t.With.Percentiles = append(t.With.Percentiles, val.(float64))
+	}
+}
+
+//validateInputs for this task
+func (t *collectHTTPTask) validateInputs() error {
+	return nil
+}
+
+// getFortioOptions constructs Fortio's HTTP runner options based on collect task inputs
+func (t *collectHTTPTask) getFortioOptions(j int) (*fhttp.HTTPRunnerOptions, error) {
+	fortioLog.SetOutput(io.Discard)
+	// basic runner
+	fo := &fhttp.HTTPRunnerOptions{
+		RunnerOptions: periodic.RunnerOptions{
+			RunType:     "Iter8 load test",
+			QPS:         float64(*t.With.RPS),
+			NumThreads:  *t.With.Connections,
+			Percentiles: t.With.Percentiles,
+			Out:         io.Discard,
+		},
+		HTTPOptions: fhttp.HTTPOptions{
+			URL: t.With.VersionInfo[j].URL,
+		},
+	}
+
+	//num requests
+	if t.With.NumRequests != nil {
+		fo.RunnerOptions.Exactly = *t.With.NumRequests
+	}
+
+	// add duration
+	var duration time.Duration
+	var err error
+	if t.With.Duration != nil {
+		duration, err = time.ParseDuration(*t.With.Duration)
+		if err == nil {
+			fo.RunnerOptions.Duration = duration
+		} else {
+			log.Logger.WithStackTrace(err.Error()).Error("unable to parse duration")
+			return nil, err
+		}
+	}
+
+	// content type & payload
+	if t.With.ContentType != nil {
+		fo.ContentType = *t.With.ContentType
+	}
+	if t.With.PayloadStr != nil {
+		fo.Payload = []byte(*t.With.PayloadStr)
+	}
+	if t.With.PayloadURL != nil {
+		b, err := getPayloadBytes(*t.With.PayloadURL)
+		if err != nil {
+			return nil, err
+		} else {
+			fo.Payload = b
+		}
+	}
+
+	// headers
+	for key, value := range t.With.VersionInfo[j].Headers {
+		fo.AddAndValidateExtraHeader(key + ":" + value)
+	}
+
+	return fo, nil
+}
+
+// resultForVersion collects Fortio result for a given version
+func (t *collectHTTPTask) resultForVersion(j int) (*fhttp.HTTPRunnerResults, error) {
+	// the main idea is to run Fortio with proper options
+
+	fo, err := t.getFortioOptions(j)
+	if err != nil {
+		return nil, err
+	}
+	log.Logger.Trace("got fortio options")
+	ifr, err := fhttp.RunHTTPTest(fo)
+	if err != nil {
+		log.Logger.WithStackTrace(err.Error()).Error("fortio failed")
+		if ifr == nil {
+			log.Logger.Error("failed to get results since fortio run was aborted")
+		}
+	}
+	log.Logger.Trace("ran fortio http test")
+	return ifr, err
+}
+
+// Run executes this task
+func (t *collectHTTPTask) Run(exp *Experiment) error {
+	err := t.validateInputs()
+	if err != nil {
+		return err
+	}
+
+	t.initializeDefaults()
+
+	if len(t.With.VersionInfo) == 0 {
+		log.Logger.Error("collect task must specify info for at least one version")
+		return errors.New("collect task must specify info for at least one version")
+	}
+
+	fm := make([]*fhttp.HTTPRunnerResults, len(t.With.VersionInfo))
+
+	// run fortio queries for each version sequentially
+	for j := range t.With.VersionInfo {
+		log.Logger.Trace("initiating fortio for version ", j)
+		var data *fhttp.HTTPRunnerResults
+		var err error
+		if t.With.VersionInfo[j] == nil {
+			data = nil
+		} else {
+			data, err = t.resultForVersion(j)
+			if err == nil {
+				fm[j] = data
+			} else {
+				return err
+			}
+		}
+	}
+
+	// this task creates insights for different versions
+	// initialize num versions
+	err = exp.Result.initInsightsWithNumVersions(len(t.With.VersionInfo))
+	if err != nil {
+		return err
+	}
+	in := exp.Result.Insights
+
+	for i := range t.With.VersionInfo {
+		if fm[i] != nil {
+			// request count
+			m := iter8BuiltInPrefix + "/" + builtInHTTPRequestCountId
+			mm := MetricMeta{
+				Description: "number of requests sent",
+				Type:        CounterMetricType,
+			}
+			in.updateMetric(m, mm, i, float64(fm[i].DurationHistogram.Count))
+
+			// error count & rate
+			val := float64(0)
+			for code, count := range fm[i].RetCodes {
+				if t.errorCode(code) {
+					val += float64(count)
+				}
+			}
+			// error count
+			m = iter8BuiltInPrefix + "/" + builtInHTTPErrorCountId
+			mm = MetricMeta{
+				Description: "number of responses that were errors",
+				Type:        CounterMetricType,
+			}
+			in.updateMetric(m, mm, i, val)
+
+			// error-rate
+			m = iter8BuiltInPrefix + "/" + builtInHTTPErrorRateId
+			rc := float64(fm[i].DurationHistogram.Count)
+			if rc != 0 {
+				mm = MetricMeta{
+					Description: "fraction of responses that were errors",
+					Type:        GaugeMetricType,
+				}
+				in.updateMetric(m, mm, i, val/rc)
+			}
+
+			// mean-latency
+			m = iter8BuiltInPrefix + "/" + builtInHTTPLatencyMeanId
+			mm = MetricMeta{
+				Description: "mean of observed latency values",
+				Type:        GaugeMetricType,
+				Units:       StringPointer("msec"),
+			}
+			in.updateMetric(m, mm, i, 1000.0*fm[i].DurationHistogram.Avg)
+
+			// stddev-latency
+			m = iter8BuiltInPrefix + "/" + builtInHTTPLatencyStdDevId
+			mm = MetricMeta{
+				Description: "standard deviation of observed latency values",
+				Type:        GaugeMetricType,
+				Units:       StringPointer("msec"),
+			}
+			in.updateMetric(m, mm, i, 1000.0*fm[i].DurationHistogram.StdDev)
+
+			// min-latency
+			m = iter8BuiltInPrefix + "/" + builtInHTTPLatencyMinId
+			mm = MetricMeta{
+				Description: "minimum of observed latency values",
+				Type:        GaugeMetricType,
+				Units:       StringPointer("msec"),
+			}
+			in.updateMetric(m, mm, i, 1000.0*fm[i].DurationHistogram.Min)
+
+			// max-latency
+			m = iter8BuiltInPrefix + "/" + builtInHTTPLatencyMaxId
+			mm = MetricMeta{
+				Description: "maximum of observed latency values",
+				Type:        GaugeMetricType,
+				Units:       StringPointer("msec"),
+			}
+			in.updateMetric(m, mm, i, 1000.0*fm[i].DurationHistogram.Max)
+
+			// percentiles
+			for _, p := range fm[i].DurationHistogram.Percentiles {
+				m = fmt.Sprintf("%v/%v%v", iter8BuiltInPrefix, builtInHTTPLatencyPercentilePrefix, p.Percentile)
+				mm = MetricMeta{
+					Description: fmt.Sprintf("%v-th percentile of observed latency values", p.Percentile),
+					Type:        GaugeMetricType,
+					Units:       StringPointer("msec"),
+				}
+				in.updateMetric(m, mm, i, 1000.0*p.Value)
+			}
+
+			// latency histogram
+			m = iter8BuiltInPrefix + "/" + builtInHTTPLatencyHistId
+			mm = MetricMeta{
+				Description: "Latency Histogram",
+				Type:        HistogramMetricType,
+				Units:       StringPointer("msec"),
+			}
+			lh := latencyHist(fm[i].DurationHistogram)
+			in.updateMetric(m, mm, i, lh)
+		}
+	}
+	return nil
+}
+
+// compute latency histogram by resampling
+func latencyHist(hd *stats.HistogramData) []HistBucket {
+	buckets := []HistBucket{}
+	for _, v := range hd.Data {
+		buckets = append(buckets, HistBucket{
+			Lower: v.Start * 1000.0, // sec to msec
+			Upper: v.End * 1000.0,
+			Count: uint64(v.Count),
+		})
+	}
+	return buckets
+}
diff --git a/base/experiment.go b/base/experiment.go
index ba739993..66c1ee38 100644
--- a/base/experiment.go
+++ b/base/experiment.go
@@ -5,37 +5,35 @@ import (
 	"errors"
 	"fmt"
 	"reflect"
+	"regexp"
+	"strconv"
+	"strings"
 	"time"
 
 	log "github.com/iter8-tools/iter8/base/log"
+	"github.com/montanaflynn/stats"
 )
 
-// Experiment specification and result
-type Experiment struct {
-	// Tasks is the sequence of tasks that constitute this experiment
-	Tasks []TaskSpec `json:"tasks" yaml:"tasks"`
-	// Result is the current results from this experiment.
-	// The experiment may not have completed in which case results may be partial.
-	Result *ExperimentResult `json:"result" yaml:"result"`
-}
-
 // Task is an object that can be run
 type Task interface {
+	// validate inputs for this task
+	validateInputs() error
+	// initializeDefault values for inputs to this task
+	initializeDefaults()
 	// Run this task
 	Run(exp *Experiment) error
-	// Get the name of this task
-	GetName() string
 }
 
-// GetIf returns the condition if any which determines whether of not if this task needs to run
-func GetIf(t Task) *string {
-	var jsonBytes []byte
-	var tm taskMeta
-	// convert t to jsonBytes
-	jsonBytes, _ = json.Marshal(t)
-	// convert jsonBytes to taskMeta
-	_ = json.Unmarshal(jsonBytes, &tm)
-	return tm.If
+// ExperimentSpec is the experiment spec
+type ExperimentSpec []Task
+
+// Experiment specification and result
+type Experiment struct {
+	// Tasks is the sequence of tasks that constitute this experiment
+	Tasks ExperimentSpec
+	// Result is the current results from this experiment.
+	// The experiment may not have completed in which case results may be partial.
+	Result *ExperimentResult
 }
 
 // ExperimentResult defines the current results from the experiment
@@ -53,130 +51,214 @@ type ExperimentResult struct {
 	Insights *Insights `json:"insights,omitempty" yaml:"insights,omitempty"`
 }
 
-// Insights is a structure to contain experiment insights
+// Insights records the number of versions in this experiment,
+// metric values and SLO indicators for each version,
+// metrics metadata for all metrics, and
+// SLO definitions for all SLOs
 type Insights struct {
 	// NumVersions is the number of app versions detected by Iter8
 	NumVersions int `json:"numVersions" yaml:"numVersions"`
 
-	// InsightInfo identifies the types of insights produced by this experiment
-	InsightTypes []InsightType `json:"insightTypes,omitempty" yaml:"insightTypes,omitempty"`
-
 	// MetricsInfo identifies the metrics involved in this experiment
 	MetricsInfo map[string]MetricMeta `json:"metricsInfo,omitempty" yaml:"metricsInfo,omitempty"`
 
-	// BuiltinLatencyPercentiles collected in this experiment
-	// this may be nil if there are no builtin metrics involved
-	BuiltinLatencyPercentiles []float64 `json:"builtinLatencyPercentiles,omitempty" yaml:"builtinLatencyPercentiles,omitempty"`
-
-	// SLOs involved in this experiment
-	SLOs []SLO `json:"SLOs,omitempty" yaml:"SLOs,omitempty"`
-
-	// MetricValues:
+	// NonHistMetricValues:
 	// the outer slice must be the same length as the number of app versions
 	// the map key must match name of a metric in MetricsInfo
 	// the inner slice contains the list of all observed metric values for given version and given metric; float value [i]["foo/bar"][k] is the [k]th observation for version [i] for the metric bar under backend foo.
-	MetricValues []map[string][]float64 `json:"metricValues,omitempty" yaml:"metricValues,omitempty"`
+	// this struct is meant exclusively for metrics of type other than histogram
+	NonHistMetricValues []map[string][]float64 `json:"nonHistMetricValues,omitempty" yaml:"nonHistMetricValues,omitempty"`
+
+	// HistMetricValues:
+	// the outer slice must be the same length as the number of app versions
+	// the map key must match name of a histogram metric in MetricsInfo
+	// the inner slice contains the list of all observed histogram buckets for a given version and given metric; value [i]["foo/bar"][k] is the [k]th observed bucket for version [i] for the hist metric `bar` under backend `foo`.
+	HistMetricValues []map[string][]HistBucket `json:"histMetricValues,omitempty" yaml:"histMetricValues,omitempty"`
+
+	// SLOs involved in this experiment
+	SLOs []SLO `json:"SLOs,omitempty" yaml:"SLOs,omitempty"`
 
 	// SLOsSatisfied:
 	// the outer slice must be of the same length as SLOs
 	// the length of the inner slice must be the number of app versions
 	// the boolean value at [i][j] indicate if SLO [i] is satisfied by version [j]
 	SLOsSatisfied [][]bool `json:"SLOsSatisfied,omitempty" yaml:"SLOsSatisfied,omitempty"`
-
-	// SLOsSatisfiedBy is the subset of versions that satisfy all SLOs
-	// every integer in this slice must be in the range 0 to NumVersions - 1 (inclusive)
-	SLOsSatisfiedBy []int `json:"SLOsSatisfiedBy,omitempty" yaml:"SLOsSatisfiedBy,omitempty"`
 }
 
-// InsightType identifies the type of insight
-type InsightType string
-
-const (
-	// InsightTypeHistMetrics indicates histogram metrics are collected during this experiment
-	InsightTypeHistMetrics InsightType = "HistMetrics"
-
-	// InsightTypeMetrics indicates metrics are collected during this experiment
-	InsightTypeMetrics InsightType = "Metrics"
-
-	// InsightTypeSLO indicatse SLOs are validated during this experiment
-	InsightTypeSLO InsightType = "SLOs"
-)
-
 // MetricMeta describes a metric
 type MetricMeta struct {
 	Description string     `json:"description" yaml:"description"`
 	Units       *string    `json:"units,omitempty" yaml:"units,omitempty"`
 	Type        MetricType `json:"type" yaml:"type"`
-	XMin        *float64   `json:"xmin" yaml:"xmin"`
-	XMax        *float64   `json:"xmax" yaml:"xmax"`
-	NumBuckets  *int       `json:"numBuckets" yaml:"numBuckets"`
 }
 
 // SLO is a service level objective
 type SLO struct {
 	// Metric is the fully qualified metric name (i.e., in the backendName/metricName format)
-	Metric string `json:"metric" yaml:"metric" validate:"gt=0,required"`
+	Metric string `json:"metric" yaml:"metric"`
 
 	// UpperLimit is the maximum acceptable value of the metric.
-	UpperLimit *float64 `json:"upperLimit,omitempty" yaml:"upperLimit,omitempty" validate:"required_without=LowerLimit"`
+	UpperLimit *float64 `json:"upperLimit,omitempty" yaml:"upperLimit,omitempty"`
 
 	// LowerLimit is the minimum acceptable value of the metric.
-	LowerLimit *float64 `json:"lowerLimit,omitempty" yaml:"lowerLimit,omitempty" validate:"required_without=UpperLimit"`
+	LowerLimit *float64 `json:"lowerLimit,omitempty" yaml:"lowerLimit,omitempty"`
 }
 
-type taskMeta struct {
+// this is embedded within each task
+type TaskMeta struct {
 	// Task is the name of the task
-	Task *string `json:"task,omitempty" yaml:"task,omitempty" validate:"required_without=Run,excluded_with=Run"`
+	Task *string `json:"task,omitempty" yaml:"task,omitempty"`
 	// Run is the script used in a run task
 	// Specify either Task or Run but not both
-	Run *string `json:"run,omitempty" yaml:"run,omitempty" validate:"required_without=Task,excluded_with=Task"`
+	Run *string `json:"run,omitempty" yaml:"run,omitempty"`
 	// If is the condition used to determine if this task needs to run.
 	If *string `json:"if,omitempty" yaml:"if,omitempty"`
 }
 
-// TaskSpec has information needed to construct a Task
-type TaskSpec struct {
-	taskMeta
-	// With contains the inputs for this task.
-	With map[string]interface{} `json:"with,omitempty" yaml:"with,omitempty"`
+// this is used during unmarshaling of tasks
+type taskMetaWith struct {
+	TaskMeta
+	// raw representation of task inputs
+	With interface{} `json:"with,omitempty" yaml:"with,omitempty"`
 }
 
-// hasInsightType returns true if the experiment has a specific insight type set
-func (in *Insights) hasInsightType(it InsightType) bool {
-	if in != nil {
-		if in.InsightTypes != nil {
-			for _, v := range in.InsightTypes {
-				if v == it {
-					return true
-				}
+// UnmarshallJSON will unmarshal an experiment spec from bytes
+func (s *ExperimentSpec) UnmarshalJSON(data []byte) error {
+	var v []taskMetaWith
+	if err := json.Unmarshal(data, &v); err != nil {
+		return err
+	}
+
+	log.Logger.Tracef("unmarshaled %v tasks into task meta", len(v))
+
+	for _, t := range v {
+		if (t.Task == nil || len(*t.Task) == 0) && (t.Run == nil) {
+			err := fmt.Errorf("invalid task found without a task name or a run command")
+			log.Logger.Error(err)
+			return err
+		}
+
+		// get byte data for this task
+		tBytes, _ := json.Marshal(t)
+		var tsk Task
+		// this is a run task
+		if t.Run != nil {
+			log.Logger.Info("found run task: ", *t.Run)
+			rt := &runTask{}
+			json.Unmarshal(tBytes, rt)
+			tsk = rt
+		} else {
+			// this is some other task
+			switch *t.Task {
+			case CollectHTTPTaskName:
+				cht := &collectHTTPTask{}
+				json.Unmarshal(tBytes, cht)
+				tsk = cht
+			case CollectGPRCTaskName:
+				cgt := &collectGRPCTask{}
+				json.Unmarshal(tBytes, cgt)
+				tsk = cgt
+			case AssessTaskName:
+				at := &assessTask{}
+				json.Unmarshal(tBytes, at)
+				tsk = at
+			default:
+				log.Logger.Error("unknown task: " + *t.Task)
+				return errors.New("unknown task: " + *t.Task)
 			}
 		}
+		n := append(*s, tsk)
+		*s = n
+		log.Logger.Trace("appended to experiment spec")
 	}
-	return false
+	log.Logger.Trace("constructed experiment spec of length: ", len(*s))
+	return nil
 }
 
-// setInsightType adds a specific InsightType to the list of experiment insights types
-func (in *Insights) setInsightType(it InsightType) {
-	if !in.hasInsightType(it) {
-		in.InsightTypes = append(in.InsightTypes, it)
+// metricTypeMatch checks if metric value is a match for its type
+func metricTypeMatch(t MetricType, val interface{}) bool {
+	switch v := val.(type) {
+	case float64:
+		if t == CounterMetricType || t == GaugeMetricType {
+			return true
+		} else {
+			return false
+		}
+	case []float64:
+		if t == SampleMetricType {
+			return true
+		} else {
+			return false
+		}
+	case []HistBucket:
+		if t == HistogramMetricType {
+			return true
+		} else {
+			return false
+		}
+	default:
+		log.Logger.Error("unsupported type for metric value: ", v)
+		return false
 	}
 }
 
-// setBuiltinLatencyPercentiles sets the BuiltinLatencyPercentiles field in insights
-// if this function is called multiple times (example, due to looping), then
-// it is intended to be called with the same argument each time
-func (in *Insights) setBuiltinLatencyPercentiles(p []float64) error {
-	if in.BuiltinLatencyPercentiles != nil {
-		if reflect.DeepEqual(in.BuiltinLatencyPercentiles, p) {
-			return nil
-		} else {
-			e := fmt.Errorf("old and new value of builtin latency percentiles conflict")
-			log.Logger.WithStackTrace(fmt.Sprint("old: ", in.BuiltinLatencyPercentiles, "new: ", p)).Error(e)
-			return e
-		}
+// updateMetricValueScalar update a scalar metric value for a given version
+func (in *Insights) updateMetricValueScalar(m string, i int, val float64) {
+	in.NonHistMetricValues[i][m] = append(in.NonHistMetricValues[i][m], val)
+}
+
+// updateMetricValueVector update a vector metric value for a given version
+func (in *Insights) updateMetricValueVector(m string, i int, val []float64) {
+	in.NonHistMetricValues[i][m] = append(in.NonHistMetricValues[i][m], val...)
+}
+
+// updateMetricValueHist update a histogram metric value for a given version
+func (in *Insights) updateMetricValueHist(m string, i int, val []HistBucket) {
+	in.HistMetricValues[i][m] = append(in.HistMetricValues[i][m], val...)
+}
+
+// registerMetric registers a new metric by adding its meta data
+func (in *Insights) registerMetric(m string, mm MetricMeta) error {
+	if old, ok := in.MetricsInfo[m]; ok && !reflect.DeepEqual(old, mm) {
+		err := fmt.Errorf("old and new metric meta for %v differ", m)
+		log.Logger.WithStackTrace(fmt.Sprintf("old: %v \nnew: %v", old, mm)).Error(err)
+		return err
+	}
+	in.MetricsInfo[m] = mm
+	return nil
+}
+
+// updateMetric registers a metric and adds a metric value for a given version
+func (in *Insights) updateMetric(m string, mm MetricMeta, i int, val interface{}) error {
+	var err error
+	if !metricTypeMatch(mm.Type, val) {
+		err = fmt.Errorf("metric value and type are incompatible; name: %v meta: %v version: %v value: %v", m, mm, i, val)
+		log.Logger.Error(err)
+		return err
+	}
+
+	if in.NumVersions <= i {
+		err := fmt.Errorf("insufficient number of versions %v with version index %v", in.NumVersions, i)
+		log.Logger.Error(err)
+		return err
+	}
+
+	err = in.registerMetric(m, mm)
+	if err != nil {
+		return err
+	}
+
+	switch mm.Type {
+	case CounterMetricType, GaugeMetricType:
+		in.updateMetricValueScalar(m, i, val.(float64))
+	case SampleMetricType:
+		in.updateMetricValueVector(m, i, val.([]float64))
+	case HistogramMetricType:
+		in.updateMetricValueHist(m, i, val.([]HistBucket))
+	default:
+		err := fmt.Errorf("unknown metric type %v", mm.Type)
+		log.Logger.Error(err)
 	}
-	// LHS will be nil
-	in.BuiltinLatencyPercentiles = p
 	return nil
 }
 
@@ -211,25 +293,6 @@ func (e *Experiment) initializeSLOsSatisfied() error {
 	return nil
 }
 
-// initialize metric values
-func (in *Insights) initMetricValues(n int) error {
-	if in.MetricValues != nil {
-		if len(in.MetricValues) != n {
-			errStr := fmt.Sprint("inconsistent number for app versions; in old metric values: ", len(in.MetricValues), " in new metric values: ", n)
-			log.Logger.Error(errStr)
-			return errors.New(errStr)
-		} else {
-			return nil
-		}
-	}
-
-	in.MetricValues = make([]map[string][]float64, n)
-	for i := 0; i < n; i++ {
-		in.MetricValues[i] = make(map[string][]float64)
-	}
-	return nil
-}
-
 func (e *Experiment) InitResults() {
 	e.Result = &ExperimentResult{
 		StartTime:         time.Now(),
@@ -238,74 +301,210 @@ func (e *Experiment) InitResults() {
 	}
 }
 
-func (r *ExperimentResult) InitInsights(n int, it []InsightType) {
-	r.Insights = &Insights{
-		NumVersions:  n,
-		InsightTypes: it,
-		MetricsInfo:  make(map[string]MetricMeta),
-		MetricValues: make([]map[string][]float64, n),
-	}
-	for i := 0; i < n; i++ {
-		r.Insights.MetricValues[i] = make(map[string][]float64)
+// initInsightsWithNumVersions is also going to initialize insights data structure
+// insights data structure contains metrics data structures, so this will also
+// init metrics
+func (r *ExperimentResult) initInsightsWithNumVersions(n int) error {
+	if r.Insights != nil {
+		if r.Insights.NumVersions != n {
+			err := fmt.Errorf("inconsistent number for app versions; old (%v); new (%v)", r.Insights.NumVersions, n)
+			log.Logger.Error(err)
+			return err
+		}
+	} else {
+		r.Insights = &Insights{
+			NumVersions: n,
+		}
 	}
+	r.Insights.initMetrics()
+	return nil
 }
 
-// SLOsBy returns true if version satisfies SLOs
-func (exp *Experiment) SLOsBy(version int) bool {
-	if !exp.ContainsInsight(InsightTypeSLO) {
-		log.Logger.Warning("experiment does not involve SLOs")
-		return true
+// initMetrics initializes the data structes inside insights that will hold metrics
+func (in *Insights) initMetrics() error {
+	if in.NonHistMetricValues != nil || in.HistMetricValues != nil {
+		if len(in.NonHistMetricValues) != in.NumVersions || len(in.HistMetricValues) != in.NumVersions {
+			err := fmt.Errorf("inconsistent number for app versions in non hist metric values (%v), hist metric values (%v), num versions (%v)", len(in.NonHistMetricValues), len(in.HistMetricValues), in.NumVersions)
+			log.Logger.Error(err)
+			return err
+		}
+		if len(in.NonHistMetricValues[0])+len(in.HistMetricValues[0]) != len(in.MetricsInfo) {
+			err := fmt.Errorf("inconsistent number for metrics in non hist metric values (%v), hist metric values (%v), metrics info (%v)", len(in.NonHistMetricValues[0]), len(in.HistMetricValues[0]), len(in.MetricsInfo))
+			log.Logger.Error(err)
+			return err
+		}
+		return nil
+	}
+	// at this point, there are no known metrics, but there are in.NumVersions
+	// initialize metrics info
+	in.MetricsInfo = make(map[string]MetricMeta)
+	// initialize non hist metric values for each version
+	in.NonHistMetricValues = make([]map[string][]float64, in.NumVersions)
+	// initialize hist metric values for each version
+	in.HistMetricValues = make([]map[string][]HistBucket, in.NumVersions)
+	for i := 0; i < in.NumVersions; i++ {
+		in.NonHistMetricValues[i] = make(map[string][]float64)
+		in.HistMetricValues[i] = make(map[string][]HistBucket)
 	}
+	return nil
+}
 
-	if exp != nil {
-		if exp.Result != nil {
-			if exp.Result.Insights != nil {
-				for _, v := range exp.Result.Insights.SLOsSatisfiedBy {
-					if v == version {
-						return true
-					}
-				}
+// getCounterOrGaugeMetricFromValuesMap gets the value of the given counter or gauge metric, for the given version, from metric values map
+func (in *Insights) getCounterOrGaugeMetricFromValuesMap(i int, m string) *float64 {
+	if mm, ok := in.MetricsInfo[m]; ok {
+		log.Logger.Tracef("found metric info for %v", m)
+		if (mm.Type != CounterMetricType) && (mm.Type != GaugeMetricType) {
+			log.Logger.Errorf("metric %v is not of type counter or gauge", m)
+			return nil
+		}
+		l := len(in.NonHistMetricValues)
+		if l <= i {
+			log.Logger.Warnf("metric values not found for version %v; initialized for %v versions", i, l)
+			return nil
+		}
+		log.Logger.Tracef("metric values found for version %v", i)
+		// grab the metric value and return
+		if vals, ok := in.NonHistMetricValues[i][m]; ok {
+			log.Logger.Tracef("found metric value for version %v and metric %v", i, m)
+			if len(vals) > 0 {
+				return float64Pointer(vals[len(vals)-1])
 			}
 		}
+		log.Logger.Infof("could not find metric value for version %v and metric %v", i, m)
 	}
-	return false
+	log.Logger.Infof("could not find metric info for %v", m)
+	return nil
 }
 
-// ContainsInsight checks if the experiment contains insight
-func (e *Experiment) ContainsInsight(in InsightType) bool {
-	if e != nil {
-		if e.Result != nil {
-			if e.Result.Insights != nil {
-				if e.Result.Insights.InsightTypes != nil {
-					for _, v := range e.Result.Insights.InsightTypes {
-						if v == in {
-							return true
-						}
-					}
+// getSampleAggregation aggregates the given base metric for the given version (i) with the given aggregation (a)
+func (in *Insights) getSampleAggregation(i int, baseMetric string, a string) *float64 {
+	at := AggregationType(a)
+	vals := in.NonHistMetricValues[i][baseMetric]
+	if len(vals) == 0 {
+		log.Logger.Infof("metric %v for version %v has no sample", baseMetric, i)
+	}
+	if len(vals) == 1 {
+		log.Logger.Warnf("metric %v for version %v has sample of size 1", baseMetric, i)
+		return float64Pointer(vals[0])
+	}
+	switch at {
+	case MeanAggregator:
+		agg, err := stats.Mean(vals)
+		if err == nil {
+			return float64Pointer(agg)
+		} else {
+			log.Logger.WithStackTrace(err.Error()).Errorf("aggregation error for version %v, metric %v, and aggregation func %v", i, baseMetric, a)
+			return nil
+		}
+	case StdDevAggregator:
+		agg, err := stats.StandardDeviation(vals)
+		if err == nil {
+			return float64Pointer(agg)
+		} else {
+			log.Logger.WithStackTrace(err.Error()).Errorf("aggregation error version %v, metric %v, and aggregation func %v", i, baseMetric, a)
+			return nil
+		}
+	case MinAggregator:
+		agg, err := stats.Min(vals)
+		if err == nil {
+			return float64Pointer(agg)
+		} else {
+			log.Logger.WithStackTrace(err.Error()).Errorf("aggregation error version %v, metric %v, and aggregation func %v", i, baseMetric, a)
+			return nil
+		}
+	case MaxAggregator:
+		agg, err := stats.Mean(vals)
+		if err == nil {
+			return float64Pointer(agg)
+		} else {
+			log.Logger.WithStackTrace(err.Error()).Errorf("aggregation error version %v, metric %v, and aggregation func %v", i, baseMetric, a)
+			return nil
+		}
+	default: // don't do anything
+	}
+
+	// at this point, 'a' must be a percentile aggregator
+	if strings.HasPrefix(a, "p") {
+		b := strings.TrimPrefix(a, "p")
+		// b must be a percent
+		if match, _ := regexp.MatchString(decimalRegex, b); match {
+			// extract percent
+			if percent, err := strconv.ParseFloat(b, 64); err != nil {
+				log.Logger.WithStackTrace(err.Error()).Errorf("error extracting percent from aggregation func %v", a)
+				return nil
+			} else {
+				// compute percentile
+				agg, err := stats.Percentile(vals, percent)
+				if err == nil {
+					return float64Pointer(agg)
+				} else {
+					log.Logger.WithStackTrace(err.Error()).Errorf("aggregation error version %v, metric %v, and aggregation func %v", i, baseMetric, a)
+					return nil
 				}
 			}
+		} else {
+			log.Logger.Errorf("unable to extract percent from agggregation func %v", a)
+			return nil
 		}
+	} else {
+		log.Logger.Errorf("invalid aggregation %v", a)
+		return nil
 	}
-	return false
 }
 
-// SLOs returns true if all versions satisfy SLOs
-func (exp *Experiment) SLOs() bool {
-	if !exp.ContainsInsight(InsightTypeSLO) {
-		log.Logger.Warning("experiment does not involve SLOs")
-		return true
+// aggregateMetric returns the aggregated metric value for a given version and metric
+func (in *Insights) aggregateMetric(i int, m string) *float64 {
+	s := strings.Split(m, "/")
+	baseMetric := s[0] + "/" + s[1]
+	if m, ok := in.MetricsInfo[baseMetric]; ok {
+		log.Logger.Tracef("found metric %v used for aggregation", baseMetric)
+		if m.Type == SampleMetricType {
+			log.Logger.Tracef("metric %v used for aggregation is a sample metric", baseMetric)
+			return in.getSampleAggregation(i, baseMetric, s[2])
+		} else {
+			log.Logger.Errorf("metric %v used for aggregation is not a sample metric", baseMetric)
+			return nil
+		}
+	} else {
+		log.Logger.Warnf("could not find metric %v used for aggregation", baseMetric)
+		return nil
 	}
+}
 
-	if exp != nil {
-		if exp.Result != nil {
-			if exp.Result.Insights != nil {
-				if exp.Result.Insights.SLOsSatisfiedBy != nil {
-					if exp.Result.Insights.NumVersions == len(exp.Result.Insights.SLOsSatisfiedBy) {
-						return true
-					}
-				}
-			}
+// NormalizeMetricName normalizes percentile values in metric names
+func NormalizeMetricName(m string) (string, error) {
+	pre := iter8BuiltInPrefix + "/" + builtInHTTPLatencyPercentilePrefix
+	if strings.HasPrefix(m, pre) { // built-in http percentile metric
+		remainder := strings.TrimPrefix(m, pre)
+		if percent, e := strconv.ParseFloat(remainder, 64); e != nil {
+			err := fmt.Errorf("cannot extract percent from metric %v", m)
+			log.Logger.WithStackTrace(e.Error()).Error(err)
+			return m, err
+		} else {
+			// return percent normalized metric name
+			return fmt.Sprintf("%v%v", pre, percent), nil
+		}
+	} else {
+		// not a built-in http percentile metric
+		return m, nil
+	}
+}
+
+// ScalarMetricValue gets the value of the given scalar metric for the given version
+func (in *Insights) ScalarMetricValue(i int, m string) *float64 {
+	s := strings.Split(m, "/")
+	if len(s) == 3 {
+		log.Logger.Tracef("%v is an aggregated metric", m)
+		return in.aggregateMetric(i, m)
+	} else if len(s) == 2 { // this appears to be a non-aggregated metric
+		if nm, err := NormalizeMetricName(m); err != nil {
+			return nil
+		} else {
+			return in.getCounterOrGaugeMetricFromValuesMap(i, nm)
 		}
+	} else {
+		log.Logger.Errorf("invalid metric name %v", m)
+		log.Logger.Error("metric names must be of the form a/b or a/b/c, where a is the id of the metrics backend, b is the id of a metric name, and c is a valid aggregation function")
+		return nil
 	}
-	return false
 }
diff --git a/base/internal/common.go b/base/internal/common.go
new file mode 100644
index 00000000..2ec059e4
--- /dev/null
+++ b/base/internal/common.go
@@ -0,0 +1,54 @@
+package internal
+
+/*
+Credit: This file is sourced from https://github.com/bojand/ghz and modified for reuse in Iter8
+*/
+
+import (
+	"net"
+	"strconv"
+
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/reflection"
+
+	"github.com/iter8-tools/iter8/base/internal/helloworld/helloworld"
+)
+
+// TestPort is the port.
+var TestPort string
+
+// TestLocalhost is the localhost.
+var TestLocalhost string
+
+// StartServer starts the server.
+//
+// For testing only.
+func StartServer(secure bool) (*helloworld.Greeter, *grpc.Server, error) {
+	lis, err := net.Listen("tcp", ":0")
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var opts []grpc.ServerOption
+
+	stats := helloworld.NewHWStats()
+
+	opts = append(opts, grpc.StatsHandler(stats))
+
+	s := grpc.NewServer(opts...)
+
+	gs := helloworld.NewGreeter()
+	helloworld.RegisterGreeterServer(s, gs)
+	reflection.Register(s)
+
+	gs.Stats = stats
+
+	TestPort = strconv.Itoa(lis.Addr().(*net.TCPAddr).Port)
+	TestLocalhost = "localhost:" + TestPort
+
+	go func() {
+		_ = s.Serve(lis)
+	}()
+
+	return gs, s, err
+}
diff --git a/base/internal/helloworld/helloworld/greeter.pb.go b/base/internal/helloworld/helloworld/greeter.pb.go
new file mode 100644
index 00000000..96303064
--- /dev/null
+++ b/base/internal/helloworld/helloworld/greeter.pb.go
@@ -0,0 +1,239 @@
+// Credit: This file is from https://github.com/bojand/ghz/
+// this file is used for test purposes
+
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.26.0
+// 	protoc        v3.19.3
+// source: helloworld/greeter.proto
+
+package helloworld
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// The request message containing the user's name.
+type HelloRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+}
+
+func (x *HelloRequest) Reset() {
+	*x = HelloRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_helloworld_greeter_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *HelloRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*HelloRequest) ProtoMessage() {}
+
+func (x *HelloRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_helloworld_greeter_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use HelloRequest.ProtoReflect.Descriptor instead.
+func (*HelloRequest) Descriptor() ([]byte, []int) {
+	return file_helloworld_greeter_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *HelloRequest) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+// The response message containing the greetings
+type HelloReply struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
+}
+
+func (x *HelloReply) Reset() {
+	*x = HelloReply{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_helloworld_greeter_proto_msgTypes[1]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *HelloReply) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*HelloReply) ProtoMessage() {}
+
+func (x *HelloReply) ProtoReflect() protoreflect.Message {
+	mi := &file_helloworld_greeter_proto_msgTypes[1]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use HelloReply.ProtoReflect.Descriptor instead.
+func (*HelloReply) Descriptor() ([]byte, []int) {
+	return file_helloworld_greeter_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *HelloReply) GetMessage() string {
+	if x != nil {
+		return x.Message
+	}
+	return ""
+}
+
+var File_helloworld_greeter_proto protoreflect.FileDescriptor
+
+var file_helloworld_greeter_proto_rawDesc = []byte{
+	0x0a, 0x18, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2f, 0x67, 0x72, 0x65,
+	0x65, 0x74, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x68, 0x65, 0x6c, 0x6c,
+	0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x22, 0x22, 0x0a, 0x0c, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52,
+	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01,
+	0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x26, 0x0a, 0x0a, 0x48, 0x65,
+	0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73,
+	0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61,
+	0x67, 0x65, 0x32, 0x98, 0x02, 0x0a, 0x07, 0x47, 0x72, 0x65, 0x65, 0x74, 0x65, 0x72, 0x12, 0x3e,
+	0x0a, 0x08, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x18, 0x2e, 0x68, 0x65, 0x6c,
+	0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71,
+	0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c,
+	0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x12, 0x42,
+	0x0a, 0x0a, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x43, 0x53, 0x12, 0x18, 0x2e, 0x68,
+	0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52,
+	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f,
+	0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00,
+	0x28, 0x01, 0x12, 0x41, 0x0a, 0x09, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x73, 0x12,
+	0x18, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c,
+	0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x68, 0x65, 0x6c, 0x6c,
+	0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c,
+	0x79, 0x22, 0x00, 0x30, 0x01, 0x12, 0x46, 0x0a, 0x0c, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c,
+	0x6f, 0x42, 0x69, 0x64, 0x69, 0x12, 0x18, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72,
+	0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
+	0x16, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c,
+	0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x42, 0x42, 0x5a,
+	0x40, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x69, 0x74, 0x65, 0x72,
+	0x38, 0x2d, 0x74, 0x6f, 0x6f, 0x6c, 0x73, 0x2f, 0x69, 0x74, 0x65, 0x72, 0x38, 0x2f, 0x62, 0x61,
+	0x73, 0x65, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x68, 0x65, 0x6c, 0x6c,
+	0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c,
+	0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+	file_helloworld_greeter_proto_rawDescOnce sync.Once
+	file_helloworld_greeter_proto_rawDescData = file_helloworld_greeter_proto_rawDesc
+)
+
+func file_helloworld_greeter_proto_rawDescGZIP() []byte {
+	file_helloworld_greeter_proto_rawDescOnce.Do(func() {
+		file_helloworld_greeter_proto_rawDescData = protoimpl.X.CompressGZIP(file_helloworld_greeter_proto_rawDescData)
+	})
+	return file_helloworld_greeter_proto_rawDescData
+}
+
+var file_helloworld_greeter_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
+var file_helloworld_greeter_proto_goTypes = []interface{}{
+	(*HelloRequest)(nil), // 0: helloworld.HelloRequest
+	(*HelloReply)(nil),   // 1: helloworld.HelloReply
+}
+var file_helloworld_greeter_proto_depIdxs = []int32{
+	0, // 0: helloworld.Greeter.SayHello:input_type -> helloworld.HelloRequest
+	0, // 1: helloworld.Greeter.SayHelloCS:input_type -> helloworld.HelloRequest
+	0, // 2: helloworld.Greeter.SayHellos:input_type -> helloworld.HelloRequest
+	0, // 3: helloworld.Greeter.SayHelloBidi:input_type -> helloworld.HelloRequest
+	1, // 4: helloworld.Greeter.SayHello:output_type -> helloworld.HelloReply
+	1, // 5: helloworld.Greeter.SayHelloCS:output_type -> helloworld.HelloReply
+	1, // 6: helloworld.Greeter.SayHellos:output_type -> helloworld.HelloReply
+	1, // 7: helloworld.Greeter.SayHelloBidi:output_type -> helloworld.HelloReply
+	4, // [4:8] is the sub-list for method output_type
+	0, // [0:4] is the sub-list for method input_type
+	0, // [0:0] is the sub-list for extension type_name
+	0, // [0:0] is the sub-list for extension extendee
+	0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_helloworld_greeter_proto_init() }
+func file_helloworld_greeter_proto_init() {
+	if File_helloworld_greeter_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_helloworld_greeter_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*HelloRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_helloworld_greeter_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*HelloReply); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_helloworld_greeter_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   2,
+			NumExtensions: 0,
+			NumServices:   1,
+		},
+		GoTypes:           file_helloworld_greeter_proto_goTypes,
+		DependencyIndexes: file_helloworld_greeter_proto_depIdxs,
+		MessageInfos:      file_helloworld_greeter_proto_msgTypes,
+	}.Build()
+	File_helloworld_greeter_proto = out.File
+	file_helloworld_greeter_proto_rawDesc = nil
+	file_helloworld_greeter_proto_goTypes = nil
+	file_helloworld_greeter_proto_depIdxs = nil
+}
diff --git a/base/internal/helloworld/helloworld/greeter.proto b/base/internal/helloworld/helloworld/greeter.proto
new file mode 100644
index 00000000..df53c02a
--- /dev/null
+++ b/base/internal/helloworld/helloworld/greeter.proto
@@ -0,0 +1,24 @@
+// Credit: This file is from https://github.com/bojand/ghz/
+// this file is used for test purposes
+syntax = "proto3";
+
+option go_package = "github.com/iter8-tools/iter8/base/internal/helloworld/helloworld";
+
+package helloworld;
+
+service Greeter {
+  rpc SayHello (HelloRequest) returns (HelloReply) {}
+  rpc SayHelloCS (stream HelloRequest) returns (HelloReply) {}
+  rpc SayHellos (HelloRequest) returns (stream HelloReply) {}
+  rpc SayHelloBidi (stream HelloRequest) returns (stream HelloReply) {}
+}
+
+// The request message containing the user's name.
+message HelloRequest {
+  string name = 1;
+}
+
+// The response message containing the greetings
+message HelloReply {
+  string message = 1;
+}
\ No newline at end of file
diff --git a/base/internal/helloworld/helloworld/greeter_grpc.pb.go b/base/internal/helloworld/helloworld/greeter_grpc.pb.go
new file mode 100644
index 00000000..ec347104
--- /dev/null
+++ b/base/internal/helloworld/helloworld/greeter_grpc.pb.go
@@ -0,0 +1,303 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+
+package helloworld
+
+import (
+	context "context"
+	grpc "google.golang.org/grpc"
+	codes "google.golang.org/grpc/codes"
+	status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+// GreeterClient is the client API for Greeter service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type GreeterClient interface {
+	SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
+	SayHelloCS(ctx context.Context, opts ...grpc.CallOption) (Greeter_SayHelloCSClient, error)
+	SayHellos(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (Greeter_SayHellosClient, error)
+	SayHelloBidi(ctx context.Context, opts ...grpc.CallOption) (Greeter_SayHelloBidiClient, error)
+}
+
+type greeterClient struct {
+	cc grpc.ClientConnInterface
+}
+
+func NewGreeterClient(cc grpc.ClientConnInterface) GreeterClient {
+	return &greeterClient{cc}
+}
+
+func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {
+	out := new(HelloReply)
+	err := c.cc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *greeterClient) SayHelloCS(ctx context.Context, opts ...grpc.CallOption) (Greeter_SayHelloCSClient, error) {
+	stream, err := c.cc.NewStream(ctx, &Greeter_ServiceDesc.Streams[0], "/helloworld.Greeter/SayHelloCS", opts...)
+	if err != nil {
+		return nil, err
+	}
+	x := &greeterSayHelloCSClient{stream}
+	return x, nil
+}
+
+type Greeter_SayHelloCSClient interface {
+	Send(*HelloRequest) error
+	CloseAndRecv() (*HelloReply, error)
+	grpc.ClientStream
+}
+
+type greeterSayHelloCSClient struct {
+	grpc.ClientStream
+}
+
+func (x *greeterSayHelloCSClient) Send(m *HelloRequest) error {
+	return x.ClientStream.SendMsg(m)
+}
+
+func (x *greeterSayHelloCSClient) CloseAndRecv() (*HelloReply, error) {
+	if err := x.ClientStream.CloseSend(); err != nil {
+		return nil, err
+	}
+	m := new(HelloReply)
+	if err := x.ClientStream.RecvMsg(m); err != nil {
+		return nil, err
+	}
+	return m, nil
+}
+
+func (c *greeterClient) SayHellos(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (Greeter_SayHellosClient, error) {
+	stream, err := c.cc.NewStream(ctx, &Greeter_ServiceDesc.Streams[1], "/helloworld.Greeter/SayHellos", opts...)
+	if err != nil {
+		return nil, err
+	}
+	x := &greeterSayHellosClient{stream}
+	if err := x.ClientStream.SendMsg(in); err != nil {
+		return nil, err
+	}
+	if err := x.ClientStream.CloseSend(); err != nil {
+		return nil, err
+	}
+	return x, nil
+}
+
+type Greeter_SayHellosClient interface {
+	Recv() (*HelloReply, error)
+	grpc.ClientStream
+}
+
+type greeterSayHellosClient struct {
+	grpc.ClientStream
+}
+
+func (x *greeterSayHellosClient) Recv() (*HelloReply, error) {
+	m := new(HelloReply)
+	if err := x.ClientStream.RecvMsg(m); err != nil {
+		return nil, err
+	}
+	return m, nil
+}
+
+func (c *greeterClient) SayHelloBidi(ctx context.Context, opts ...grpc.CallOption) (Greeter_SayHelloBidiClient, error) {
+	stream, err := c.cc.NewStream(ctx, &Greeter_ServiceDesc.Streams[2], "/helloworld.Greeter/SayHelloBidi", opts...)
+	if err != nil {
+		return nil, err
+	}
+	x := &greeterSayHelloBidiClient{stream}
+	return x, nil
+}
+
+type Greeter_SayHelloBidiClient interface {
+	Send(*HelloRequest) error
+	Recv() (*HelloReply, error)
+	grpc.ClientStream
+}
+
+type greeterSayHelloBidiClient struct {
+	grpc.ClientStream
+}
+
+func (x *greeterSayHelloBidiClient) Send(m *HelloRequest) error {
+	return x.ClientStream.SendMsg(m)
+}
+
+func (x *greeterSayHelloBidiClient) Recv() (*HelloReply, error) {
+	m := new(HelloReply)
+	if err := x.ClientStream.RecvMsg(m); err != nil {
+		return nil, err
+	}
+	return m, nil
+}
+
+// GreeterServer is the server API for Greeter service.
+// All implementations must embed UnimplementedGreeterServer
+// for forward compatibility
+type GreeterServer interface {
+	SayHello(context.Context, *HelloRequest) (*HelloReply, error)
+	SayHelloCS(Greeter_SayHelloCSServer) error
+	SayHellos(*HelloRequest, Greeter_SayHellosServer) error
+	SayHelloBidi(Greeter_SayHelloBidiServer) error
+	mustEmbedUnimplementedGreeterServer()
+}
+
+// UnimplementedGreeterServer must be embedded to have forward compatible implementations.
+type UnimplementedGreeterServer struct {
+}
+
+func (UnimplementedGreeterServer) SayHello(context.Context, *HelloRequest) (*HelloReply, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented")
+}
+func (UnimplementedGreeterServer) SayHelloCS(Greeter_SayHelloCSServer) error {
+	return status.Errorf(codes.Unimplemented, "method SayHelloCS not implemented")
+}
+func (UnimplementedGreeterServer) SayHellos(*HelloRequest, Greeter_SayHellosServer) error {
+	return status.Errorf(codes.Unimplemented, "method SayHellos not implemented")
+}
+func (UnimplementedGreeterServer) SayHelloBidi(Greeter_SayHelloBidiServer) error {
+	return status.Errorf(codes.Unimplemented, "method SayHelloBidi not implemented")
+}
+func (UnimplementedGreeterServer) mustEmbedUnimplementedGreeterServer() {}
+
+// UnsafeGreeterServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to GreeterServer will
+// result in compilation errors.
+type UnsafeGreeterServer interface {
+	mustEmbedUnimplementedGreeterServer()
+}
+
+func RegisterGreeterServer(s grpc.ServiceRegistrar, srv GreeterServer) {
+	s.RegisterService(&Greeter_ServiceDesc, srv)
+}
+
+func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(HelloRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(GreeterServer).SayHello(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/helloworld.Greeter/SayHello",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _Greeter_SayHelloCS_Handler(srv interface{}, stream grpc.ServerStream) error {
+	return srv.(GreeterServer).SayHelloCS(&greeterSayHelloCSServer{stream})
+}
+
+type Greeter_SayHelloCSServer interface {
+	SendAndClose(*HelloReply) error
+	Recv() (*HelloRequest, error)
+	grpc.ServerStream
+}
+
+type greeterSayHelloCSServer struct {
+	grpc.ServerStream
+}
+
+func (x *greeterSayHelloCSServer) SendAndClose(m *HelloReply) error {
+	return x.ServerStream.SendMsg(m)
+}
+
+func (x *greeterSayHelloCSServer) Recv() (*HelloRequest, error) {
+	m := new(HelloRequest)
+	if err := x.ServerStream.RecvMsg(m); err != nil {
+		return nil, err
+	}
+	return m, nil
+}
+
+func _Greeter_SayHellos_Handler(srv interface{}, stream grpc.ServerStream) error {
+	m := new(HelloRequest)
+	if err := stream.RecvMsg(m); err != nil {
+		return err
+	}
+	return srv.(GreeterServer).SayHellos(m, &greeterSayHellosServer{stream})
+}
+
+type Greeter_SayHellosServer interface {
+	Send(*HelloReply) error
+	grpc.ServerStream
+}
+
+type greeterSayHellosServer struct {
+	grpc.ServerStream
+}
+
+func (x *greeterSayHellosServer) Send(m *HelloReply) error {
+	return x.ServerStream.SendMsg(m)
+}
+
+func _Greeter_SayHelloBidi_Handler(srv interface{}, stream grpc.ServerStream) error {
+	return srv.(GreeterServer).SayHelloBidi(&greeterSayHelloBidiServer{stream})
+}
+
+type Greeter_SayHelloBidiServer interface {
+	Send(*HelloReply) error
+	Recv() (*HelloRequest, error)
+	grpc.ServerStream
+}
+
+type greeterSayHelloBidiServer struct {
+	grpc.ServerStream
+}
+
+func (x *greeterSayHelloBidiServer) Send(m *HelloReply) error {
+	return x.ServerStream.SendMsg(m)
+}
+
+func (x *greeterSayHelloBidiServer) Recv() (*HelloRequest, error) {
+	m := new(HelloRequest)
+	if err := x.ServerStream.RecvMsg(m); err != nil {
+		return nil, err
+	}
+	return m, nil
+}
+
+// Greeter_ServiceDesc is the grpc.ServiceDesc for Greeter service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var Greeter_ServiceDesc = grpc.ServiceDesc{
+	ServiceName: "helloworld.Greeter",
+	HandlerType: (*GreeterServer)(nil),
+	Methods: []grpc.MethodDesc{
+		{
+			MethodName: "SayHello",
+			Handler:    _Greeter_SayHello_Handler,
+		},
+	},
+	Streams: []grpc.StreamDesc{
+		{
+			StreamName:    "SayHelloCS",
+			Handler:       _Greeter_SayHelloCS_Handler,
+			ClientStreams: true,
+		},
+		{
+			StreamName:    "SayHellos",
+			Handler:       _Greeter_SayHellos_Handler,
+			ServerStreams: true,
+		},
+		{
+			StreamName:    "SayHelloBidi",
+			Handler:       _Greeter_SayHelloBidi_Handler,
+			ServerStreams: true,
+			ClientStreams: true,
+		},
+	},
+	Metadata: "helloworld/greeter.proto",
+}
diff --git a/base/internal/helloworld/helloworld/greeter_server.go b/base/internal/helloworld/helloworld/greeter_server.go
new file mode 100644
index 00000000..e4c6d07d
--- /dev/null
+++ b/base/internal/helloworld/helloworld/greeter_server.go
@@ -0,0 +1,343 @@
+// Credit: This file is sourced from https://github.com/bojand/ghz/
+// this file is used for test purposes
+
+package helloworld
+
+import (
+	"fmt"
+	"io"
+	"math/rand"
+	"strings"
+	"sync"
+	"time"
+
+	context "golang.org/x/net/context"
+	"google.golang.org/grpc/metadata"
+	"google.golang.org/grpc/stats"
+)
+
+// CallType represents one of the gRPC call types:
+// unary, client streaming, server streaming, bidi
+type CallType string
+
+// Unary is a unary call
+var Unary CallType = "unary"
+
+// ClientStream is a client streaming call
+var ClientStream CallType = "cs"
+
+// ServerStream is a server streaming call
+var ServerStream CallType = "ss"
+
+// Bidi is a bidi / duplex call
+var Bidi CallType = "bidi"
+
+// Greeter implements the GreeterServer for tests
+type Greeter struct {
+	StreamData []*HelloReply
+
+	Stats *HWStatsHandler
+
+	mutex      *sync.RWMutex
+	callCounts map[CallType]int
+	calls      map[CallType][][]*HelloRequest
+
+	sendMutex  *sync.RWMutex
+	sendCounts map[CallType]map[int]int
+	// the following line is a modification on top of the source (see source credit above)
+	// must embed unimplemented GreeterServer
+	UnimplementedGreeterServer
+}
+
+func randomSleep(max int) {
+	msCount := rand.Intn(max) + 1
+	time.Sleep(time.Millisecond * time.Duration(msCount))
+}
+
+func (s *Greeter) recordCall(ct CallType) int {
+	s.mutex.Lock()
+	defer s.mutex.Unlock()
+
+	s.callCounts[ct]++
+	var messages []*HelloRequest
+	s.calls[ct] = append(s.calls[ct], messages)
+
+	return len(s.calls[ct]) - 1
+}
+
+func (s *Greeter) recordMessage(ct CallType, callIdx int, msg *HelloRequest) {
+	s.mutex.Lock()
+	defer s.mutex.Unlock()
+
+	s.calls[ct][callIdx] = append(s.calls[ct][callIdx], msg)
+}
+
+func (s *Greeter) recordStreamSendCounter(ct CallType, callIdx int) {
+	s.sendMutex.Lock()
+	defer s.sendMutex.Unlock()
+
+	s.sendCounts[ct][callIdx] = s.sendCounts[ct][callIdx] + 1
+}
+
+// SayHello implements helloworld.GreeterServer
+func (s *Greeter) SayHello(ctx context.Context, in *HelloRequest) (*HelloReply, error) {
+	callIdx := s.recordCall(Unary)
+
+	if in.GetName() == "__record_metadata__" {
+		mdval := ""
+		md, ok := metadata.FromIncomingContext(ctx)
+		if ok {
+			for k, v := range md {
+				if k == "token" {
+					mdval = mdval + k + ":"
+					for _, vv := range v {
+						mdval = mdval + vv
+					}
+				}
+			}
+		}
+
+		newReq := &HelloRequest{Name: in.GetName() + "||" + mdval}
+		s.recordMessage(Unary, callIdx, newReq)
+	} else {
+		s.recordMessage(Unary, callIdx, in)
+	}
+
+	randomSleep(4)
+
+	return &HelloReply{Message: "Hello " + in.Name}, nil
+}
+
+// SayHellos lists all hellos
+func (s *Greeter) SayHellos(req *HelloRequest, stream Greeter_SayHellosServer) error {
+	callIdx := s.recordCall(ServerStream)
+	s.recordMessage(ServerStream, callIdx, req)
+
+	for _, msg := range s.StreamData {
+		if err := stream.Send(msg); err != nil {
+			return err
+		}
+
+		randomSleep(4)
+
+		s.recordStreamSendCounter(ServerStream, callIdx)
+	}
+
+	return nil
+}
+
+// SayHelloCS is client streaming handler
+func (s *Greeter) SayHelloCS(stream Greeter_SayHelloCSServer) error {
+	callIdx := s.recordCall(ClientStream)
+
+	randomSleep(4)
+
+	msgCount := 0
+
+	for {
+		in, err := stream.Recv()
+		if err == io.EOF {
+			msgStr := fmt.Sprintf("Hello count: %d", msgCount)
+			return stream.SendAndClose(&HelloReply{Message: msgStr})
+		}
+		if err != nil {
+			return err
+		}
+		s.recordMessage(ClientStream, callIdx, in)
+		msgCount++
+	}
+}
+
+// SayHelloBidi duplex call handler
+func (s *Greeter) SayHelloBidi(stream Greeter_SayHelloBidiServer) error {
+	callIdx := s.recordCall(Bidi)
+
+	for {
+		in, err := stream.Recv()
+		if err == io.EOF {
+			return nil
+		}
+		if err != nil {
+			return err
+		}
+
+		s.recordMessage(Bidi, callIdx, in)
+
+		msg := "Hello " + in.Name
+		if err := stream.Send(&HelloReply{Message: msg}); err != nil {
+			return err
+		}
+
+		s.recordStreamSendCounter(ServerStream, callIdx)
+	}
+}
+
+// ResetCounters resets the call counts
+func (s *Greeter) ResetCounters() {
+	s.mutex.Lock()
+
+	s.callCounts = make(map[CallType]int)
+	s.callCounts[Unary] = 0
+	s.callCounts[ServerStream] = 0
+	s.callCounts[ClientStream] = 0
+	s.callCounts[Bidi] = 0
+
+	s.calls = make(map[CallType][][]*HelloRequest)
+	s.calls[Unary] = make([][]*HelloRequest, 0)
+	s.calls[ServerStream] = make([][]*HelloRequest, 0)
+	s.calls[ClientStream] = make([][]*HelloRequest, 0)
+	s.calls[Bidi] = make([][]*HelloRequest, 0)
+
+	s.mutex.Unlock()
+
+	s.sendMutex.Lock()
+	s.sendCounts = make(map[CallType]map[int]int)
+	s.sendCounts[Unary] = make(map[int]int)
+	s.sendCounts[ServerStream] = make(map[int]int)
+	s.sendCounts[ClientStream] = make(map[int]int)
+	s.sendCounts[Bidi] = make(map[int]int)
+	s.sendMutex.Unlock()
+
+	if s.Stats != nil {
+		s.Stats.mutex.Lock()
+		s.Stats.connCount = 0
+		s.Stats.mutex.Unlock()
+	}
+}
+
+// GetCount gets the count for specific call type
+func (s *Greeter) GetCount(key CallType) int {
+	s.mutex.RLock()
+	val, ok := s.callCounts[key]
+	s.mutex.RUnlock()
+	if ok {
+		return val
+	}
+	return -1
+}
+
+// GetCalls gets the received messages for specific call type
+func (s *Greeter) GetCalls(key CallType) [][]*HelloRequest {
+	s.mutex.Lock()
+	val, ok := s.calls[key]
+	s.mutex.Unlock()
+
+	if ok {
+		return val
+	}
+	return nil
+}
+
+// GetSendCounts gets the stream send counts
+func (s *Greeter) GetSendCounts(key CallType) map[int]int {
+	s.sendMutex.RLock()
+	defer s.sendMutex.RUnlock()
+
+	val, ok := s.sendCounts[key]
+
+	if ok {
+		cm := map[int]int{}
+		for k, v := range val {
+			cm[k] = v
+		}
+
+		return cm
+	}
+
+	return nil
+}
+
+// GetConnectionCount gets the connection count
+func (s *Greeter) GetConnectionCount() int {
+	return s.Stats.GetConnectionCount()
+}
+
+// NewGreeter creates new greeter server
+func NewGreeter() *Greeter {
+	streamData := []*HelloReply{
+		{Message: "Hello Bob"},
+		{Message: "Hello Kate"},
+		{Message: "Hello Jim"},
+		{Message: "Hello Sara"},
+	}
+
+	greeter := &Greeter{StreamData: streamData, mutex: &sync.RWMutex{}, sendMutex: &sync.RWMutex{}}
+	greeter.ResetCounters()
+
+	return greeter
+}
+
+// NewHWStats creates new stats handler
+func NewHWStats() *HWStatsHandler {
+	return &HWStatsHandler{connCount: 0, mutex: &sync.RWMutex{}}
+}
+
+// HWStatsHandler is for gRPC stats
+type HWStatsHandler struct {
+	mutex     *sync.RWMutex
+	connCount int
+}
+
+// GetConnectionCount gets the connection count
+func (c *HWStatsHandler) GetConnectionCount() int {
+	c.mutex.RLock()
+	val := c.connCount
+	c.mutex.RUnlock()
+
+	return val
+}
+
+// GetCountByWorker gets count of requests by goroutine
+func (s *Greeter) GetCountByWorker(key CallType) map[string]int {
+	s.mutex.Lock()
+	val, ok := s.calls[key]
+	s.mutex.Unlock()
+
+	if !ok {
+		return nil
+	}
+
+	counts := make(map[string]int)
+
+	for _, reqs := range val {
+		for _, req := range reqs {
+			name := req.GetName()
+			if strings.Contains(name, "worker:") {
+				parts := strings.Split(name, ":")
+				wid := parts[len(parts)-1]
+				wc, ok := counts[wid]
+				if !ok {
+					counts[wid] = 0
+				}
+
+				counts[wid] = wc + 1
+			}
+		}
+	}
+
+	return counts
+}
+
+// HandleConn handle the connection
+func (c *HWStatsHandler) HandleConn(ctx context.Context, cs stats.ConnStats) {
+	// no-op
+}
+
+// TagConn exists to satisfy gRPC stats.Handler.
+func (c *HWStatsHandler) TagConn(ctx context.Context, cti *stats.ConnTagInfo) context.Context {
+	c.mutex.Lock()
+	c.connCount++
+	c.mutex.Unlock()
+
+	return ctx
+}
+
+// HandleRPC implements per-RPC tracing and stats instrumentation.
+func (c *HWStatsHandler) HandleRPC(ctx context.Context, rs stats.RPCStats) {
+	// no-op
+}
+
+// TagRPC implements per-RPC context management.
+func (c *HWStatsHandler) TagRPC(ctx context.Context, info *stats.RPCTagInfo) context.Context {
+	return ctx
+}
diff --git a/base/metrics.go b/base/metrics.go
index 4e9befee..07d369c4 100644
--- a/base/metrics.go
+++ b/base/metrics.go
@@ -5,18 +5,40 @@ const (
 	iter8BuiltInPrefix = "built-in"
 )
 
+// HistBucket is a single bucket in a histogram
+type HistBucket struct {
+	Lower float64 `json:"lower" yaml:"lower"`
+	Upper float64 `json:"upper" yaml:"upper"`
+	Count uint64  `json:"count" yaml:"count"`
+}
+
 // MetricType identifies the type of the metric.
 type MetricType string
 
+// AggregationType identifies the type of the metric aggregator.
+type AggregationType string
+
 const (
 	// CounterMetricType corresponds to Prometheus Counter metric type
 	CounterMetricType MetricType = "Counter"
-
 	// GaugeMetricType corresponds to Prometheus Gauge metric type
 	GaugeMetricType MetricType = "Gauge"
-
 	// HistogramMetricType corresponds to a Histogram metric type
 	HistogramMetricType MetricType = "Histogram"
+	// SampleMetricType corresponds to a Sample metric type
+	SampleMetricType MetricType = "Sample"
+
+	// decimalRegex is the regex used to identify percentiles
+	decimalRegex = `^([\d]+(\.[\d]*)?|\.[\d]+)$`
+
+	// Mean aggregator corresponds to aggregation of type mean
+	MeanAggregator AggregationType = "mean"
+	// Stddev aggregator corresponds to aggregation of type stddev
+	StdDevAggregator AggregationType = "stddev"
+	// Min aggregator corresponds to aggregation of type min
+	MinAggregator AggregationType = "min"
+	// Max aggregator corresponds to aggregation of type max
+	MaxAggregator AggregationType = "max"
 )
 
 // // AuthType identifies the type of authentication used in the HTTP request
diff --git a/base/run.go b/base/run.go
index 4072ece3..f7d3480c 100644
--- a/base/run.go
+++ b/base/run.go
@@ -1,16 +1,10 @@
 package base
 
 import (
-	"bytes"
-	"encoding/json"
-	"errors"
 	"fmt"
 	"os"
 	"os/exec"
-	"text/template"
 
-	"github.com/Masterminds/sprig"
-	"github.com/go-playground/validator/v10"
 	log "github.com/iter8-tools/iter8/base/log"
 )
 
@@ -23,101 +17,39 @@ var (
 	tempDirEnv string = fmt.Sprintf("TEMP_DIR=%v", os.TempDir())
 )
 
-// runInputs contains inputs for the run task
-type runInputs struct {
-	Template bool `json:"template" yaml:"template"`
-}
-
 // runTask enables running a shell script
 type runTask struct {
-	taskMeta
-	With runInputs `json:"with" yaml:"with"`
-}
-
-// MakeRun constructs a RunTask out of a run task spec
-func MakeRun(t *TaskSpec) (Task, error) {
-	if t.Run == nil {
-		return nil, errors.New("task need to have a run command")
-	}
-	var err error
-	var jsonBytes []byte
-	var bt Task
-	// convert t to jsonBytes
-	jsonBytes, err = json.Marshal(t)
-	// convert jsonString to RunTask
-	if err == nil {
-		rt := &runTask{}
-		err = json.Unmarshal(jsonBytes, &rt)
-		bt = rt
-	}
-	if err != nil {
-		log.Logger.WithStackTrace(err.Error()).Error("invalid run task specification")
-		return nil, err
-	}
-
-	validate := validator.New()
-	err = validate.Struct(bt)
-	if err != nil {
-		log.Logger.WithStackTrace(err.Error()).Error("invalid run task specification")
-		return nil, err
-	}
-
-	return bt, err
+	TaskMeta
 }
 
-// interpolate the script.
-func (t *runTask) interpolate(exp *Experiment) (string, error) {
-	// ensure it is a valid template
-	tmpl, err := template.New("tpl").Funcs(sprig.TxtFuncMap()).Option("missingkey=error").Parse(*t.taskMeta.Run)
-	if err != nil {
-		log.Logger.WithStackTrace(err.Error()).Error("unable to parse templated run command")
-		return "", err
-	}
-
-	// execute template
-	var b bytes.Buffer
-	err = tmpl.Execute(&b, exp)
-	if err != nil {
-		log.Logger.WithStackTrace(err.Error()).Error("unable to execute command template")
-		return "", err
-	}
-
-	// print output
-	return b.String(), nil
+// initializeDefaults sets default values for task inputs
+func (t *runTask) initializeDefaults() {}
 
+//validateInputs for this task
+func (t *runTask) validateInputs() error {
+	return nil
 }
 
 // get the command
-func (t *runTask) getCommand(exp *Experiment) (*exec.Cmd, error) {
-	var cmdStr string
-	var err error
-	if t.With.Template {
-		cmdStr, err = t.interpolate(exp)
-	} else {
-		cmdStr = *t.taskMeta.Run
-	}
-	if err != nil {
-		return nil, err
-	}
-
+func (t *runTask) getCommand() *exec.Cmd {
+	cmdStr := *t.TaskMeta.Run
 	// create command to be executed
 	cmd := exec.Command("/bin/bash", "-c", cmdStr)
 	// append the environment variable for temp dir
 	cmd.Env = append(os.Environ(), tempDirEnv)
-	return cmd, nil
-}
-
-// GetName returns the name of the run task
-func (t *runTask) GetName() string {
-	return RunTaskName
+	return cmd
 }
 
 // Run the command.
 func (t *runTask) Run(exp *Experiment) error {
-	cmd, err := t.getCommand(exp)
+	err := t.validateInputs()
 	if err != nil {
 		return err
 	}
+
+	t.initializeDefaults()
+
+	cmd := t.getCommand()
 	out, err := cmd.CombinedOutput()
 	if err != nil {
 		log.Logger.WithStackTrace(err.Error()).Error("combined execution failed")
diff --git a/base/util.go b/base/util.go
index 589819fb..bcc4cb21 100644
--- a/base/util.go
+++ b/base/util.go
@@ -1,8 +1,15 @@
 package base
 
 import (
+	"errors"
+	"io/ioutil"
+	"net/http"
+	"os"
 	"path/filepath"
 	"runtime"
+	"time"
+
+	"github.com/iter8-tools/iter8/base/log"
 )
 
 // int64Pointer takes an int64 as input, creates a new variable with the input value, and returns a pointer to the variable
@@ -36,3 +43,33 @@ func CompletePath(prefix string, suffix string) string {
 	_, filename, _, _ := runtime.Caller(1) // one step up the call stack
 	return filepath.Join(filepath.Dir(filename), prefix, suffix)
 }
+
+// getPayloadBytes downloads payload from URL and returns a byte slice
+func getPayloadBytes(url string) ([]byte, error) {
+	var myClient = &http.Client{Timeout: 10 * time.Second}
+	r, err := myClient.Get(url)
+	if err != nil || r.StatusCode >= 400 {
+		e := errors.New("error while fetching payload")
+		log.Logger.WithStackTrace(err.Error()).Error(e)
+		return nil, e
+	}
+	defer r.Body.Close()
+	body, err := ioutil.ReadAll(r.Body)
+	return body, err
+}
+
+// getFileFromURL downloads contents from URL into the given file
+func getFileFromURL(url string, fileName string) error {
+	var myClient = &http.Client{Timeout: 10 * time.Second}
+	r, err := myClient.Get(url)
+	if err != nil || r.StatusCode >= 400 {
+		return errors.New("error while fetching payload")
+	}
+	defer r.Body.Close()
+	body, err := ioutil.ReadAll(r.Body)
+	if err != nil {
+		return err
+	}
+	err = os.WriteFile(fileName, body, 0644)
+	return err
+}
diff --git a/basecli/assert.go b/basecli/assert.go
index f47ddf15..d4b2f67c 100644
--- a/basecli/assert.go
+++ b/basecli/assert.go
@@ -7,7 +7,6 @@ import (
 	"strings"
 	"time"
 
-	"github.com/iter8-tools/iter8/base"
 	"github.com/iter8-tools/iter8/base/log"
 	"github.com/spf13/cobra"
 )
@@ -72,9 +71,6 @@ iter8 assert -c completed,nofailures,slosby=0 -t 5s
 `,
 		RunE: func(cmd *cobra.Command, args []string) error {
 			// build experiment
-			exp := &Experiment{
-				Experiment: &base.Experiment{},
-			}
 			log.Logger.Trace("build started")
 			// replace FileExpIO with ClusterExpIO to build from cluster
 			fio := &FileExpIO{}
@@ -125,7 +121,7 @@ func (exp *Experiment) Assert(conditions []string, to time.Duration) (bool, erro
 					log.Logger.Info("experiment failed")
 				}
 			} else if strings.ToLower(cond) == SLOs {
-				slos := exp.SLOs()
+				slos := exp.slos()
 				allGood = allGood && slos
 				if slos {
 					log.Logger.Info("SLOs are satisfied")
@@ -137,7 +133,7 @@ func (exp *Experiment) Assert(conditions []string, to time.Duration) (bool, erro
 				if err != nil {
 					return false, err
 				}
-				iv := exp.SLOsBy(version)
+				iv := exp.slosBy(version)
 				allGood = allGood && iv
 				if iv {
 					log.Logger.Info("version ", version, " satisfies objectives")
diff --git a/basecli/experiment.go b/basecli/experiment.go
index 654d558e..4667148b 100644
--- a/basecli/experiment.go
+++ b/basecli/experiment.go
@@ -4,7 +4,6 @@ import (
 	"errors"
 	"io/ioutil"
 
-	"github.com/go-playground/validator/v10"
 	"github.com/iter8-tools/iter8/base"
 	"github.com/iter8-tools/iter8/base/log"
 	"sigs.k8s.io/yaml"
@@ -12,17 +11,16 @@ import (
 
 // Experiment type that includes a list of runnable tasks derived from the experiment spec
 type Experiment struct {
-	tasks []base.Task
-	*base.Experiment
+	base.Experiment
 }
 
 // ExpIO enables interacting with experiment spec and result stored externally
 type ExpIO interface {
 	// ReadSpec reads the experiment spec
-	ReadSpec() ([]base.TaskSpec, error)
-	// ReadResult reads the experiment results
+	ReadSpec() (base.ExperimentSpec, error)
+	// ReadResult reads the experiment result
 	ReadResult() (*base.ExperimentResult, error)
-	// WriteResult writes the experimeent results
+	// WriteResult writes the experiment result
 	WriteResult(r *Experiment) error
 }
 
@@ -33,9 +31,7 @@ const (
 
 // Build an experiment
 func Build(withResult bool, expio ExpIO) (*Experiment, error) {
-	e := &Experiment{
-		Experiment: &base.Experiment{},
-	}
+	e := &Experiment{}
 	var err error
 	// read it in
 	log.Logger.Trace("build started")
@@ -50,68 +46,15 @@ func Build(withResult bool, expio ExpIO) (*Experiment, error) {
 			return nil, err
 		}
 	}
-
-	err = e.buildTasks()
-	if err != nil {
-		return nil, err
-	}
-
 	return e, err
 }
 
-// build experiment tasks
-func (e *Experiment) buildTasks() error {
-	for _, t := range e.Tasks {
-		if (t.Task == nil || len(*t.Task) == 0) && (t.Run == nil) {
-			log.Logger.Error("invalid task found without a task name or a run command")
-			return errors.New("invalid task found without a task name or a run command")
-		}
-
-		var err error
-		var task base.Task
-
-		validate := validator.New()
-		err = validate.Struct(t)
-		if err != nil {
-			log.Logger.WithStackTrace(err.Error()).Error("invalid task specification")
-			return err
-		}
-
-		// this is a run task
-		if t.Run != nil {
-			task, err = base.MakeRun(&t)
-			e.tasks = append(e.tasks, task)
-			if err != nil {
-				return err
-			}
-		} else {
-			// this is some other task
-			switch *t.Task {
-			case base.CollectTaskName:
-				task, err = base.MakeCollect(&t)
-				e.tasks = append(e.tasks, task)
-			case base.AssessTaskName:
-				task, err = base.MakeAssess(&t)
-				e.tasks = append(e.tasks, task)
-			default:
-				log.Logger.Error("unknown task: " + *t.Task)
-				return errors.New("unknown task: " + *t.Task)
-			}
-
-			if err != nil {
-				return err
-			}
-		}
-	}
-	return nil
-}
-
 //FileExpIO enables reading and writing experiment spec and result files
 type FileExpIO struct{}
 
 // SpecFromBytes reads experiment spec from bytes
-func SpecFromBytes(b []byte) ([]base.TaskSpec, error) {
-	e := []base.TaskSpec{}
+func SpecFromBytes(b []byte) (base.ExperimentSpec, error) {
+	e := base.ExperimentSpec{}
 	err := yaml.Unmarshal(b, &e)
 	if err != nil {
 		log.Logger.WithStackTrace(err.Error()).Error("unable to unmarshal experiment spec")
@@ -132,7 +75,7 @@ func ResultFromBytes(b []byte) (*base.ExperimentResult, error) {
 }
 
 // ReadSpec reads experiment spec from file
-func (f *FileExpIO) ReadSpec() ([]base.TaskSpec, error) {
+func (f *FileExpIO) ReadSpec() (base.ExperimentSpec, error) {
 	b, err := ioutil.ReadFile(experimentSpecPath)
 	if err != nil {
 		log.Logger.WithStackTrace(err.Error()).Error("unable to read experiment spec")
@@ -187,3 +130,63 @@ func (exp *Experiment) NoFailure() bool {
 	}
 	return false
 }
+
+// getSLOsSatisfiedBy returns the set of versions which satisfy SLOs
+func (exp *Experiment) getSLOsSatisfiedBy() []int {
+	if exp == nil {
+		log.Logger.Error("nil experiment")
+		return nil
+	}
+	if exp.Result == nil {
+		log.Logger.Error("nil experiment result")
+		return nil
+	}
+	if exp.Result.Insights == nil {
+		log.Logger.Error("nil insights in experiment result")
+		return nil
+	}
+	if exp.Result.Insights.NumVersions == 0 {
+		log.Logger.Error("experiment does not involve any versions")
+		return nil
+	}
+	if exp.Result.Insights.SLOs == nil {
+		log.Logger.Info("experiment does not involve any SLOs")
+		sat := []int{}
+		for j := 0; j < exp.Result.Insights.NumVersions; j++ {
+			sat = append(sat, j)
+		}
+		return sat
+	}
+	log.Logger.Trace("experiment involves at least one version and at least one SLO")
+	sat := []int{}
+	for j := 0; j < exp.Result.Insights.NumVersions; j++ {
+		satThis := true
+		for i := 0; i < len(exp.Result.Insights.SLOs); i++ {
+			satThis = satThis && exp.Result.Insights.SLOsSatisfied[i][j]
+			if !satThis {
+				break
+			}
+		}
+		if satThis {
+			sat = append(sat, j)
+		}
+	}
+	return sat
+}
+
+// SLOsBy returns true if version satisfies SLOs
+func (exp *Experiment) slosBy(version int) bool {
+	sby := exp.getSLOsSatisfiedBy()
+	for _, v := range sby {
+		if v == version {
+			return true
+		}
+	}
+	return false
+}
+
+// SLOs returns true if all versions satisfy SLOs
+func (exp *Experiment) slos() bool {
+	sby := exp.getSLOsSatisfiedBy()
+	return exp.Result.Insights.NumVersions == len(sby)
+}
diff --git a/basecli/htmlreport.tpl b/basecli/htmlreport.tpl
new file mode 100644
index 00000000..03a2d564
--- /dev/null
+++ b/basecli/htmlreport.tpl
@@ -0,0 +1,166 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <!-- Required meta tags -->
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+    <!-- Font Awesome -->
+    <script src="https://kit.fontawesome.com/db794f5235.js" crossorigin="anonymous"></script>
+    <!-- Bootstrap CSS -->
+    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
+    <style>
+      html {
+        font-size: 18px;
+      }		
+    </style>
+    <title>Iter8 Experiment Report</title>
+  </head>
+
+  <body>
+    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
+    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
+    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
+  
+    <!-- Plotly.js -->
+    <script src="https://cdn.plot.ly/plotly-2.8.3.min.js"></script>
+
+    <div class="container">
+      <h1 class="display-4"><a href="https://iter8.tools">Iter8</a> Experiment Report</h1>
+      <hr>
+      
+      <div class="toast fade {{ .RenderStr "showClassStatus" }} mw-100" role="alert" aria-live="assertive" aria-atomic="true">
+        <div class="toast-header {{ .RenderStr "textColorStatus" }}">
+          <strong class="mr-auto">
+            Experiment Status
+            &nbsp;&nbsp;
+            <i class="fas fa-thumbs-{{ .RenderStr "thumbsStatus" }}"></i>
+          </strong>
+          <button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
+            <span aria-hidden="true">&times;</span>
+          </button>
+        </div>
+        <div class="toast-body {{ .RenderStr "textColorStatus" }}">
+          {{ .RenderStr "msgStatus" }}
+        </div>
+      </div>
+    
+      <script>
+      $(document).ready(function(){
+        $(function () {
+          $('[data-toggle="tooltip"]').tooltip()
+        });	
+
+        $(".toast").toast({
+          autohide: false,
+          delay: 10000
+        }).toast('show');
+      });	
+      </script>
+
+      <section class="mt-5">
+        <h3 class="display-6">Service level objectives (SLOs)</h3>
+        <h4 class="display-7 text-muted">Whether or not SLOs are satisfied</h4>
+        <hr>
+        <table class="table">
+          <thead class="thead-light">
+            <tr>
+              <th scope="col">SLO Conditions</th>
+              {{- if ge .Result.Insights.NumVersions 2 }}
+              {{- range until .Result.Insights.NumVersions }}
+              <th scope="col" class="text-center">Version {{ . }} </th>
+              {{- end}}              
+              {{- else }} 
+              <th scope="col" class="text-center">Satisfied</th>
+              {{- end }}
+            </tr>
+          </thead>
+          <tbody>
+              {{- range $ind, $slo := .Result.Insights.SLOs }}
+              <tr scope="row">
+                <td>
+                  {{ if $slo.LowerLimit }} {{- $slo.LowerLimit }} &leq; {{ end -}}
+                  <a href="javascript:void(0)" data-toggle="tooltip" data-placement="top" title="{{ $.MetricDescriptionHTML $slo.Metric }}">
+                    {{ $.MetricWithUnits $slo.Metric }}
+                  </a>
+                  {{ if $slo.UpperLimit }} &leq; {{ $slo.UpperLimit -}} {{- end }}
+                </td>
+                {{- range (index $.Result.Insights.SLOsSatisfied $ind) }}
+                <td class="{{ renderSLOSatisfiedCellClass .  }} text-center">
+                  <i class="far {{ renderSLOSatisfiedHTML . }}"></i>                
+                </td>
+                {{- end }}
+              </tr>
+              {{- end}}          
+          </tbody>
+        </table>
+      </section>
+
+  		<section class="mt-5">
+        <h3 class="display-6">Metric Histograms</h3>
+        <hr>
+
+        {{- range $ind, $mn := .SortedVectorMetrics }}
+        <div id="vm-{{ $mn }}"></div>
+        <script>
+          var data = [];
+          {{- range until $.Result.Insights.NumVersions }}
+          data.push({
+            x: {{ $.VectorMetricValue . $mn }},
+            name: "Version {{ . }}",
+            histnorm: "percent", 
+            opacity: 0.5, 
+            type: "histogram"
+          })
+          {{- end }}
+
+          var layout = {
+            bargroupgap: 0.2, 
+            barmode: "overlay", 
+            title: "Histogram of {{ $.MetricWithUnits $mn }}", 
+            xaxis: {title: "{{ $.MetricWithUnits $mn }}"}, 
+            yaxis: {title: "% of observations"}
+          };
+          Plotly.newPlot("vm-{{ $mn }}", data, layout);
+        </script>
+        {{- end }}
+			</section>
+
+  	  <section class="mt-5">
+        <h3 class="display-6">Latest observed values for metrics</h3>
+        <hr>
+        <table class="table">
+          <thead class="thead-light">
+            <tr>
+              <th scope="col">Metric</th>
+              {{- if ge .Result.Insights.NumVersions 2 }}
+              {{- range until .Result.Insights.NumVersions }}
+              <th scope="col" class="text-center">Version {{ . }} </th>
+              {{- end}}              
+              {{- else }} 
+              <th scope="col" class="text-center">Value</th>
+              {{- end }}
+            </tr>
+          </thead>
+          <tbody>
+              {{- range $ind, $mn := .SortedScalarMetrics }}
+              <tr scope="row">
+                <td>
+                  <a href="javascript:void(0)" data-toggle="tooltip" data-placement="top" title="{{ $.MetricDescriptionHTML $mn }}">
+                    {{ $.MetricWithUnits $mn }}
+                  </a>
+                </td>
+                {{- range until $.Result.Insights.NumVersions }}
+                <td class="text-center">
+                {{ $.ScalarMetricValueStr . $mn }}
+                </td>
+                {{- end }}
+              </tr>
+              {{- end}}          
+          </tbody>
+        </table>
+      </section>
+
+    </div>
+  </body>
+</html>
diff --git a/basecli/htmltemplate.go b/basecli/htmltemplate.go
index 41e885c2..588b7957 100644
--- a/basecli/htmltemplate.go
+++ b/basecli/htmltemplate.go
@@ -3,9 +3,12 @@ package basecli
 import (
 	"encoding/json"
 	"fmt"
+	"math/rand"
 	"sort"
 	"strings"
 
+	_ "embed"
+
 	"github.com/iter8-tools/iter8/base"
 	"github.com/iter8-tools/iter8/base/log"
 )
@@ -34,108 +37,89 @@ func (hd *histograms) toJSON() string {
 	return string(jb)
 }
 
-// formatHTML is the HTML template of the experiment results
-var formatHTML = `
-	<!doctype html>
-	<html lang="en">
-
-		{{ headSection }}
-
-		<body>
-
-			{{ dependencies }}
-
-			<div class="container">
-
-				<h1 class="display-4">Experiment Report</h1>
-				<h3 class="display-6">Insights from Iter8 Experiment</h3>
-				<hr>
-
-				{{ .HTMLStatus }}
-
-				{{ if .ContainsInsight "SLOs" }} 
-					{{ .HTMLSLOSection }}
-				{{- end }}
-
-				{{ if .ContainsInsight "HistMetrics" }}
-					{{ .HTMLHistMetricsSection }}
-				{{- end }}
-
-				{{ if .ContainsInsight "Metrics" }} 
-					{{ .HTMLMetricsSection }}
-				{{- end }}
-
-			</div>
-		
-			{{ if .ContainsInsight "HistMetrics" }}
-				{{ styleSection }}
-				{{ .HTMLHistData }}
-				{{ .HTMLHistCharts }}
-			{{- end }}
-
-		</body>
-	</html>
-	`
-
-// styleSection is the style section required for histogram charts
-func styleSection() string {
-	return `
-<style>
-.nvd3 text {
-	font-size: 16px;
-}
-svg {
-		display: block;
-		margin: 0px;
-		padding: 0px;
-		height: 100%;
-		width: 100%;
-}
-</style>
-`
-}
-
-// headSection is the fixed head section for experiment report
-func headSection() string {
-	return `
-	<head>
-		<!-- Required meta tags -->
-		<meta charset="utf-8">
-		<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
-
-
-		<!-- Font Awesome -->
-		<script src="https://kit.fontawesome.com/db794f5235.js" crossorigin="anonymous"></script>
-
-		<!-- Bootstrap CSS -->
-		<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
-
-		<style>
-			html {
-				font-size: 18px;
-			}		
-		</style>
-
-		<title>Iter8 Experiment Report</title>
-	</head>
-`
-}
-
-// dependencies is the dependencies section for the HTML report
-func dependencies() string {
-	return `
-	<!-- jQuery first, then Popper.js, then Bootstrap JS -->
-	<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
-	<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
-	<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
-
-	<!-- NVD3 -->
-  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/nvd3@1.8.6/build/nv.d3.css">
-	<!-- Include d3.js first -->
-	<script src="https://cdn.jsdelivr.net/npm/d3@3.5.3/d3.min.js"></script>
-	<script src="https://cdn.jsdelivr.net/npm/nvd3@1.8.6/build/nv.d3.js"></script>
-`
-}
+// reportHTML is the HTML report template
+//go:embed htmlreport.tpl
+var reportHTML string
+
+// var templateHTML = `
+// 	<!doctype html>
+// 	<html lang="en">
+
+// 		<head>
+// 			<!-- Required meta tags -->
+// 			<meta charset="utf-8">
+// 			<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+
+// 			<!-- Font Awesome -->
+// 			<script src="https://kit.fontawesome.com/db794f5235.js" crossorigin="anonymous"></script>
+
+// 			<!-- Bootstrap CSS -->
+// 			<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
+
+// 			<style>
+// 				html {
+// 					font-size: 18px;
+// 				}
+// 			</style>
+
+// 			<title>Iter8 Experiment Report</title>
+// 		</head>
+
+// 		<body>
+
+// 			<!-- jQuery first, then Popper.js, then Bootstrap JS -->
+// 			<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
+// 			<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
+// 			<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
+
+// 			<!-- NVD3 -->
+// 		  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/nvd3@1.8.6/build/nv.d3.css">
+// 			<!-- Include d3.js first -->
+// 			<script src="https://cdn.jsdelivr.net/npm/d3@3.5.3/d3.min.js"></script>
+// 			<script src="https://cdn.jsdelivr.net/npm/nvd3@1.8.6/build/nv.d3.js"></script>
+
+// 			<div class="container">
+
+// 				<h1 class="display-4">Experiment Report</h1>
+// 				<h3 class="display-6">Insights from Iter8 Experiment</h3>
+// 				<hr>
+
+// 				{{ .HTMLStatus }}
+
+// 				{{ if .ContainsInsight "SLOs" }}
+// 					{{ .HTMLSLOSection }}
+// 				{{- end }}
+
+// 				{{ if .ContainsInsight "HistMetrics" }}
+// 					{{ .HTMLHistMetricsSection }}
+// 				{{- end }}
+
+// 				{{ if .ContainsInsight "Metrics" }}
+// 					{{ .HTMLMetricsSection }}
+// 				{{- end }}
+
+// 			</div>
+
+// 			{{ if .ContainsInsight "HistMetrics" }}
+// 				<style>
+// 					.nvd3 text {
+// 						font-size: 16px;
+// 					}
+// 					svg {
+// 							display: block;
+// 							margin: 0px;
+// 							padding: 0px;
+// 							height: 100%;
+// 							width: 100%;
+// 					}
+// 				</style>
+// 				{{ .HTMLHistData }}
+// 				{{ .HTMLHistCharts }}
+// 			{{- end }}
+
+// 		</body>
+// 	</html>
+// 	`
 
 // HTMLHistData returns histogram data section in HTML report
 func (e *Experiment) HTMLHistData() string {
@@ -154,45 +138,46 @@ func (e *Experiment) HTMLHistData() string {
 
 // HistData provides histogram data for all histogram metrics
 func (e *Experiment) HistData() []histograms {
-	gramsList := []histograms{}
-	for mname, minfo := range e.Result.Insights.MetricsInfo {
-		if minfo.Type == base.HistogramMetricType {
-			// figure out xAxisLabel
-			xAxisLabel := fmt.Sprintf("%v", mname)
-			if minfo.Units != nil {
-				xAxisLabel += " (" + *minfo.Units + ")"
-			}
-
-			grams := histograms{
-				XAxisLabel: xAxisLabel,
-				Datum:      []hist{},
-				Width:      (*minfo.XMax - *minfo.XMin) / float64(*minfo.NumBuckets),
-			}
-			for i := 0; i < e.Result.Insights.NumVersions; i++ {
-				key := fmt.Sprintf("Version %v", i)
-				if e.Result.Insights.NumVersions == 1 {
-					key = "count"
-				}
-				gram := hist{
-					Values: []histBar{},
-					Key:    key,
-				}
-				if counts, ok := e.Result.Insights.MetricValues[i][mname]; ok && len(counts) > 0 {
-					for j := 0; j < len(counts); j++ {
-						gram.Values = append(gram.Values, histBar{
-							X: *minfo.XMin + float64(j)*(*minfo.XMax-*minfo.XMin)/float64(*minfo.NumBuckets),
-							Y: counts[j],
-						})
-					}
-					grams.Datum = append(grams.Datum, gram)
-				}
-			}
-			if len(grams.Datum) > 0 {
-				gramsList = append(gramsList, grams)
-			}
-		}
-	}
-	return gramsList
+	return nil
+	// gramsList := []histograms{}
+	// for mname, minfo := range e.Result.Insights.MetricsInfo {
+	// 	if minfo.Type == base.HistogramMetricType {
+	// 		// figure out xAxisLabel
+	// 		xAxisLabel := fmt.Sprintf("%v", mname)
+	// 		if minfo.Units != nil {
+	// 			xAxisLabel += " (" + *minfo.Units + ")"
+	// 		}
+
+	// 		grams := histograms{
+	// 			XAxisLabel: xAxisLabel,
+	// 			Datum:      []hist{},
+	// 			Width:      (*minfo.XMax - *minfo.XMin) / float64(*minfo.NumBuckets),
+	// 		}
+	// 		for i := 0; i < e.Result.Insights.NumVersions; i++ {
+	// 			key := fmt.Sprintf("Version %v", i)
+	// 			if e.Result.Insights.NumVersions == 1 {
+	// 				key = "count"
+	// 			}
+	// 			gram := hist{
+	// 				Values: []histBar{},
+	// 				Key:    key,
+	// 			}
+	// 			if counts, ok := e.Result.Insights.MetricValues[i][mname]; ok && len(counts) > 0 {
+	// 				for j := 0; j < len(counts); j++ {
+	// 					gram.Values = append(gram.Values, histBar{
+	// 						X: *minfo.XMin + float64(j)*(*minfo.XMax-*minfo.XMin)/float64(*minfo.NumBuckets),
+	// 						Y: counts[j],
+	// 					})
+	// 				}
+	// 				grams.Datum = append(grams.Datum, gram)
+	// 			}
+	// 		}
+	// 		if len(grams.Datum) > 0 {
+	// 			gramsList = append(gramsList, grams)
+	// 		}
+	// 	}
+	// }
+	// return gramsList
 }
 
 // HTMLHistCharts returns histogram charts section in HTML report
@@ -246,299 +231,134 @@ func (e *Experiment) HTMLHistCharts() string {
 	`
 }
 
-// HTMLStatus prints the current state of the experiment
-func (e *Experiment) HTMLStatus() string {
-	completionStatus := "Experiment is complete."
-	if !e.Completed() {
-		completionStatus = "Experiment is yet to complete."
-	}
-
-	failureStatus := "Experiment has failures."
-	showClass := "show"
-	textColor := "text-danger"
-	thumbs := "down"
-	autoHide := "false"
-
-	if e.NoFailure() {
-		failureStatus = "Experiment has no failures."
-		textColor = "text-success"
-		showClass = ""
-		thumbs = "up"
-	}
-
-	taskStatus := fmt.Sprintf("%v out of %v tasks are complete.", len(e.Tasks), e.Result.NumCompletedTasks)
-
-	msg := fmt.Sprintln(completionStatus)
-	msg += fmt.Sprintln(failureStatus)
-	msg += fmt.Sprintln(taskStatus)
-
-	return fmt.Sprintf(`
-		<div class="toast fade %v mw-100" role="alert" aria-live="assertive" aria-atomic="true">
-			<div class="toast-header %v">
-				<strong class="mr-auto">
-					Experiment Status
-					&nbsp;&nbsp;
-					<i class="fas fa-thumbs-%v"></i>
-				</strong>
-				<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
-					<span aria-hidden="true">&times;</span>
-				</button>
-			</div>
-			<div class="toast-body %v">
-				%v
-			</div>
-	 	</div>
-	
-		<script>
-		$(document).ready(function(){
-			$(function () {
-				$('[data-toggle="tooltip"]').tooltip()
-			});	
-
-			$(".toast").toast({
-				autohide: %v,
-				delay: 10000
-			}).toast('show');
-		});	
-		</script>
-	`, showClass, textColor, thumbs, textColor, msg, autoHide)
-}
-
-// HTMLSLOSection prints the SLO section in HTML report
-func (e *Experiment) HTMLSLOSection() string {
-	if e.ContainsInsight(base.InsightTypeSLO) {
-		if e.printableSLOs() {
-			return e.printHTMLSLOs()
-		} else {
-			return e.printHTMLNoSLOs()
+// RenderStrHTML is a helper method for rendering strings
+// Used in HTML template
+func (e *Experiment) RenderStr(what string) (string, error) {
+	var val string = ""
+	var err error = nil
+	switch what {
+	case "showClassStatus":
+		val = "show"
+		if e.NoFailure() {
+			val = ""
+		}
+	case "textColorStatus":
+		val = "text-danger"
+		if e.NoFailure() {
+			val = "text-success"
+		}
+	case "thumbsStatus":
+		val = "down"
+		if e.NoFailure() {
+			val = "up"
 		}
+	case "msgStatus":
+		val = ""
+		completionStatus := "Experiment completed."
+		if !e.Completed() {
+			completionStatus = "Experiment has not completed."
+		}
+		failureStatus := "Experiment has failures."
+		if e.NoFailure() {
+			failureStatus = "Experiment has no failures."
+		}
+		taskStatus := fmt.Sprintf("%v out of %v tasks are complete.", len(e.Tasks), e.Result.NumCompletedTasks)
+		val = fmt.Sprint(completionStatus)
+		val += " "
+		val += fmt.Sprint(failureStatus)
+		val += " "
+		val += fmt.Sprint(taskStatus)
+	default:
+		err = fmt.Errorf("do not know how to render %v", what)
 	}
-	return ""
+	return val, err
 }
 
-func (e *Experiment) printHTMLSLOVersions() string {
+func (e *Experiment) MetricWithUnits(metricName string) (string, error) {
 	in := e.Result.Insights
-	out := ""
-	if in.NumVersions > 1 {
-		for i := 0; i < in.NumVersions; i++ {
-			out += fmt.Sprintf(`
-			<th scope="col">Version %v</th>
-			`, i)
-		}
-	} else {
-		out += `
-		<th scope="col" class="text-center">Satisfied</th>
-		`
+	nm, err := base.NormalizeMetricName(metricName)
+	if err != nil {
+		return "", err
 	}
-	return out
-}
-
-func getMetricWithUnitsAndDescription(in *base.Insights, metricName string) (string, string, error) {
-	m, ok := in.MetricsInfo[metricName]
+	m, ok := in.MetricsInfo[nm]
 	if !ok {
-		e := fmt.Errorf("unknown metric name %v", metricName)
+		e := fmt.Errorf("unknown metric name %v", nm)
 		log.Logger.Error(e)
-		return "", "", e
+		return "", e
 	}
-	str := metricName
+	str := nm
 	if m.Units != nil {
 		str = fmt.Sprintf("%v (%v)", str, *m.Units)
 	}
-	return str, m.Description, nil
-}
-
-func getMetricWithUnitsAndDescriptionHTML(in *base.Insights, metricName string) (string, error) {
-	str, desc, err := getMetricWithUnitsAndDescription(in, metricName)
-	// TODO: Tooltip with description
-	str = fmt.Sprintf(`<a href="javascript:void(0)" data-toggle="tooltip" data-placement="top" title="%v">%v</a>`, desc, str)
-	return str, err
+	return str, nil
 }
 
-func getSLOStrHTML(in *base.Insights, i int) (string, error) {
-	slo := in.SLOs[i]
-	// get metric with units and description
-	str, err := getMetricWithUnitsAndDescriptionHTML(in, slo.Metric)
+func (e *Experiment) MetricDescriptionHTML(metricName string) (string, error) {
+	in := e.Result.Insights
+	nm, err := base.NormalizeMetricName(metricName)
 	if err != nil {
 		return "", err
 	}
-	// add lower limit if needed
-	if slo.LowerLimit != nil {
-		str = fmt.Sprintf("%0.2f &leq; %v", *slo.LowerLimit, str)
-	}
-	// add upper limit if needed
-	if slo.UpperLimit != nil {
-		str = fmt.Sprintf("%v &leq; %0.2f", str, *slo.UpperLimit)
+	m, ok := in.MetricsInfo[nm]
+	if !ok {
+		e := fmt.Errorf("unknown metric name %v", nm)
+		log.Logger.Error(e)
+		return "", e
 	}
-	return str, nil
+	return m.Description, nil
 }
 
-func getSLOSatisfiedHTML(in *base.Insights, i int, j int) string {
-	if in.SLOsSatisfied[i][j] {
-		return `<i class="far fa-check-circle"></i>`
+func renderSLOSatisfiedHTML(s bool) string {
+	if s {
+		return "fa-check-circle"
+	} else {
+		return "fa-times-circle"
 	}
-	return `<i class="far fa-times-circle"></i>`
 }
 
-func (e *Experiment) printHTMLSLORows() string {
-	in := e.Result.Insights
-	out := ""
-	for i := 0; i < len(in.SLOs); i++ {
-		str, err := getSLOStrHTML(in, i)
-		if err == nil {
-			out += `<tr scope="row">` + "\n"
-			out += fmt.Sprintf(`
-			<td>%v</td>
-			`, str)
-
-			for j := 0; j < in.NumVersions; j++ {
-				cellClass := "text-success"
-				if !in.SLOsSatisfied[i][j] {
-					cellClass = "text-danger"
-				}
-				out += fmt.Sprintf(`
-				<td class="%v text-center">%v</td>
-				`, cellClass, getSLOSatisfiedHTML(in, i, j))
-			}
-		}
+func renderSLOSatisfiedCellClass(s bool) string {
+	if s {
+		return "text-success"
+	} else {
+		return "text-danger"
 	}
-	return out
 }
 
-// print HTML SLO validation results
-func (e *Experiment) printHTMLSLOs() string {
-	sloStrs := `
-	<section class="mt-5">
-			<h3 class="display-6">Service level objectives (SLOs)</h3>
-			<h4 class="display-7 text-muted">Whether or not SLOs are satisfied</h4>
-			<hr>
-			<table class="table">
-			<thead class="thead-light">
-				<tr>
-					<th scope="col">SLO</th>
-	` +
-		e.printHTMLSLOVersions() +
-		`</tr>
-	</thead>
-	` +
-		`
-	<tbody>
-	` +
-		e.printHTMLSLORows() +
-		`
-		</tbody>
-		</table>
-		</section>
-		`
-
-	return sloStrs
-}
-
-// print HTML no SLOs
-func (e *Experiment) printHTMLNoSLOs() string {
-	return `
-	<section class="mt-5">
-		<h2>SLOs Unavailable</h2>
-	</section>
-	`
-}
-
-// HTMLHistMetricsSection prints histogram metrics in the HTML report
-func (e *Experiment) HTMLHistMetricsSection() string {
-	hd := e.HistData()
-	if len(hd) > 0 {
-		divs := []string{}
-		for i := 0; i < len(hd); i++ {
-			divs = append(divs, fmt.Sprintf(`
-					<div class='with-3d-shadow with-transitions'>
-						<svg id="hist-chart-%v" style="height:500px"></svg>
-					</div>
-			</section>
-			`, i))
+// SortedVectorMetrics extracts vector metric names from experiment in sorted order
+func (e *Experiment) SortedVectorMetrics() []string {
+	keys := []string{}
+	for k, mm := range e.Result.Insights.MetricsInfo {
+		if mm.Type == base.HistogramMetricType || mm.Type == base.SampleMetricType {
+			keys = append(keys, k)
 		}
-		return `
-		<section class="mt-5">
-		<h3 class="display-6">Metric Histograms</h3>
-		<hr>
-
-		` + strings.Join(divs, "\n") +
-			`</section>`
 	}
-	return ``
+	sort.Strings(keys)
+	return keys
 }
 
-func (e *Experiment) printHTMLMetricVersions() string {
-	in := e.Result.Insights
-	out := ""
-	if in.NumVersions > 1 {
-		for i := 0; i < in.NumVersions; i++ {
-			out += fmt.Sprintf(`
-			<th scope="col">Version %v</th>
-			`, i)
+// sampleHist samples values from a histogram
+func sampleHist(h []base.HistBucket) []float64 {
+	vals := []float64{}
+	for _, b := range h {
+		for i := 0; i < int(b.Count); i++ {
+			vals = append(vals, b.Lower+(b.Upper-b.Lower)*rand.Float64())
 		}
-	} else {
-		out += `
-		<th scope="col">Metric values</th>
-		`
 	}
-	return out
+	return vals
 }
 
-func (e *Experiment) printHTMLMetricRows() string {
+// VectorMetricValue gets the value of the given vector metric for the given version
+// If it is a histogram metric, then its values are sampled from the histogram
+func (e *Experiment) VectorMetricValue(i int, m string) []float64 {
 	in := e.Result.Insights
-
-	// sort metrics
-	keys := []string{}
-	for k := range in.MetricsInfo {
-		keys = append(keys, k)
+	mm, ok := in.MetricsInfo[m]
+	if !ok {
+		log.Logger.Error("could not find vector metric: ", m)
+		return nil
 	}
-	sort.Strings(keys)
-
-	out := ""
-	for i := 0; i < len(keys); i++ {
-		if e.Result.Insights.MetricsInfo[keys[i]].Type != base.HistogramMetricType {
-			str, err := getMetricWithUnitsAndDescriptionHTML(in, keys[i])
-			if err == nil {
-				out += `<tr scope="row">` + "\n"
-				out += fmt.Sprintf(`
-				<td>%v</td>
-				`, str)
-
-				for j := 0; j < in.NumVersions; j++ {
-					out += fmt.Sprintf(`
-					<td>%v</td>
-					`, e.getMetricValue(keys[i], j))
-				}
-			}
-		}
+	if mm.Type == base.SampleMetricType {
+		return in.NonHistMetricValues[i][m]
 	}
-	return out
-}
-
-// HTMLMetricsSection prints metrics in the HTML report
-func (e *Experiment) HTMLMetricsSection() string {
-	metricStrs := `
-	<section class="mt-5">
-			<h3 class="display-6">Latest observed values for metrics</h3>
-			<hr>
-
-			<table class="table">
-			<thead class="thead-light">
-				<tr>
-					<th scope="col">Metrics</th>
-	` +
-		e.printHTMLMetricVersions() +
-		`</tr>
-	</thead>
-	` +
-		`
-	<tbody>
-	` +
-		e.printHTMLMetricRows() +
-		`
-		</tbody>
-		</table>
-		</section>
-		`
-
-	return metricStrs
+	// this is a hist metric
+	return sampleHist(in.HistMetricValues[i][m])
 }
diff --git a/basecli/k8s.go b/basecli/k8s.go
index 86a54e4a..e3ea59a1 100644
--- a/basecli/k8s.go
+++ b/basecli/k8s.go
@@ -23,7 +23,7 @@ const (
 
 type k8sExperiment struct {
 	// Tasks are the set of tasks specifying the experiment
-	Tasks []base.TaskSpec
+	Tasks base.ExperimentSpec
 	// Values used to generate the experiment
 	Values chartutil.Values
 }
diff --git a/basecli/report.go b/basecli/report.go
index 0a59027a..aecb953d 100644
--- a/basecli/report.go
+++ b/basecli/report.go
@@ -3,12 +3,15 @@ package basecli
 import (
 	"bytes"
 	"fmt"
+	htemplate "html/template"
 	"io"
 	"os"
+	"sort"
 	"strings"
 	"text/template"
 
 	"github.com/Masterminds/sprig"
+	"github.com/iter8-tools/iter8/base"
 	"github.com/iter8-tools/iter8/base/log"
 	"github.com/spf13/cobra"
 )
@@ -107,31 +110,50 @@ func ExecTemplate(t executable, exp *Experiment) error {
 
 func init() {
 	// create text template
-	tmpl, err := template.New(TextOutputFormatKey).Funcs(template.FuncMap{
-		"formatText": formatText,
-	}).Option("missingkey=error").Funcs(sprig.TxtFuncMap()).Parse("{{ formatText . }}")
+	ttpl, err := template.New(TextOutputFormatKey).Option("missingkey=error").Funcs(sprig.TxtFuncMap()).Parse(reportText)
 	if err != nil {
 		log.Logger.WithStackTrace(err.Error()).Error("unable to parse text template")
 		os.Exit(1)
 	}
 	// register text template
-	RegisterTextTemplate(TextOutputFormatKey, tmpl)
-
-	// create HTML template (for now, this will still use the text templating functionality)
-	htmpl, err := template.New(TextOutputFormatKey).Funcs(template.FuncMap{
-		"styleSection": styleSection,
-		"headSection":  headSection,
-		"dependencies": dependencies,
-	}).Option("missingkey=error").Funcs(sprig.TxtFuncMap()).Parse(formatHTML)
+	RegisterTextTemplate(TextOutputFormatKey, ttpl)
+
+	// create HTML template
+	htpl, err := htemplate.New(HTMLOutputFormatKey).Option("missingkey=error").Funcs(sprig.FuncMap()).Funcs(htemplate.FuncMap{
+		"renderSLOSatisfiedHTML":      renderSLOSatisfiedHTML,
+		"renderSLOSatisfiedCellClass": renderSLOSatisfiedCellClass,
+	}).Parse(reportHTML)
 	if err != nil {
 		log.Logger.WithStackTrace(err.Error()).Error("unable to parse html template")
 		os.Exit(1)
 	}
-
 	// register HTML template
-	RegisterTextTemplate(HTMLOutputFormatKey, htmpl)
+	RegisterHTMLTemplate(HTMLOutputFormatKey, htpl)
 
 	reportCmd = NewReportCmd()
-
 	RootCmd.AddCommand(reportCmd)
 }
+
+/* Following functions/methods are common to both text and html templates */
+
+// SortedScalarMetrics extracts scalar metric names from experiment in sorted order
+func (e *Experiment) SortedScalarMetrics() []string {
+	keys := []string{}
+	for k, mm := range e.Result.Insights.MetricsInfo {
+		if mm.Type == base.CounterMetricType || mm.Type == base.GaugeMetricType {
+			keys = append(keys, k)
+		}
+	}
+	sort.Strings(keys)
+	return keys
+}
+
+// ScalarMetricValueStr extracts metric value string for given version and scalar metric name
+func (e *Experiment) ScalarMetricValueStr(j int, mn string) string {
+	val := e.Result.Insights.ScalarMetricValue(j, mn)
+	if val != nil {
+		return fmt.Sprintf("%0.2f", *val)
+	} else {
+		return "unavailable"
+	}
+}
diff --git a/basecli/run.go b/basecli/run.go
index 41de14bf..8d9042f5 100644
--- a/basecli/run.go
+++ b/basecli/run.go
@@ -1,6 +1,7 @@
 package basecli
 
 import (
+	"encoding/json"
 	"fmt"
 	"os"
 
@@ -49,6 +50,7 @@ Render the file named "experiment.yaml" by combining an experiment chart with va
 				log.Logger.Info("starting experiment run")
 				err := exp.Run(fio)
 				if err != nil {
+					log.Logger.Error("exiting with code 1")
 					os.Exit(1)
 				} else {
 					log.Logger.Info("experiment completed successfully")
@@ -75,11 +77,11 @@ func (e *Experiment) Run(expio ExpIO) error {
 	if e.Result == nil {
 		e.InitResults()
 	}
-	for i, t := range e.tasks {
-		log.Logger.Info("task " + fmt.Sprintf("%v: %v", i+1, t.GetName()) + " : started")
+	for i, t := range e.Tasks {
+		log.Logger.Info("task " + fmt.Sprintf("%v: %v", i+1, *getName(t)) + " : started")
 		shouldRun := true
 		// if task has a condition
-		if cond := base.GetIf(t); cond != nil {
+		if cond := getIf(t); cond != nil {
 			// condition evaluates to false ... then shouldRun is false
 			program, err := expr.Compile(*cond, expr.Env(e), expr.AsBool())
 			if err != nil {
@@ -96,18 +98,21 @@ func (e *Experiment) Run(expio ExpIO) error {
 			shouldRun = output.(bool)
 		}
 		if shouldRun {
-			err = t.Run(e.Experiment)
+			err = t.Run(&e.Experiment)
 			if err != nil {
-				log.Logger.Error("task " + fmt.Sprintf("%v: %v", i+1, t.GetName()) + " : " + "failure")
+				log.Logger.Error("task " + fmt.Sprintf("%v: %v", i+1, *getName(t)) + " : " + "failure")
 				e.failExperiment()
 				return err
 			}
-			log.Logger.Info("task " + fmt.Sprintf("%v: %v", i+1, t.GetName()) + " : " + "completed")
+			log.Logger.Info("task " + fmt.Sprintf("%v: %v", i+1, *getName(t)) + " : " + "completed")
 		} else {
-			log.Logger.WithStackTrace(fmt.Sprint("false condition: ", *base.GetIf(t))).Info("task " + fmt.Sprintf("%v: %v", i+1, t.GetName()) + " : " + "skipped")
+			log.Logger.WithStackTrace(fmt.Sprint("false condition: ", *getIf(t))).Info("task " + fmt.Sprintf("%v: %v", i+1, *getName(t)) + " : " + "skipped")
 		}
 
-		e.incrementNumCompletedTasks()
+		err = e.incrementNumCompletedTasks()
+		if err != nil {
+			return err
+		}
 		err = expio.WriteResult(e)
 		if err != nil {
 			return err
@@ -136,3 +141,35 @@ func (e *Experiment) incrementNumCompletedTasks() error {
 	e.Result.NumCompletedTasks++
 	return nil
 }
+
+// getIf returns the condition (if any) which determine
+// whether of not if this task needs to run
+func getIf(t base.Task) *string {
+	var jsonBytes []byte
+	var tm base.TaskMeta
+	// convert t to jsonBytes
+	jsonBytes, _ = json.Marshal(t)
+	// convert jsonBytes to TaskMeta
+	_ = json.Unmarshal(jsonBytes, &tm)
+	return tm.If
+}
+
+// getName returns the name of this task
+func getName(t base.Task) *string {
+	var jsonBytes []byte
+	var tm base.TaskMeta
+	// convert t to jsonBytes
+	jsonBytes, _ = json.Marshal(t)
+	// convert jsonBytes to TaskMeta
+	_ = json.Unmarshal(jsonBytes, &tm)
+
+	if tm.Task == nil {
+		if tm.Run != nil {
+			return base.StringPointer(base.RunTaskName)
+		}
+	} else {
+		return tm.Task
+	}
+	log.Logger.Error("task spec with no name or run value")
+	return nil
+}
diff --git a/basecli/textreport.tpl b/basecli/textreport.tpl
new file mode 100644
index 00000000..271f0b1d
--- /dev/null
+++ b/basecli/textreport.tpl
@@ -0,0 +1,18 @@
+
+Experiment summary:
+*******************
+
+  Experiment completed: {{ .Completed }}
+  No failed tasks: {{ .NoFailure }}
+  Total number of tasks: {{ len .Tasks }}
+  Number of completed tasks: {{ .Result.NumCompletedTasks }}
+
+Whether or not service level objectives (SLOs) are satisfied:
+*************************************************************
+
+{{ .PrintSLOsText | indent 2 }}
+
+Latest observed values for metrics:
+***********************************
+
+{{ .PrintMetricsText | indent 2 }}
\ No newline at end of file
diff --git a/basecli/texttemplate.go b/basecli/texttemplate.go
index e3fd55db..61e12353 100644
--- a/basecli/texttemplate.go
+++ b/basecli/texttemplate.go
@@ -3,208 +3,110 @@ package basecli
 import (
 	"bytes"
 	"fmt"
-	"sort"
-	"strconv"
 	"text/tabwriter"
 
-	"github.com/iter8-tools/iter8/base"
+	_ "embed"
+
+	"github.com/iter8-tools/iter8/base/log"
 )
 
-// formatText provides a text description of the experiment
-func formatText(e *Experiment) string {
+// reportText is the text report template
+//go:embed textreport.tpl
+var reportText string
+
+// PrintSLOsText returns SLOs in text report format
+func (e *Experiment) PrintSLOsText() string {
 	var b bytes.Buffer
-	w := tabwriter.NewWriter(&b, 0, 0, 1, ' ', tabwriter.AlignRight|tabwriter.Debug)
-	e.printState(w)
-	if e.ContainsInsight(base.InsightTypeSLO) {
-		if e.printableSLOs() {
-			e.printSLOs(w)
-		} else {
-			e.printNoSLOs(w)
-		}
-	}
-	if e.ContainsInsight(base.InsightTypeMetrics) {
-		if e.printableMetrics() {
-			e.printMetrics(w)
-		} else {
-			e.printNoMetrics(w)
-		}
-	}
+	w := tabwriter.NewWriter(&b, 0, 0, 1, ' ', tabwriter.Debug)
+	e.printSLOsText(w)
 	return b.String()
 }
 
-// number of completed tasks in the experiment
-func (e *Experiment) numCompletedTasksString() string {
-	if e == nil || e.Result == nil {
-		return "unknown"
-	} else {
-		return fmt.Sprint(e.Result.NumCompletedTasks)
+func (e *Experiment) getSLOStrText(i int) (string, error) {
+	in := e.Result.Insights
+	slo := in.SLOs[i]
+	// get metric with units and description
+	str, err := e.MetricWithUnits(slo.Metric)
+	if err != nil {
+		return "", err
 	}
-}
-
-// print the current state of the experiment
-func (e *Experiment) printState(w *tabwriter.Writer) {
-	fmt.Fprintln(w, "")
-	fmt.Fprintln(w, "-----------------------------\t-----")
-	fmt.Fprintln(w, "Experiment summary\t")
-	fmt.Fprintln(w, "-----------------------------\t-----")
-	fmt.Fprintln(w, "Experiment completed \t"+strconv.FormatBool(e.Completed()))
-	fmt.Fprintln(w, "-----------------------------\t-----")
-	fmt.Fprintln(w, "Experiment failed \t"+strconv.FormatBool(!e.NoFailure()))
-	fmt.Fprintln(w, "-----------------------------\t-----")
-	fmt.Fprintln(w, "Number of completed tasks \t"+e.numCompletedTasksString())
-	fmt.Fprintln(w, "-----------------------------\t-----")
-	fmt.Fprintln(w, "")
-	w.Flush()
-}
-
-// are SLOs in a printable condition in this experiment
-func (e *Experiment) printableSLOs() bool {
-	if e != nil {
-		if e.Result != nil {
-			if e.Result.Insights != nil {
-				if len(e.Result.Insights.SLOs) > 0 {
-					if e.Result.Insights.NumVersions > 0 {
-						if len(e.Result.Insights.SLOsSatisfied) == len(e.Result.Insights.SLOs) {
-							if e.Result.Insights.SLOsSatisfied[0] != nil {
-								if len(e.Result.Insights.SLOsSatisfied[0]) == e.Result.Insights.NumVersions {
-									return true
-								}
-							}
-						}
-					}
-				}
-			}
-		}
+	// add lower limit if needed
+	if slo.LowerLimit != nil {
+		str = fmt.Sprintf("%v <= %v", *slo.LowerLimit, str)
+	}
+	// add upper limit if needed
+	if slo.UpperLimit != nil {
+		str = fmt.Sprintf("%v <= %v", str, *slo.UpperLimit)
 	}
-	return false
+	return str, nil
 }
 
-// print SLOs
-func (e *Experiment) printSLOs(w *tabwriter.Writer) {
+// printSLOsText prints SLOs into tab writer
+func (e *Experiment) printSLOsText(w *tabwriter.Writer) {
 	in := e.Result.Insights
-	fmt.Fprint(w, "\n\n")
-	fmt.Fprintln(w, "-----------------------------\t-----")
-	fmt.Fprint(w, "SLOs")
+	fmt.Fprint(w, "SLO Conditions")
 	if in.NumVersions > 1 {
 		for i := 0; i < in.NumVersions; i++ {
 			fmt.Fprintf(w, "\t version %v", i)
 		}
 	} else {
-		fmt.Fprintf(w, "\t")
+		fmt.Fprintf(w, "\tSatisfied")
 	}
 	fmt.Fprintln(w)
-	fmt.Fprintln(w, "-----------------------------\t-----")
+	fmt.Fprintln(w, "--------------\t---------")
 
 	for i := 0; i < len(in.SLOs); i++ {
-		str, _, err := getMetricWithUnitsAndDescription(in, in.SLOs[i].Metric)
+		str, err := e.getSLOStrText(i)
 		if err == nil {
 			fmt.Fprint(w, str)
 			for j := 0; j < in.NumVersions; j++ {
 				fmt.Fprintf(w, "\t%v", in.SLOsSatisfied[i][j])
 				fmt.Fprintln(w)
 			}
-			fmt.Fprintln(w, "-----------------------------\t-----")
 		}
 	}
 
 	w.Flush()
 }
 
-// print no SLOs
-func (e *Experiment) printNoSLOs(w *tabwriter.Writer) {
-	fmt.Fprint(w, "\n\n")
-	fmt.Fprintln(w, "-----------------------------\t-----")
-	fmt.Fprint(w, "SLOs\tunavailable")
-	fmt.Fprintln(w)
-	fmt.Fprintln(w, "-----------------------------\t-----")
-
-	w.Flush()
-}
-
-// are metrics in a printable condition in this experiment
-func (e *Experiment) printableMetrics() bool {
-	if e != nil {
-		if e.Result != nil {
-			if e.Result.Insights != nil {
-				if e.Result.Insights.MetricsInfo != nil && len(e.Result.Insights.MetricsInfo) > 0 {
-					if e.Result.Insights.NumVersions > 0 {
-						if len(e.Result.Insights.MetricValues) > 0 {
-							if e.Result.Insights.MetricValues[0] != nil {
-								return true
-							}
-						}
-					}
-				}
-			}
-		}
-	}
-	return false
+// PrintMetricsText returns metrics in text report format
+func (e *Experiment) PrintMetricsText() string {
+	var b bytes.Buffer
+	w := tabwriter.NewWriter(&b, 0, 0, 1, ' ', tabwriter.Debug)
+	e.printMetricsText(w)
+	return b.String()
 }
 
-// print metrics collected
-func (e *Experiment) printMetrics(w *tabwriter.Writer) {
+// printMetricsText prints metrics into tab writer
+func (e *Experiment) printMetricsText(w *tabwriter.Writer) {
 	in := e.Result.Insights
-	fmt.Fprint(w, "\n\n")
-	fmt.Fprintln(w, "-----------------------------\t-----")
-	fmt.Fprint(w, "Metrics")
+	fmt.Fprint(w, "Metric")
 	if in.NumVersions > 1 {
 		for i := 0; i < in.NumVersions; i++ {
-			fmt.Fprintf(w, "\t version %v", i)
+			fmt.Fprintf(w, "\tversion %v", i)
 		}
 	} else {
-		fmt.Fprintf(w, "\t")
+		fmt.Fprintf(w, "\tvalue")
 	}
 	fmt.Fprintln(w)
-	fmt.Fprintln(w, "-----------------------------\t-----")
+	fmt.Fprintln(w, "-------\t-----")
 
-	// sort metrics
-	keys := []string{}
-	for k := range in.MetricsInfo {
-		keys = append(keys, k)
-	}
-	sort.Strings(keys)
-
-	for i := 0; i < len(keys); i++ {
-		u := ""
-		// add units if available
-		units := e.Result.Insights.MetricsInfo[keys[i]].Units
-		if units != nil {
-			u += " (" + *units + ")"
-		}
+	// keys contain normalized scalar metric names in sorted order
+	keys := e.SortedScalarMetrics()
 
-		fmt.Fprint(w, keys[i], u)
-		for j := 0; j < in.NumVersions; j++ {
-			fmt.Fprintf(w, "\t%v", e.getMetricValue(keys[i], j))
+	for _, mn := range keys {
+		mwu, err := e.MetricWithUnits(mn)
+		if err == nil {
+			// add metric name with units
+			fmt.Fprint(w, mwu)
+			// add value
+			for j := 0; j < in.NumVersions; j++ {
+				fmt.Fprintf(w, "\t%v", e.ScalarMetricValueStr(j, mn))
+			}
 			fmt.Fprintln(w)
+		} else {
+			log.Logger.Error(err)
 		}
-		fmt.Fprintln(w, "-----------------------------\t-----")
 	}
 	w.Flush()
 }
-
-// print no metrics
-func (e *Experiment) printNoMetrics(w *tabwriter.Writer) {
-	fmt.Fprint(w, "\n\n")
-	fmt.Fprintln(w, "-----------------------------\t-----")
-	fmt.Fprint(w, "Metrics\tunavailable")
-	fmt.Fprintln(w)
-	fmt.Fprintln(w, "-----------------------------\t-----")
-
-	w.Flush()
-}
-
-// get value of the metric
-func (e *Experiment) getMetricValue(m string, j int) string {
-	vals := e.Result.Insights.MetricValues[j][m]
-	if len(vals) == 0 {
-		return "unavailable"
-	}
-	// get the latest observed value for this metric/version pair
-	floatVal := vals[len(vals)-1]
-	val := fmt.Sprint(floatVal)
-	// if the floatVal is not integral, take two decimal places
-	if floatVal != float64(int(floatVal)) {
-		val = fmt.Sprintf("%0.2f", floatVal)
-	}
-	return val
-}
diff --git a/cmd/kubernetes.go b/cmd/kubernetes.go
index 895bcc59..2508336f 100644
--- a/cmd/kubernetes.go
+++ b/cmd/kubernetes.go
@@ -180,7 +180,7 @@ type KubernetesExpIO struct {
 }
 
 // read experiment spec from secret in the Kubernetes context
-func (f *KubernetesExpIO) ReadSpec() ([]base.TaskSpec, error) {
+func (f *KubernetesExpIO) ReadSpec() (base.ExperimentSpec, error) {
 
 	s, err := getSecretWithRetry(f.Client, f.Namespace, f.Name)
 	if err != nil {
diff --git a/go.mod b/go.mod
index ed9fdb76..7cdc3547 100644
--- a/go.mod
+++ b/go.mod
@@ -21,14 +21,18 @@ require (
 	fortio.org/fortio v1.20.0
 	github.com/Masterminds/sprig v2.22.0+incompatible
 	github.com/antonmedv/expr v1.9.0
-	github.com/go-playground/validator/v10 v10.10.0
+	github.com/bojand/ghz v0.105.0
 	github.com/hashicorp/go-getter v1.5.10
 	github.com/jarcoal/httpmock v1.1.0
+	github.com/montanaflynn/stats v0.6.6
 	github.com/sirupsen/logrus v1.8.1
 	github.com/spf13/cobra v1.3.0
 	github.com/spf13/pflag v1.0.5
 	github.com/spf13/viper v1.10.1
 	github.com/stretchr/testify v1.7.0
+	golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d
+	google.golang.org/grpc v1.43.0
+	google.golang.org/protobuf v1.27.1
 	helm.sh/helm/v3 v3.7.2
 	k8s.io/api v0.22.4
 	k8s.io/apimachinery v0.22.4
diff --git a/go.sum b/go.sum
index 57617360..b138c143 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,7 @@
 bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw=
 cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
 cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
 cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
@@ -83,6 +84,7 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
 github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
 github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
 github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
+github.com/Djarvur/go-err113 v0.0.0-20200511133814-5174e21577d5/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs=
 github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E=
 github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
 github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
@@ -120,21 +122,29 @@ github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:m
 github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
 github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM=
 github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
 github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
 github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
 github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
 github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=
 github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
+github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
+github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
+github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
+github.com/alecthomas/kingpin v1.3.8-0.20191105203113-8c96d1c22481/go.mod h1:b6br6/pDFSfMkBgC96TbpOji05q5pa+v5rIlS0Y6XtI=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
 github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0=
+github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
 github.com/antonmedv/expr v1.9.0 h1:j4HI3NHEdgDnN9p6oI6Ndr0G5QryMY0FNxT4ONrFDGU=
 github.com/antonmedv/expr v1.9.0/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmHhwGEk8=
+github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
@@ -163,6 +173,10 @@ github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqO
 github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
 github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
+github.com/bojand/ghz v0.105.0 h1:a+o8j0lu/oySf5d24P1JlEoOtJ3Dj2WR+pMoaeURoNY=
+github.com/bojand/ghz v0.105.0/go.mod h1:l3GXrJtJB3KbuhJ3Inr4Osu2fVwIEvJ5U69I2YA1h14=
+github.com/bojand/hri v1.1.0/go.mod h1:qwGosuHpNn1S0nyw/mExN0+WZrDf4bQyWjhWh51y3VY=
+github.com/bombsimon/wsl/v3 v3.1.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc=
 github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
 github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70=
 github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
@@ -174,6 +188,7 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0Bsq
 github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o=
 github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/census-instrumentation/opencensus-proto v0.3.0 h1:t/LhUZLVitR1Ow2YOnduCsavhwFUklBMoGVYUCqmCqk=
 github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
 github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
@@ -200,12 +215,14 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
 github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 h1:hzAQntlaYRkVSFEfj9OTWlVV1H155FMD8BTKktLv0QI=
 github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
 github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 h1:KwaoQzs/WeUxxJqiJsZ4euOly1Az/IgZXXSxlD/UBNk=
 github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
 github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo=
@@ -326,11 +343,14 @@ github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1S
 github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s=
 github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8=
 github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I=
+github.com/daixiang0/gci v0.2.4/go.mod h1:+AV8KmHTGxxwp/pY84TLQfFKp2vuKXXJVzF3kD/hfR4=
 github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE=
+github.com/denis-tingajkin/go-header v0.3.1/go.mod h1:sq/2IxMhaZX+RRcgHfCRx/m0M5na0fBt4/CRe7Lrji0=
+github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM=
 github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
 github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=
 github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
@@ -364,7 +384,11 @@ github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNE
 github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
 github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
+github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
+github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
 github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
 github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
 github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
@@ -376,9 +400,12 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m
 github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
 github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
 github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
+github.com/envoyproxy/go-control-plane v0.10.1 h1:cgDRLG7bs59Zd+apAWuzLQL95obVYAymNJek76W3mgw=
 github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/envoyproxy/protoc-gen-validate v0.6.2 h1:JiO+kJTpmYGjEodY7O1Zk8oZcNz1+f30UtwtXoFUPzE=
 github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=
+github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
 github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/evanphx/json-patch v4.11.0+incompatible h1:glyUF9yIYtMHzn8xaKw5rMhdWcwsYV8dZHIq5567/xs=
 github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
@@ -406,6 +433,7 @@ github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebK
 github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
 github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-critic/go-critic v0.5.2/go.mod h1:cc0+HvdE3lFpqLecgqMaJcvWWH77sLdBp+wLGPM1Yyo=
 github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
 github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
@@ -422,6 +450,7 @@ github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7
 github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
 github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc=
 github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
+github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
 github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
 github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
 github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
@@ -435,16 +464,24 @@ github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh
 github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
 github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng=
 github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
-github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
-github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
-github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
-github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
-github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
-github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
-github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0=
-github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
+github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
+github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
+github.com/go-playground/validator v9.30.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig=
+github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
 github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4=
+github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ=
+github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
+github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw=
+github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU=
+github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI=
+github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc=
+github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
+github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
+github.com/go-toolsmith/typep v1.0.2/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
+github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
 github.com/gobuffalo/logger v1.0.3/go.mod h1:SoeejUwldiS7ZsyCBphOGURmWdwUFXs0J7TCjEhjKxM=
 github.com/gobuffalo/packd v1.0.0/go.mod h1:6VTc4htmJRFB7u1m/4LeMTWjFoYrUiBkU9Fdec9hrhI=
 github.com/gobuffalo/packr/v2 v2.8.1/go.mod h1:c/PLlOuTU+p3SybaJATW3H6lX/iK7xEz5OeMf+NnJpg=
@@ -456,10 +493,12 @@ github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6
 github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/godror/godror v0.24.2/go.mod h1:wZv/9vPiUib6tkoDl+AZ/QLf5YZgMravZ7jxH2eQWAE=
+github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
 github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
 github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU=
 github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
 github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
 github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
@@ -502,7 +541,23 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
 github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
 github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4=
+github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk=
+github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0=
+github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8=
+github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o=
+github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU=
+github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU=
+github.com/golangci/golangci-lint v1.31.0/go.mod h1:aMQuNCA+NDU5+4jLL5pEuFHoue0IznKE2+/GsFvvs8A=
+github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc/go.mod h1:e5tpTHCfVze+7EpLEozzMB3eafxo2KT5veNg1k6byQU=
+github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg=
+github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o=
+github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA=
+github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bzsHdTM0bsB7+8mt0GUMvjCgwLpTapNZHU8AajI=
+github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4=
+github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ=
 github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//k/eakGydO4jKRoRL2j92ZKSzTgj9tclaCrvXHk=
 github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k=
 github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=
@@ -563,16 +618,22 @@ github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3i
 github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU=
 github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=
 github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA=
+github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU=
+github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
 github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
 github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
 github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
+github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
 github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
 github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
 github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
 github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
 github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
 github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
+github.com/gostaticanalysis/analysisutil v0.0.3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
 github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
 github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM=
 github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
@@ -649,10 +710,21 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt
 github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA=
 github.com/jarcoal/httpmock v1.1.0 h1:F47ChZj1Y2zFsCXxNkBPwNNKnAyOATcdQibk0qEdVCE=
 github.com/jarcoal/httpmock v1.1.0/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
+github.com/jhump/protoreflect v1.9.0 h1:npqHz788dryJiR/l6K/RUQAyh2SwV91+d1dnh4RjO9w=
+github.com/jhump/protoreflect v1.9.0/go.mod h1:7GcYQDdMU/O/BBrl/cX6PNHpXh6cenjd8pneu5yW7Tg=
+github.com/jingyugao/rowserrcheck v0.0.0-20191204022205-72ab7603b68a/go.mod h1:xRskid8CManxVta/ALEhJha/pweKBaVG6fWgc0yH25s=
+github.com/jinzhu/configor v1.1.1 h1:gntDP+ffGhs7aJ0u8JvjCDts2OsxsI7bnz3q+jC+hSY=
+github.com/jinzhu/configor v1.1.1/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc=
+github.com/jinzhu/gorm v1.9.11/go.mod h1:bu/pK8szGZ2puuErfU0RwyeNdsf3e6nCX/noXaVxkfw=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/jirfag/go-printf-func-name v0.0.0-20191110105641-45db9963cdd3/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0=
 github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc=
 github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
+github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
+github.com/jmoiron/sqlx v1.2.1-0.20190826204134-d7d95172beb5/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
 github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
 github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
@@ -676,6 +748,8 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW
 github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
 github.com/klauspost/compress v1.11.2/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
 github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
 github.com/klauspost/compress v1.11.13 h1:eSvu8Tmq6j2psUJqJrLcWH6K3w5Dwc+qipbaA6eVEN4=
@@ -688,23 +762,27 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
 github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
-github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
-github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kyoh86/exportloopref v0.1.7/go.mod h1:h1rDl2Kdj97+Kwh4gdz3ujE7XHmH51Q0lUiZ1z4NLj8=
+github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
+github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
 github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
 github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
-github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
-github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
+github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
+github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
 github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
 github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc=
+github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
 github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
 github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
 github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=
@@ -717,18 +795,23 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN
 github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
 github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
 github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/maratori/testpackage v1.0.1/go.mod h1:ddKdw+XG0Phzhx8BFDTKgpWP4i7MpApTE5fXSKAqwDU=
 github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc=
 github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI=
 github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
 github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho=
+github.com/matoous/godox v0.0.0-20190911065817-5d6d842e92eb/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
 github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
 github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
 github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
 github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
 github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
 github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
 github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
 github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
 github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
 github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
@@ -741,10 +824,14 @@ github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
 github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
 github.com/mattn/go-shellwords v1.0.11/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
+github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
 github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
+github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
 github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
+github.com/mfridman/tparse v0.8.3/go.mod h1:LzZWLkqcQrOfgvqZn7LOSBzgZwWnqI5NQsfgQVOT1o8=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
 github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
@@ -759,6 +846,7 @@ github.com/mitchellh/copystructure v1.1.1/go.mod h1:EBArHfARyrSWO/+Wyr9zwEkc6XMF
 github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
 github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
 github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
 github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
@@ -791,16 +879,23 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0=
 github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4=
+github.com/montanaflynn/stats v0.6.6 h1:Duep6KMIDpY4Yo11iFsvyqJDyfzLF9+sndUKT+v64GQ=
+github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
 github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
 github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/mozilla/tls-observatory v0.0.0-20200317151703-4fa42e1c2dee/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
 github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
 github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
+github.com/nakabonne/nestif v0.3.0/go.mod h1:dI314BppzXjJ4HsCnbo7XzrJHPszZsjnk5wEBSYHI2c=
+github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
 github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/nishanths/exhaustive v0.0.0-20200811152831-6cf413ae40e0/go.mod h1:wBEpHwM2OdmeNpdCvRPUlkEbBuaFmcK4Wv8Q7FuGW3c=
+github.com/nishanths/predeclared v0.0.0-20200524104333-86fad755b4d3/go.mod h1:nt3d53pc1VYcphSCIaYAJtnPYnr3Zyn8fMq2wvPGPso=
 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
 github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
 github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
@@ -809,13 +904,16 @@ github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6
 github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0=
 github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
 github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
 github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
+github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
@@ -847,8 +945,10 @@ github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqi
 github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo=
 github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8=
 github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
 github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
@@ -856,9 +956,10 @@ github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhEC
 github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
 github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
 github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
+github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d/go.mod h1:3OzsM7FXDQlpCiw2j81fOmAwQLnZnLGXVKUzeKQXIAw=
 github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
 github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
-github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -873,6 +974,7 @@ github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSg
 github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
 github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
 github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
 github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
 github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
@@ -882,12 +984,14 @@ github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf
 github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
 github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
 github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
 github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
@@ -897,6 +1001,7 @@ github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzz
 github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
 github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
@@ -908,33 +1013,45 @@ github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
 github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
 github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
+github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI=
+github.com/quasilyte/go-ruleguard v0.2.0/go.mod h1:2RT/tf0Ce0UDj5y243iWKosQogJd8+1G3Rs2fxmlYnw=
+github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0=
+github.com/rakyll/statik v0.1.6/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs=
+github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/rivo/tview v0.0.0-20200219210816-cd38d7432498/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84=
 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
-github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
-github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
-github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
+github.com/rogpeppe/go-internal v1.6.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 github.com/rubenv/sql-migrate v0.0.0-20210614095031-55d5740dbbcc/go.mod h1:HFLT6i9iR4QBOF5rdCyjddC9t59ArqWJV2xx+jwcCMo=
 github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/ryancurrah/gomodguard v1.1.0/go.mod h1:4O8tr7hBODaGE6VIhfJDHcwzh5GUccKSJBU0UMXJFVM=
+github.com/ryanrolds/sqlclosecheck v0.3.0/go.mod h1:1gREqxyTGR3lVtpngyFo3hZAgk0KCtEdgEkHwDbigdA=
 github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
 github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
 github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
 github.com/sagikazarmark/crypt v0.4.0/go.mod h1:ALv2SRj7GxYV4HO9elxH9nS6M9gW+xDNxqmyJ6RfDFM=
 github.com/sanity-io/litter v1.2.0/go.mod h1:JF6pZUFgu2Q0sBZ+HSV35P8TVPI1TTzEwyu9FXAw2W4=
 github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
+github.com/securego/gosec/v2 v2.4.0/go.mod h1:0/Q4cjmlFDfDUj1+Fib61sc+U5IQb2w+Iv9/C3wPVko=
 github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
 github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
+github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c/go.mod h1:/PevMnwAxekIXwN8qQyfc5gl2NlkB3CQlkizAbOkeBs=
+github.com/shirou/gopsutil v0.0.0-20190901111213-e4ec7b275ada/go.mod h1:WWnYX4lzhCH5h/3YBfyVA3VbLYjlMZZAQcW9ojMexNc=
+github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
 github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
 github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
+github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
 github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
@@ -950,6 +1067,8 @@ github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:s
 github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
 github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
 github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
+github.com/sonatard/noctx v0.0.1/go.mod h1:9D2D/EoULe8Yy2joDHJj7bv3sZoq9AaSb8B4lqBjiZI=
+github.com/sourcegraph/go-diff v0.6.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs=
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
 github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
 github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
@@ -979,10 +1098,12 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
 github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
+github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
 github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
 github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM=
 github.com/spf13/viper v1.10.1 h1:nuJZuYpG7gTj/XqiUwg8bA0cp1+M2mC3J4g5luUYBKk=
 github.com/spf13/viper v1.10.1/go.mod h1:IGlFPqhNAPKRxohIzWpI5QEy4kuI7tcl5WvR+8qy1rU=
+github.com/ssgreg/nlreturn/v2 v2.1.0/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I=
 github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8=
 github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
 github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -1005,17 +1126,29 @@ github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG
 github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
 github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
 github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I=
+github.com/tdakkota/asciicheck v0.0.0-20200416190851-d7f85be797a2/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM=
+github.com/tetafro/godot v0.4.8/go.mod h1:/7NLHhv08H1+8DNj0MElpAACw1ajsCuf3TKNQxA5S+0=
+github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/tommy-muehle/go-mnd v1.3.1-0.20200224220436-e6f9a994e8fa/go.mod h1:dSUh0FtTP8VhvkL1S+gUR1OKd9ZnSaozuI6r3m6wOig=
 github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
 github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
 github.com/ulikunitz/xz v0.5.8 h1:ERv8V6GKqVi23rgu5cj9pVfVzJbOqAY2Ntl88O6c2nQ=
 github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
+github.com/ultraware/funlen v0.0.3/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA=
+github.com/ultraware/whitespace v0.0.4/go.mod h1:aVMh/gQve5Maj9hQ/hg+F75lr/X5A89uZnzAmWSineA=
 github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
 github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
 github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
+github.com/uudashr/gocognit v1.0.1/go.mod h1:j44Ayx2KW4+oB6SWMv8KsmHzZrOInQav7D3cQMJ5JUM=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasthttp v1.15.1/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA=
+github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
+github.com/valyala/quicktemplate v1.6.2/go.mod h1:mtEJpQtUiBV0SHhMX6RtiJtqxncgrfmjcUy5T68X8TM=
+github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
 github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
 github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
 github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
@@ -1063,6 +1196,7 @@ go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3M
 go.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc=
 go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4=
 go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
+go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
 go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
 go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -1087,11 +1221,17 @@ go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 h1:+FNtrFTmVw0YZGpBGX56XDee33
 go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o=
 go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
 go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
+go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
 go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
+go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
 go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
 go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
 golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -1116,7 +1256,6 @@ golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
 golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
-golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
 golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1164,6 +1303,7 @@ golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73r
 golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -1192,6 +1332,7 @@ golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/
 golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
@@ -1249,6 +1390,7 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1267,6 +1409,7 @@ golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1301,6 +1444,7 @@ golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1341,6 +1485,7 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210820121016-41cdb8703e55/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1372,13 +1517,19 @@ golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxb
 golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs=
 golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190221204921-83362c3779f5/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
@@ -1388,10 +1539,14 @@ golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgw
 golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190719005602-e377ae9d6386/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
 golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -1402,6 +1557,7 @@ golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtn
 golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200117220505-0cba7a3a9ee9/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
@@ -1412,15 +1568,27 @@ golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapK
 golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
 golang.org/x/tools v0.0.0-20200308013534-11ec41452d41/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
 golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200321224714-0d839f3cf2ed/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
+golang.org/x/tools v0.0.0-20200324003944-a576cf524670/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
 golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
+golang.org/x/tools v0.0.0-20200414032229-332987a829c3/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200422022333-3d57cf2e726e/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200519015757-0d0afa43d58a/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200625211823-6506e20df31f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200701041122-1837592efa10/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200812195022-5ae4c3c160a0/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
 golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
@@ -1440,6 +1608,7 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
+google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
 google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
@@ -1486,6 +1655,7 @@ google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID
 google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@@ -1554,6 +1724,7 @@ google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ6
 google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa h1:I0YcKz0I7OAhddo7ya8kMnvprhcWM045PmkBdMO9zN0=
 google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
 google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
+google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -1597,6 +1768,7 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.25.1-0.20200805231151-a709e31e5d12/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
@@ -1615,6 +1787,7 @@ gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qS
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
+gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
 gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw=
 gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
@@ -1649,6 +1822,7 @@ gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
 gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
 helm.sh/helm/v3 v3.7.2 h1:xn1OxcZEpgKpp4CCpPz1KKUyb9gAtTouXV2E3S8ChYQ=
 helm.sh/helm/v3 v3.7.2/go.mod h1:UXuiAn0+FfBpqbiMuwWt8/aAKkfJvnWLBJ6f4HcFs0M=
+honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@@ -1656,6 +1830,7 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.0.1-2020.1.5/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo=
 k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ=
 k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8=
@@ -1706,6 +1881,10 @@ k8s.io/metrics v0.22.4/go.mod h1:6F/iwuYb1w2QDCoHkeMFLf4pwHBcYKLm4mPtVHKYrIw=
 k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
 k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a h1:8dYfu/Fc9Gz2rNJKB9IQRGgQOh2clmRzNIPPY1xLY5g=
 k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
+mvdan.cc/gofumpt v0.0.0-20200709182408-4fd085cb6d5f/go.mod h1:9VQ397fNXEnF84t90W4r4TRCQK+pg9f8ugVfyj+S26w=
+mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc=
+mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
+mvdan.cc/unparam v0.0.0-20190720180237-d51796306d8f/go.mod h1:4G1h5nDURzA3bwVMZIVpwbkw+04kSxk3rAtzlimaUJw=
 oras.land/oras-go v0.4.0 h1:u6+7D+raZDYHwlz/uOwNANiRmyYDSSMW7A9E1xXycUQ=
 oras.land/oras-go v0.4.0/go.mod h1:VJcU+VE4rkclUbum5C0O7deEZbBYnsnpbGSACwTjOcg=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
diff --git a/mkdocs/docs/getting-started/concepts.md b/mkdocs/docs/getting-started/concepts.md
index 31a1e89c..195785ce 100644
--- a/mkdocs/docs/getting-started/concepts.md
+++ b/mkdocs/docs/getting-started/concepts.md
@@ -38,7 +38,7 @@ Experiments are specified using an `experiment.yaml` file as shown below.
 # and validates error-rate and 95th percentile service level objectives (SLOs)
 # 
 # task 1: generate requests for the app and collect built-in metrics
-- task: gen-load-and-collect-metrics
+- task: gen-load-and-collect-metrics-http
   with:
     versionInfo:
     - url: https://example.com
diff --git a/mkdocs/docs/getting-started/your-first-experiment.md b/mkdocs/docs/getting-started/your-first-experiment.md
index c7ff502e..b0c6a98d 100644
--- a/mkdocs/docs/getting-started/your-first-experiment.md
+++ b/mkdocs/docs/getting-started/your-first-experiment.md
@@ -49,12 +49,12 @@ iter8 run --set url=https://example.com \
 The `iter8 run` command combines an experiment chart with the supplied values to generate the `experiment.yaml` file, runs the experiment, and writes results into the `result.yaml` file.
 
 ??? note "Look inside experiment.yaml"
-    This experiment contains the [`gen-load-and-collect-metrics` task](../user-guide/tasks/collect.md) for generating load and collecting metrics, and the [`assess-app-versions` task](../user-guide/tasks/assess.md) for validating SLOs.
+    This experiment contains the [`gen-load-and-collect-metrics-http` task](../user-guide/tasks/collect.md) for generating load and collecting metrics, and the [`assess-app-versions` task](../user-guide/tasks/assess.md) for validating SLOs.
 
     ```yaml
     # task 1: generate HTTP requests for application URL
     # collect Iter8's built-in latency and error-related metrics
-    - task: gen-load-and-collect-metrics
+    - task: gen-load-and-collect-metrics-http
       with:
         percentiles: 
         - 90
@@ -80,8 +80,8 @@ The `iter8 run` command combines an experiment chart with the supplied values to
 
     ```shell
     INFO[2021-12-14 10:23:26] starting experiment run                      
-    INFO[2021-12-14 10:23:26] task 1: gen-load-and-collect-metrics : started 
-    INFO[2021-12-14 10:23:39] task 1: gen-load-and-collect-metrics : completed 
+    INFO[2021-12-14 10:23:26] task 1: gen-load-and-collect-metrics-http : started 
+    INFO[2021-12-14 10:23:39] task 1: gen-load-and-collect-metrics-http : completed 
     INFO[2021-12-14 10:23:39] task 2: assess-app-versions : started        
     INFO[2021-12-14 10:23:39] task 2: assess-app-versions : completed      
     INFO[2021-12-14 10:23:39] experiment completed successfully    
diff --git a/mkdocs/docs/user-guide/tasks/assess.md b/mkdocs/docs/user-guide/tasks/assess.md
index d355f346..f768ef6a 100644
--- a/mkdocs/docs/user-guide/tasks/assess.md
+++ b/mkdocs/docs/user-guide/tasks/assess.md
@@ -5,7 +5,7 @@ template: main.html
 # `assess-app-versions`
 This task assesses if app versions satisfy service level objectives (SLOs). SLO inputs are specified in the form of metrics along with acceptable upper and lower limits on their values.
 
-This task should be preceded in the experiment spec by other tasks that collect metrics such as the [`gen-load-and-collect-metrics` task](collect.md).
+This task should be preceded in the experiment spec by other tasks that collect metrics such as the [`gen-load-and-collect-metrics-http` task](collect.md).
 
 ## Example
 Validate service level objectives (SLOs) for app versions based on [Iter8's built-in metrics](collect.md).
diff --git a/mkdocs/docs/user-guide/tasks/collect.md b/mkdocs/docs/user-guide/tasks/collect.md
index 19892121..c6ffa46d 100644
--- a/mkdocs/docs/user-guide/tasks/collect.md
+++ b/mkdocs/docs/user-guide/tasks/collect.md
@@ -2,13 +2,13 @@
 template: main.html
 ---
 
-# `gen-load-and-collect-metrics`
-The `gen-load-and-collect-metrics` task enables collection of [Iter8's built-in metrics](#built-in-metrics). It generates a stream of HTTP GET or POST requests to one or more app versions, and collects latency and error-related metrics.
+# `gen-load-and-collect-metrics-http`
+The `gen-load-and-collect-metrics-http` task enables collection of [Iter8's built-in metrics](#built-in-metrics). It generates a stream of HTTP GET or POST requests to one or more app versions, and collects latency and error-related metrics.
 
 ## Examples
 Generate load and collect built-in metrics for an app.
 ```yaml
-- task: gen-load-and-collect-metrics
+- task: gen-load-and-collect-metrics-http
   with:
     versionInfo:
     - url: https://example.com
@@ -16,7 +16,7 @@ Generate load and collect built-in metrics for an app.
 
 Customize this task using various inputs.
 ```yaml
-- task: gen-load-and-collect-metrics
+- task: gen-load-and-collect-metrics-http
   with:
     # Number of requests sent to each version.
     numQueries: 200
@@ -63,7 +63,7 @@ Customize this task using various inputs.
 
 Generate load and collect built-in metrics for two versions of an app.
 ```yaml
-- task: gen-load-and-collect-metrics
+- task: gen-load-and-collect-metrics-http
   with:
     versionInfo:
     - url: http://iter8-app.default.svc:8000
diff --git a/mkdocs/docs/user-guide/topics/versionnumbering.md b/mkdocs/docs/user-guide/topics/versionnumbering.md
index 5b5de8c9..4c070ee1 100644
--- a/mkdocs/docs/user-guide/topics/versionnumbering.md
+++ b/mkdocs/docs/user-guide/topics/versionnumbering.md
@@ -6,6 +6,6 @@ template: main.html
 
 Iter8 can observe metrics for multiple versions of an app during an experiment. The number of versions equals the length of the `versionInfo` input field of the following task.
 
-* [`gen-load-and-collect-metrics`](../tasks/collect.md)
+* [`gen-load-and-collect-metrics-http`](../tasks/collect.md)
 
 If there are `n` versions, they are numbered `0, ..., n-1`.
\ No newline at end of file
diff --git a/mkdocs/mkdocs.yml b/mkdocs/mkdocs.yml
index acbac819..80b7b81d 100644
--- a/mkdocs/mkdocs.yml
+++ b/mkdocs/mkdocs.yml
@@ -158,7 +158,7 @@ nav:
     - iter8 k logs: user-guide/commands/iter8_k_logs.md
     - iter8 k delete: user-guide/commands/iter8_k_delete.md
   - Tasks:
-    - gen-load-and-collect-metrics: user-guide/tasks/collect.md
+    - gen-load-and-collect-metrics-http: user-guide/tasks/collect.md
     - assess-app-versions: user-guide/tasks/assess.md
     - run (a bash script): user-guide/tasks/run.md
 - Contributing: contributing.md


Test patch:
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index f2fe26a5..421327b4 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -23,10 +23,10 @@ jobs:
       run: |
         export COVERAGE=$(go tool cover -func coverage.out | grep total | awk '{print substr($3, 1, length($3)-1)}')
         echo "code coverage is at ${COVERAGE}"
-        if [ 1 -eq "$(echo "${COVERAGE} > 68.0" | bc)" ]; then \
-          echo "all good... coverage is above 68.0%"; 
+        if [ 1 -eq "$(echo "${COVERAGE} > 69.0" | bc)" ]; then \
+          echo "all good... coverage is above 69.0%"; 
         else \
-          echo "not good... coverage is not above 68.0%";
+          echo "not good... coverage is not above 69.0%";
           exit 1
         fi
     - name: Upload coverage to Codecov
diff --git a/base/assess_test.go b/base/assess_test.go
index 9f9a3265..e3ca3fa8 100644
--- a/base/assess_test.go
+++ b/base/assess_test.go
@@ -6,85 +6,33 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func TestMakeWrongTask(t *testing.T) {
-	ts := &TaskSpec{
-		taskMeta: taskMeta{
-			Task: StringPointer(CollectTaskName),
-		},
-		With: map[string]interface{}{
-			"hello": "world",
-		},
-	}
-	task, err := MakeAssess(ts)
-	assert.Error(t, err)
-	assert.Nil(t, task)
-}
-
-func TestMakeAssess(t *testing.T) {
-	// simple assess without any SLOs
-	// should succeed
-	ts := &TaskSpec{
-		taskMeta: taskMeta{
-			Task: StringPointer(AssessTaskName),
-		},
-	}
-	task, err := MakeAssess(ts)
-	assert.NoError(t, err)
-	assert.NotNil(t, task)
-
-	// incorrect with clause
-	// should fail
-	ts = &TaskSpec{
-		taskMeta: taskMeta{Task: StringPointer(AssessTaskName)},
-		With: map[string]interface{}{
-			"SLOs": "hello world",
-		},
-	}
-	task, err = MakeAssess(ts)
-	assert.Error(t, err)
-	assert.Nil(t, task)
-}
-
 // Test a runnable assert condition here
 func TestRunAssess(t *testing.T) {
 	// simple assess without any SLOs
 	// should succeed
-	ts := &TaskSpec{
-		taskMeta: taskMeta{
+	task := &assessTask{
+		TaskMeta: TaskMeta{
 			Task: StringPointer(AssessTaskName),
 		},
+		With: assessInputs{},
 	}
-	task, _ := MakeAssess(ts)
 	exp := &Experiment{
-		Tasks: []TaskSpec{},
+		Tasks: []Task{task},
 	}
 	exp.InitResults()
-	task.Run(exp)
+	exp.Result.initInsightsWithNumVersions(1)
+	err := task.Run(exp)
+	assert.NoError(t, err)
 
 	// assess with an SLO
 	// should succeed
-	ts = &TaskSpec{
-		taskMeta: taskMeta{Task: StringPointer(AssessTaskName)},
-		With: map[string]interface{}{
-			"SLOs": []SLO{{
-				Metric:     "m",
-				UpperLimit: float64Pointer(20.0),
-			}},
-		},
-	}
-	task, _ = MakeAssess(ts)
-	exp = &Experiment{
-		Tasks:  []TaskSpec{},
-		Result: &ExperimentResult{},
+	task.With = assessInputs{
+		SLOs: []SLO{{
+			Metric:     "a/b",
+			UpperLimit: float64Pointer(20.0),
+		}},
 	}
-	exp.InitResults()
-	exp.Result.InitInsights(1, []InsightType{InsightTypeMetrics})
-	err := task.Run(exp)
-	assert.NoError(t, err)
-
-	// assess with an experiment where num versions is 1
-	exp.Result.Insights.NumVersions = 1
+	task.Run(exp)
 	err = task.Run(exp)
 	assert.NoError(t, err)
-
 }
diff --git a/base/collect_grpc_test.go b/base/collect_grpc_test.go
new file mode 100644
index 00000000..79fcf251
--- /dev/null
+++ b/base/collect_grpc_test.go
@@ -0,0 +1,138 @@
+package base
+
+import (
+	"testing"
+	"time"
+
+	"github.com/bojand/ghz/runner"
+	"github.com/iter8-tools/iter8/base/internal"
+	"github.com/iter8-tools/iter8/base/internal/helloworld/helloworld"
+	"github.com/stretchr/testify/assert"
+)
+
+// Credit: Several of the tests in this file are based on
+// https://github.com/bojand/ghz/blob/master/runner/run_test.go
+func TestRunCollectGRPCUnary(t *testing.T) {
+	callType := helloworld.Unary
+	gs, s, err := internal.StartServer(false)
+	if err != nil {
+		assert.FailNow(t, err.Error())
+	}
+	defer s.Stop()
+
+	// valid collect GRPC task... should succeed
+	ct := &collectGRPCTask{
+		TaskMeta: TaskMeta{
+			Task: StringPointer(CollectGPRCTaskName),
+		},
+		With: collectGRPCInputs{
+			Config: runner.Config{
+				N:           1,
+				C:           1,
+				Timeout:     runner.Duration(20 * time.Second),
+				Data:        map[string]interface{}{"name": "bob"},
+				DialTimeout: runner.Duration(20 * time.Second),
+			},
+			ProtoURL: StringPointer("https://raw.githubusercontent.com/bojand/ghz/v0.105.0/testdata/greeter.proto"),
+			VersionInfo: []*versionGRPC{{
+				Call: "helloworld.Greeter.SayHello",
+				Host: internal.TestLocalhost,
+			}},
+		},
+	}
+
+	exp := &Experiment{
+		Tasks:  []Task{ct},
+		Result: &ExperimentResult{},
+	}
+	exp.InitResults()
+	err = ct.Run(exp)
+	assert.NoError(t, err)
+	assert.Equal(t, exp.Result.Insights.NumVersions, 1)
+
+	count := gs.GetCount(callType)
+	assert.Equal(t, 1, count)
+}
+
+func TestMockGRPCWithSLOsAndPercentiles(t *testing.T) {
+	callType := helloworld.Unary
+	gs, s, err := internal.StartServer(false)
+	if err != nil {
+		assert.FailNow(t, err.Error())
+	}
+	defer s.Stop()
+
+	// valid collect GRPC task... should succeed
+	ct := &collectGRPCTask{
+		TaskMeta: TaskMeta{
+			Task: StringPointer(CollectGPRCTaskName),
+		},
+		With: collectGRPCInputs{
+			Config: runner.Config{
+				N:           100,
+				RPS:         20,
+				C:           1,
+				Timeout:     runner.Duration(20 * time.Second),
+				Data:        map[string]interface{}{"name": "bob"},
+				DialTimeout: runner.Duration(20 * time.Second),
+			},
+			ProtoURL: StringPointer("https://raw.githubusercontent.com/bojand/ghz/v0.105.0/testdata/greeter.proto"),
+			VersionInfo: []*versionGRPC{{
+				Call: "helloworld.Greeter.SayHello",
+				Host: internal.TestLocalhost,
+			}},
+		},
+	}
+
+	at := &assessTask{
+		TaskMeta: TaskMeta{
+			Task: StringPointer(AssessTaskName),
+		},
+		With: assessInputs{
+			SLOs: []SLO{{
+				Metric:     "built-in/grpc-latency/mean",
+				UpperLimit: float64Pointer(100),
+			}, {
+				Metric:     "built-in/grpc-latency/p95.00",
+				UpperLimit: float64Pointer(200),
+			}, {
+				Metric:     "built-in/grpc-latency/stddev",
+				UpperLimit: float64Pointer(20),
+			}, {
+				Metric:     "built-in/grpc-latency/max",
+				UpperLimit: float64Pointer(200),
+			}, {
+				Metric:     "built-in/grpc-latency/min",
+				LowerLimit: float64Pointer(0),
+			}, {
+				Metric:     "built-in/grpc-error-count",
+				UpperLimit: float64Pointer(0),
+			}, {
+				Metric:     "built-in/grpc-request-count",
+				UpperLimit: float64Pointer(100),
+				LowerLimit: float64Pointer(100),
+			}},
+		},
+	}
+	exp := &Experiment{
+		Tasks: []Task{ct, at},
+	}
+
+	exp.InitResults()
+	exp.Result.initInsightsWithNumVersions(1)
+	err = exp.Tasks[0].Run(exp)
+	assert.NoError(t, err)
+	err = exp.Tasks[1].Run(exp)
+	assert.NoError(t, err)
+
+	// assert SLOs are satisfied
+	for _, v := range exp.Result.Insights.SLOsSatisfied {
+		for _, b := range v {
+			assert.True(t, b)
+		}
+	}
+
+	count := gs.GetCount(callType)
+	assert.Equal(t, int(ct.With.N), count)
+
+}
diff --git a/base/collect_http_test.go b/base/collect_http_test.go
new file mode 100644
index 00000000..8c6d0edf
--- /dev/null
+++ b/base/collect_http_test.go
@@ -0,0 +1,44 @@
+package base
+
+import (
+	"testing"
+
+	"github.com/jarcoal/httpmock"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRunCollectHTTP(t *testing.T) {
+	// valid collect HTTP task... should succeed
+	ct := &collectHTTPTask{
+		TaskMeta: TaskMeta{
+			Task: StringPointer(CollectHTTPTaskName),
+		},
+		With: collectHTTPInputs{
+			Duration:   StringPointer("1s"),
+			PayloadURL: StringPointer("https://data.police.uk/api/crimes-street-dates"),
+			VersionInfo: []*versionHTTP{{
+				Headers: map[string]string{},
+				URL:     "https://something.com",
+			}},
+		},
+	}
+
+	httpmock.Activate()
+	defer httpmock.DeactivateAndReset()
+
+	// Exact URL match
+	httpmock.RegisterResponder("POST", "https://something.com",
+		httpmock.NewStringResponder(200, `[{"id": 1, "name": "My Great Thing"}]`))
+
+	httpmock.RegisterResponder("GET", "https://data.police.uk/api/crimes-street-dates",
+		httpmock.NewStringResponder(200, `[{"my": 1, "great": "payload"}]`))
+
+	exp := &Experiment{
+		Tasks:  []Task{ct},
+		Result: &ExperimentResult{},
+	}
+	exp.InitResults()
+	err := ct.Run(exp)
+	assert.NoError(t, err)
+	assert.Equal(t, exp.Result.Insights.NumVersions, 1)
+}
diff --git a/base/collect_test.go b/base/collect_test.go
deleted file mode 100644
index bcf02cf0..00000000
--- a/base/collect_test.go
+++ /dev/null
@@ -1,114 +0,0 @@
-package base
-
-import (
-	"encoding/json"
-	"testing"
-
-	"github.com/jarcoal/httpmock"
-	"github.com/stretchr/testify/assert"
-)
-
-func TestMakeWrongCollectTask(t *testing.T) {
-	ts := &TaskSpec{
-		taskMeta: taskMeta{
-			Task: StringPointer(AssessTaskName),
-		},
-		With: map[string]interface{}{
-			"hello": "world",
-		},
-	}
-	task, err := MakeCollect(ts)
-	assert.Error(t, err)
-	assert.Nil(t, task)
-}
-
-func TestMakeCollect(t *testing.T) {
-	// collect without version info ... should fail
-	ts := &TaskSpec{
-		taskMeta: taskMeta{
-			Task: StringPointer(CollectTaskName),
-		},
-	}
-	task, err := MakeCollect(ts)
-	assert.Error(t, err)
-	assert.Nil(t, task)
-
-	// collect task with only nil versions... should fail
-	ct := &collectTask{
-		taskMeta: taskMeta{
-			Task: StringPointer(CollectTaskName),
-		},
-		With: collectInputs{
-			Duration:    StringPointer("1s"),
-			VersionInfo: []*version{nil, nil},
-		},
-	}
-
-	b, _ := json.Marshal(ct)
-	json.Unmarshal(b, ts)
-	task, err = MakeCollect(ts)
-	assert.Error(t, err)
-	assert.Nil(t, task)
-
-	// valid collect task... should succeed
-	ct = &collectTask{
-		taskMeta: taskMeta{
-			Task: StringPointer(CollectTaskName),
-		},
-		With: collectInputs{
-			Duration: StringPointer("1s"),
-			VersionInfo: []*version{nil, {
-				Headers: map[string]string{},
-				URL:     "https://something.com",
-			}},
-		},
-	}
-
-	b, _ = json.Marshal(ct)
-	json.Unmarshal(b, ts)
-	task, err = MakeCollect(ts)
-	assert.NoError(t, err)
-	assert.NotNil(t, task)
-}
-
-func TestRunCollect(t *testing.T) {
-	// valid collect task... should succeed
-	ct := &collectTask{
-		taskMeta: taskMeta{
-			Task: StringPointer(CollectTaskName),
-		},
-		With: collectInputs{
-			Duration: StringPointer("1s"),
-			VersionInfo: []*version{{
-				Headers: map[string]string{},
-				URL:     "https://something.com",
-			}},
-		},
-	}
-
-	ts := &TaskSpec{}
-	b, err := json.Marshal(ct)
-	assert.NoError(t, err)
-	err = json.Unmarshal(b, ts)
-	assert.NoError(t, err)
-
-	httpmock.Activate()
-	defer httpmock.DeactivateAndReset()
-
-	// Exact URL match
-	httpmock.RegisterResponder("GET", "https://something.com",
-		httpmock.NewStringResponder(200, `[{"id": 1, "name": "My Great Thing"}]`))
-
-	exp := &Experiment{
-		Tasks:  []TaskSpec{*ts},
-		Result: &ExperimentResult{},
-	}
-	exp.InitResults()
-	err = ct.Run(exp)
-	assert.NoError(t, err)
-	assert.Equal(t, exp.Result.Insights.NumVersions, 1)
-
-	// experiment should contain histogram metrics
-	assert.True(t, exp.ContainsInsight(InsightTypeHistMetrics))
-
-}
diff --git a/base/experiment_test.go b/base/experiment_test.go
index 14700e46..94129d69 100644
--- a/base/experiment_test.go
+++ b/base/experiment_test.go
@@ -1,50 +1,54 @@
 package base
 
 import (
-	"encoding/json"
+	"io/ioutil"
 	"testing"
 
 	"github.com/jarcoal/httpmock"
 	"github.com/stretchr/testify/assert"
+	"sigs.k8s.io/yaml"
 )
 
+func TestReadExperiment(t *testing.T) {
+	b, err := ioutil.ReadFile(CompletePath("../testdata", "experiment.yaml"))
+	assert.NoError(t, err)
+	es := &ExperimentSpec{}
+	err = yaml.Unmarshal(b, es)
+	assert.NoError(t, err)
+	assert.Equal(t, 2, len(*es))
+
+	b, err = ioutil.ReadFile(CompletePath("../testdata", "experiment_grpc.yaml"))
+	assert.NoError(t, err)
+	es = &ExperimentSpec{}
+	err = yaml.Unmarshal(b, es)
+	assert.NoError(t, err)
+	assert.Equal(t, 3, len(*es))
+}
 func TestRunExperiment(t *testing.T) {
 	// valid collect task... should succeed
-	ct := &collectTask{
-		taskMeta: taskMeta{
-			Task: StringPointer(CollectTaskName),
+	ct := &collectHTTPTask{
+		TaskMeta: TaskMeta{
+			Task: StringPointer(CollectHTTPTaskName),
 		},
-		With: collectInputs{
+		With: collectHTTPInputs{
 			Duration:    StringPointer("1s"),
-			VersionInfo: []*version{{Headers: map[string]string{}, URL: "https://something.com"}},
+			VersionInfo: []*versionHTTP{{Headers: map[string]string{}, URL: "https://something.com"}},
 		},
 	}
 
-	tsc := &TaskSpec{}
-	b, err := json.Marshal(ct)
-	assert.NoError(t, err)
-	err = json.Unmarshal(b, tsc)
-	assert.NoError(t, err)
-
 	// valid assess task... should succeed
 	at := &assessTask{
-		taskMeta: taskMeta{
+		TaskMeta: TaskMeta{
 			Task: StringPointer(AssessTaskName),
 		},
 		With: assessInputs{
 			SLOs: []SLO{{
-				Metric:     iter8BuiltInPrefix + "/" + errorCountMetricName,
+				Metric:     iter8BuiltInPrefix + "/" + builtInHTTPErrorCountId,
 				UpperLimit: float64Pointer(0),
 			}},
 		},
 	}
 
-	tsa := &TaskSpec{}
-	b, err = json.Marshal(at)
-	assert.NoError(t, err)
-	err = json.Unmarshal(b, tsa)
-	assert.NoError(t, err)
-
 	httpmock.Activate()
 	defer httpmock.DeactivateAndReset()
 
@@ -53,17 +57,16 @@ func TestRunExperiment(t *testing.T) {
 		httpmock.NewStringResponder(200, `[{"id": 1, "name": "My Great Thing"}]`))
 
 	exp := &Experiment{
-		Tasks:  []TaskSpec{*tsc, *tsa},
+		Tasks:  []Task{ct, at},
 		Result: &ExperimentResult{},
 	}
 	exp.InitResults()
-	err = ct.Run(exp)
+	err := ct.Run(exp)
 	assert.NoError(t, err)
 	assert.Equal(t, exp.Result.Insights.NumVersions, 1)
 
-	// experiment should contain histogram metrics
-	assert.True(t, exp.ContainsInsight(InsightTypeHistMetrics))
-
 	// SLOs should be satisfied by app
-	assert.True(t, exp.SLOs())
+	for i := 0; i < len(exp.Result.Insights.SLOs); i++ { // i^th SLO
+		assert.True(t, exp.Result.Insights.SLOsSatisfied[i][0]) // satisfied by only version
+	}
 }
diff --git a/base/mock_qs_test.go b/base/mock_qs_test.go
new file mode 100644
index 00000000..87cdcab6
--- /dev/null
+++ b/base/mock_qs_test.go
@@ -0,0 +1,113 @@
+package base
+
+import (
+	"testing"
+
+	"github.com/jarcoal/httpmock"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestMockQuickStartWithSLOs(t *testing.T) {
+	// mock the http endpoint
+	httpmock.Activate()
+	defer httpmock.DeactivateAndReset()
+	// Exact URL match
+	httpmock.RegisterResponder("GET", "https://example.com",
+		httpmock.NewStringResponder(200, `all good`))
+
+	// valid collect HTTP task... should succeed
+	ct := &collectHTTPTask{
+		TaskMeta: TaskMeta{
+			Task: StringPointer(CollectHTTPTaskName),
+		},
+		With: collectHTTPInputs{
+			Duration: StringPointer("1s"),
+			VersionInfo: []*versionHTTP{{
+				Headers: map[string]string{},
+				URL:     "https://example.com",
+			}},
+		},
+	}
+
+	at := &assessTask{
+		TaskMeta: TaskMeta{
+			Task: StringPointer(AssessTaskName),
+		},
+		With: assessInputs{
+			SLOs: []SLO{{
+				Metric:     "built-in/http-latency-mean",
+				UpperLimit: float64Pointer(100),
+			}},
+		},
+	}
+	exp := &Experiment{
+		Tasks: []Task{ct, at},
+	}
+
+	exp.InitResults()
+	exp.Result.initInsightsWithNumVersions(1)
+	err := exp.Tasks[0].Run(exp)
+	assert.NoError(t, err)
+	err = exp.Tasks[1].Run(exp)
+	assert.NoError(t, err)
+	// assert SLOs are satisfied
+	for _, v := range exp.Result.Insights.SLOsSatisfied {
+		for _, b := range v {
+			assert.True(t, b)
+		}
+	}
+}
+
+func TestMockQuickStartWithSLOsAndPercentiles(t *testing.T) {
+	// mock the http endpoint
+	httpmock.Activate()
+	defer httpmock.DeactivateAndReset()
+	// Exact URL match
+	httpmock.RegisterResponder("GET", "https://example.com",
+		httpmock.NewStringResponder(200, `all good`))
+
+	// valid collect HTTP task... should succeed
+	ct := &collectHTTPTask{
+		TaskMeta: TaskMeta{
+			Task: StringPointer(CollectHTTPTaskName),
+		},
+		With: collectHTTPInputs{
+			Duration: StringPointer("1s"),
+			VersionInfo: []*versionHTTP{{
+				Headers: map[string]string{},
+				URL:     "https://example.com",
+			}},
+		},
+	}
+
+	at := &assessTask{
+		TaskMeta: TaskMeta{
+			Task: StringPointer(AssessTaskName),
+		},
+		With: assessInputs{
+			SLOs: []SLO{{
+				Metric:     "built-in/http-latency-mean",
+				UpperLimit: float64Pointer(100),
+			}, {
+				Metric:     "built-in/http-latency-p95.00",
+				UpperLimit: float64Pointer(200),
+			}},
+		},
+	}
+	exp := &Experiment{
+		Tasks: []Task{ct, at},
+	}
+
+	exp.InitResults()
+	exp.Result.initInsightsWithNumVersions(1)
+	err := exp.Tasks[0].Run(exp)
+	assert.NoError(t, err)
+	err = exp.Tasks[1].Run(exp)
+	assert.NoError(t, err)
+	// assert SLOs are satisfied
+	for _, v := range exp.Result.Insights.SLOsSatisfied {
+		for _, b := range v {
+			assert.True(t, b)
+		}
+	}
+}
diff --git a/base/run_test.go b/base/run_test.go
index 7a6866df..db8d1e64 100644
--- a/base/run_test.go
+++ b/base/run_test.go
@@ -1,78 +1,24 @@
 package base
 
 import (
-	"encoding/json"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
 )
 
-func TestMakeWrongRunTask(t *testing.T) {
-	// no run ...
-	ts := &TaskSpec{
-		taskMeta: taskMeta{
-			Task: StringPointer(AssessTaskName),
-		},
-		With: map[string]interface{}{
-			"hello": "world",
-		},
-	}
-	task, err := MakeRun(ts)
-	assert.Error(t, err)
-	assert.Nil(t, task)
-
-	// both run and task
-	ts = &TaskSpec{
-		taskMeta: taskMeta{
-			Task: StringPointer(AssessTaskName),
-			Run:  StringPointer("echo hello"),
-		},
-		With: map[string]interface{}{
-			"hello": "world",
-		},
-	}
-	task, err = MakeRun(ts)
-	assert.Error(t, err)
-	assert.Nil(t, task)
-}
-
-func TestMakeRun(t *testing.T) {
-	// valid run task... should succeed
-	rt := &runTask{
-		taskMeta: taskMeta{
-			Run: StringPointer("echo hello"),
-		},
-	}
-
-	ts := &TaskSpec{}
-	b, _ := json.Marshal(rt)
-	json.Unmarshal(b, ts)
-	task, err := MakeRun(ts)
-	assert.NoError(t, err)
-	assert.NotNil(t, task)
-	assert.Nil(t, GetIf(task))
-}
-
 func TestRunRun(t *testing.T) {
 	// valid run task... should succeed
 	rt := &runTask{
-		taskMeta: taskMeta{
+		TaskMeta: TaskMeta{
 			Run: StringPointer("echo hello"),
 		},
 	}
 
-	ts := &TaskSpec{}
-	b, _ := json.Marshal(rt)
-	json.Unmarshal(b, ts)
-	task, err := MakeRun(ts)
-	assert.NoError(t, err)
-	assert.NotNil(t, task)
-
 	exp := &Experiment{
-		Tasks:  []TaskSpec{*ts},
+		Tasks:  []Task{rt},
 		Result: &ExperimentResult{},
 	}
 	exp.InitResults()
-	err = rt.Run(exp)
+	err := rt.Run(exp)
 	assert.NoError(t, err)
 }
diff --git a/basecli/hub_test.go b/basecli/hub_test.go
index 67c55a69..e8fd5071 100644
--- a/basecli/hub_test.go
+++ b/basecli/hub_test.go
@@ -15,7 +15,7 @@ func TestHubGoodFolder(t *testing.T) {
 
 	os.Chdir(dir)
 	// Todo: fix location below
-	os.Setenv("ITER8HUB", "github.com/iter8-tools/iter8.git?ref=master//hub/")
+	os.Setenv("ITER8HUB", "github.com/sriumcp/iter8.git?ref=grpc//hub/")
 	hubFolder = "load-test"
 	// make sure load test folder is present
 	err := hubCmd.RunE(nil, nil)
diff --git a/basecli/mock_qs_test.go b/basecli/mock_qs_test.go
index 7ac10552..bca87502 100644
--- a/basecli/mock_qs_test.go
+++ b/basecli/mock_qs_test.go
@@ -12,7 +12,7 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func TestMockQuickStart(t *testing.T) {
+func TestMockQuickStartWithoutSLOs(t *testing.T) {
 	// get into the experiment chart folder
 	os.Chdir(base.CompletePath("../", "hub/load-test"))
 
@@ -37,11 +37,23 @@ func TestMockQuickStart(t *testing.T) {
 	}
 	err = assertCmd.RunE(nil, nil)
 	assert.NoError(t, err)
+}
+
+func TestMockQuickStartWithSLOs(t *testing.T) {
+	// get into the experiment chart folder
+	os.Chdir(base.CompletePath("../", "hub/load-test"))
+
+	// mock the http endpoint
+	httpmock.Activate()
+	defer httpmock.DeactivateAndReset()
+	// Exact URL match
+	httpmock.RegisterResponder("GET", "https://example.com",
+		httpmock.NewStringResponder(200, `all good`))
 
 	// with SLOs next
-	GenOptions.Values = append(GenOptions.Values, "url=https://example.com", "SLOs.error-rate=0", "SLOs.mean-latency=100")
+	GenOptions.Values = append(GenOptions.Values, "url=https://example.com", "SLOs.error-rate=0", "SLOs.mean-latency=100", "duration=2s")
 	GenOptions.ValueFiles = append(GenOptions.ValueFiles, base.CompletePath("../", "testdata/percentileandslos/values.yaml"))
-	err = runCmd.RunE(nil, nil)
+	err := runCmd.RunE(nil, nil)
 	assert.NoError(t, err)
 
 	// assert
@@ -51,16 +63,28 @@ func TestMockQuickStart(t *testing.T) {
 	}
 	err = assertCmd.RunE(nil, nil)
 	assert.NoError(t, err)
+}
+
+func TestMockQuickStartWithSLOsAndPercentiles(t *testing.T) {
+	// get into the experiment chart folder
+	os.Chdir(base.CompletePath("../", "hub/load-test"))
+
+	// mock the http endpoint
+	httpmock.Activate()
+	defer httpmock.DeactivateAndReset()
+	// Exact URL match
+	httpmock.RegisterResponder("GET", "https://example.com",
+		httpmock.NewStringResponder(200, `all good`))
 
 	// with SLOs and percentiles also
 	GenOptions.Values = append(GenOptions.Values, "url=https://example.com", "SLOs.error-rate=0", "SLOs.mean-latency=100", "SLOs.p50=100")
 	GenOptions.ValueFiles = append(GenOptions.ValueFiles, base.CompletePath("../", "testdata/percentileandslos/values.yaml"))
-	err = runCmd.RunE(nil, nil)
+	err := runCmd.RunE(nil, nil)
 	assert.NoError(t, err)
 
 	// assert
 	AssertOptions = AssertOptionsType{
-		Conds:   []string{Completed, NoFailure, SLOs},
+		Conds:   []string{Completed, NoFailure, SLOs, "slosby=0"},
 		Timeout: 0,
 	}
 	err = assertCmd.RunE(nil, nil)
@@ -87,7 +111,8 @@ func TestDryRun(t *testing.T) {
 	defer os.RemoveAll(dir)
 
 	os.Chdir(dir)
-	os.Setenv("ITER8HUB", "github.com/iter8-tools/iter8.git?ref=master//hub/")
+	// ToDo: Fix this location
+	os.Setenv("ITER8HUB", "github.com/sriumcp/iter8.git?ref=grpc//hub/")
 	hubFolder = "load-test"
 	// hub
 	err := hubCmd.RunE(nil, nil)
diff --git a/hub/load-test/README.md b/hub/load-test/README.md
index 7669f363..c5af6138 100644
--- a/hub/load-test/README.md
+++ b/hub/load-test/README.md
@@ -50,7 +50,7 @@ Fetch JSON content from a payload URL. Use this JSON as payload and explicitly s
 
 ```shell
 iter8 run --set url=http://127.0.0.1/post \
-          --set payloadURL=https://json-generator.com/ \
+          --set payloadURL=https://data.police.uk/api/crimes-street-dates \
           --set contentType="application/json" \
           --set SLOs.error-rate=0 \
           --set SLOs.mean-latency=50 \
@@ -67,7 +67,7 @@ Consider the following command.
 iter8 run --set url=https://example.com \
           --set SLOs.error-rate=0 \
           --set SLOs.mean-latency=50 \
-          --set 'percentiles={25.0, 75.0}'
+          --set 'percentiles={25.0, 75.0}' \
           --set SLOs.p90=100 \
           --set SLOs.p'97\.5'=200
 ```
diff --git a/hub/load-test/templates/_experiment.tpl b/hub/load-test/templates/_experiment.tpl
index 8c04cfab..5e2d0832 100644
--- a/hub/load-test/templates/_experiment.tpl
+++ b/hub/load-test/templates/_experiment.tpl
@@ -1,11 +1,11 @@
 {{ define "load-test.experiment" -}}
 # task 1: generate HTTP requests for application URL
-# collect Iter8's built-in latency and error-related metrics
-- task: gen-load-and-collect-metrics
+# collect Iter8's built-in HTTP latency and error-related metrics
+- task: gen-load-and-collect-metrics-http
   with:
 
     {{- if .Values.numQueries }}
-    numQueries: {{ .Values.numQueries}}
+    numRequests: {{ .Values.numQueries}}
     {{- end }}
 
     {{- if .Values.duration }}
@@ -13,7 +13,7 @@
     {{- end }}
 
     {{- if .Values.qps }}
-    qps: {{ .Values.qps}}
+    rps: {{ .Values.qps}}
     {{- end }}
 
     {{- if .Values.connections }}
@@ -66,8 +66,14 @@
   with:
     SLOs:
     {{- range $key, $value := .Values.SLOs }}
-    {{- if or (regexMatch "error-rate" $key) (regexMatch "error-count" $key) (regexMatch "mean-latency" $key) (regexMatch "^p\\d+(?:\\.\\d)?$" $key) }}
-    - metric: "built-in/{{ $key }}"
+    {{- if or (regexMatch "error-rate" $key) (regexMatch "error-count" $key) }}
+    - metric: "built-in/http-{{ $key }}"
+      upperLimit: {{ $value }}
+    {{- else if (regexMatch "mean-latency" $key) }}
+    - metric: "built-in/http-latency-mean"
+      upperLimit: {{ $value }}
+    {{- else if (regexMatch "^p\\d+(?:\\.\\d)?$" $key) }}
+    - metric: "built-in/http-latency-{{ $key }}"
       upperLimit: {{ $value }}
     {{- else }}
     {{- fail "Invalid SLO metric specified" }}
diff --git a/mkdocs/docs/tutorials/load-test/payload.md b/mkdocs/docs/tutorials/load-test/payload.md
index 240d336b..7f80fae4 100644
--- a/mkdocs/docs/tutorials/load-test/payload.md
+++ b/mkdocs/docs/tutorials/load-test/payload.md
@@ -61,7 +61,7 @@ Iter8 enables you to send any type of content as payload during the load test, e
     Fetch JSON content from a payload URL. Use this JSON as payload and explicitly set the content type to `application/json`.
     ```shell
     iter8 run --set url=http://127.0.0.1/post \
-              --set payloadURL=https://json-generator.com/ \
+              --set payloadURL=https://data.police.uk/api/crimes-street-dates \
               --set contentType="application/json" \
               --set SLOs.error-rate=0 \
               --set SLOs.mean-latency=50 \
diff --git a/testdata/experiment.example.yaml b/testdata/experiment.example.yaml
index d0d96143..69c01f0a 100644
--- a/testdata/experiment.example.yaml
+++ b/testdata/experiment.example.yaml
@@ -1,6 +1,6 @@
 # task 1: generate HTTP requests for https://example.com
 # collect Iter8's built-in latency and error-related metrics
-- task: gen-load-and-collect-metrics
+- task: gen-load-and-collect-metrics-http
   with:
     versionInfo:
     - url: https://example.com
diff --git a/testdata/experiment.kn.yaml b/testdata/experiment.kn.yaml
index b7f88296..fcd2d242 100644
--- a/testdata/experiment.kn.yaml
+++ b/testdata/experiment.kn.yaml
@@ -1,6 +1,6 @@
 # task 1: generate HTTP requests for https://example.com
 # collect Iter8's built-in latency and error-related metrics
-- task: gen-load-and-collect-metrics
+- task: gen-load-and-collect-metrics-http
   with:
     versionInfo:
     - url: http://hello.default.127.0.0.1.nip.io
diff --git a/testdata/experiment.yaml b/testdata/experiment.yaml
index 594a8376..73b92fd0 100644
--- a/testdata/experiment.yaml
+++ b/testdata/experiment.yaml
@@ -1,17 +1,34 @@
-# task 1: generate HTTP requests for https://example.com
-# collect Iter8's built-in latency and error-related metrics
-- task: gen-load-and-collect-metrics
+# task 1: generate HTTP requests for application URL
+# collect Iter8's built-in HTTP latency and error-related metrics
+- task: gen-load-and-collect-metrics-http
   with:
+    duration: 2s
+    errorRanges:
+    - lower: 500
+    percentiles: 
+    - 50
+    - 75
+    - 90
+    - 95
+    - 97.5
+    - 99
+    - 99.9
     versionInfo:
     - url: https://example.com
-# task 2: validate service level objectives for https://example.com using
+# task 2: validate service level objectives for app using
 # the metrics collected in the above task
 - task: assess-app-versions
   with:
     SLOs:
-      # error rate must be 0
-    - metric: built-in/error-rate
+    - metric: "built-in/http-error-rate"
       upperLimit: 0
-      # 95th percentile latency must be under 1 msec
-    - metric: built-in/p95.0
+    - metric: "built-in/http-latency-mean"
       upperLimit: 100
+    - metric: "built-in/http-latency-p50"
+      upperLimit: 100
+    - metric: "built-in/http-latency-p50.0"
+      upperLimit: 100
+    - metric: "built-in/http-latency-p95.0"
+      upperLimit: 250
+    - metric: "built-in/http-latency-p97.5"
+      upperLimit: 500
diff --git a/testdata/experiment_grpc.yaml b/testdata/experiment_grpc.yaml
new file mode 100644
index 00000000..05d0ddf1
--- /dev/null
+++ b/testdata/experiment_grpc.yaml
@@ -0,0 +1,34 @@
+# task 1: generate gRPC requests for application
+# collect Iter8's built-in gRPC latency and error-related metrics
+- task: gen-load-and-collect-metrics-grpc
+  with:
+    total: 200
+    concurrency: 5
+    data:
+      name: bob
+    timeout: 10s
+    connect-timeeout: 5s
+    protoURL: "https://raw.githubusercontent.com/bojand/ghz/v0.105.0/testdata/greeter.proto"
+    versionInfo:
+    - call: "helloworld.Greeter.SayHello"
+      host: "127.0.0.1"
+# task 2: validate service level objectives for app using
+# the metrics collected in the above task
+- task: assess-app-versions
+  with:
+    SLOs:
+    - metric: "built-in/grpc-error-rate"
+      upperLimit: 0
+    - metric: "built-in/grpc-latency/mean"
+      upperLimit: 100
+    - metric: "built-in/grpc-latency/p50"
+      upperLimit: 100
+    - metric: "built-in/grpc-latency/p50.0"
+      upperLimit: 100
+    - metric: "built-in/grpc-latency/p95.0"
+      upperLimit: 250
+    - metric: "built-in/grpc-latency/p97.5"
+      upperLimit: 500
+# task 3: conditional run task
+- if: SLOs()
+  run: echo "all good"
diff --git a/testdata/go.tpl b/testdata/go.tpl
index f7168a2c..35d0f9af 100644
--- a/testdata/go.tpl
+++ b/testdata/go.tpl
@@ -36,7 +36,7 @@ stringData:
   experiment: |
     # task 1: generate HTTP requests for https://example.com
     # collect Iter8's built-in latency and error-related metrics
-    - task: gen-load-and-collect-metrics
+    - task: gen-load-and-collect-metrics-http
       with:
         versionInfo:
         - url: https://example.com