From 8841206072c50c81f4b80f514c9a8f129edc3548 Mon Sep 17 00:00:00 2001 From: eduardo Date: Tue, 31 Aug 2021 12:22:16 -0300 Subject: [PATCH 01/11] basic pyrobench cli reusing config/config.go --- benchmark/cmd/command/common.go | 125 +++++++++++++++++++++++++++++++ benchmark/cmd/command/loadgen.go | 22 ++++++ benchmark/cmd/command/root.go | 54 +++++++++++++ benchmark/cmd/main.go | 20 +++++ pkg/config/config.go | 4 + 5 files changed, 225 insertions(+) create mode 100644 benchmark/cmd/command/common.go create mode 100644 benchmark/cmd/command/loadgen.go create mode 100644 benchmark/cmd/command/root.go create mode 100644 benchmark/cmd/main.go diff --git a/benchmark/cmd/command/common.go b/benchmark/cmd/command/common.go new file mode 100644 index 0000000000..3ea9e10683 --- /dev/null +++ b/benchmark/cmd/command/common.go @@ -0,0 +1,125 @@ +// Copied as is from github.com/pyroscope-io/pyroscope/cmd/command/common.go +package command + +import ( + "fmt" + "os" + "reflect" + "strings" + + goexec "os/exec" + + "github.com/mitchellh/mapstructure" + // benchCfg "github.com/pyroscope-io/pyroscope/benchmark/config" + "github.com/pyroscope-io/pyroscope/pkg/config" + "github.com/pyroscope-io/pyroscope/pkg/util/bytesize" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +type cmdRunFn func(cmd *cobra.Command, args []string) error +type cmdStartFn func(cmd *cobra.Command, args []string, logger config.LoggerFunc) error + +func createCmdRunFn(cfg interface{}, vpr *viper.Viper, requiresArgs bool, fn cmdStartFn) cmdRunFn { + return func(cmd *cobra.Command, args []string) error { + var err error + if err = bindFlags(cfg, cmd, vpr); err != nil { + return fmt.Errorf("invalid configuration: %w", err) + } + + var logger func(s string) + if l, ok := cfg.(config.LoggerConfiger); ok { + logger = l.InitializeLogging() + } + + if c, ok := cfg.(config.FileConfiger); ok { + if err = loadConfigFile(c.ConfigFilePath(), cmd, vpr, cfg); err != nil { + return fmt.Errorf("loading configuration file: %w", err) + } + } + + if (requiresArgs && len(args) == 0) || (len(args) > 0 && args[0] == "help") { + _ = cmd.Help() + return nil + } + + if err = fn(cmd, args, logger); err != nil { + cmd.SilenceUsage = true + } + + // Normally, if the program ran, the call should return ExitError and + // the exit code must be preserved. Otherwise, the error originates from + // pyroscope and will be printed. + if e, ok := err.(*goexec.ExitError); ok { + os.Exit(e.ExitCode()) + } + + return err + } +} + +func bindFlags(cfg interface{}, cmd *cobra.Command, vpr *viper.Viper) error { + if err := vpr.BindPFlags(cmd.Flags()); err != nil { + return err + } + return viperUnmarshalWithBytesHook(vpr, cfg) +} + +func newViper() *viper.Viper { + v := viper.New() + v.SetEnvPrefix("PYROSCOPE") + v.AutomaticEnv() + v.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_")) + return v +} + +func loadConfigFile(path string, cmd *cobra.Command, vpr *viper.Viper, v interface{}) error { + if path == "" { + return nil + } + + vpr.SetConfigFile(path) + err := vpr.ReadInConfig() + switch { + case err == nil: + return viperUnmarshalWithBytesHook(vpr, v) + case isUserDefined(cmd.Flag("config"), vpr): + // User-defined configuration can not be read. + return err + case os.IsNotExist(err): + // Default configuration file not found. + return nil + default: + return err + } +} + +func viperUnmarshalWithBytesHook(vpr *viper.Viper, cfg interface{}) error { + return vpr.Unmarshal(cfg, viper.DecodeHook( + mapstructure.ComposeDecodeHookFunc( + // Function to add a special type for «env. mode» + func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) { + if t != reflect.TypeOf(bytesize.Byte) { + return data, nil + } + + stringData, ok := data.(string) + if !ok { + return data, nil + } + + return bytesize.Parse(stringData) + }, + // Function to support net.IP + mapstructure.StringToIPHookFunc(), + // Appended by the two default functions + mapstructure.StringToTimeDurationHookFunc(), + mapstructure.StringToSliceHookFunc(","), + ), + )) +} + +func isUserDefined(f *pflag.Flag, v *viper.Viper) bool { + return f.Changed || (f.DefValue != "" && f.DefValue != v.GetString(f.Name)) +} diff --git a/benchmark/cmd/command/loadgen.go b/benchmark/cmd/command/loadgen.go new file mode 100644 index 0000000000..bda42d634a --- /dev/null +++ b/benchmark/cmd/command/loadgen.go @@ -0,0 +1,22 @@ +package command + +import ( + // benchConfig "github.com/pyroscope-io/pyroscope/benchmark/config" + "github.com/pyroscope-io/pyroscope/pkg/config" + "github.com/spf13/cobra" +) + +func newLoadGen(cfg *config.Bench) *cobra.Command { + vpr := newViper() + loadgenCmd := &cobra.Command{ + Use: "loadgen [flags]", + Short: "Generates load", + RunE: createCmdRunFn(cfg, vpr, false, func(cmd *cobra.Command, args []string, logger config.LoggerFunc) error { + return nil + // return cli.StartServer(cfg) + }), + } + + // cli.PopulateFlagSet(cfg, loadgenCmd.Flags(), vpr, cli.WithSkip("metric-export-rules")) + return loadgenCmd +} diff --git a/benchmark/cmd/command/root.go b/benchmark/cmd/command/root.go new file mode 100644 index 0000000000..3f41f8d26b --- /dev/null +++ b/benchmark/cmd/command/root.go @@ -0,0 +1,54 @@ +package command + +import ( + "fmt" + "os" + "runtime" + "strings" + + "github.com/pyroscope-io/pyroscope/pkg/config" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func newRootCmd(cfg *config.Config) *cobra.Command { + rootCmd := &cobra.Command{ + Use: "pyrobench [flags] ", + } + + return rootCmd +} + +// Initialize adds all child commands to the root command and sets flags appropriately +func Initialize() error { + var cfg config.Config + + rootCmd := newRootCmd(&cfg) + rootCmd.SilenceErrors = true + rootCmd.AddCommand( + newLoadGen(&cfg.Bench), + ) + + logrus.SetReportCaller(true) + logrus.SetFormatter(&logrus.TextFormatter{ + TimestampFormat: "2006-01-02T15:04:05.000000", + FullTimestamp: true, + CallerPrettyfier: func(f *runtime.Frame) (string, string) { + filename := f.File + if len(filename) > 38 { + filename = filename[38:] + } + return "", fmt.Sprintf(" %s:%d", filename, f.Line) + }, + }) + + args := os.Args[1:] + for i, arg := range args { + if len(arg) > 2 && strings.HasPrefix(arg, "-") && !strings.HasPrefix(arg, "--") { + args[i] = fmt.Sprintf("-%s", arg) + } + } + + rootCmd.SetArgs(args) + return rootCmd.Execute() +} diff --git a/benchmark/cmd/main.go b/benchmark/cmd/main.go new file mode 100644 index 0000000000..281a89d98f --- /dev/null +++ b/benchmark/cmd/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "fmt" + "os" + + "github.com/fatih/color" + "github.com/pyroscope-io/pyroscope/benchmark/cmd/command" +) + +func main() { + if err := command.Initialize(); err != nil { + fatalf("%s %v\n\n", color.RedString("Error:"), err) + } +} + +func fatalf(format string, args ...interface{}) { + _, _ = fmt.Fprintf(os.Stderr, format, args...) + os.Exit(1) +} diff --git a/pkg/config/config.go b/pkg/config/config.go index 189b6ec34d..64f4806744 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -14,6 +14,7 @@ type Config struct { Convert Convert `skip:"true" mapstructure:",squash"` Exec Exec `skip:"true" mapstructure:",squash"` DbManager DbManager `skip:"true" mapstructure:",squash"` + Bench Bench `skip:"true" mapstructure:", squash'` } type Agent struct { @@ -186,3 +187,6 @@ type Exec struct { Tags map[string]string `name:"tag" def:"" desc:"tag in key=value form. The flag may be specified multiple times" mapstructure:"tags"` } + +type Bench struct { +} From a0659df4ab2fa676d1c4c812c5da575e77dd4792 Mon Sep 17 00:00:00 2001 From: eduardo Date: Tue, 31 Aug 2021 13:36:35 -0300 Subject: [PATCH 02/11] basic pyrobench cli using its own config.go --- benchmark/cmd/command/common.go | 3 +-- benchmark/cmd/command/loadgen.go | 7 ++----- benchmark/cmd/command/root.go | 4 ++-- benchmark/config/config.go | 9 +++++++++ pkg/config/config.go | 4 ---- 5 files changed, 14 insertions(+), 13 deletions(-) create mode 100644 benchmark/config/config.go diff --git a/benchmark/cmd/command/common.go b/benchmark/cmd/command/common.go index 3ea9e10683..80602d3ca0 100644 --- a/benchmark/cmd/command/common.go +++ b/benchmark/cmd/command/common.go @@ -10,8 +10,7 @@ import ( goexec "os/exec" "github.com/mitchellh/mapstructure" - // benchCfg "github.com/pyroscope-io/pyroscope/benchmark/config" - "github.com/pyroscope-io/pyroscope/pkg/config" + "github.com/pyroscope-io/pyroscope/benchmark/config" "github.com/pyroscope-io/pyroscope/pkg/util/bytesize" "github.com/spf13/cobra" "github.com/spf13/pflag" diff --git a/benchmark/cmd/command/loadgen.go b/benchmark/cmd/command/loadgen.go index bda42d634a..fb1805e27b 100644 --- a/benchmark/cmd/command/loadgen.go +++ b/benchmark/cmd/command/loadgen.go @@ -1,22 +1,19 @@ package command import ( - // benchConfig "github.com/pyroscope-io/pyroscope/benchmark/config" - "github.com/pyroscope-io/pyroscope/pkg/config" + "github.com/pyroscope-io/pyroscope/benchmark/config" "github.com/spf13/cobra" ) -func newLoadGen(cfg *config.Bench) *cobra.Command { +func newLoadGen(cfg *config.Config) *cobra.Command { vpr := newViper() loadgenCmd := &cobra.Command{ Use: "loadgen [flags]", Short: "Generates load", RunE: createCmdRunFn(cfg, vpr, false, func(cmd *cobra.Command, args []string, logger config.LoggerFunc) error { return nil - // return cli.StartServer(cfg) }), } - // cli.PopulateFlagSet(cfg, loadgenCmd.Flags(), vpr, cli.WithSkip("metric-export-rules")) return loadgenCmd } diff --git a/benchmark/cmd/command/root.go b/benchmark/cmd/command/root.go index 3f41f8d26b..ec88b02c81 100644 --- a/benchmark/cmd/command/root.go +++ b/benchmark/cmd/command/root.go @@ -6,7 +6,7 @@ import ( "runtime" "strings" - "github.com/pyroscope-io/pyroscope/pkg/config" + "github.com/pyroscope-io/pyroscope/benchmark/config" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -26,7 +26,7 @@ func Initialize() error { rootCmd := newRootCmd(&cfg) rootCmd.SilenceErrors = true rootCmd.AddCommand( - newLoadGen(&cfg.Bench), + newLoadGen(&cfg), ) logrus.SetReportCaller(true) diff --git a/benchmark/config/config.go b/benchmark/config/config.go new file mode 100644 index 0000000000..aac335c180 --- /dev/null +++ b/benchmark/config/config.go @@ -0,0 +1,9 @@ +package config + +type Config struct { + Version bool `mapstructure:"version"` +} + +type LoggerFunc func(s string) +type LoggerConfiger interface{ InitializeLogging() LoggerFunc } +type FileConfiger interface{ ConfigFilePath() string } diff --git a/pkg/config/config.go b/pkg/config/config.go index 64f4806744..189b6ec34d 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -14,7 +14,6 @@ type Config struct { Convert Convert `skip:"true" mapstructure:",squash"` Exec Exec `skip:"true" mapstructure:",squash"` DbManager DbManager `skip:"true" mapstructure:",squash"` - Bench Bench `skip:"true" mapstructure:", squash'` } type Agent struct { @@ -187,6 +186,3 @@ type Exec struct { Tags map[string]string `name:"tag" def:"" desc:"tag in key=value form. The flag may be specified multiple times" mapstructure:"tags"` } - -type Bench struct { -} From 580ed5fe3b43026211bbeea5cae46f4757927714 Mon Sep 17 00:00:00 2001 From: eduardo Date: Tue, 31 Aug 2021 14:00:28 -0300 Subject: [PATCH 03/11] pyrobench: make cli style same as the original cli --- benchmark/cmd/command/banner.go | 58 +++++++++++++++++++ benchmark/cmd/command/common.go | 2 +- benchmark/cmd/command/loadgen.go | 6 ++ benchmark/cmd/command/root.go | 11 ++++ benchmark/cmd/command/usage.go | 98 ++++++++++++++++++++++++++++++++ 5 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 benchmark/cmd/command/banner.go create mode 100644 benchmark/cmd/command/usage.go diff --git a/benchmark/cmd/command/banner.go b/benchmark/cmd/command/banner.go new file mode 100644 index 0000000000..c5f324d6e5 --- /dev/null +++ b/benchmark/cmd/command/banner.go @@ -0,0 +1,58 @@ +// Copied from github.com/pyroscope-io/pyroscope/cmd/command/banner.go +package command + +import ( + "strings" + + "github.com/aybabtme/rgbterm" + "github.com/fatih/color" +) + +// made here http://patorjk.com/software/taag/#p=display&f=Doom&t=Pyrobench +var banner = ` +______ _ _ +| ___ \ | | | | +| |_/ / _ _ __ ___ | |__ ___ _ __ ___| |__ +| __/ | | | '__/ _ \| '_ \ / _ \ '_ \ / __| '_ \ +| | | |_| | | | (_) | |_) | __/ | | | (__| | | | +\_| \__, |_| \___/|_.__/ \___|_| |_|\___|_| |_| + __/ | + |___/ +` + +func init() { + // removes extra new lines + banner = banner[1 : len(banner)-2] +} + +const ( + startColor = 0xffd651 + endColor = 0xf64d3d +) + +func gradient(start, end, offset int, progress float64) uint8 { + start = (start >> offset) & 0xff + end = (end >> offset) & 0xff + return uint8(start + int(float64(end-start)*progress)) +} + +func gradientBanner() string { + if color.NoColor { + return banner + "\n" + } + + str := "" + arr := strings.Split(banner, "\n") + l := len(arr) + for i, line := range arr { + if line == "" { + break + } + progress := float64(i) / float64(l-1) + r := gradient(startColor, endColor, 16, progress) + g := gradient(startColor, endColor, 8, progress) + b := gradient(startColor, endColor, 0, progress) + str += rgbterm.FgString(line, r, g, b) + "\n" + } + return str +} diff --git a/benchmark/cmd/command/common.go b/benchmark/cmd/command/common.go index 80602d3ca0..76bb845324 100644 --- a/benchmark/cmd/command/common.go +++ b/benchmark/cmd/command/common.go @@ -67,7 +67,7 @@ func bindFlags(cfg interface{}, cmd *cobra.Command, vpr *viper.Viper) error { func newViper() *viper.Viper { v := viper.New() - v.SetEnvPrefix("PYROSCOPE") + v.SetEnvPrefix("PYROBENCH") v.AutomaticEnv() v.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_")) return v diff --git a/benchmark/cmd/command/loadgen.go b/benchmark/cmd/command/loadgen.go index fb1805e27b..f5eacb7b0c 100644 --- a/benchmark/cmd/command/loadgen.go +++ b/benchmark/cmd/command/loadgen.go @@ -1,7 +1,10 @@ package command import ( + "fmt" + "github.com/pyroscope-io/pyroscope/benchmark/config" + "github.com/pyroscope-io/pyroscope/pkg/cli" "github.com/spf13/cobra" ) @@ -11,9 +14,12 @@ func newLoadGen(cfg *config.Config) *cobra.Command { Use: "loadgen [flags]", Short: "Generates load", RunE: createCmdRunFn(cfg, vpr, false, func(cmd *cobra.Command, args []string, logger config.LoggerFunc) error { + fmt.Println("address", cfg.ServerAddress) + return nil }), } + cli.PopulateFlagSet(cfg, loadgenCmd.Flags(), vpr) return loadgenCmd } diff --git a/benchmark/cmd/command/root.go b/benchmark/cmd/command/root.go index ec88b02c81..c4bc6cf824 100644 --- a/benchmark/cmd/command/root.go +++ b/benchmark/cmd/command/root.go @@ -16,6 +16,17 @@ func newRootCmd(cfg *config.Config) *cobra.Command { Use: "pyrobench [flags] ", } + rootCmd.SetUsageFunc(func(cmd *cobra.Command) error { + fmt.Println(gradientBanner()) + fmt.Println(DefaultUsageFunc(cmd.Flags(), cmd)) + return nil + }) + + rootCmd.SetHelpFunc(func(cmd *cobra.Command, a []string) { + fmt.Println(gradientBanner()) + fmt.Println(DefaultUsageFunc(cmd.Flags(), cmd)) + }) + return rootCmd } diff --git a/benchmark/cmd/command/usage.go b/benchmark/cmd/command/usage.go new file mode 100644 index 0000000000..d05fb6a59e --- /dev/null +++ b/benchmark/cmd/command/usage.go @@ -0,0 +1,98 @@ +// Copied as is from github.com/pyroscope-io/pyroscope/cmd/command/usage.go +package command + +import ( + "fmt" + "strings" + "text/tabwriter" + + "github.com/fatih/color" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +var ( + headerClr *color.Color + itemClr *color.Color + descClr *color.Color + defClr *color.Color +) + +func init() { + headerClr = color.New(color.FgGreen) + itemClr = color.New(color.Bold) + // itemClr = color.New() + descClr = color.New() + defClr = color.New(color.FgYellow) +} + +// TODO: Do we want to keep this or use cobra default one? Maybe banner + cobra default? Or something else? +// This is mostly copied from ffcli package +func DefaultUsageFunc(sf *pflag.FlagSet, c *cobra.Command) string { + var b strings.Builder + + fmt.Fprintf(&b, "continuous profiling platform\n\n") + headerClr.Fprintf(&b, "USAGE\n") + if c.Use != "" { + fmt.Fprintf(&b, " %s\n", c.Use) + } else { + fmt.Fprintf(&b, " %s\n", c.Name()) + } + fmt.Fprintf(&b, "\n") + + if c.Long != "" { + fmt.Fprintf(&b, "%s\n\n", c.Long) + } + + if c.HasSubCommands() { + headerClr.Fprintf(&b, "SUBCOMMANDS\n") + tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0) + for _, subcommand := range c.Commands() { + if !subcommand.Hidden { + fmt.Fprintf(tw, " %s\t%s\n", itemClr.Sprintf(subcommand.Name()), subcommand.Short) + } + } + tw.Flush() + fmt.Fprintf(&b, "\n") + } + + if countFlags(c.Flags()) > 0 { + // headerClr.Fprintf(&b, "FLAGS\n") + tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0) + fmt.Fprintf(tw, "%s\t %s@new-line@\n", headerClr.Sprintf("FLAGS"), defClr.Sprint("DEFAULT VALUES")) + + // TODO: it would be nice to sort by how often people would use these. + // But for that we'd have to have a conversion from flag-set back to struct + sf.VisitAll(func(f *pflag.Flag) { + if f.Hidden { + return + } + def := f.DefValue + // if def == "" { + // def = "..." + // } + def = defClr.Sprint(def) + // def = fmt.Sprintf("(%s)", def) + fmt.Fprintf(tw, " %s\t%s", itemClr.Sprintf("--"+f.Name), def) + if f.Usage != "" { + fmt.Fprintf(tw, "@new-line@ ") + descClr.Fprint(tw, f.Usage) + } + descClr.Fprint(tw, "@new-line@") + fmt.Fprint(tw, "\n") + }) + tw.Flush() + // fmt.Fprintf(&b, "\n") + } + + if c.HasSubCommands() { + b.WriteString("Run 'pyroscope SUBCOMMAND --help' for more information on a subcommand.\n") + } + + return strings.ReplaceAll(b.String(), "@new-line@", "\n") +} + +func countFlags(fs *pflag.FlagSet) (n int) { + fs.VisitAll(func(*pflag.Flag) { n++ }) + return n +} From 1536b758b23d99c99dfd856ca7559439de59dae6 Mon Sep 17 00:00:00 2001 From: eduardo Date: Tue, 31 Aug 2021 16:44:38 -0300 Subject: [PATCH 04/11] pyrobench: basic load generation --- benchmark/cmd/command/loadgen.go | 9 +- benchmark/cmd/command/root.go | 4 +- benchmark/cmd/logging.go | 23 +++++ benchmark/config/config.go | 20 ++-- benchmark/config/interface.go | 18 ++++ benchmark/loadgen/loadgen.go | 170 +++++++++++++++++++++++++++++++ 6 files changed, 230 insertions(+), 14 deletions(-) create mode 100644 benchmark/cmd/logging.go create mode 100644 benchmark/config/interface.go create mode 100644 benchmark/loadgen/loadgen.go diff --git a/benchmark/cmd/command/loadgen.go b/benchmark/cmd/command/loadgen.go index f5eacb7b0c..8c28647a38 100644 --- a/benchmark/cmd/command/loadgen.go +++ b/benchmark/cmd/command/loadgen.go @@ -1,22 +1,19 @@ package command import ( - "fmt" - "github.com/pyroscope-io/pyroscope/benchmark/config" + "github.com/pyroscope-io/pyroscope/benchmark/loadgen" "github.com/pyroscope-io/pyroscope/pkg/cli" "github.com/spf13/cobra" ) -func newLoadGen(cfg *config.Config) *cobra.Command { +func newLoadGen(cfg *config.LoadGen) *cobra.Command { vpr := newViper() loadgenCmd := &cobra.Command{ Use: "loadgen [flags]", Short: "Generates load", RunE: createCmdRunFn(cfg, vpr, false, func(cmd *cobra.Command, args []string, logger config.LoggerFunc) error { - fmt.Println("address", cfg.ServerAddress) - - return nil + return loadgen.Cli(cfg) }), } diff --git a/benchmark/cmd/command/root.go b/benchmark/cmd/command/root.go index c4bc6cf824..45aaf1bf38 100644 --- a/benchmark/cmd/command/root.go +++ b/benchmark/cmd/command/root.go @@ -11,7 +11,7 @@ import ( "github.com/spf13/cobra" ) -func newRootCmd(cfg *config.Config) *cobra.Command { +func newRootCmd(cfg *config.LoadGen) *cobra.Command { rootCmd := &cobra.Command{ Use: "pyrobench [flags] ", } @@ -32,7 +32,7 @@ func newRootCmd(cfg *config.Config) *cobra.Command { // Initialize adds all child commands to the root command and sets flags appropriately func Initialize() error { - var cfg config.Config + var cfg config.LoadGen rootCmd := newRootCmd(&cfg) rootCmd.SilenceErrors = true diff --git a/benchmark/cmd/logging.go b/benchmark/cmd/logging.go new file mode 100644 index 0000000000..a26535298d --- /dev/null +++ b/benchmark/cmd/logging.go @@ -0,0 +1,23 @@ +// Copied as is from github.com/pyroscope-io/pyroscope/cmd/logging.go +package main + +import ( + "log" + "os" + "runtime" + + "github.com/fatih/color" + "github.com/sirupsen/logrus" +) + +func init() { + log.SetFlags(log.Lshortfile | log.Ldate | log.Ltime) + + logrus.SetFormatter(&logrus.TextFormatter{}) + logrus.SetOutput(os.Stdout) + logrus.SetLevel(logrus.DebugLevel) + + if runtime.GOOS == "windows" { + color.NoColor = true + } +} diff --git a/benchmark/config/config.go b/benchmark/config/config.go index aac335c180..1acb74b7e6 100644 --- a/benchmark/config/config.go +++ b/benchmark/config/config.go @@ -1,9 +1,17 @@ package config -type Config struct { - Version bool `mapstructure:"version"` -} +type LoadGen struct { + LogLevel string `def:"info" desc:"log level: debug|info|warn|error" mapstructure:"log-level"` + + ServerAddress string `def:"http://localhost:4040" desc:"address of the pyroscope instance being attacked" mapstructure:"server-address"` + RandSeed int `def:"23061912" desc:""` + ProfileWidth int `def:"20"` + ProfileDepth int `def:"20"` + ProfileSymbolLength int `def:"30"` + Fixtures int `def:"30" desc:"how many different profiles to generate per app"` + Apps int `def:"20" desc:"how many pyroscope apps to emulate"` + Clients int `def:"20" desc:"how many pyroscope clients to emulate"` + Requests int `def:"10000" desc:"how many requests each clients should make"` -type LoggerFunc func(s string) -type LoggerConfiger interface{ InitializeLogging() LoggerFunc } -type FileConfiger interface{ ConfigFilePath() string } + WaitUntilAvailable bool `def:"true" desc:"wait until endpoint is available"` +} diff --git a/benchmark/config/interface.go b/benchmark/config/interface.go new file mode 100644 index 0000000000..13e2e5ea08 --- /dev/null +++ b/benchmark/config/interface.go @@ -0,0 +1,18 @@ +package config + +import ( + "github.com/sirupsen/logrus" +) + +type FileConfiger interface{ ConfigFilePath() string } + +type LoggerFunc func(s string) +type LoggerConfiger interface{ InitializeLogging() LoggerFunc } + +func (cfg LoadGen) InitializeLogging() LoggerFunc { + if l, err := logrus.ParseLevel(cfg.LogLevel); err == nil { + logrus.SetLevel(l) + } + + return nil +} diff --git a/benchmark/loadgen/loadgen.go b/benchmark/loadgen/loadgen.go new file mode 100644 index 0000000000..4e07923469 --- /dev/null +++ b/benchmark/loadgen/loadgen.go @@ -0,0 +1,170 @@ +package loadgen + +import ( + "encoding/hex" + "fmt" + "math/rand" + "net/http" + "sync" + "time" + + "github.com/pyroscope-io/pyroscope/benchmark/config" + "github.com/pyroscope-io/pyroscope/pkg/agent/upstream" + "github.com/pyroscope-io/pyroscope/pkg/agent/upstream/remote" + "github.com/pyroscope-io/pyroscope/pkg/structs/transporttrie" + "github.com/sirupsen/logrus" +) + +// how many retries to check the pyroscope server is up +const MaxReadinessRetries = 10 + +type Fixtures [][]*transporttrie.Trie + +type LoadGen struct { + Config *config.LoadGen + Rand *rand.Rand + SymbolBuf []byte +} + +func Cli(cfg *config.LoadGen) error { + r := rand.New(rand.NewSource(int64(cfg.RandSeed))) + l := &LoadGen{ + Config: cfg, + Rand: r, + SymbolBuf: make([]byte, cfg.ProfileSymbolLength), + } + + return l.Run(cfg) +} + +func (l *LoadGen) Run(cfg *config.LoadGen) error { + logrus.Info("checking server is available...") + err := waitUntilEndpointReady(cfg.ServerAddress) + if err != nil { + return err + } + + logrus.Info("generating fixtures") + fixtures := l.generateFixtures() + logrus.Debug("done generating fixtures.") + + logrus.Info("starting sending requests") + wg := sync.WaitGroup{} + wg.Add(l.Config.Apps * l.Config.Clients) + appNameBuf := make([]byte, 25) + + for i := 0; i < l.Config.Apps; i++ { + // generate a random app name + l.Rand.Read(appNameBuf) + appName := hex.EncodeToString(appNameBuf) + for j := 0; j < l.Config.Clients; j++ { + go l.startClientThread(appName, &wg, fixtures[i]) + } + } + wg.Wait() + + logrus.Debug("done sending requests") + return nil +} + +func (l *LoadGen) generateFixtures() Fixtures { + var f Fixtures + + for i := 0; i < l.Config.Apps; i++ { + f = append(f, []*transporttrie.Trie{}) + + randomGen := rand.New(rand.NewSource(int64(l.Config.RandSeed + i))) + p := l.generateProfile(randomGen) + for j := 0; j < l.Config.Fixtures; j++ { + f[i] = append(f[i], p) + } + } + + return f +} + +func (l *LoadGen) startClientThread(appName string, wg *sync.WaitGroup, appFixtures []*transporttrie.Trie) { + rc := remote.RemoteConfig{ + UpstreamThreads: 1, + UpstreamAddress: l.Config.ServerAddress, + UpstreamRequestTimeout: 10 * time.Second, + } + r, err := remote.New(rc, logrus.StandardLogger()) + if err != nil { + panic(err) + } + + requestsCount := l.Config.Requests + + threadStartTime := time.Now().Truncate(10 * time.Second) + threadStartTime = threadStartTime.Add(time.Duration(-1*requestsCount) * (10 * time.Second)) + + st := threadStartTime + + for i := 0; i < requestsCount; i++ { + t := appFixtures[i%len(appFixtures)] + + st = st.Add(10 * time.Second) + et := st.Add(10 * time.Second) + err := r.UploadSync(&upstream.UploadJob{ + Name: appName + "{}", + StartTime: st, + EndTime: et, + SpyName: "gospy", + SampleRate: 100, + Units: "samples", + AggregationType: "sum", + Trie: t, + }) + if err != nil { + // TODO(eh-am): calculate errors + time.Sleep(time.Second) + } else { + // TODO(eh-am): calculate success + } + } + + wg.Done() +} + +func (l *LoadGen) generateProfile(randomGen *rand.Rand) *transporttrie.Trie { + t := transporttrie.New() + + for w := 0; w < l.Config.ProfileWidth; w++ { + symbol := []byte("root") + for d := 0; d < 2+l.Rand.Intn(l.Config.ProfileDepth); d++ { + randomGen.Read(l.SymbolBuf) + symbol = append(symbol, byte(';')) + symbol = append(symbol, []byte(hex.EncodeToString(l.SymbolBuf))...) + if l.Rand.Intn(100) <= 20 { + t.Insert(symbol, uint64(l.Rand.Intn(100)), true) + } + } + + t.Insert(symbol, uint64(l.Rand.Intn(100)), true) + } + return t +} + +// TODO(eh-am) exponential backoff and whatnot +func waitUntilEndpointReady(url string) error { + client := http.Client{Timeout: 10 * time.Second} + retries := 0 + + for { + _, err := client.Get(url) + + // all good? + if err == nil { + return nil + } + if retries >= MaxReadinessRetries { + break + } + + time.Sleep(time.Second) + retries++ + } + + return fmt.Errorf("maximum retries exceeded ('%d') waiting for server ('%s') to respond", retries, url) +} From 387170cce473b85e167316121489322f39120cdc Mon Sep 17 00:00:00 2001 From: eduardo Date: Tue, 31 Aug 2021 16:57:35 -0300 Subject: [PATCH 05/11] pyrobench: copy over refactor from main --- benchmark/cmd/command/common.go | 37 ++++++++------------------------ benchmark/cmd/command/loadgen.go | 2 +- benchmark/config/config.go | 3 +++ 3 files changed, 13 insertions(+), 29 deletions(-) diff --git a/benchmark/cmd/command/common.go b/benchmark/cmd/command/common.go index 76bb845324..8653620251 100644 --- a/benchmark/cmd/command/common.go +++ b/benchmark/cmd/command/common.go @@ -7,10 +7,9 @@ import ( "reflect" "strings" - goexec "os/exec" - "github.com/mitchellh/mapstructure" "github.com/pyroscope-io/pyroscope/benchmark/config" + "github.com/pyroscope-io/pyroscope/pkg/cli" "github.com/pyroscope-io/pyroscope/pkg/util/bytesize" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -18,42 +17,24 @@ import ( ) type cmdRunFn func(cmd *cobra.Command, args []string) error -type cmdStartFn func(cmd *cobra.Command, args []string, logger config.LoggerFunc) error -func createCmdRunFn(cfg interface{}, vpr *viper.Viper, requiresArgs bool, fn cmdStartFn) cmdRunFn { +func createCmdRunFn(cfg interface{}, vpr *viper.Viper, fn cmdRunFn) cmdRunFn { return func(cmd *cobra.Command, args []string) error { var err error - if err = bindFlags(cfg, cmd, vpr); err != nil { - return fmt.Errorf("invalid configuration: %w", err) - } - - var logger func(s string) - if l, ok := cfg.(config.LoggerConfiger); ok { - logger = l.InitializeLogging() + if err = vpr.BindPFlags(cmd.Flags()); err != nil { + return err } - - if c, ok := cfg.(config.FileConfiger); ok { - if err = loadConfigFile(c.ConfigFilePath(), cmd, vpr, cfg); err != nil { + if c, ok := cfg.(config.File); ok { + if err = loadConfigFile(c.Path(), cmd, vpr, cfg); err != nil { return fmt.Errorf("loading configuration file: %w", err) } } - - if (requiresArgs && len(args) == 0) || (len(args) > 0 && args[0] == "help") { - _ = cmd.Help() - return nil + if err = cli.Unmarshal(vpr, cfg); err != nil { + return err } - - if err = fn(cmd, args, logger); err != nil { + if err = fn(cmd, args); err != nil { cmd.SilenceUsage = true } - - // Normally, if the program ran, the call should return ExitError and - // the exit code must be preserved. Otherwise, the error originates from - // pyroscope and will be printed. - if e, ok := err.(*goexec.ExitError); ok { - os.Exit(e.ExitCode()) - } - return err } } diff --git a/benchmark/cmd/command/loadgen.go b/benchmark/cmd/command/loadgen.go index 8c28647a38..a794348d91 100644 --- a/benchmark/cmd/command/loadgen.go +++ b/benchmark/cmd/command/loadgen.go @@ -12,7 +12,7 @@ func newLoadGen(cfg *config.LoadGen) *cobra.Command { loadgenCmd := &cobra.Command{ Use: "loadgen [flags]", Short: "Generates load", - RunE: createCmdRunFn(cfg, vpr, false, func(cmd *cobra.Command, args []string, logger config.LoggerFunc) error { + RunE: createCmdRunFn(cfg, vpr, func(_ *cobra.Command, _ []string) error { return loadgen.Cli(cfg) }), } diff --git a/benchmark/config/config.go b/benchmark/config/config.go index 1acb74b7e6..50a0ee83ae 100644 --- a/benchmark/config/config.go +++ b/benchmark/config/config.go @@ -15,3 +15,6 @@ type LoadGen struct { WaitUntilAvailable bool `def:"true" desc:"wait until endpoint is available"` } + +// File can be read from file system. +type File interface{ Path() string } From e0e614c49bf8d11f914c82948589ee11cbe009e2 Mon Sep 17 00:00:00 2001 From: eduardo Date: Tue, 31 Aug 2021 18:34:57 -0300 Subject: [PATCH 06/11] pyrobench: use a wrapper config --- benchmark/cmd/command/root.go | 6 +++--- benchmark/config/config.go | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/benchmark/cmd/command/root.go b/benchmark/cmd/command/root.go index 45aaf1bf38..21cc48129d 100644 --- a/benchmark/cmd/command/root.go +++ b/benchmark/cmd/command/root.go @@ -32,12 +32,12 @@ func newRootCmd(cfg *config.LoadGen) *cobra.Command { // Initialize adds all child commands to the root command and sets flags appropriately func Initialize() error { - var cfg config.LoadGen + var cfg config.Config - rootCmd := newRootCmd(&cfg) + rootCmd := newRootCmd(&cfg.LoadGen) rootCmd.SilenceErrors = true rootCmd.AddCommand( - newLoadGen(&cfg), + newLoadGen(&cfg.LoadGen), ) logrus.SetReportCaller(true) diff --git a/benchmark/config/config.go b/benchmark/config/config.go index 50a0ee83ae..b8de249551 100644 --- a/benchmark/config/config.go +++ b/benchmark/config/config.go @@ -1,5 +1,9 @@ package config +type Config struct { + LoadGen LoadGen `skip:"true" mapstructure:",squash"` +} + type LoadGen struct { LogLevel string `def:"info" desc:"log level: debug|info|warn|error" mapstructure:"log-level"` From 1b82733c46b277b4140e7fd3bf4975c2e8647253 Mon Sep 17 00:00:00 2001 From: eduardo Date: Wed, 1 Sep 2021 11:32:55 -0300 Subject: [PATCH 07/11] move gradient banner generation to pkg/cli --- benchmark/cmd/command/banner.go | 36 ++--------------------------- cmd/pyroscope/command/banner.go | 35 ++--------------------------- pkg/cli/banner.go | 40 +++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 67 deletions(-) create mode 100644 pkg/cli/banner.go diff --git a/benchmark/cmd/command/banner.go b/benchmark/cmd/command/banner.go index c5f324d6e5..142a997875 100644 --- a/benchmark/cmd/command/banner.go +++ b/benchmark/cmd/command/banner.go @@ -1,11 +1,7 @@ -// Copied from github.com/pyroscope-io/pyroscope/cmd/command/banner.go package command import ( - "strings" - - "github.com/aybabtme/rgbterm" - "github.com/fatih/color" + "github.com/pyroscope-io/pyroscope/pkg/cli" ) // made here http://patorjk.com/software/taag/#p=display&f=Doom&t=Pyrobench @@ -25,34 +21,6 @@ func init() { banner = banner[1 : len(banner)-2] } -const ( - startColor = 0xffd651 - endColor = 0xf64d3d -) - -func gradient(start, end, offset int, progress float64) uint8 { - start = (start >> offset) & 0xff - end = (end >> offset) & 0xff - return uint8(start + int(float64(end-start)*progress)) -} - func gradientBanner() string { - if color.NoColor { - return banner + "\n" - } - - str := "" - arr := strings.Split(banner, "\n") - l := len(arr) - for i, line := range arr { - if line == "" { - break - } - progress := float64(i) / float64(l-1) - r := gradient(startColor, endColor, 16, progress) - g := gradient(startColor, endColor, 8, progress) - b := gradient(startColor, endColor, 0, progress) - str += rgbterm.FgString(line, r, g, b) + "\n" - } - return str + return cli.GradientBanner(banner) } diff --git a/cmd/pyroscope/command/banner.go b/cmd/pyroscope/command/banner.go index 488f5bb75e..4dc347e6b9 100644 --- a/cmd/pyroscope/command/banner.go +++ b/cmd/pyroscope/command/banner.go @@ -1,10 +1,7 @@ package command import ( - "strings" - - "github.com/aybabtme/rgbterm" - "github.com/fatih/color" + "github.com/pyroscope-io/pyroscope/pkg/cli" ) // made here http://patorjk.com/software/taag/#p=display&f=Doom&t=Pyroscope @@ -23,34 +20,6 @@ func init() { banner = banner[1 : len(banner)-2] } -const ( - startColor = 0xffd651 - endColor = 0xf64d3d -) - -func gradient(start, end, offset int, progress float64) uint8 { - start = (start >> offset) & 0xff - end = (end >> offset) & 0xff - return uint8(start + int(float64(end-start)*progress)) -} - func gradientBanner() string { - if color.NoColor { - return banner + "\n" - } - - str := "" - arr := strings.Split(banner, "\n") - l := len(arr) - for i, line := range arr { - if line == "" { - break - } - progress := float64(i) / float64(l-1) - r := gradient(startColor, endColor, 16, progress) - g := gradient(startColor, endColor, 8, progress) - b := gradient(startColor, endColor, 0, progress) - str += rgbterm.FgString(line, r, g, b) + "\n" - } - return str + return cli.GradientBanner(banner) } diff --git a/pkg/cli/banner.go b/pkg/cli/banner.go new file mode 100644 index 0000000000..6da9ff74bf --- /dev/null +++ b/pkg/cli/banner.go @@ -0,0 +1,40 @@ +package cli + +import ( + "strings" + + "github.com/aybabtme/rgbterm" + "github.com/fatih/color" +) + +const ( + startColor = 0xffd651 + endColor = 0xf64d3d +) + +func GradientBanner(banner string) string { + if color.NoColor { + return banner + "\n" + } + + str := "" + arr := strings.Split(banner, "\n") + l := len(arr) + for i, line := range arr { + if line == "" { + break + } + progress := float64(i) / float64(l-1) + r := gradient(startColor, endColor, 16, progress) + g := gradient(startColor, endColor, 8, progress) + b := gradient(startColor, endColor, 0, progress) + str += rgbterm.FgString(line, r, g, b) + "\n" + } + return str +} + +func gradient(start, end, offset int, progress float64) uint8 { + start = (start >> offset) & 0xff + end = (end >> offset) & 0xff + return uint8(start + int(float64(end-start)*progress)) +} From f8a6f575619101afb5135e65333b5fff51aba455 Mon Sep 17 00:00:00 2001 From: eduardo Date: Wed, 1 Sep 2021 12:04:42 -0300 Subject: [PATCH 08/11] move command.go stuff to /pkg so that it can be reused by the pyrobench cli --- benchmark/cmd/command/command.go | 10 +++ benchmark/cmd/command/common.go | 105 ----------------------- benchmark/cmd/command/loadgen.go | 2 +- cmd/pyroscope/command/agent.go | 2 +- cmd/pyroscope/command/command.go | 124 +-------------------------- cmd/pyroscope/command/connect.go | 2 +- cmd/pyroscope/command/convert.go | 2 +- cmd/pyroscope/command/dbmanager.go | 2 +- cmd/pyroscope/command/exec.go | 2 +- cmd/pyroscope/command/exec_test.go | 6 +- cmd/pyroscope/command/server.go | 2 +- pkg/cli/command.go | 129 +++++++++++++++++++++++++++++ 12 files changed, 151 insertions(+), 237 deletions(-) create mode 100644 benchmark/cmd/command/command.go delete mode 100644 benchmark/cmd/command/common.go create mode 100644 pkg/cli/command.go diff --git a/benchmark/cmd/command/command.go b/benchmark/cmd/command/command.go new file mode 100644 index 0000000000..7c30a4bba3 --- /dev/null +++ b/benchmark/cmd/command/command.go @@ -0,0 +1,10 @@ +package command + +import ( + "github.com/pyroscope-io/pyroscope/pkg/cli" + "github.com/spf13/viper" +) + +func newViper() *viper.Viper { + return cli.NewViper("PYROBENCH") +} diff --git a/benchmark/cmd/command/common.go b/benchmark/cmd/command/common.go deleted file mode 100644 index 8653620251..0000000000 --- a/benchmark/cmd/command/common.go +++ /dev/null @@ -1,105 +0,0 @@ -// Copied as is from github.com/pyroscope-io/pyroscope/cmd/command/common.go -package command - -import ( - "fmt" - "os" - "reflect" - "strings" - - "github.com/mitchellh/mapstructure" - "github.com/pyroscope-io/pyroscope/benchmark/config" - "github.com/pyroscope-io/pyroscope/pkg/cli" - "github.com/pyroscope-io/pyroscope/pkg/util/bytesize" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "github.com/spf13/viper" -) - -type cmdRunFn func(cmd *cobra.Command, args []string) error - -func createCmdRunFn(cfg interface{}, vpr *viper.Viper, fn cmdRunFn) cmdRunFn { - return func(cmd *cobra.Command, args []string) error { - var err error - if err = vpr.BindPFlags(cmd.Flags()); err != nil { - return err - } - if c, ok := cfg.(config.File); ok { - if err = loadConfigFile(c.Path(), cmd, vpr, cfg); err != nil { - return fmt.Errorf("loading configuration file: %w", err) - } - } - if err = cli.Unmarshal(vpr, cfg); err != nil { - return err - } - if err = fn(cmd, args); err != nil { - cmd.SilenceUsage = true - } - return err - } -} - -func bindFlags(cfg interface{}, cmd *cobra.Command, vpr *viper.Viper) error { - if err := vpr.BindPFlags(cmd.Flags()); err != nil { - return err - } - return viperUnmarshalWithBytesHook(vpr, cfg) -} - -func newViper() *viper.Viper { - v := viper.New() - v.SetEnvPrefix("PYROBENCH") - v.AutomaticEnv() - v.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_")) - return v -} - -func loadConfigFile(path string, cmd *cobra.Command, vpr *viper.Viper, v interface{}) error { - if path == "" { - return nil - } - - vpr.SetConfigFile(path) - err := vpr.ReadInConfig() - switch { - case err == nil: - return viperUnmarshalWithBytesHook(vpr, v) - case isUserDefined(cmd.Flag("config"), vpr): - // User-defined configuration can not be read. - return err - case os.IsNotExist(err): - // Default configuration file not found. - return nil - default: - return err - } -} - -func viperUnmarshalWithBytesHook(vpr *viper.Viper, cfg interface{}) error { - return vpr.Unmarshal(cfg, viper.DecodeHook( - mapstructure.ComposeDecodeHookFunc( - // Function to add a special type for «env. mode» - func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) { - if t != reflect.TypeOf(bytesize.Byte) { - return data, nil - } - - stringData, ok := data.(string) - if !ok { - return data, nil - } - - return bytesize.Parse(stringData) - }, - // Function to support net.IP - mapstructure.StringToIPHookFunc(), - // Appended by the two default functions - mapstructure.StringToTimeDurationHookFunc(), - mapstructure.StringToSliceHookFunc(","), - ), - )) -} - -func isUserDefined(f *pflag.Flag, v *viper.Viper) bool { - return f.Changed || (f.DefValue != "" && f.DefValue != v.GetString(f.Name)) -} diff --git a/benchmark/cmd/command/loadgen.go b/benchmark/cmd/command/loadgen.go index a794348d91..4c0ce0d29d 100644 --- a/benchmark/cmd/command/loadgen.go +++ b/benchmark/cmd/command/loadgen.go @@ -12,7 +12,7 @@ func newLoadGen(cfg *config.LoadGen) *cobra.Command { loadgenCmd := &cobra.Command{ Use: "loadgen [flags]", Short: "Generates load", - RunE: createCmdRunFn(cfg, vpr, func(_ *cobra.Command, _ []string) error { + RunE: cli.CreateCmdRunFn(cfg, vpr, func(_ *cobra.Command, args []string) error { return loadgen.Cli(cfg) }), } diff --git a/cmd/pyroscope/command/agent.go b/cmd/pyroscope/command/agent.go index 48d60b04f1..e77a90a769 100644 --- a/cmd/pyroscope/command/agent.go +++ b/cmd/pyroscope/command/agent.go @@ -14,7 +14,7 @@ func newAgentCmd(cfg *config.Agent) *cobra.Command { Short: "Start pyroscope agent", DisableFlagParsing: true, - RunE: createCmdRunFn(cfg, vpr, func(_ *cobra.Command, _ []string) error { + RunE: cli.CreateCmdRunFn(cfg, vpr, func(_ *cobra.Command, _ []string) error { return cli.StartAgent(cfg) }), } diff --git a/cmd/pyroscope/command/command.go b/cmd/pyroscope/command/command.go index adb304ff31..7c30a4bba3 100644 --- a/cmd/pyroscope/command/command.go +++ b/cmd/pyroscope/command/command.go @@ -1,130 +1,10 @@ package command import ( - "fmt" - "os" - "strings" - - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "github.com/spf13/viper" - "github.com/pyroscope-io/pyroscope/pkg/cli" - "github.com/pyroscope-io/pyroscope/pkg/config" - "github.com/pyroscope-io/pyroscope/pkg/util/slices" + "github.com/spf13/viper" ) -// https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html#tag_12_02 -const optionsEnd = "--" - -type cmdRunFn func(cmd *cobra.Command, args []string) error - -func createCmdRunFn(cfg interface{}, vpr *viper.Viper, fn cmdRunFn) cmdRunFn { - return func(cmd *cobra.Command, args []string) error { - var err error - if err = vpr.BindPFlags(cmd.Flags()); err != nil { - return err - } - if c, ok := cfg.(config.File); ok { - if err = loadConfigFile(c.Path(), cmd, vpr, cfg); err != nil { - return fmt.Errorf("loading configuration file: %w", err) - } - } - if err = cli.Unmarshal(vpr, cfg); err != nil { - return err - } - - var xargs []string - x := firstArgumentIndex(cmd.Flags(), prependDash(args)) - if x >= 0 { - xargs = args[:x] - args = args[x:] - } else { - xargs = args - args = nil - } - if err = cmd.Flags().Parse(prependDash(xargs)); err != nil { - return err - } - if slices.StringContains(xargs, "--help") { - _ = cmd.Help() - return nil - } - - if err = fn(cmd, args); err != nil { - cmd.SilenceUsage = true - } - return err - } -} - -func prependDash(args []string) []string { - for i, arg := range args { - if len(arg) > 2 && strings.HasPrefix(arg, "-") && !strings.HasPrefix(arg, "--") { - args[i] = "-" + arg - } - } - return args -} - -// firstArgumentIndex returns index of the first encountered argument. -// If args does not contain arguments, or contains undefined flags, -// the call returns -1. -func firstArgumentIndex(flags *pflag.FlagSet, args []string) int { - for i := 0; i < len(args); i++ { - a := args[i] - switch { - default: - return i - case a == optionsEnd: - return i + 1 - case strings.HasPrefix(a, optionsEnd) && len(a) > 2: - x := strings.SplitN(a[2:], "=", 2) - f := flags.Lookup(x[0]) - if f == nil { - return -1 - } - if f.Value.Type() == "bool" { - continue - } - if len(x) == 1 { - i++ - } - } - } - // Should have returned earlier. - return -1 -} - func newViper() *viper.Viper { - v := viper.New() - v.SetEnvPrefix("PYROSCOPE") - v.AutomaticEnv() - v.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_")) - return v -} - -func loadConfigFile(path string, cmd *cobra.Command, vpr *viper.Viper, v interface{}) error { - if path == "" { - return nil - } - - vpr.SetConfigFile(path) - err := vpr.ReadInConfig() - switch { - case err == nil: - return nil - case isUserDefined(cmd.Flag("config"), vpr): - // User-defined configuration can not be read. - return err - case os.IsNotExist(err): - // Default configuration file not found. - return nil - default: - return err - } -} - -func isUserDefined(f *pflag.Flag, v *viper.Viper) bool { - return f.Changed || (f.DefValue != "" && f.DefValue != v.GetString(f.Name)) + return cli.NewViper("PYROBENCH") } diff --git a/cmd/pyroscope/command/connect.go b/cmd/pyroscope/command/connect.go index e1ca6cac49..2f3d8b077e 100644 --- a/cmd/pyroscope/command/connect.go +++ b/cmd/pyroscope/command/connect.go @@ -15,7 +15,7 @@ func newConnectCmd(cfg *config.Exec) *cobra.Command { Short: "Connect to an existing process and profile it", DisableFlagParsing: true, - RunE: createCmdRunFn(cfg, vpr, func(_ *cobra.Command, args []string) error { + RunE: cli.CreateCmdRunFn(cfg, vpr, func(_ *cobra.Command, args []string) error { return exec.Cli(cfg, args) }), } diff --git a/cmd/pyroscope/command/convert.go b/cmd/pyroscope/command/convert.go index e16d564a9d..296dcb3599 100644 --- a/cmd/pyroscope/command/convert.go +++ b/cmd/pyroscope/command/convert.go @@ -22,7 +22,7 @@ func newConvertCmd(cfg *config.Convert) *cobra.Command { Hidden: true, DisableFlagParsing: true, - RunE: createCmdRunFn(cfg, vpr, func(_ *cobra.Command, _ []string) error { + RunE: cli.CreateCmdRunFn(cfg, vpr, func(_ *cobra.Command, _ []string) error { return parse(os.Stdin, cfg.Format) }), } diff --git a/cmd/pyroscope/command/dbmanager.go b/cmd/pyroscope/command/dbmanager.go index 07dcd2c3dc..3cdff31f50 100644 --- a/cmd/pyroscope/command/dbmanager.go +++ b/cmd/pyroscope/command/dbmanager.go @@ -17,7 +17,7 @@ func newDbManagerCmd(cfg *config.CombinedDbManager) *cobra.Command { Hidden: true, DisableFlagParsing: true, - RunE: createCmdRunFn(cfg, vpr, func(_ *cobra.Command, args []string) error { + RunE: cli.CreateCmdRunFn(cfg, vpr, func(_ *cobra.Command, args []string) error { return dbmanager.Cli(cfg.DbManager, cfg.Server, args) }), } diff --git a/cmd/pyroscope/command/exec.go b/cmd/pyroscope/command/exec.go index de51aa08d7..ce5f598fa0 100644 --- a/cmd/pyroscope/command/exec.go +++ b/cmd/pyroscope/command/exec.go @@ -19,7 +19,7 @@ func newExecCmd(cfg *config.Exec) *cobra.Command { Args: cobra.MinimumNArgs(1), DisableFlagParsing: true, - RunE: createCmdRunFn(cfg, vpr, func(_ *cobra.Command, args []string) error { + RunE: cli.CreateCmdRunFn(cfg, vpr, func(_ *cobra.Command, args []string) error { err := exec.Cli(cfg, args) // Normally, if the program ran, the call should return ExitError and // the exit code must be preserved. Otherwise, the error originates from diff --git a/cmd/pyroscope/command/exec_test.go b/cmd/pyroscope/command/exec_test.go index ed6d286f64..6ccf85b76d 100644 --- a/cmd/pyroscope/command/exec_test.go +++ b/cmd/pyroscope/command/exec_test.go @@ -38,7 +38,7 @@ func TestExecCommand(t *testing.T) { }, { description: "delimiter", - inputArgs: []string{optionsEnd}, + inputArgs: []string{cli.OptionsEnd}, expectedArgs: []string{}, }, { @@ -53,7 +53,7 @@ func TestExecCommand(t *testing.T) { }, { description: "exec_separated", - inputArgs: execArgs("-spy-name", "debugspy", optionsEnd), + inputArgs: execArgs("-spy-name", "debugspy", cli.OptionsEnd), expectedArgs: appArgs, }, { @@ -79,7 +79,7 @@ func TestExecCommand(t *testing.T) { cmd := &cobra.Command{ SilenceErrors: true, DisableFlagParsing: true, - RunE: createCmdRunFn(cfg, vpr, func(_ *cobra.Command, args []string) error { + RunE: cli.CreateCmdRunFn(cfg, vpr, func(_ *cobra.Command, args []string) error { cmdArgs = args return nil }), diff --git a/cmd/pyroscope/command/server.go b/cmd/pyroscope/command/server.go index 8120673309..c7b12a286b 100644 --- a/cmd/pyroscope/command/server.go +++ b/cmd/pyroscope/command/server.go @@ -14,7 +14,7 @@ func newServerCmd(cfg *config.Server) *cobra.Command { Short: "Start pyroscope server. This is the database + web-based user interface", DisableFlagParsing: true, - RunE: createCmdRunFn(cfg, vpr, func(_ *cobra.Command, _ []string) error { + RunE: cli.CreateCmdRunFn(cfg, vpr, func(_ *cobra.Command, _ []string) error { return cli.StartServer(cfg) }), } diff --git a/pkg/cli/command.go b/pkg/cli/command.go new file mode 100644 index 0000000000..3f58f20ff3 --- /dev/null +++ b/pkg/cli/command.go @@ -0,0 +1,129 @@ +package cli + +import ( + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" + + "github.com/pyroscope-io/pyroscope/pkg/config" + "github.com/pyroscope-io/pyroscope/pkg/util/slices" +) + +// https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html#tag_12_02 +const OptionsEnd = "--" + +type cmdRunFn func(cmd *cobra.Command, args []string) error + +func CreateCmdRunFn(cfg interface{}, vpr *viper.Viper, fn cmdRunFn) cmdRunFn { + return func(cmd *cobra.Command, args []string) error { + var err error + if err = vpr.BindPFlags(cmd.Flags()); err != nil { + return err + } + if c, ok := cfg.(config.File); ok { + if err = loadConfigFile(c.Path(), cmd, vpr, cfg); err != nil { + return fmt.Errorf("loading configuration file: %w", err) + } + } + if err = Unmarshal(vpr, cfg); err != nil { + return err + } + + var xargs []string + x := firstArgumentIndex(cmd.Flags(), prependDash(args)) + if x >= 0 { + xargs = args[:x] + args = args[x:] + } else { + xargs = args + args = nil + } + if err = cmd.Flags().Parse(prependDash(xargs)); err != nil { + return err + } + if slices.StringContains(xargs, "--help") { + _ = cmd.Help() + return nil + } + + if err = fn(cmd, args); err != nil { + cmd.SilenceUsage = true + } + return err + } +} + +func prependDash(args []string) []string { + for i, arg := range args { + if len(arg) > 2 && strings.HasPrefix(arg, "-") && !strings.HasPrefix(arg, "--") { + args[i] = "-" + arg + } + } + return args +} + +// firstArgumentIndex returns index of the first encountered argument. +// If args does not contain arguments, or contains undefined flags, +// the call returns -1. +func firstArgumentIndex(flags *pflag.FlagSet, args []string) int { + for i := 0; i < len(args); i++ { + a := args[i] + switch { + default: + return i + case a == OptionsEnd: + return i + 1 + case strings.HasPrefix(a, OptionsEnd) && len(a) > 2: + x := strings.SplitN(a[2:], "=", 2) + f := flags.Lookup(x[0]) + if f == nil { + return -1 + } + if f.Value.Type() == "bool" { + continue + } + if len(x) == 1 { + i++ + } + } + } + // Should have returned earlier. + return -1 +} + +func NewViper(prefix string) *viper.Viper { + v := viper.New() + v.SetEnvPrefix(prefix) + v.AutomaticEnv() + v.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_")) + return v +} + +func loadConfigFile(path string, cmd *cobra.Command, vpr *viper.Viper, v interface{}) error { + if path == "" { + return nil + } + + vpr.SetConfigFile(path) + err := vpr.ReadInConfig() + switch { + case err == nil: + return nil + case isUserDefined(cmd.Flag("config"), vpr): + // User-defined configuration can not be read. + return err + case os.IsNotExist(err): + // Default configuration file not found. + return nil + default: + return err + } +} + +func isUserDefined(f *pflag.Flag, v *viper.Viper) bool { + return f.Changed || (f.DefValue != "" && f.DefValue != v.GetString(f.Name)) +} From 2d2007e1683598afe2473156bb514c27ae57f8b4 Mon Sep 17 00:00:00 2001 From: eduardo Date: Wed, 1 Sep 2021 12:14:51 -0300 Subject: [PATCH 09/11] move usage.go to pkg/cli for better reuse --- benchmark/cmd/command/root.go | 5 +- benchmark/cmd/command/usage.go | 98 --------------------- cmd/pyroscope/command/root.go | 2 +- {cmd/pyroscope/command => pkg/cli}/usage.go | 2 +- 4 files changed, 5 insertions(+), 102 deletions(-) delete mode 100644 benchmark/cmd/command/usage.go rename {cmd/pyroscope/command => pkg/cli}/usage.go (99%) diff --git a/benchmark/cmd/command/root.go b/benchmark/cmd/command/root.go index 21cc48129d..d1ea584b3b 100644 --- a/benchmark/cmd/command/root.go +++ b/benchmark/cmd/command/root.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/pyroscope-io/pyroscope/benchmark/config" + "github.com/pyroscope-io/pyroscope/pkg/cli" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -18,13 +19,13 @@ func newRootCmd(cfg *config.LoadGen) *cobra.Command { rootCmd.SetUsageFunc(func(cmd *cobra.Command) error { fmt.Println(gradientBanner()) - fmt.Println(DefaultUsageFunc(cmd.Flags(), cmd)) + fmt.Println(cli.DefaultUsageFunc(cmd.Flags(), cmd)) return nil }) rootCmd.SetHelpFunc(func(cmd *cobra.Command, a []string) { fmt.Println(gradientBanner()) - fmt.Println(DefaultUsageFunc(cmd.Flags(), cmd)) + fmt.Println(cli.DefaultUsageFunc(cmd.Flags(), cmd)) }) return rootCmd diff --git a/benchmark/cmd/command/usage.go b/benchmark/cmd/command/usage.go deleted file mode 100644 index d05fb6a59e..0000000000 --- a/benchmark/cmd/command/usage.go +++ /dev/null @@ -1,98 +0,0 @@ -// Copied as is from github.com/pyroscope-io/pyroscope/cmd/command/usage.go -package command - -import ( - "fmt" - "strings" - "text/tabwriter" - - "github.com/fatih/color" - "github.com/spf13/cobra" - "github.com/spf13/pflag" -) - -var ( - headerClr *color.Color - itemClr *color.Color - descClr *color.Color - defClr *color.Color -) - -func init() { - headerClr = color.New(color.FgGreen) - itemClr = color.New(color.Bold) - // itemClr = color.New() - descClr = color.New() - defClr = color.New(color.FgYellow) -} - -// TODO: Do we want to keep this or use cobra default one? Maybe banner + cobra default? Or something else? -// This is mostly copied from ffcli package -func DefaultUsageFunc(sf *pflag.FlagSet, c *cobra.Command) string { - var b strings.Builder - - fmt.Fprintf(&b, "continuous profiling platform\n\n") - headerClr.Fprintf(&b, "USAGE\n") - if c.Use != "" { - fmt.Fprintf(&b, " %s\n", c.Use) - } else { - fmt.Fprintf(&b, " %s\n", c.Name()) - } - fmt.Fprintf(&b, "\n") - - if c.Long != "" { - fmt.Fprintf(&b, "%s\n\n", c.Long) - } - - if c.HasSubCommands() { - headerClr.Fprintf(&b, "SUBCOMMANDS\n") - tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0) - for _, subcommand := range c.Commands() { - if !subcommand.Hidden { - fmt.Fprintf(tw, " %s\t%s\n", itemClr.Sprintf(subcommand.Name()), subcommand.Short) - } - } - tw.Flush() - fmt.Fprintf(&b, "\n") - } - - if countFlags(c.Flags()) > 0 { - // headerClr.Fprintf(&b, "FLAGS\n") - tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0) - fmt.Fprintf(tw, "%s\t %s@new-line@\n", headerClr.Sprintf("FLAGS"), defClr.Sprint("DEFAULT VALUES")) - - // TODO: it would be nice to sort by how often people would use these. - // But for that we'd have to have a conversion from flag-set back to struct - sf.VisitAll(func(f *pflag.Flag) { - if f.Hidden { - return - } - def := f.DefValue - // if def == "" { - // def = "..." - // } - def = defClr.Sprint(def) - // def = fmt.Sprintf("(%s)", def) - fmt.Fprintf(tw, " %s\t%s", itemClr.Sprintf("--"+f.Name), def) - if f.Usage != "" { - fmt.Fprintf(tw, "@new-line@ ") - descClr.Fprint(tw, f.Usage) - } - descClr.Fprint(tw, "@new-line@") - fmt.Fprint(tw, "\n") - }) - tw.Flush() - // fmt.Fprintf(&b, "\n") - } - - if c.HasSubCommands() { - b.WriteString("Run 'pyroscope SUBCOMMAND --help' for more information on a subcommand.\n") - } - - return strings.ReplaceAll(b.String(), "@new-line@", "\n") -} - -func countFlags(fs *pflag.FlagSet) (n int) { - fs.VisitAll(func(*pflag.Flag) { n++ }) - return n -} diff --git a/cmd/pyroscope/command/root.go b/cmd/pyroscope/command/root.go index 9787c4454e..4c9aa53700 100644 --- a/cmd/pyroscope/command/root.go +++ b/cmd/pyroscope/command/root.go @@ -74,7 +74,7 @@ func printUsageMessage(cmd *cobra.Command) error { func printHelpMessage(cmd *cobra.Command, _ []string) { cmd.Println(gradientBanner()) - cmd.Println(DefaultUsageFunc(cmd.Flags(), cmd)) + cmd.Println(cli.DefaultUsageFunc(cmd.Flags(), cmd)) } func addHelpSubcommand(cmd *cobra.Command) { diff --git a/cmd/pyroscope/command/usage.go b/pkg/cli/usage.go similarity index 99% rename from cmd/pyroscope/command/usage.go rename to pkg/cli/usage.go index 6aef2ff963..fabeb2c081 100644 --- a/cmd/pyroscope/command/usage.go +++ b/pkg/cli/usage.go @@ -1,4 +1,4 @@ -package command +package cli import ( "fmt" From 9f28783f730729ff6e05a406e137bfcbfd7a9762 Mon Sep 17 00:00:00 2001 From: eduardo Date: Wed, 1 Sep 2021 12:20:05 -0300 Subject: [PATCH 10/11] move cmd/logging stuff to /pkg/cli for better reuse --- benchmark/cmd/logging.go | 18 ++---------------- cmd/pyroscope/logging.go | 17 ++--------------- pkg/cli/logging.go | 22 ++++++++++++++++++++++ 3 files changed, 26 insertions(+), 31 deletions(-) create mode 100644 pkg/cli/logging.go diff --git a/benchmark/cmd/logging.go b/benchmark/cmd/logging.go index a26535298d..ed9fc91bc5 100644 --- a/benchmark/cmd/logging.go +++ b/benchmark/cmd/logging.go @@ -1,23 +1,9 @@ -// Copied as is from github.com/pyroscope-io/pyroscope/cmd/logging.go package main import ( - "log" - "os" - "runtime" - - "github.com/fatih/color" - "github.com/sirupsen/logrus" + "github.com/pyroscope-io/pyroscope/pkg/cli" ) func init() { - log.SetFlags(log.Lshortfile | log.Ldate | log.Ltime) - - logrus.SetFormatter(&logrus.TextFormatter{}) - logrus.SetOutput(os.Stdout) - logrus.SetLevel(logrus.DebugLevel) - - if runtime.GOOS == "windows" { - color.NoColor = true - } + cli.InitLogging() } diff --git a/cmd/pyroscope/logging.go b/cmd/pyroscope/logging.go index 4b5152ebbb..ed9fc91bc5 100644 --- a/cmd/pyroscope/logging.go +++ b/cmd/pyroscope/logging.go @@ -1,22 +1,9 @@ package main import ( - "log" - "os" - "runtime" - - "github.com/fatih/color" - "github.com/sirupsen/logrus" + "github.com/pyroscope-io/pyroscope/pkg/cli" ) func init() { - log.SetFlags(log.Lshortfile | log.Ldate | log.Ltime) - - logrus.SetFormatter(&logrus.TextFormatter{}) - logrus.SetOutput(os.Stdout) - logrus.SetLevel(logrus.DebugLevel) - - if runtime.GOOS == "windows" { - color.NoColor = true - } + cli.InitLogging() } diff --git a/pkg/cli/logging.go b/pkg/cli/logging.go new file mode 100644 index 0000000000..f51647faa8 --- /dev/null +++ b/pkg/cli/logging.go @@ -0,0 +1,22 @@ +package cli + +import ( + "log" + "os" + "runtime" + + "github.com/fatih/color" + "github.com/sirupsen/logrus" +) + +func InitLogging() { + log.SetFlags(log.Lshortfile | log.Ldate | log.Ltime) + + logrus.SetFormatter(&logrus.TextFormatter{}) + logrus.SetOutput(os.Stdout) + logrus.SetLevel(logrus.DebugLevel) + + if runtime.GOOS == "windows" { + color.NoColor = true + } +} From 6ac82b92887468ac824c5e98ade92388babdd71d Mon Sep 17 00:00:00 2001 From: eduardo Date: Wed, 1 Sep 2021 12:26:59 -0300 Subject: [PATCH 11/11] fix env var prefix --- cmd/pyroscope/command/command.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/pyroscope/command/command.go b/cmd/pyroscope/command/command.go index 7c30a4bba3..0ca9d6c1a4 100644 --- a/cmd/pyroscope/command/command.go +++ b/cmd/pyroscope/command/command.go @@ -6,5 +6,5 @@ import ( ) func newViper() *viper.Viper { - return cli.NewViper("PYROBENCH") + return cli.NewViper("PYROSCOPE") }