github.com/yandex/pandora@v0.5.32/cli/cli.go (about) 1 package cli 2 3 import ( 4 "bufio" 5 "context" 6 "flag" 7 "fmt" 8 "io/ioutil" 9 "net/http" 10 "os" 11 "os/signal" 12 "path/filepath" 13 "runtime" 14 "runtime/pprof" 15 "strconv" 16 "strings" 17 "syscall" 18 "time" 19 20 "github.com/spf13/viper" 21 "github.com/yandex/pandora/core/config" 22 "github.com/yandex/pandora/core/engine" 23 "github.com/yandex/pandora/lib/zaputil" 24 "go.uber.org/zap" 25 "go.uber.org/zap/zapcore" 26 ) 27 28 const Version = "0.5.32" 29 const defaultConfigFile = "load" 30 const stdinConfigSelector = "-" 31 32 var configSearchDirs = []string{"./", "./config", "/etc/pandora"} 33 34 type CliConfig struct { 35 Engine engine.Config `config:",squash"` 36 Log logConfig `config:"log"` 37 Monitoring monitoringConfig `config:"monitoring"` 38 } 39 40 type logConfig struct { 41 Level zapcore.Level `config:"level"` 42 File string `config:"file"` 43 } 44 45 // TODO(skipor): log sampling with WARN when first message is dropped, and WARN at finish with all 46 // filtered out entries num. Message is filtered out when zapcore.CoreEnable returns true but 47 // zapcore.Core.Check return nil. 48 func newLogger(conf logConfig) *zap.Logger { 49 zapConf := zap.NewDevelopmentConfig() 50 zapConf.OutputPaths = []string{conf.File} 51 zapConf.Level.SetLevel(conf.Level) 52 log, err := zapConf.Build(zap.AddCaller()) 53 if err != nil { 54 zap.L().Fatal("Logger build failed", zap.Error(err)) 55 } 56 return log 57 } 58 59 func DefaultConfig() *CliConfig { 60 return &CliConfig{ 61 Log: logConfig{ 62 Level: zap.InfoLevel, 63 File: "stdout", 64 }, 65 Monitoring: monitoringConfig{ 66 Expvar: &expvarConfig{ 67 Enabled: false, 68 Port: 1234, 69 }, 70 CPUProfile: &cpuprofileConfig{ 71 Enabled: false, 72 File: "cpuprofile.log", 73 }, 74 MemProfile: &memprofileConfig{ 75 Enabled: false, 76 File: "memprofile.log", 77 }, 78 }, 79 } 80 } 81 82 // TODO(skipor): make nice spf13/cobra CLI and integrate it with viper 83 // TODO(skipor): on special command (help or smth else) print list of available plugins 84 85 func Run() { 86 flag.Usage = func() { 87 fmt.Fprintf(os.Stderr, "Usage of Pandora: pandora [<config_filename>]\n"+"<config_filename> is './%s.(yaml|json|...)' by default\n", defaultConfigFile) 88 flag.PrintDefaults() 89 } 90 var ( 91 example bool 92 expvar bool 93 version bool 94 ) 95 flag.BoolVar(&example, "example", false, "print example config to STDOUT and exit") 96 flag.BoolVar(&version, "version", false, "print pandora core version") 97 flag.BoolVar(&expvar, "expvar", false, "enable expvar service (DEPRECATED, use monitoring config section instead)") 98 flag.Parse() 99 100 if expvar { 101 fmt.Fprintf(os.Stderr, "-expvar flag is DEPRECATED. Use monitoring config section instead\n") 102 } 103 104 if example { 105 panic("Not implemented yet") 106 // TODO: print example config file content 107 } 108 109 if version { 110 fmt.Fprintf(os.Stderr, "Pandora core/%s\n", Version) 111 return 112 } 113 114 ReadConfigAndRunEngine() 115 } 116 117 func ReadConfigAndRunEngine() { 118 conf := readConfig(flag.Args()) 119 log := newLogger(conf.Log) 120 zap.ReplaceGlobals(log) 121 zap.RedirectStdLog(log) 122 123 closeMonitoring := startMonitoring(conf.Monitoring) 124 defer closeMonitoring() 125 m := engine.NewMetrics("engine") 126 startReport(m) 127 128 pandora := engine.New(log, m, conf.Engine) 129 130 ctx, cancel := context.WithCancel(context.Background()) 131 defer cancel() 132 133 errs := make(chan error) 134 go runEngine(ctx, pandora, errs) 135 136 // waiting for signal or error message from engine 137 awaitPandoraTermination(pandora, cancel, errs, log) 138 log.Info("Engine run successfully finished") 139 } 140 141 // helper function that awaits pandora run 142 func awaitPandoraTermination(pandora *engine.Engine, gracefulShutdown func(), errs chan error, log *zap.Logger) { 143 sigs := make(chan os.Signal, 2) 144 signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 145 146 select { 147 case sig := <-sigs: 148 var interruptTimeout = 3 * time.Second 149 switch sig { 150 case syscall.SIGINT: 151 // await gun timeout but no longer than 30 sec. 152 interruptTimeout = 30 * time.Second 153 log.Info("SIGINT received. Graceful shutdown.", zap.Duration("timeout", interruptTimeout)) 154 gracefulShutdown() 155 case syscall.SIGTERM: 156 log.Info("SIGTERM received. Trying to stop gracefully.", zap.Duration("timeout", interruptTimeout)) 157 gracefulShutdown() 158 default: 159 log.Fatal("Unexpected signal received. Quiting.", zap.Stringer("signal", sig)) 160 } 161 162 select { 163 case <-time.After(interruptTimeout): 164 log.Fatal("Interrupt timeout exceeded") 165 case sig := <-sigs: 166 log.Fatal("Another signal received. Quiting.", zap.Stringer("signal", sig)) 167 case err := <-errs: 168 log.Fatal("Engine interrupted", zap.Error(err)) 169 } 170 171 case err := <-errs: 172 switch err { 173 case nil: 174 log.Info("Pandora engine successfully finished it's work") 175 case err: 176 const awaitTimeout = 3 * time.Second 177 log.Error("Engine run failed. Awaiting started tasks.", zap.Error(err), zap.Duration("timeout", awaitTimeout)) 178 gracefulShutdown() 179 time.AfterFunc(awaitTimeout, func() { 180 log.Fatal("Engine tasks timeout exceeded.") 181 }) 182 pandora.Wait() 183 log.Fatal("Engine run failed. Pandora graceful shutdown successfully finished") 184 } 185 } 186 } 187 188 func runEngine(ctx context.Context, engine *engine.Engine, errs chan error) { 189 ctx, cancel := context.WithCancel(ctx) 190 defer cancel() 191 errs <- engine.Run(ctx) 192 } 193 194 func readConfig(args []string) *CliConfig { 195 log, err := zap.NewDevelopment(zap.AddCaller()) 196 if err != nil { 197 panic(err) 198 } 199 log = log.WithOptions(zap.WrapCore(zaputil.NewStackExtractCore)) 200 zap.ReplaceGlobals(log) 201 zap.RedirectStdLog(log) 202 203 v := newViper() 204 205 var useStdinConfig = false 206 if len(args) > 0 { 207 switch { 208 case len(args) > 1: 209 zap.L().Fatal("Too many command line arguments", zap.Strings("args", args)) 210 case args[0] == stdinConfigSelector: 211 log.Info("Reading config from standard input") 212 useStdinConfig = true 213 default: 214 v.SetConfigFile(args[0]) 215 if filepath.Ext(args[0]) == "" { 216 v.SetConfigType("yaml") 217 } 218 } 219 } 220 221 log.Info("Pandora version", zap.String("version", Version)) 222 if useStdinConfig { 223 v.SetConfigType("yaml") 224 configBuffer, err := ioutil.ReadAll(bufio.NewReader(os.Stdin)) 225 if err != nil { 226 log.Fatal("Cannot read from standard input", zap.Error(err)) 227 } 228 err = v.ReadConfig(strings.NewReader(string(configBuffer))) 229 if err != nil { 230 log.Fatal("Config parsing failed", zap.Error(err)) 231 } 232 } else { 233 err = v.ReadInConfig() 234 log.Info("Reading config", zap.String("file", v.ConfigFileUsed())) 235 if err != nil { 236 log.Fatal("Config read failed", zap.Error(err)) 237 } 238 } 239 pools := v.Get("pools").([]any) 240 for i, pool := range pools { 241 poolMap := pool.(map[string]any) 242 if _, ok := poolMap["discard_overflow"]; !ok { 243 poolMap["discard_overflow"] = true 244 } 245 pools[i] = poolMap 246 } 247 v.Set("pools", pools) 248 249 conf := DefaultConfig() 250 err = config.DecodeAndValidate(v.AllSettings(), conf) 251 if err != nil { 252 log.Fatal("Config decode failed", zap.Error(err)) 253 } 254 return conf 255 } 256 257 func newViper() *viper.Viper { 258 v := viper.New() 259 v.SetConfigName(defaultConfigFile) 260 for _, dir := range configSearchDirs { 261 v.AddConfigPath(dir) 262 } 263 return v 264 } 265 266 type monitoringConfig struct { 267 Expvar *expvarConfig 268 CPUProfile *cpuprofileConfig 269 MemProfile *memprofileConfig 270 } 271 272 type expvarConfig struct { 273 Enabled bool `config:"enabled"` 274 Port int `config:"port" validate:"required"` 275 } 276 277 type cpuprofileConfig struct { 278 Enabled bool `config:"enabled"` 279 File string `config:"file"` 280 } 281 282 type memprofileConfig struct { 283 Enabled bool `config:"enabled"` 284 File string `config:"file"` 285 } 286 287 func startMonitoring(conf monitoringConfig) (stop func()) { 288 zap.L().Debug("Start monitoring", zap.Reflect("conf", conf)) 289 if conf.Expvar != nil { 290 if conf.Expvar.Enabled { 291 go func() { 292 err := http.ListenAndServe(":"+strconv.Itoa(conf.Expvar.Port), nil) 293 zap.L().Fatal("Monitoring server failed", zap.Error(err)) 294 }() 295 } 296 } 297 var stops []func() 298 if conf.CPUProfile.Enabled { 299 f, err := os.Create(conf.CPUProfile.File) 300 if err != nil { 301 zap.L().Fatal("CPU profile file create fail", zap.Error(err)) 302 } 303 zap.L().Info("Starting CPU profiling") 304 err = pprof.StartCPUProfile(f) 305 if err != nil { 306 zap.L().Info("CPU profiling is already enabled") 307 } 308 stops = append(stops, func() { 309 pprof.StopCPUProfile() 310 err := f.Close() 311 if err != nil { 312 zap.L().Info("Error closing CPUProfile file") 313 } 314 }) 315 } 316 if conf.MemProfile.Enabled { 317 f, err := os.Create(conf.MemProfile.File) 318 if err != nil { 319 zap.L().Fatal("Memory profile file create fail", zap.Error(err)) 320 } 321 stops = append(stops, func() { 322 zap.L().Info("Writing memory profile") 323 runtime.GC() 324 err := pprof.WriteHeapProfile(f) 325 if err != nil { 326 zap.L().Info("Error writing HeapProfile file") 327 } 328 err = f.Close() 329 if err != nil { 330 zap.L().Info("Error closing HeapProfile file") 331 } 332 }) 333 } 334 stop = func() { 335 for _, s := range stops { 336 s() 337 } 338 } 339 return 340 }