Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redirect console to a file. #833

Merged
merged 2 commits into from Jan 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 10 additions & 2 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion cmd/archive.go
Expand Up @@ -75,7 +75,10 @@ An archive is a fully self-contained test run, and can be executed identically e
return err
}

r.SetOptions(conf.Options)
err = r.SetOptions(conf.Options)
if err != nil {
return err
}

// Archive.
arc := r.MakeArchive()
Expand Down
5 changes: 4 additions & 1 deletion cmd/cloud.go
Expand Up @@ -93,7 +93,10 @@ This will execute the test on the Load Impact cloud service. Use "k6 login cloud
return err
}

r.SetOptions(conf.Options)
err = r.SetOptions(conf.Options)
if err != nil {
return err
}

// Cloud config
cloudConfig := cloud.NewConfig().Apply(conf.Collectors.Cloud)
Expand Down
10 changes: 10 additions & 0 deletions cmd/options.go
Expand Up @@ -67,6 +67,7 @@ func optionFlagSet() *pflag.FlagSet {
flags.String("summary-time-unit", "", "define the time unit used to display the trend stats. Possible units are: 's', 'ms' and 'us'")
flags.StringSlice("system-tags", lib.DefaultSystemTagList, "only include these system tags in metrics")
flags.StringSlice("tag", nil, "add a `tag` to be applied to all samples, as `[name]=[value]`")
flags.String("console-output", "", "redirects the console logging to the provided output file")
flags.Bool("discard-response-bodies", false, "Read but don't process or save HTTP response bodies")
return flags
}
Expand Down Expand Up @@ -174,6 +175,15 @@ func getOptions(flags *pflag.FlagSet) (lib.Options, error) {
opts.RunTags = stats.IntoSampleTags(&parsedRunTags)
}

redirectConFile, err := flags.GetString("console-output")
if err != nil {
return opts, err
}

if redirectConFile != "" {
opts.ConsoleOutput = null.StringFrom(redirectConFile)
}

return opts, nil
}

Expand Down
4 changes: 3 additions & 1 deletion cmd/run.go
Expand Up @@ -171,7 +171,9 @@ a commandline interface for interacting with it.`,
}

// Write options back to the runner too.
r.SetOptions(conf.Options)
if err = r.SetOptions(conf.Options); err != nil {
return err
}

// Create a local executor wrapping the runner.
fprintf(stdout, "%s executor\r", initBar.String())
Expand Down
34 changes: 25 additions & 9 deletions js/console.go
Expand Up @@ -22,21 +22,37 @@ package js

import (
"context"
"os"
"strconv"

"github.com/dop251/goja"
log "github.com/sirupsen/logrus"
)

type Console struct {
// console represents a JS console implemented as a logrus.Logger.
type console struct {
Logger *log.Logger
}

func NewConsole() *Console {
return &Console{log.StandardLogger()}
// Creates a console with the standard logrus logger.
func newConsole() *console {
return &console{log.StandardLogger()}
}

func (c Console) log(ctx *context.Context, level log.Level, msgobj goja.Value, args ...goja.Value) {
// Creates a console logger with its output set to the file at the provided `filepath`.
func newFileConsole(filepath string) (*console, error) {
f, err := os.OpenFile(filepath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
if err != nil {
return nil, err
}

l := log.New()
l.SetOutput(f)

return &console{l}, nil
}

func (c console) log(ctx *context.Context, level log.Level, msgobj goja.Value, args ...goja.Value) {
if ctx != nil && *ctx != nil {
select {
case <-(*ctx).Done():
Expand All @@ -63,22 +79,22 @@ func (c Console) log(ctx *context.Context, level log.Level, msgobj goja.Value, a
}
}

func (c Console) Log(ctx *context.Context, msg goja.Value, args ...goja.Value) {
func (c console) Log(ctx *context.Context, msg goja.Value, args ...goja.Value) {
c.Info(ctx, msg, args...)
}

func (c Console) Debug(ctx *context.Context, msg goja.Value, args ...goja.Value) {
func (c console) Debug(ctx *context.Context, msg goja.Value, args ...goja.Value) {
c.log(ctx, log.DebugLevel, msg, args...)
}

func (c Console) Info(ctx *context.Context, msg goja.Value, args ...goja.Value) {
func (c console) Info(ctx *context.Context, msg goja.Value, args ...goja.Value) {
c.log(ctx, log.InfoLevel, msg, args...)
}

func (c Console) Warn(ctx *context.Context, msg goja.Value, args ...goja.Value) {
func (c console) Warn(ctx *context.Context, msg goja.Value, args ...goja.Value) {
c.log(ctx, log.WarnLevel, msg, args...)
}

func (c Console) Error(ctx *context.Context, msg goja.Value, args ...goja.Value) {
func (c console) Error(ctx *context.Context, msg goja.Value, args ...goja.Value) {
c.log(ctx, log.ErrorLevel, msg, args...)
}
88 changes: 87 additions & 1 deletion js/console_test.go
Expand Up @@ -23,8 +23,12 @@ package js
import (
"context"
"fmt"
"io/ioutil"
"os"
"testing"

"gopkg.in/guregu/null.v3"

"github.com/dop251/goja"
"github.com/loadimpact/k6/js/common"
"github.com/loadimpact/k6/lib"
Expand All @@ -41,7 +45,7 @@ func TestConsoleContext(t *testing.T) {

ctxPtr := new(context.Context)
logger, hook := logtest.NewNullLogger()
rt.Set("console", common.Bind(rt, &Console{logger}, ctxPtr))
rt.Set("console", common.Bind(rt, &console{logger}, ctxPtr))

_, err := common.RunString(rt, `console.log("a")`)
assert.NoError(t, err)
Expand Down Expand Up @@ -122,3 +126,85 @@ func TestConsole(t *testing.T) {
})
}
}

func TestFileConsole(t *testing.T) {
logFile := "/tmp/loadtest.log"
levels := map[string]log.Level{
"log": log.InfoLevel,
"debug": log.DebugLevel,
"info": log.InfoLevel,
"warn": log.WarnLevel,
"error": log.ErrorLevel,
}
argsets := map[string]struct {
Message string
Data log.Fields
}{
`"string"`: {Message: "string"},
`"string","a","b"`: {Message: "string", Data: log.Fields{"0": "a", "1": "b"}},
`"string",1,2`: {Message: "string", Data: log.Fields{"0": "1", "1": "2"}},
`{}`: {Message: "[object Object]"},
}
for name, level := range levels {
t.Run(name, func(t *testing.T) {
for args, result := range argsets {
t.Run(args, func(t *testing.T) {
r, err := New(&lib.SourceData{
Filename: "/script",
Data: []byte(fmt.Sprintf(
`export default function() { console.%s(%s); }`,
name, args,
)),
}, afero.NewMemMapFs(), lib.RuntimeOptions{})
assert.NoError(t, err)

err = r.SetOptions(lib.Options{
ConsoleOutput: null.StringFrom(logFile),
})
assert.NoError(t, err)

samples := make(chan stats.SampleContainer, 100)
vu, err := r.newVU(samples)
assert.NoError(t, err)

vu.Console.Logger.Level = log.DebugLevel
hook := logtest.NewLocal(vu.Console.Logger)

err = vu.RunOnce(context.Background())
assert.NoError(t, err)

// Test if the file was created.
_, err = os.Stat(logFile)
assert.NoError(t, err)

entry := hook.LastEntry()
if assert.NotNil(t, entry, "nothing logged") {
assert.Equal(t, level, entry.Level)
assert.Equal(t, result.Message, entry.Message)

data := result.Data
if data == nil {
data = make(log.Fields)
}
assert.Equal(t, data, entry.Data)

// Test if what we logged to the hook is the same as what we logged
// to the file.
entryStr, err := entry.String()
assert.NoError(t, err)

f, err := os.Open(logFile)
assert.NoError(t, err)

fileContent, err := ioutil.ReadAll(f)
assert.NoError(t, err)

assert.Equal(t, entryStr, string(fileContent))
}

os.Remove(logFile)
})
}
})
}
}
24 changes: 19 additions & 5 deletions js/runner.go
Expand Up @@ -58,6 +58,7 @@ type Runner struct {
Resolver *dnscache.Resolver
RPSLimit *rate.Limiter

console *console
setupData []byte
}

Expand Down Expand Up @@ -92,10 +93,12 @@ func NewFromBundle(b *Bundle) (*Runner, error) {
KeepAlive: 30 * time.Second,
DualStack: true,
},
console: newConsole(),
Resolver: dnscache.New(0),
}
r.SetOptions(r.Bundle.Options)
return r, nil

err = r.SetOptions(r.Bundle.Options)
return r, err
}

func (r *Runner) MakeArchive() *lib.Archive {
Expand Down Expand Up @@ -179,7 +182,7 @@ func (r *Runner) newVU(samplesOut chan<- stats.SampleContainer) (*VU, error) {
Dialer: dialer,
CookieJar: cookieJar,
TLSConfig: tlsConfig,
Console: NewConsole(),
Console: r.console,
BPool: bpool.NewBufferPool(100),
Samples: samplesOut,
}
Expand Down Expand Up @@ -260,13 +263,24 @@ func (r *Runner) GetOptions() lib.Options {
return r.Bundle.Options
}

func (r *Runner) SetOptions(opts lib.Options) {
func (r *Runner) SetOptions(opts lib.Options) error {
r.Bundle.Options = opts

r.RPSLimit = nil
if rps := opts.RPS; rps.Valid {
r.RPSLimit = rate.NewLimiter(rate.Limit(rps.Int64), 1)
}

if consoleOutputFile := opts.ConsoleOutput; consoleOutputFile.Valid {
c, err := newFileConsole(consoleOutputFile.String)
if err != nil {
return err
}

r.console = c
}

return nil
}

// Runs an exported function in its own temporary VU, optionally with an argument. Execution is
Expand Down Expand Up @@ -322,7 +336,7 @@ type VU struct {
ID int64
Iteration int64

Console *Console
Console *console
BPool *bpool.BufferPool

Samples chan<- stats.SampleContainer
Expand Down
7 changes: 7 additions & 0 deletions lib/options.go
Expand Up @@ -272,6 +272,9 @@ type Options struct {

// Discard Http Responses Body
DiscardResponseBodies null.Bool `json:"discardResponseBodies" envconfig:"discard_response_bodies"`

// Redirect console logging to a file
ConsoleOutput null.String `json:"-" envconfig:"console_output"`
}

// Returns the result of overwriting any fields with any that are set on the argument.
Expand Down Expand Up @@ -385,6 +388,10 @@ func (o Options) Apply(opts Options) Options {
if opts.DiscardResponseBodies.Valid {
o.DiscardResponseBodies = opts.DiscardResponseBodies
}
if opts.ConsoleOutput.Valid {
o.ConsoleOutput = opts.ConsoleOutput
}

return o
}

Expand Down
5 changes: 3 additions & 2 deletions lib/runner.go
Expand Up @@ -65,7 +65,7 @@ type Runner interface {
// `export let options = {}`); cmd/run.go will mix this in with CLI-, config- and env-provided
// values and write it back to the runner.
GetOptions() Options
SetOptions(opts Options)
SetOptions(opts Options) error
}

// A VU is a Virtual User, that can be scheduled by an Executor.
Expand Down Expand Up @@ -138,8 +138,9 @@ func (r MiniRunner) GetOptions() Options {
return r.Options
}

func (r *MiniRunner) SetOptions(opts Options) {
func (r *MiniRunner) SetOptions(opts Options) error {
r.Options = opts
return nil
}

// A VU spawned by a MiniRunner.
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.