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  }