From 5b35c1c01df1488d9128cf8f3bbb74fde54775c6 Mon Sep 17 00:00:00 2001 From: niklr Date: Tue, 15 Jun 2021 00:30:03 +0200 Subject: [PATCH] Implement pprof module extension --- .gitignore | 3 + api/service/manager.go | 3 + api/service/pprof/service.go | 235 ++++++++++++++++++++++++++++++ api/service/pprof/service_test.go | 113 ++++++++++++++ cmd/harmony/config.go | 8 +- cmd/harmony/config_migrations.go | 10 ++ cmd/harmony/default.go | 3 +- cmd/harmony/flags.go | 44 ++++++ cmd/harmony/flags_test.go | 58 +++++++- cmd/harmony/main.go | 30 ++-- internal/cli/flag.go | 19 +++ internal/cli/parse.go | 21 +++ 12 files changed, 525 insertions(+), 22 deletions(-) create mode 100644 api/service/pprof/service.go create mode 100644 api/service/pprof/service_test.go diff --git a/.gitignore b/.gitignore index a686e5da9f..2932860c9e 100644 --- a/.gitignore +++ b/.gitignore @@ -91,3 +91,6 @@ explorer_storage_* # local blskeys for testing .hmy/blskeys + +# pprof profiles +profiles/*.pb.gz \ No newline at end of file diff --git a/api/service/manager.go b/api/service/manager.go index 1bb53137b5..3e286fe1bb 100644 --- a/api/service/manager.go +++ b/api/service/manager.go @@ -20,6 +20,7 @@ const ( Consensus BlockProposal NetworkInfo + Pprof Prometheus Synchronize ) @@ -36,6 +37,8 @@ func (t Type) String() string { return "BlockProposal" case NetworkInfo: return "NetworkInfo" + case Pprof: + return "Pprof" case Prometheus: return "Prometheus" case Synchronize: diff --git a/api/service/pprof/service.go b/api/service/pprof/service.go new file mode 100644 index 0000000000..7d57514627 --- /dev/null +++ b/api/service/pprof/service.go @@ -0,0 +1,235 @@ +package pprof + +import ( + "errors" + "fmt" + "net/http" + "os" + "path/filepath" + "runtime" + "runtime/pprof" + "strings" + "sync" + "time" + + "github.com/ethereum/go-ethereum/rpc" + "github.com/harmony-one/harmony/internal/utils" +) + +// Config is the config for the pprof service +type Config struct { + Enabled bool + ListenAddr string + Folder string + ProfileNames []string + ProfileIntervals []int + ProfileDebugValues []int +} + +type Profile struct { + Name string + Interval int + Debug int +} + +func (p Config) String() string { + return fmt.Sprintf("%v, %v, %v, %v/%v/%v", p.Enabled, p.ListenAddr, p.Folder, p.ProfileNames, p.ProfileIntervals, p.ProfileDebugValues) +} + +// Constants for profile names +const ( + CPU = "cpu" +) + +// Service provides access to pprof profiles via HTTP and can save them to local disk periodically as user settings. +type Service struct { + config Config + profiles map[string]Profile +} + +var ( + initOnce sync.Once + svc = &Service{} +) + +// NewService creates the new pprof service +func NewService(cfg Config) *Service { + initOnce.Do(func() { + svc = newService(cfg) + }) + return svc +} + +func newService(cfg Config) *Service { + if !cfg.Enabled { + utils.Logger().Info().Msg("Pprof service disabled...") + return nil + } + + utils.Logger().Debug().Str("cfg", cfg.String()).Msg("Pprof") + svc.config = cfg + + if profiles, err := cfg.unpackProfilesIntoMap(); err != nil { + utils.Logger().Error().Err(err).Msg("Could not unpack profiles into map") + return nil + } else { + svc.profiles = profiles + } + + go func() { + utils.Logger().Info().Str("address", cfg.ListenAddr).Msg("Starting pprof HTTP service") + http.ListenAndServe(cfg.ListenAddr, nil) + }() + + return svc +} + +// Start start the service +func (s *Service) Start() error { + dir := getBasePath(s.config.Folder) + err := os.MkdirAll(dir, os.FileMode(0755)) + if err != nil { + return err + } + + if cpuProfile, ok := s.profiles[CPU]; ok { + handleCpuProfile(cpuProfile, dir, false) + } + + for _, profile := range s.profiles { + scheduleProfile(profile, dir) + } + + return nil +} + +// Stop stop the service +func (s *Service) Stop() error { + dir := getBasePath(s.config.Folder) + for _, profile := range s.profiles { + if profile.Name == CPU { + pprof.StopCPUProfile() + } else { + saveProfile(profile, dir) + } + } + return nil +} + +// APIs return all APIs of the service +func (s *Service) APIs() []rpc.API { + return nil +} + +// scheduleProfile schedules the provided profile based on the specified interval (e.g. saves the profile every x seconds) +func scheduleProfile(profile Profile, dir string) { + go func() { + if profile.Interval > 0 { + ticker := time.NewTicker(time.Second * time.Duration(profile.Interval)) + defer ticker.Stop() + for { + select { + case <-ticker.C: + if profile.Name == CPU { + handleCpuProfile(profile, dir, true) + } else { + saveProfile(profile, dir) + } + } + } + } + }() +} + +// saveProfile saves the provided profile in the specified directory +func saveProfile(profile Profile, dir string) { + f, err := newTempFile(dir, profile.Name, ".pb.gz") + if err == nil { + defer f.Close() + if p := pprof.Lookup(profile.Name); p == nil { + utils.Logger().Error().Msg(fmt.Sprintf("Profile does not exist: %s", profile.Name)) + } else { + runtime.GC() // get up-to-date statistics + if p.WriteTo(f, profile.Debug); err == nil { + utils.Logger().Info().Msg(fmt.Sprintf("Saved profile in: %s", f.Name())) + } + } + } else { + utils.Logger().Error().Err(err).Msg(fmt.Sprintf("Could not save profile: %s", profile.Name)) + } +} + +// handleCpuProfile handles the provided CPU profile by stopping or starting it as specified +func handleCpuProfile(profile Profile, dir string, shouldStop bool) { + if shouldStop { + pprof.StopCPUProfile() + } + f, err := newTempFile(dir, profile.Name, ".pb.gz") + if err == nil { + pprof.StartCPUProfile(f) + utils.Logger().Info().Msg(fmt.Sprintf("Saved CPU profile in: %s", f.Name())) + } else { + utils.Logger().Error().Err(err).Msg("Could not start CPU profile") + } +} + +// unpackProfilesIntoMap unpacks the profiles specified in the configuration into a map +func (config *Config) unpackProfilesIntoMap() (map[string]Profile, error) { + result := make(map[string]Profile) + if len(config.ProfileNames) > 0 { + for index, name := range config.ProfileNames { + if _, ok := result[name]; ok { + return nil, errors.New(fmt.Sprintf("Pprof profile names contains duplicate: %v", name)) + } else { + profile := &Profile{ + Name: name, + Interval: 0, + Debug: 0, + } + // Try set interval value + if len(config.ProfileIntervals) == len(config.ProfileNames) { + profile.Interval = config.ProfileIntervals[index] + } else if len(config.ProfileIntervals) > 0 { + profile.Interval = config.ProfileIntervals[0] + } + // Try set debug value + if len(config.ProfileDebugValues) == len(config.ProfileNames) { + profile.Debug = config.ProfileDebugValues[index] + } else if len(config.ProfileDebugValues) > 0 { + profile.Debug = config.ProfileDebugValues[0] + } + result[name] = *profile + } + } + } + return result, nil +} + +func getBasePath(origPath string) string { + modifiedPath := strings.Replace(origPath, "\\", "/", -1) + if modifiedPath == "" || strings.HasSuffix(modifiedPath, string(os.PathSeparator)) { + return filepath.Dir(modifiedPath) + } else { + return filepath.Dir(modifiedPath + string(os.PathSeparator)) + } +} + +func joinPath(dir, name string) string { + return filepath.Join(append(strings.Split(dir, string(os.PathSeparator)), []string{name}...)...) +} + +// newTempFile returns a new output file in dir with the provided prefix and suffix. +// Similar to https://github.com/google/pprof/blob/a478d1d731e942fbe69291748caafefe749a19ea/internal/driver/tempfile.go#L25 +func newTempFile(dir, name, suffix string) (*os.File, error) { + prefix := name + "." + for index := 1; index < 10000; index++ { + switch f, err := os.OpenFile(joinPath(dir, fmt.Sprintf("%s%03d%s", prefix, index, suffix)), os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666); { + case err == nil: + return f, nil + case !os.IsExist(err): + return nil, err + } + } + // Give up + return nil, fmt.Errorf("could not create file of the form %s%03d%s", prefix, 1, suffix) +} diff --git a/api/service/pprof/service_test.go b/api/service/pprof/service_test.go new file mode 100644 index 0000000000..22e5c67361 --- /dev/null +++ b/api/service/pprof/service_test.go @@ -0,0 +1,113 @@ +package pprof + +import ( + "errors" + "fmt" + "os" + "reflect" + "strings" + "testing" +) + +var Separator = string(os.PathSeparator) + +func exerciseGetBasePath(t *testing.T, source, expected string) { + if actual := getBasePath(source); actual != expected { + t.Errorf("expected basePath %#v; got %#v", expected, actual) + } +} + +func TestGetBasePath(t *testing.T) { + exerciseGetBasePath(t, "", ".") + exerciseGetBasePath(t, "./test", "test") + exerciseGetBasePath(t, "./test/", "test") + exerciseGetBasePath(t, "./test1/test2", fmt.Sprintf("test1%stest2", Separator)) + exerciseGetBasePath(t, ".\\test", "test") + exerciseGetBasePath(t, ".\\test\\", "test") + exerciseGetBasePath(t, ".\\test1\\test2\\", fmt.Sprintf("test1%stest2", Separator)) +} + +func exerciseJoinPath(t *testing.T, dir, name, expected string) { + if actual := joinPath(dir, name); actual != expected { + t.Errorf("expected joinPath %#v; got %#v", expected, actual) + } +} + +func TestJoinPath(t *testing.T) { + exerciseJoinPath(t, "", "", "") + exerciseJoinPath(t, "test1", "", "test1") + exerciseJoinPath(t, "", "test2", "test2") + exerciseJoinPath(t, "test1", Separator, "test1") + exerciseJoinPath(t, "test1", "test2", fmt.Sprintf("test1%stest2", Separator)) +} + +func TestUnpackProfilesIntoMap(t *testing.T) { + tests := []struct { + input *Config + expMap map[string]Profile + expErr error + }{ + { + input: &Config{}, + expMap: make(map[string]Profile), + }, + { + input: &Config{ + ProfileNames: []string{"test", "test"}, + }, + expMap: nil, + expErr: errors.New("Pprof profile names contains duplicate: test"), + }, + { + input: &Config{ + ProfileNames: []string{"test"}, + }, + expMap: map[string]Profile{ + "test": { + Name: "test", + }, + }, + }, + { + input: &Config{ + ProfileNames: []string{"test1", "test2"}, + ProfileIntervals: []int{0, 60}, + ProfileDebugValues: []int{1}, + }, + expMap: map[string]Profile{ + "test1": { + Name: "test1", + Interval: 0, + Debug: 1, + }, + "test2": { + Name: "test2", + Interval: 60, + Debug: 1, + }, + }, + }, + } + for i, test := range tests { + actual, err := test.input.unpackProfilesIntoMap() + if assErr := assertError(err, test.expErr); assErr != nil { + t.Fatalf("Test %v: %v", i, assErr) + } + if !reflect.DeepEqual(actual, test.expMap) { + t.Errorf("Test %v: unexpected map\n\t%+v\n\t%+v", i, actual, test.expMap) + } + } +} + +func assertError(gotErr, expErr error) error { + if (gotErr == nil) != (expErr == nil) { + return fmt.Errorf("error unexpected [%v] / [%v]", gotErr, expErr) + } + if gotErr == nil { + return nil + } + if !strings.Contains(gotErr.Error(), expErr.Error()) { + return fmt.Errorf("error unexpected [%v] / [%v]", gotErr, expErr) + } + return nil +} diff --git a/cmd/harmony/config.go b/cmd/harmony/config.go index 8de48664dc..649d320c5d 100644 --- a/cmd/harmony/config.go +++ b/cmd/harmony/config.go @@ -95,8 +95,12 @@ type txPoolConfig struct { } type pprofConfig struct { - Enabled bool - ListenAddr string + Enabled bool + ListenAddr string + Folder string + ProfileNames []string `toml:",omitempty"` + ProfileIntervals []int `toml:",omitempty"` + ProfileDebugValues []int `toml:",omitempty"` } type logConfig struct { diff --git a/cmd/harmony/config_migrations.go b/cmd/harmony/config_migrations.go index 87e0090baf..ef9650a848 100644 --- a/cmd/harmony/config_migrations.go +++ b/cmd/harmony/config_migrations.go @@ -156,4 +156,14 @@ func init() { confTree.Set("Version", "2.0.0") return confTree } + + migrations["2.0.0"] = func(confTree *toml.Tree) *toml.Tree { + // Legacy conf missing fields + if confTree.Get("Pprof.Folder") == nil { + confTree.Set("Pprof.Folder", defaultConfig.Pprof.Folder) + } + + confTree.Set("Version", "2.1.0") + return confTree + } } diff --git a/cmd/harmony/default.go b/cmd/harmony/default.go index 5f104cf2f6..c1b227d344 100644 --- a/cmd/harmony/default.go +++ b/cmd/harmony/default.go @@ -2,7 +2,7 @@ package main import nodeconfig "github.com/harmony-one/harmony/internal/configs/node" -const tomlConfigVersion = "2.0.0" +const tomlConfigVersion = "2.1.0" const ( defNetworkType = nodeconfig.Mainnet @@ -62,6 +62,7 @@ var defaultConfig = harmonyConfig{ Pprof: pprofConfig{ Enabled: false, ListenAddr: "127.0.0.1:6060", + Folder: "./profiles", }, Log: logConfig{ Folder: "./latest", diff --git a/cmd/harmony/flags.go b/cmd/harmony/flags.go index 432bcf7fd1..1056a7a40a 100644 --- a/cmd/harmony/flags.go +++ b/cmd/harmony/flags.go @@ -124,6 +124,10 @@ var ( pprofFlags = []cli.Flag{ pprofEnabledFlag, pprofListenAddrFlag, + pprofFolderFlag, + pprofProfileNamesFlag, + pprofProfileIntervalFlag, + pprofProfileDebugFlag, } logFlags = []cli.Flag{ @@ -955,6 +959,30 @@ var ( Usage: "listen address for pprof", DefValue: defaultConfig.Pprof.ListenAddr, } + pprofFolderFlag = cli.StringFlag{ + Name: "pprof.folder", + Usage: "folder to put pprof profiles", + DefValue: defaultConfig.Pprof.Folder, + Hidden: true, + } + pprofProfileNamesFlag = cli.StringSliceFlag{ + Name: "pprof.profile.names", + Usage: "a list of pprof profile names (separated by ,) e.g. cpu,heap,goroutine", + DefValue: defaultConfig.Pprof.ProfileNames, + Hidden: true, + } + pprofProfileIntervalFlag = cli.IntSliceFlag{ + Name: "pprof.profile.intervals", + Usage: "a list of pprof profile interval integer values (separated by ,)", + DefValue: defaultConfig.Pprof.ProfileIntervals, + Hidden: true, + } + pprofProfileDebugFlag = cli.IntSliceFlag{ + Name: "pprof.profile.debug", + Usage: "a list of pprof profile debug integer values (separated by ,)", + DefValue: defaultConfig.Pprof.ProfileDebugValues, + Hidden: true, + } ) func applyPprofFlags(cmd *cobra.Command, config *harmonyConfig) { @@ -963,6 +991,22 @@ func applyPprofFlags(cmd *cobra.Command, config *harmonyConfig) { config.Pprof.ListenAddr = cli.GetStringFlagValue(cmd, pprofListenAddrFlag) pprofSet = true } + if cli.IsFlagChanged(cmd, pprofFolderFlag) { + config.Pprof.Folder = cli.GetStringFlagValue(cmd, pprofFolderFlag) + pprofSet = true + } + if cli.IsFlagChanged(cmd, pprofProfileNamesFlag) { + config.Pprof.ProfileNames = cli.GetStringSliceFlagValue(cmd, pprofProfileNamesFlag) + pprofSet = true + } + if cli.IsFlagChanged(cmd, pprofProfileIntervalFlag) { + config.Pprof.ProfileIntervals = cli.GetIntSliceFlagValue(cmd, pprofProfileIntervalFlag) + pprofSet = true + } + if cli.IsFlagChanged(cmd, pprofProfileDebugFlag) { + config.Pprof.ProfileDebugValues = cli.GetIntSliceFlagValue(cmd, pprofProfileDebugFlag) + pprofSet = true + } if cli.IsFlagChanged(cmd, pprofEnabledFlag) { config.Pprof.Enabled = cli.GetBoolFlagValue(cmd, pprofEnabledFlag) } else if pprofSet { diff --git a/cmd/harmony/flags_test.go b/cmd/harmony/flags_test.go index c47136f00c..6f1a3e932f 100644 --- a/cmd/harmony/flags_test.go +++ b/cmd/harmony/flags_test.go @@ -98,6 +98,7 @@ func TestHarmonyFlags(t *testing.T) { Pprof: pprofConfig{ Enabled: false, ListenAddr: "127.0.0.1:6060", + Folder: "./profiles", }, Log: logConfig{ Folder: "./latest", @@ -768,22 +769,67 @@ func TestPprofFlags(t *testing.T) { { args: []string{"--pprof"}, expConfig: pprofConfig{ - Enabled: true, - ListenAddr: defaultConfig.Pprof.ListenAddr, + Enabled: true, + ListenAddr: defaultConfig.Pprof.ListenAddr, + Folder: defaultConfig.Pprof.Folder, + ProfileNames: defaultConfig.Pprof.ProfileNames, + ProfileIntervals: defaultConfig.Pprof.ProfileIntervals, + ProfileDebugValues: defaultConfig.Pprof.ProfileDebugValues, }, }, { args: []string{"--pprof.addr", "8.8.8.8:9001"}, expConfig: pprofConfig{ - Enabled: true, - ListenAddr: "8.8.8.8:9001", + Enabled: true, + ListenAddr: "8.8.8.8:9001", + Folder: defaultConfig.Pprof.Folder, + ProfileNames: defaultConfig.Pprof.ProfileNames, + ProfileIntervals: defaultConfig.Pprof.ProfileIntervals, + ProfileDebugValues: defaultConfig.Pprof.ProfileDebugValues, }, }, { args: []string{"--pprof=false", "--pprof.addr", "8.8.8.8:9001"}, expConfig: pprofConfig{ - Enabled: false, - ListenAddr: "8.8.8.8:9001", + Enabled: false, + ListenAddr: "8.8.8.8:9001", + Folder: defaultConfig.Pprof.Folder, + ProfileNames: defaultConfig.Pprof.ProfileNames, + ProfileIntervals: defaultConfig.Pprof.ProfileIntervals, + ProfileDebugValues: defaultConfig.Pprof.ProfileDebugValues, + }, + }, + { + args: []string{"--pprof.profile.names", "cpu,heap,mutex"}, + expConfig: pprofConfig{ + Enabled: true, + ListenAddr: defaultConfig.Pprof.ListenAddr, + Folder: defaultConfig.Pprof.Folder, + ProfileNames: []string{"cpu", "heap", "mutex"}, + ProfileIntervals: defaultConfig.Pprof.ProfileIntervals, + ProfileDebugValues: defaultConfig.Pprof.ProfileDebugValues, + }, + }, + { + args: []string{"--pprof.profile.intervals", "0,1"}, + expConfig: pprofConfig{ + Enabled: true, + ListenAddr: defaultConfig.Pprof.ListenAddr, + Folder: defaultConfig.Pprof.Folder, + ProfileNames: defaultConfig.Pprof.ProfileNames, + ProfileIntervals: []int{0, 1}, + ProfileDebugValues: defaultConfig.Pprof.ProfileDebugValues, + }, + }, + { + args: []string{"--pprof.profile.debug", "0,1,0"}, + expConfig: pprofConfig{ + Enabled: true, + ListenAddr: defaultConfig.Pprof.ListenAddr, + Folder: defaultConfig.Pprof.Folder, + ProfileNames: defaultConfig.Pprof.ProfileNames, + ProfileIntervals: defaultConfig.Pprof.ProfileIntervals, + ProfileDebugValues: []int{0, 1, 0}, }, }, } diff --git a/cmd/harmony/main.go b/cmd/harmony/main.go index 48ee85c757..5c83617c5e 100644 --- a/cmd/harmony/main.go +++ b/cmd/harmony/main.go @@ -5,7 +5,6 @@ import ( "io/ioutil" "math/big" "math/rand" - "net/http" _ "net/http/pprof" "os" "os/signal" @@ -23,6 +22,7 @@ import ( "github.com/spf13/cobra" "github.com/harmony-one/harmony/api/service" + "github.com/harmony-one/harmony/api/service/pprof" "github.com/harmony-one/harmony/api/service/prometheus" "github.com/harmony-one/harmony/api/service/synchronize" "github.com/harmony-one/harmony/common/fdlimit" @@ -131,7 +131,6 @@ func runHarmonyNode(cmd *cobra.Command, args []string) { } setupNodeLog(cfg) - setupPprof(cfg) setupNodeAndRun(cfg) } @@ -242,17 +241,6 @@ func setupNodeLog(config harmonyConfig) { } } -func setupPprof(config harmonyConfig) { - enabled := config.Pprof.Enabled - addr := config.Pprof.ListenAddr - - if enabled { - go func() { - http.ListenAndServe(addr, nil) - }() - } -} - func setupNodeAndRun(hc harmonyConfig) { var err error @@ -388,6 +376,9 @@ func setupNodeAndRun(hc harmonyConfig) { } else if currentNode.NodeConfig.Role() == nodeconfig.ExplorerNode { currentNode.RegisterExplorerServices() } + if hc.Pprof.Enabled { + setupPprofService(currentNode, hc) + } if hc.Prometheus.Enabled { setupPrometheusService(currentNode, hc, nodeConfig.ShardID) } @@ -712,6 +703,19 @@ func setupConsensusAndNode(hc harmonyConfig, nodeConfig *nodeconfig.ConfigType) return currentNode } +func setupPprofService(node *node.Node, hc harmonyConfig) { + pprofConfig := pprof.Config{ + Enabled: hc.Pprof.Enabled, + ListenAddr: hc.Pprof.ListenAddr, + Folder: hc.Pprof.Folder, + ProfileNames: hc.Pprof.ProfileNames, + ProfileIntervals: hc.Pprof.ProfileIntervals, + ProfileDebugValues: hc.Pprof.ProfileDebugValues, + } + s := pprof.NewService(pprofConfig) + node.RegisterService(service.Pprof, s) +} + func setupPrometheusService(node *node.Node, hc harmonyConfig, sid uint32) { prometheusConfig := prometheus.Config{ Enabled: hc.Prometheus.Enabled, diff --git a/internal/cli/flag.go b/internal/cli/flag.go index ef71a10d03..f178789bd1 100644 --- a/internal/cli/flag.go +++ b/internal/cli/flag.go @@ -79,6 +79,23 @@ func (f StringSliceFlag) RegisterTo(fs *pflag.FlagSet) error { return markHiddenOrDeprecated(fs, f.Name, f.Deprecated, f.Hidden) } +// IntSliceFlag is the flag with int slice value +type IntSliceFlag struct { + Name string + Shorthand string + Usage string + Deprecated string + Hidden bool + + DefValue []int +} + +// RegisterTo register the string slice flag to FlagSet +func (f IntSliceFlag) RegisterTo(fs *pflag.FlagSet) error { + fs.IntSliceP(f.Name, f.Shorthand, f.DefValue, f.Usage) + return markHiddenOrDeprecated(fs, f.Name, f.Deprecated, f.Hidden) +} + func markHiddenOrDeprecated(fs *pflag.FlagSet, name string, deprecated string, hidden bool) error { if len(deprecated) != 0 { // TODO: after totally removed node.sh, change MarkHidden to MarkDeprecated @@ -103,6 +120,8 @@ func getFlagName(flag Flag) string { return f.Name case StringSliceFlag: return f.Name + case IntSliceFlag: + return f.Name } return "" } diff --git a/internal/cli/parse.go b/internal/cli/parse.go index 7b767e6e10..2364267a60 100644 --- a/internal/cli/parse.go +++ b/internal/cli/parse.go @@ -105,6 +105,27 @@ func getStringSliceFlagValue(fs *pflag.FlagSet, flag StringSliceFlag) []string { return val } +// GetIntSliceFlagValue get the int slice value for the given IntSliceFlag from +// the local flags of the cobra command. +func GetIntSliceFlagValue(cmd *cobra.Command, flag IntSliceFlag) []int { + return getIntSliceFlagValue(cmd.Flags(), flag) +} + +// GetIntSlicePersistentFlagValue get the int slice value for the given IntSliceFlag +// from the persistent flags of the cobra command. +func GetIntSlicePersistentFlagValue(cmd *cobra.Command, flag IntSliceFlag) []int { + return getIntSliceFlagValue(cmd.PersistentFlags(), flag) +} + +func getIntSliceFlagValue(fs *pflag.FlagSet, flag IntSliceFlag) []int { + val, err := fs.GetIntSlice(flag.Name) + if err != nil { + handleParseError(err) + return nil + } + return val +} + // IsFlagChanged returns whether the flag has been changed in command func IsFlagChanged(cmd *cobra.Command, flag Flag) bool { name := getFlagName(flag)