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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,6 +30,7 @@ import ( | |
"path/filepath" | ||
"strings" | ||
"sync" | ||
"time" | ||
|
||
"github.com/fatih/color" | ||
"github.com/mattn/go-colorable" | ||
|
@@ -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 | ||
|
@@ -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() | ||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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), | ||
|
@@ -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(), | ||
|
@@ -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 { | ||
|
@@ -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 | ||
|
@@ -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 | ||
} |
There was a problem hiding this comment.
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...There was a problem hiding this comment.
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