Skip to content

Commit b4c207e

Browse files
authored
iter8 gen exp subcommand (iter8-tools#1107)
* iter8 gen exp Signed-off-by: Srinivasan Parthasarathy <spartha@us.ibm.com> * your first experiment and install Signed-off-by: Srinivasan Parthasarathy <spartha@us.ibm.com> * fixing mkdocs.yml Signed-off-by: Srinivasan Parthasarathy <spartha@us.ibm.com> * removing images not needed atm Signed-off-by: Srinivasan Parthasarathy <spartha@us.ibm.com> * readded illustration and fixed gen exp Signed-off-by: Srinivasan Parthasarathy <spartha@us.ibm.com> * fixing readme Signed-off-by: Srinivasan Parthasarathy <spartha@us.ibm.com> * fixing assert flags Signed-off-by: Srinivasan Parthasarathy <spartha@us.ibm.com> * updated hub folder Signed-off-by: Srinivasan Parthasarathy <spartha@us.ibm.com> * required url value Signed-off-by: Srinivasan Parthasarathy <spartha@us.ibm.com> * fixed mock qs Signed-off-by: Srinivasan Parthasarathy <spartha@us.ibm.com> * improving go doc Signed-off-by: Srinivasan Parthasarathy <spartha@us.ibm.com> * fixing exp docs Signed-off-by: Srinivasan Parthasarathy <spartha@us.ibm.com> * fixig mkdocs Signed-off-by: Srinivasan Parthasarathy <spartha@us.ibm.com> * fixing k8s.go doc Signed-off-by: Srinivasan Parthasarathy <spartha@us.ibm.com> * fixing k8s.go Signed-off-by: Srinivasan Parthasarathy <spartha@us.ibm.com> * fixing go doc Signed-off-by: Srinivasan Parthasarathy <spartha@us.ibm.com> * fixing app and id related documentation Signed-off-by: Srinivasan Parthasarathy <spartha@us.ibm.com> * fixing docs for k8s.go Signed-off-by: Srinivasan Parthasarathy <spartha@us.ibm.com>
1 parent a5febd0 commit b4c207e

File tree

187 files changed

+602
-5272
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

187 files changed

+602
-5272
lines changed

.github/workflows/tests.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ jobs:
2323
run: |
2424
export COVERAGE=$(go tool cover -func coverage.out | grep total | awk '{print substr($3, 1, length($3)-1)}')
2525
echo "code coverage is at ${COVERAGE}"
26-
if [ 1 -eq "$(echo "${COVERAGE} > 66.0" | bc)" ]; then \
27-
echo "all good... coverage is above 66.0%";
26+
if [ 1 -eq "$(echo "${COVERAGE} > 67.0" | bc)" ]; then \
27+
echo "all good... coverage is above 67.0%";
2828
else \
29-
echo "not good... coverage is not above 66.0%";
29+
echo "not good... coverage is not above 67.0%";
3030
exit 1
3131
fi
3232
- name: Upload coverage to Codecov

README.md

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,43 +11,45 @@
1111

1212
### Kubernetes-friendly metrics-driven <strong>experiments</strong> and <strong>safe rollouts</strong>.
1313

14-
#### Built for DevOps/SRE/MLOps/data science teams.
15-
1614
## Use Cases
1715

1816
1. Load testing with SLOs
19-
2. A/B(/n) testing with business reward metrics
20-
3. SLOs with metrics from any backend
21-
4. Traffic mirroring
22-
5. User segmentation
23-
6. Session affinity
24-
7. Gradual rollout
17+
2. A/B(/n) testing for improving business value with each release of app/ML model
18+
3. Safe rollout for multi-cluster and edge
19+
4. Traffic mirroring experiments
2520

26-
The traffic engineering use-cases (4 - 7 above) are achieved by using Iter8 along with a Kubernetes service mesh or ingress.
21+
The traffic mirroring use-case is achieved by using Iter8 along with a Kubernetes service mesh or ingress that supports mirroring.
2722

2823
## Quick Start
2924

3025
### 1. Install Iter8
3126
Install Iter8 using [Go 1.16+](https://golang.org/) as follows.
3227
```shell
3328
go install github.com/iter8-tools/iter8@latest
34-
# you can now run iter8 (from your gopath bin/ directory)
3529
```
30+
You can now run `iter8` (from your gopath bin/ directory)
3631

37-
## 2. Download experiment
38-
Download the `load-test` experiment folder from Iter8 hub as follows.
32+
## 2. Download experiment chart
33+
Download the `load-test` experiment chart from Iter8 hub as follows.
3934

4035
```shell
4136
iter8 hub -e load-test
4237
```
4338

44-
## 3. Run experiment
45-
Iter8 experiments are specified using the `experiment.yaml` file. The `iter8 run` command reads this file, runs the specified experiment, and writes the results of the experiment into the `result.yaml` file.
39+
This creates a local folder called `load-test` containing the chart.
4640

47-
Run the experiment you downloaded above as follows.
41+
## 3. Generate `experiment.yaml`
42+
Generate the `experiment.yaml` file which specifies your load test experiment.
4843

4944
```shell
5045
cd load-test
46+
iter8 gen exp --set url=https://example.com
47+
```
48+
49+
## 4. Run experiment
50+
The iter8 run command reads the `experiment.yaml` file, runs the specified experiment, and writes the results of the experiment into the `result.yaml` file. Run the experiment as follows.
51+
52+
```shell
5153
iter8 run
5254
```
5355

@@ -81,6 +83,20 @@ iter8 report -o text
8183

8284
Congratulations! :tada: You completed your first Iter8 experiment.
8385

84-
## [Documentation](https://iter8.tools)
86+
## Documentation
87+
Iter8 documentation is available at https://iter8.tools.
88+
89+
## Contributing
90+
We are delighted that you want to contribute to Iter8! 💖
91+
92+
As you get started, you are in the best position to give us feedback on areas of
93+
our project that we need help with including:
94+
95+
* Problems found during setup of Iter8
96+
* Gaps in our quick start tutorial and other documentation
97+
* Bugs in our test and automation scripts
98+
99+
If anything doesn't make sense, or doesn't work when you run it, please open a
100+
bug report and let us know!
85101

86-
## [Contributing](https://iter8.tools/latest/contributing/overview/)
102+
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.

base/assess.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Package base provides the core definitions and primitives for Iter8 experiment and experimeent tasks.
12
package base
23

34
import (
@@ -17,6 +18,7 @@ type assessInputs struct {
1718
// assessTask enables assessment of versions
1819
type assessTask struct {
1920
taskMeta
21+
// With contains the inputs for the assessTask
2022
With assessInputs `json:"with" yaml:"with"`
2123
}
2224

base/assess_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
func TestMakeWrongTask(t *testing.T) {
1010
ts := &TaskSpec{
1111
taskMeta: taskMeta{
12-
Task: stringPointer(CollectTaskName),
12+
Task: StringPointer(CollectTaskName),
1313
},
1414
With: map[string]interface{}{
1515
"hello": "world",
@@ -25,7 +25,7 @@ func TestMakeAssess(t *testing.T) {
2525
// should succeed
2626
ts := &TaskSpec{
2727
taskMeta: taskMeta{
28-
Task: stringPointer(AssessTaskName),
28+
Task: StringPointer(AssessTaskName),
2929
},
3030
}
3131
task, err := MakeAssess(ts)
@@ -35,7 +35,7 @@ func TestMakeAssess(t *testing.T) {
3535
// incorrect with clause
3636
// should fail
3737
ts = &TaskSpec{
38-
taskMeta: taskMeta{Task: stringPointer(AssessTaskName)},
38+
taskMeta: taskMeta{Task: StringPointer(AssessTaskName)},
3939
With: map[string]interface{}{
4040
"SLOs": "hello world",
4141
},
@@ -51,7 +51,7 @@ func TestRunAssess(t *testing.T) {
5151
// should succeed
5252
ts := &TaskSpec{
5353
taskMeta: taskMeta{
54-
Task: stringPointer(AssessTaskName),
54+
Task: StringPointer(AssessTaskName),
5555
},
5656
}
5757
task, _ := MakeAssess(ts)
@@ -64,7 +64,7 @@ func TestRunAssess(t *testing.T) {
6464
// assess with an SLO
6565
// should succeed
6666
ts = &TaskSpec{
67-
taskMeta: taskMeta{Task: stringPointer(AssessTaskName)},
67+
taskMeta: taskMeta{Task: StringPointer(AssessTaskName)},
6868
With: map[string]interface{}{
6969
"SLOs": []SLO{{
7070
Metric: "m",

base/collect.go

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@ type version struct {
2727
URL string `json:"url" yaml:"url" validate:"required,url"`
2828
}
2929

30-
// HTTP status code within this range is considered an error
30+
// errorRange has lower and upper limits for HTTP status codes. HTTP status code within this range is considered an error
3131
type errorRange struct {
32+
// Lower end of the range
3233
Lower *int `json:"lower" yaml:"lower" validate:"required_without=Upper"`
34+
// Upper end of the range
3335
Upper *int `json:"upper" yaml:"upper" validate:"required_without=Lower"`
3436
}
3537

@@ -53,11 +55,12 @@ type collectInputs struct {
5355
ErrorRanges []errorRange `json:"errorRanges" yaml:"errorRanges"`
5456
// Percentiles are the latency percentiles computed 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,}.
5557
Percentiles []float64 `json:"percentiles" yaml:"percentiles" validate:"unique,dive,gte=0.0,lte=100.0"`
56-
// A non-empty list of version values.
58+
// VersionInfo is a non-empty list of version values.
5759
VersionInfo []*version `json:"versionInfo" yaml:"versionInfo" validate:"required,notallnil"`
5860
}
5961

6062
const (
63+
// CollectTaskName is the name of this task which performs load generation and metrics collection.
6164
CollectTaskName = "gen-load-and-collect-metrics"
6265
defaultQPS = float32(8)
6366
defaultNumRequests = int64(100)
@@ -247,7 +250,7 @@ func (t *collectTask) resultForVersion(j int) (*fhttp.HTTPRunnerResults, error)
247250
return ifr, err
248251
}
249252

250-
// GetName returns the name of the assess task
253+
// GetName returns the name of the collect task
251254
func (t *collectTask) GetName() string {
252255
return CollectTaskName
253256
}
@@ -348,7 +351,7 @@ func (t *collectTask) Run(exp *Experiment) error {
348351
in.MetricsInfo[m] = MetricMeta{
349352
Description: "mean of observed latency values",
350353
Type: GaugeMetricType,
351-
Units: stringPointer("msec"),
354+
Units: StringPointer("msec"),
352355
}
353356
in.MetricValues[i][m] = append(in.MetricValues[i][m], 1000.0*fm[i].DurationHistogram.Avg)
354357

@@ -357,7 +360,7 @@ func (t *collectTask) Run(exp *Experiment) error {
357360
in.MetricsInfo[m] = MetricMeta{
358361
Description: "standard deviation of observed latency values",
359362
Type: GaugeMetricType,
360-
Units: stringPointer("msec"),
363+
Units: StringPointer("msec"),
361364
}
362365
in.MetricValues[i][m] = append(in.MetricValues[i][m], 1000.0*fm[i].DurationHistogram.StdDev)
363366

@@ -366,7 +369,7 @@ func (t *collectTask) Run(exp *Experiment) error {
366369
in.MetricsInfo[m] = MetricMeta{
367370
Description: "minimum of observed latency values",
368371
Type: GaugeMetricType,
369-
Units: stringPointer("msec"),
372+
Units: StringPointer("msec"),
370373
}
371374
in.MetricValues[i][m] = append(in.MetricValues[i][m], 1000.0*fm[i].DurationHistogram.Min)
372375

@@ -375,7 +378,7 @@ func (t *collectTask) Run(exp *Experiment) error {
375378
in.MetricsInfo[m] = MetricMeta{
376379
Description: "maximum of observed latency values",
377380
Type: GaugeMetricType,
378-
Units: stringPointer("msec"),
381+
Units: StringPointer("msec"),
379382
}
380383
in.MetricValues[i][m] = append(in.MetricValues[i][m], 1000.0*fm[i].DurationHistogram.Max)
381384

@@ -385,7 +388,7 @@ func (t *collectTask) Run(exp *Experiment) error {
385388
in.MetricsInfo[m] = MetricMeta{
386389
Description: fmt.Sprintf("%0.1f-th percentile of observed latency values", p.Percentile),
387390
Type: GaugeMetricType,
388-
Units: stringPointer("msec"),
391+
Units: StringPointer("msec"),
389392
}
390393
in.MetricValues[i][m] = append(in.MetricValues[i][m], 1000.0*p.Value)
391394
}
@@ -395,7 +398,7 @@ func (t *collectTask) Run(exp *Experiment) error {
395398
in.MetricsInfo[m] = MetricMeta{
396399
Description: "Latency Histogram",
397400
Type: HistogramMetricType,
398-
Units: stringPointer("msec"),
401+
Units: StringPointer("msec"),
399402
XMin: float64Pointer(xMin),
400403
XMax: float64Pointer(xMax),
401404
NumBuckets: intPointer(20),
@@ -414,7 +417,7 @@ func (t *collectTask) Run(exp *Experiment) error {
414417
return nil
415418
}
416419

417-
// sum up two vectors
420+
// vectorSum produces the sum of two equi-length vectors
418421
func vectorSum(a []float64, b []float64) ([]float64, error) {
419422
if len(a) != len(b) {
420423
log.Logger.Error("vector lengths do not match: ", len(a), " != ", len(b))

base/collect_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
func TestMakeWrongCollectTask(t *testing.T) {
1212
ts := &TaskSpec{
1313
taskMeta: taskMeta{
14-
Task: stringPointer(AssessTaskName),
14+
Task: StringPointer(AssessTaskName),
1515
},
1616
With: map[string]interface{}{
1717
"hello": "world",
@@ -26,7 +26,7 @@ func TestMakeCollect(t *testing.T) {
2626
// collect without version info ... should fail
2727
ts := &TaskSpec{
2828
taskMeta: taskMeta{
29-
Task: stringPointer(CollectTaskName),
29+
Task: StringPointer(CollectTaskName),
3030
},
3131
}
3232
task, err := MakeCollect(ts)
@@ -36,7 +36,7 @@ func TestMakeCollect(t *testing.T) {
3636
// collect task with only nil versions... should fail
3737
ct := &collectTask{
3838
taskMeta: taskMeta{
39-
Task: stringPointer(CollectTaskName),
39+
Task: StringPointer(CollectTaskName),
4040
},
4141
With: collectInputs{
4242
VersionInfo: []*version{nil, nil},
@@ -52,7 +52,7 @@ func TestMakeCollect(t *testing.T) {
5252
// valid collect task... should succeed
5353
ct = &collectTask{
5454
taskMeta: taskMeta{
55-
Task: stringPointer(CollectTaskName),
55+
Task: StringPointer(CollectTaskName),
5656
},
5757
With: collectInputs{
5858
VersionInfo: []*version{nil, {
@@ -73,7 +73,7 @@ func TestRunCollect(t *testing.T) {
7373
// valid collect task... should succeed
7474
ct := &collectTask{
7575
taskMeta: taskMeta{
76-
Task: stringPointer(CollectTaskName),
76+
Task: StringPointer(CollectTaskName),
7777
},
7878
With: collectInputs{
7979
VersionInfo: []*version{{

base/experiment.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,21 @@ import (
1313
// Experiment specification and result
1414
type Experiment struct {
1515
// Tasks is the sequence of tasks that constitute this experiment
16-
Tasks []TaskSpec `json:"tasks" yaml:"tasks"`
16+
Tasks []TaskSpec `json:"tasks" yaml:"tasks"`
17+
// Result is the current results from this experiment.
18+
// The experiment may not have completed in which case results may be partial.
1719
Result *ExperimentResult `json:"result" yaml:"result"`
1820
}
1921

2022
// Task is an object that can be run
2123
type Task interface {
24+
// Run this task
2225
Run(exp *Experiment) error
26+
// Get the name of this task
2327
GetName() string
2428
}
2529

30+
// GetIf returns the condition if any which determines whether of not if this task needs to run
2631
func GetIf(t Task) *string {
2732
var jsonBytes []byte
2833
var tm taskMeta
@@ -116,14 +121,19 @@ type SLO struct {
116121
}
117122

118123
type taskMeta struct {
124+
// Task is the name of the task
119125
Task *string `json:"task,omitempty" yaml:"task,omitempty" validate:"required_without=Run,excluded_with=Run"`
120-
Run *string `json:"run,omitempty" yaml:"run,omitempty" validate:"required_without=Task,excluded_with=Task"`
121-
If *string `json:"if,omitempty" yaml:"if,omitempty"`
126+
// Run is the script used in a run task
127+
// Specify either Task or Run but not both
128+
Run *string `json:"run,omitempty" yaml:"run,omitempty" validate:"required_without=Task,excluded_with=Task"`
129+
// If is the condition used to determine if this task needs to run.
130+
If *string `json:"if,omitempty" yaml:"if,omitempty"`
122131
}
123132

124133
// TaskSpec has information needed to construct a Task
125134
type TaskSpec struct {
126135
taskMeta
136+
// With contains the inputs for this task.
127137
With map[string]interface{} `json:"with,omitempty" yaml:"with,omitempty"`
128138
}
129139

base/log/log.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Package log provides primitives for logging.
12
package log
23

34
import (
@@ -8,20 +9,21 @@ import (
89
"github.com/spf13/viper"
910
)
1011

11-
// Iter8Logger inherits all methods from logrus logger
12+
// Iter8Logger inherits all methods from logrus logger.
1213
type Iter8Logger struct {
1314
*logrus.Logger
1415
}
1516

16-
// StackTrace is the trace from external components like a shell scripts run by an Iter8 task
17+
// StackTrace is the trace from external components like a shell scripts run by an Iter8 task.
1718
type StackTrace struct {
19+
// Trace is the raw trace
1820
Trace string
1921
}
2022

21-
// Logger to be used in all of Iter8
23+
// Logger to be used in all of Iter8.
2224
var Logger *Iter8Logger
2325

24-
// Initialize logger
26+
// init initializes the logger.
2527
func init() {
2628
Logger = &Iter8Logger{logrus.New()}
2729
Logger.SetFormatter(&logrus.TextFormatter{
@@ -39,17 +41,20 @@ func init() {
3941
SetLogLevel(ll)
4042
}
4143

44+
// SetLogLevel to a given logrus log level.
4245
func SetLogLevel(ll logrus.Level) {
4346
Logger.SetLevel(ll)
4447
}
4548

46-
// WithStackTrace yields a log entry with a formatted stack trace field embedded in it
49+
// WithStackTrace yields a log entry with a formatted stack trace field embedded in it.
4750
func (l *Iter8Logger) WithStackTrace(t string) *logrus.Entry {
4851
return l.WithField("stack-trace", &StackTrace{
4952
Trace: t,
5053
})
5154
}
5255

56+
// String processes the stack trace by prefixing each line of the trace with ::Trace::.
57+
// This enables other tools like grep to easily filter out these traces if needed.
5358
func (st *StackTrace) String() string {
5459
out := "below ... \n"
5560
scanner := bufio.NewScanner(strings.NewReader(st.Trace))

0 commit comments

Comments
 (0)