Skip to content

Commit

Permalink
Add ability to display testRunDetails and logs from k6 cloud
Browse files Browse the repository at this point in the history
  • Loading branch information
na-- committed Jan 23, 2023
1 parent 106404d commit 4388a35
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 22 deletions.
48 changes: 37 additions & 11 deletions cloudapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http"
"strconv"

"github.com/sirupsen/logrus"
"go.k6.io/k6/lib"
)

Expand All @@ -28,9 +29,17 @@ type TestRun struct {
Duration int64 `json:"duration"`
}

// LogEntry can be used by the cloud to tell k6 to log something to the console,
// so the user can see it.
type LogEntry struct {
Level string `json:"level"`
Message string `json:"message"`
}

type CreateTestRunResponse struct {
ReferenceID string `json:"reference_id"`
ConfigOverride *Config `json:"config"`
ReferenceID string `json:"reference_id"`
ConfigOverride *Config `json:"config"`
Logs []LogEntry `json:"logs"`
}

type TestProgressResponse struct {
Expand All @@ -44,6 +53,20 @@ type LoginResponse struct {
Token string `json:"token"`
}

func (c *Client) handleLogEntriesFromCloud(ctrr CreateTestRunResponse) {
logger := c.logger.WithField("source", "grafana-k6-cloud")
for _, logEntry := range ctrr.Logs {
level, err := logrus.ParseLevel(logEntry.Level)
if err != nil {
logger.Debugf("invalid message level '%s' for message '%s': %s", logEntry.Level, logEntry.Message, err)
level = logrus.ErrorLevel
}
logger.Log(level, logEntry.Message)
}
}

// CreateTestRun is used when a test run is being executed locally, while the
// results are streamed to the cloud, i.e. `k6 run --out cloud script.js`.
func (c *Client) CreateTestRun(testRun *TestRun) (*CreateTestRunResponse, error) {
url := fmt.Sprintf("%s/tests", c.baseURL)
req, err := c.NewRequest("POST", url, testRun)
Expand All @@ -57,54 +80,57 @@ func (c *Client) CreateTestRun(testRun *TestRun) (*CreateTestRunResponse, error)
return nil, err
}

c.handleLogEntriesFromCloud(ctrr)
if ctrr.ReferenceID == "" {
return nil, fmt.Errorf("failed to get a reference ID")
}

return &ctrr, nil
}

func (c *Client) StartCloudTestRun(name string, projectID int64, arc *lib.Archive) (string, error) {
// StartCloudTestRun starts a cloud test run, i.e. `k6 cloud script.js`.
func (c *Client) StartCloudTestRun(name string, projectID int64, arc *lib.Archive) (*CreateTestRunResponse, error) {
requestUrl := fmt.Sprintf("%s/archive-upload", c.baseURL)

var buf bytes.Buffer
mp := multipart.NewWriter(&buf)

if err := mp.WriteField("name", name); err != nil {
return "", err
return nil, err
}

if projectID != 0 {
if err := mp.WriteField("project_id", strconv.FormatInt(projectID, 10)); err != nil {
return "", err
return nil, err
}
}

fw, err := mp.CreateFormFile("file", "archive.tar")
if err != nil {
return "", err
return nil, err
}

if err := arc.Write(fw); err != nil {
return "", err
return nil, err
}

if err := mp.Close(); err != nil {
return "", err
return nil, err
}

req, err := http.NewRequest("POST", requestUrl, &buf)
if err != nil {
return "", err
return nil, err
}

req.Header.Set("Content-Type", mp.FormDataContentType())

ctrr := CreateTestRunResponse{}
if err := c.Do(req, &ctrr); err != nil {
return "", err
return nil, err
}
return ctrr.ReferenceID, nil
c.handleLogEntriesFromCloud(ctrr)
return &ctrr, nil
}

func (c *Client) TestFinished(referenceID string, thresholds ThresholdResult, tained bool, runStatus lib.RunStatus) error {
Expand Down
15 changes: 10 additions & 5 deletions cloudapi/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
)

// Config holds all the necessary data and options for sending metrics to the Load Impact cloud.
//
//nolint:lll
type Config struct {
// TODO: refactor common stuff between cloud execution and output
Expand All @@ -21,11 +22,12 @@ type Config struct {
Host null.String `json:"host" envconfig:"K6_CLOUD_HOST"`
Timeout types.NullDuration `json:"timeout" envconfig:"K6_CLOUD_TIMEOUT"`

LogsTailURL null.String `json:"-" envconfig:"K6_CLOUD_LOGS_TAIL_URL"`
PushRefID null.String `json:"pushRefID" envconfig:"K6_CLOUD_PUSH_REF_ID"`
WebAppURL null.String `json:"webAppURL" envconfig:"K6_CLOUD_WEB_APP_URL"`
NoCompress null.Bool `json:"noCompress" envconfig:"K6_CLOUD_NO_COMPRESS"`
StopOnError null.Bool `json:"stopOnError" envconfig:"K6_CLOUD_STOP_ON_ERROR"`
LogsTailURL null.String `json:"-" envconfig:"K6_CLOUD_LOGS_TAIL_URL"`
PushRefID null.String `json:"pushRefID" envconfig:"K6_CLOUD_PUSH_REF_ID"`
WebAppURL null.String `json:"webAppURL" envconfig:"K6_CLOUD_WEB_APP_URL"`
TestRunDetails null.String `json:"testRunDetails" envconfig:"K6_CLOUD_TEST_RUN_DETAILS"`
NoCompress null.Bool `json:"noCompress" envconfig:"K6_CLOUD_NO_COMPRESS"`
StopOnError null.Bool `json:"stopOnError" envconfig:"K6_CLOUD_STOP_ON_ERROR"`

MaxMetricSamplesPerPackage null.Int `json:"maxMetricSamplesPerPackage" envconfig:"K6_CLOUD_MAX_METRIC_SAMPLES_PER_PACKAGE"`

Expand Down Expand Up @@ -186,6 +188,9 @@ func (c Config) Apply(cfg Config) Config {
if cfg.WebAppURL.Valid {
c.WebAppURL = cfg.WebAppURL
}
if cfg.TestRunDetails.Valid {
c.TestRunDetails = cfg.TestRunDetails
}
if cfg.NoCompress.Valid {
c.NoCompress = cfg.NoCompress
}
Expand Down
4 changes: 4 additions & 0 deletions cloudapi/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,9 @@ package cloudapi

// URLForResults returns the cloud URL with the test run results.
func URLForResults(refID string, config Config) string {
if config.TestRunDetails.Valid {
return config.TestRunDetails.String
}

return config.WebAppURL.String + "/runs/" + refID
}
6 changes: 5 additions & 1 deletion cmd/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,14 @@ func (c *cmdCloud) run(cmd *cobra.Command, args []string) error {
}

modifyAndPrintBar(c.gs, progressBar, pb.WithConstProgress(0, "Uploading archive"))
refID, err := client.StartCloudTestRun(name, cloudConfig.ProjectID.Int64, arc)
cloudTestRun, err := client.StartCloudTestRun(name, cloudConfig.ProjectID.Int64, arc)
if err != nil {
return err
}
refID := cloudTestRun.ReferenceID
if cloudTestRun.ConfigOverride != nil {
cloudConfig = cloudConfig.Apply(*cloudTestRun.ConfigOverride)
}

// Trap Interrupts, SIGINTs and SIGTERMs.
gracefulStop := func(sig os.Signal) {
Expand Down
2 changes: 2 additions & 0 deletions cmd/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,11 @@ func TestConfigCmd(t *testing.T) {
}

for _, data := range testdata {
data := data
t.Run(data.Name, func(t *testing.T) {
t.Parallel()
for _, test := range data.Tests {
test := test
t.Run(`"`+test.Name+`"`, func(t *testing.T) {
t.Parallel()
fs := configFlagSet()
Expand Down
42 changes: 37 additions & 5 deletions cmd/tests/cmd_cloud_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func getMockCloud(

func getSimpleCloudTestState(
t *testing.T, script []byte, cliFlags []string,
progressCallback func() cloudapi.TestProgressResponse,
archiveUpload http.Handler, progressCallback func() cloudapi.TestProgressResponse,
) *GlobalTestState {
if script == nil {
script = []byte(`export default function() {}`)
Expand All @@ -70,7 +70,7 @@ func getSimpleCloudTestState(
cliFlags = []string{"--verbose", "--log-output=stdout"}
}

srv := getMockCloud(t, 123, nil, progressCallback)
srv := getMockCloud(t, 123, archiveUpload, progressCallback)

ts := NewGlobalTestState(t)
require.NoError(t, afero.WriteFile(ts.FS, filepath.Join(ts.Cwd, "test.js"), script, 0o644))
Expand All @@ -85,7 +85,7 @@ func getSimpleCloudTestState(
func TestCloudNotLoggedIn(t *testing.T) {
t.Parallel()

ts := getSimpleCloudTestState(t, nil, nil, nil)
ts := getSimpleCloudTestState(t, nil, nil, nil, nil)
delete(ts.Env, "K6_CLOUD_TOKEN")
ts.ExpectedExitCode = -1 // TODO: use a more specific exit code?
cmd.ExecuteWithGlobalState(ts.GlobalState)
Expand All @@ -112,7 +112,7 @@ func TestCloudLoggedInWithScriptToken(t *testing.T) {
export default function() {};
`

ts := getSimpleCloudTestState(t, []byte(script), nil, nil)
ts := getSimpleCloudTestState(t, []byte(script), nil, nil, nil)
delete(ts.Env, "K6_CLOUD_TOKEN")
cmd.ExecuteWithGlobalState(ts.GlobalState)

Expand All @@ -134,7 +134,7 @@ func TestCloudExitOnRunning(t *testing.T) {
}
}

ts := getSimpleCloudTestState(t, nil, []string{"--exit-on-running", "--log-output=stdout"}, cs)
ts := getSimpleCloudTestState(t, nil, []string{"--exit-on-running", "--log-output=stdout"}, nil, cs)
cmd.ExecuteWithGlobalState(ts.GlobalState)

stdout := ts.Stdout.String()
Expand All @@ -143,3 +143,35 @@ func TestCloudExitOnRunning(t *testing.T) {
assert.Contains(t, stdout, `output: https://app.k6.io/runs/123`)
assert.Contains(t, stdout, `test status: Running`)
}

func TestCloudWithConfigOverride(t *testing.T) {
t.Parallel()

configOverride := http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
resp.WriteHeader(http.StatusOK)
_, err := fmt.Fprint(resp, `{
"reference_id": "123",
"config": {
"webAppURL": "https://bogus.url",
"testRunDetails": "something from the cloud"
},
"logs": [
{"level": "invalid", "message": "test debug message"},
{"level": "warning", "message": "test warning"},
{"level": "error", "message": "test error"}
]
}`)
assert.NoError(t, err)
})
ts := getSimpleCloudTestState(t, nil, nil, configOverride, nil)
cmd.ExecuteWithGlobalState(ts.GlobalState)

stdout := ts.Stdout.String()
t.Log(stdout)
assert.Contains(t, stdout, "execution: cloud")
assert.Contains(t, stdout, "output: something from the cloud")
assert.Contains(t, stdout, `level=debug msg="invalid message level 'invalid' for message 'test debug message'`)
assert.Contains(t, stdout, `level=error msg="test debug message" source=grafana-k6-cloud`)
assert.Contains(t, stdout, `level=warning msg="test warning" source=grafana-k6-cloud`)
assert.Contains(t, stdout, `level=error msg="test error" source=grafana-k6-cloud`)
}
36 changes: 36 additions & 0 deletions cmd/tests/cmd_run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1578,6 +1578,42 @@ func TestRunWithCloudOutputOverrides(t *testing.T) {
assert.Contains(t, stdout, "iterations...........: 1")
}

func TestRunWithCloudOutputMoreOverrides(t *testing.T) {
t.Parallel()

ts := getSingleFileTestState(
t, "export default function () {};",
[]string{"-v", "--log-output=stdout", "--out=cloud"}, 0,
)

configOverride := http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
resp.WriteHeader(http.StatusOK)
_, err := fmt.Fprint(resp, `{
"reference_id": "1337",
"config": {
"webAppURL": "https://bogus.url",
"testRunDetails": "https://some.other.url/foo/tests/org/1337?bar=baz"
},
"logs": [
{"level": "debug", "message": "test debug message"},
{"level": "info", "message": "test message"}
]
}`)
assert.NoError(t, err)
})
srv := getCloudTestEndChecker(t, 1337, configOverride, lib.RunStatusFinished, cloudapi.ResultStatusPassed)
ts.Env["K6_CLOUD_HOST"] = srv.URL

cmd.ExecuteWithGlobalState(ts.GlobalState)

stdout := ts.Stdout.String()
t.Log(stdout)
assert.Contains(t, stdout, "execution: local")
assert.Contains(t, stdout, "output: cloud (https://some.other.url/foo/tests/org/1337?bar=baz)")
assert.Contains(t, stdout, `level=debug msg="test debug message" output=cloud source=grafana-k6-cloud`)
assert.Contains(t, stdout, `level=info msg="test message" output=cloud source=grafana-k6-cloud`)
}

func TestPrometheusRemoteWriteOutput(t *testing.T) {
t.Parallel()

Expand Down

0 comments on commit 4388a35

Please sign in to comment.