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

Make k6 wait for loki to finish pushing before stopping #1694

Merged
merged 3 commits into from Nov 25, 2020
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
2 changes: 1 addition & 1 deletion cmd/config_consolidation_test.go
Expand Up @@ -124,7 +124,7 @@ func mostFlagSets() []flagSetInit {
i, fsi := i, fsi // go...
result = append(result, func() *pflag.FlagSet {
flags := pflag.NewFlagSet(fmt.Sprintf("superContrivedFlags_%d", i), pflag.ContinueOnError)
flags.AddFlagSet(rootCmdPersistentFlagSet())
flags.AddFlagSet(new(rootCommand).rootCmdPersistentFlagSet())
flags.AddFlagSet(fsi())
return flags
})
Expand Down
211 changes: 131 additions & 80 deletions cmd/root.go
Expand Up @@ -30,6 +30,7 @@ import (
"path/filepath"
"strings"
"sync"
"time"

"github.com/fatih/color"
"github.com/mattn/go-colorable"
Expand All @@ -54,7 +55,10 @@ var (
stderr = &consoleWriter{colorable.NewColorableStderr(), stderrTTY, outMutex, nil}
)

const defaultConfigFileName = "config.json"
const (
defaultConfigFileName = "config.json"
waitRemoteLoggerTimeout = time.Second * 5
)

//TODO: remove these global variables
//nolint:gochecknoglobals
Expand All @@ -65,53 +69,38 @@ var configFilePath = os.Getenv("K6_CONFIG") // Overridden by `-c`/`--config` fla
//nolint:gochecknoglobals
var (
// TODO: have environment variables for configuring these? hopefully after we move away from global vars though...
verbose bool
quiet bool
noColor bool
logOutput string
logFmt string
address string
quiet bool
noColor bool
address string
)

func getRootCmd(ctx context.Context, logger *logrus.Logger, fallbackLogger logrus.FieldLogger) *cobra.Command {
// RootCmd represents the base command when called without any subcommands.
RootCmd := &cobra.Command{
Use: "k6",
Short: "a next-generation load generator",
Long: BannerColor.Sprintf("\n%s", consts.Banner()),
SilenceUsage: true,
SilenceErrors: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if !cmd.Flags().Changed("log-output") {
if envLogOutput, ok := os.LookupEnv("K6_LOG_OUTPUT"); ok {
logOutput = envLogOutput
}
}
err := setupLoggers(ctx, logger, fallbackLogger, logFmt, logOutput)
if err != nil {
return err
}
// This is to keep all fields needed for the main/root k6 command
type rootCommand struct {
ctx context.Context
logger *logrus.Logger
fallbackLogger logrus.FieldLogger
cmd *cobra.Command
loggerStopped <-chan struct{}
logOutput string
logFmt string
loggerIsRemote bool
verbose bool
}

if noColor {
// TODO: figure out something else... currently, with the wrappers
// below, we're stripping any colors from the output after we've
// added them. The problem is that, besides being very inefficient,
// this actually also strips other special characters from the
// intended output, like the progressbar formatting ones, which
// would otherwise be fine (in a TTY).
//
// It would be much better if we avoid messing with the output and
// instead have a parametrized instance of the color library. It
// will return colored output if colors are enabled and simply
// return the passed input as-is (i.e. be a noop) if colors are
// disabled...
stdout.Writer = colorable.NewNonColorable(os.Stdout)
stderr.Writer = colorable.NewNonColorable(os.Stderr)
}
stdlog.SetOutput(logger.Writer())
logger.Debugf("k6 version: v%s", consts.FullVersion())
return nil
},
func newRootCommand(ctx context.Context, logger *logrus.Logger, fallbackLogger logrus.FieldLogger) *rootCommand {
c := &rootCommand{
ctx: ctx,
logger: logger,
fallbackLogger: fallbackLogger,
}
// the base command when called without any subcommands.
c.cmd = &cobra.Command{
Use: "k6",
Short: "a next-generation load generator",
Long: BannerColor.Sprintf("\n%s", consts.Banner()),
SilenceUsage: true,
SilenceErrors: true,
PersistentPreRunE: c.persistentPreRunE,
}

confDir, err := os.UserConfigDir()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At some later point in this refactoring series, we should probably have an interface with some of the same methods as the os package (Environ(), Getwd() and the other FS ones, etc.) and pass that in as a parameter to these constructors...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm ... that is a good idea

Expand All @@ -126,15 +115,53 @@ func getRootCmd(ctx context.Context, logger *logrus.Logger, fallbackLogger logru
defaultConfigFileName,
)

RootCmd.PersistentFlags().AddFlagSet(rootCmdPersistentFlagSet())
c.cmd.PersistentFlags().AddFlagSet(c.rootCmdPersistentFlagSet())
return c
}

func (c *rootCommand) persistentPreRunE(cmd *cobra.Command, args []string) error {
var err error
if !cmd.Flags().Changed("log-output") {
if envLogOutput, ok := os.LookupEnv("K6_LOG_OUTPUT"); ok {
c.logOutput = envLogOutput
}
}
c.loggerStopped, err = c.setupLoggers()
if err != nil {
return err
}
select {
case <-c.loggerStopped:
default:
c.loggerIsRemote = true
}

return RootCmd
if noColor {
// TODO: figure out something else... currently, with the wrappers
// below, we're stripping any colors from the output after we've
// added them. The problem is that, besides being very inefficient,
// this actually also strips other special characters from the
// intended output, like the progressbar formatting ones, which
// would otherwise be fine (in a TTY).
//
// It would be much better if we avoid messing with the output and
// instead have a parametrized instance of the color library. It
// will return colored output if colors are enabled and simply
// return the passed input as-is (i.e. be a noop) if colors are
// disabled...
Comment on lines +140 to +151
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strong 👍 for fixing this. I didn't find an existing GH issue for it, so can you create one and link it here instead of the explanation?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just moved this around ... this was written by @na-- a while ago a9cc130 😉 I don't think having this in an issue will help anyone, if you start working on it, maybe make one and we can discuss it but in general I consider this a refactoring and having refactoring issues given the amount of issues we have that IMO we will never actually work on .. so they are basically "ideas" makes the issues hard to use so ... yeah, I don't think this needs to be added to an issue.

I myself have added issues (mostly refactoring ones) that again are hardly unlikely for us to ever work on ... and even if we did the fact that there is an issue would've not helped in any way.

On related note, I have a branch with 1 more commit where I get EVEN more of the global variables like noColor in a struct and get it passed around, but that will have to wait for v0.30.0 maybe I can try this then

stdout.Writer = colorable.NewNonColorable(os.Stdout)
stderr.Writer = colorable.NewNonColorable(os.Stderr)
}
stdlog.SetOutput(c.logger.Writer())
c.logger.Debugf("k6 version: v%s", consts.FullVersion())
return nil
}

// Execute adds all child commands to the root command sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
ctx := context.Background()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
logger := &logrus.Logger{
Out: os.Stderr,
Formatter: new(logrus.TextFormatter),
Expand All @@ -149,11 +176,11 @@ func Execute() {
Level: logrus.InfoLevel,
}

RootCmd := getRootCmd(ctx, logger, fallbackLogger)
c := newRootCommand(ctx, logger, fallbackLogger)

loginCmd := getLoginCmd()
loginCmd.AddCommand(getLoginCloudCommand(logger), getLoginInfluxDBCommand(logger))
RootCmd.AddCommand(
c.cmd.AddCommand(
getArchiveCmd(logger),
getCloudCmd(ctx, logger),
getConvertCmd(),
Expand All @@ -168,7 +195,7 @@ func Execute() {
getVersionCmd(),
)

if err := RootCmd.Execute(); err != nil {
if err := c.cmd.Execute(); err != nil {
fields := logrus.Fields{}
code := -1
if e, ok := err.(ExitCode); ok {
Expand All @@ -177,20 +204,40 @@ func Execute() {
fields["hint"] = e.Hint
}
}

logger.WithFields(fields).Error(err)
if c.loggerIsRemote {
fallbackLogger.WithFields(fields).Error(err)
cancel()
c.waitRemoteLogger()
}

os.Exit(code)
}

cancel()
c.waitRemoteLogger()
}

func rootCmdPersistentFlagSet() *pflag.FlagSet {
func (c *rootCommand) waitRemoteLogger() {
if c.loggerIsRemote {
select {
case <-c.loggerStopped:
case <-time.After(waitRemoteLoggerTimeout):
c.fallbackLogger.Error("Remote logger didn't stop in %s", waitRemoteLoggerTimeout)
}
}
}

func (c *rootCommand) rootCmdPersistentFlagSet() *pflag.FlagSet {
flags := pflag.NewFlagSet("", pflag.ContinueOnError)
// TODO: figure out a better way to handle the CLI flags - global variables are not very testable... :/
flags.BoolVarP(&verbose, "verbose", "v", false, "enable debug logging")
flags.BoolVarP(&c.verbose, "verbose", "v", false, "enable debug logging")
flags.BoolVarP(&quiet, "quiet", "q", false, "disable progress updates")
flags.BoolVar(&noColor, "no-color", false, "disable colored output")
flags.StringVar(&logOutput, "log-output", "stderr",
flags.StringVar(&c.logOutput, "log-output", "stderr",
"change the output for k6 logs, possible values are stderr,stdout,none,loki[=host:port]")
flags.StringVar(&logFmt, "logformat", "", "log output format") // TODO rename to log-format and warn on old usage
flags.StringVar(&c.logFmt, "logformat", "", "log output format") // TODO rename to log-format and warn on old usage
flags.StringVarP(&address, "address", "a", "localhost:6565", "address for the api server")

// TODO: Fix... This default value needed, so both CLI flags and environment variables work
Expand Down Expand Up @@ -219,44 +266,48 @@ func (f RawFormatter) Format(entry *logrus.Entry) ([]byte, error) {
return append([]byte(entry.Message), '\n'), nil
}

func setupLoggers(
ctx context.Context, logger *logrus.Logger, fallbackLogger logrus.FieldLogger, logFmt, logOutput string,
) error {
if verbose {
logger.SetLevel(logrus.DebugLevel)
// The returned channel will be closed when the logger has finished flushing and pushing logs after
// the provided context is closed. It is closed if the logger isn't buffering and sending messages
// Asynchronously
func (c *rootCommand) setupLoggers() (<-chan struct{}, error) {
ch := make(chan struct{})
close(ch)

if c.verbose {
c.logger.SetLevel(logrus.DebugLevel)
}
switch logOutput {
switch c.logOutput {
case "stderr":
logger.SetOutput(stderr)
c.logger.SetOutput(stderr)
case "stdout":
logger.SetOutput(stdout)
c.logger.SetOutput(stdout)
case "none":
logger.SetOutput(ioutil.Discard)
c.logger.SetOutput(ioutil.Discard)
default:
if !strings.HasPrefix(logOutput, "loki") {
return fmt.Errorf("unsupported log output `%s`", logOutput)
if !strings.HasPrefix(c.logOutput, "loki") {
return nil, fmt.Errorf("unsupported log output `%s`", c.logOutput)
}
// TODO use some context that we can cancel
hook, err := log.LokiFromConfigLine(ctx, fallbackLogger, logOutput)
ch = make(chan struct{})
hook, err := log.LokiFromConfigLine(c.ctx, c.fallbackLogger, c.logOutput, ch)
if err != nil {
return err
return nil, err
}
logger.AddHook(hook)
logger.SetOutput(ioutil.Discard) // don't output to anywhere else
logFmt = "raw"
c.logger.AddHook(hook)
c.logger.SetOutput(ioutil.Discard) // don't output to anywhere else
c.logFmt = "raw"
noColor = true // disable color
}

switch logFmt {
switch c.logFmt {
case "raw":
logger.SetFormatter(&RawFormatter{})
logger.Debug("Logger format: RAW")
c.logger.SetFormatter(&RawFormatter{})
c.logger.Debug("Logger format: RAW")
case "json":
logger.SetFormatter(&logrus.JSONFormatter{})
logger.Debug("Logger format: JSON")
c.logger.SetFormatter(&logrus.JSONFormatter{})
c.logger.Debug("Logger format: JSON")
default:
logger.SetFormatter(&logrus.TextFormatter{ForceColors: stderrTTY, DisableColors: noColor})
logger.Debug("Logger format: TEXT")
c.logger.SetFormatter(&logrus.TextFormatter{ForceColors: stderrTTY, DisableColors: noColor})
c.logger.Debug("Logger format: TEXT")
}
return nil
return ch, nil
}
8 changes: 4 additions & 4 deletions js/modules/k6/http/request.go
Expand Up @@ -146,7 +146,7 @@ func (h *HTTP) parseRequest(
}

formatFormVal := func(v interface{}) string {
//TODO: handle/warn about unsupported/nested values
// TODO: handle/warn about unsupported/nested values
return fmt.Sprintf("%v", v)
}

Expand Down Expand Up @@ -213,7 +213,7 @@ func (h *HTTP) parseRequest(
if body != nil {
switch data := body.(type) {
case map[string]goja.Value:
//TODO: fix forms submission and serialization in k6/html before fixing this..
// TODO: fix forms submission and serialization in k6/html before fixing this..
newData := map[string]interface{}{}
for k, v := range data {
newData[k] = v.Export()
Expand Down Expand Up @@ -303,11 +303,11 @@ func (h *HTTP) parseRequest(
result.ActiveJar = v.jar
}
case "compression":
var algosString = strings.TrimSpace(params.Get(k).ToString().String())
algosString := strings.TrimSpace(params.Get(k).ToString().String())
if algosString == "" {
continue
}
var algos = strings.Split(algosString, ",")
algos := strings.Split(algosString, ",")
var err error
result.Compressions = make([]httpext.CompressionType, len(algos))
for index, algo := range algos {
Expand Down