router/cmd/main.go (171 lines of code) (raw):
package cmd
import (
"context"
"errors"
"flag"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"time"
"github.com/joho/godotenv"
"github.com/wundergraph/cosmo/router/core"
"github.com/wundergraph/cosmo/router/internal/timex"
"github.com/wundergraph/cosmo/router/internal/versioninfo"
"github.com/wundergraph/cosmo/router/pkg/config"
"github.com/wundergraph/cosmo/router/pkg/logging"
"github.com/wundergraph/cosmo/router/pkg/profile"
"github.com/wundergraph/cosmo/router/pkg/watcher"
"go.uber.org/zap"
)
var (
overrideEnvFlag = flag.String("override-env", os.Getenv("OVERRIDE_ENV"), "Path to .env file to override environment variables")
routerVersion = flag.Bool("version", false, "Prints the version and dependency information")
pprofListenAddr = flag.String("pprof-addr", os.Getenv("PPROF_ADDR"), "Address to listen for pprof requests. e.g. :6060 for localhost:6060")
memProfilePath = flag.String("memprofile", "", "Path to write memory profile. Memory is a snapshot taken at the time the program exits")
cpuProfilePath = flag.String("cpuprofile", "", "Path to write cpu profile. CPU is measured from when the program starts until the program exits")
help = flag.Bool("help", false, "Prints the help message")
// Register the custom flag types
configPathFlag = newMultipleString("config", os.Getenv("CONFIG_PATH"), "Path to the router config file e.g. config.yaml, in case the path is a comma separated file list e.g. \"config.yaml,override.yaml\", the configs will be merged")
)
func Main() {
_ = godotenv.Load()
_ = godotenv.Load(".env.local")
// Parse flags before calling profile.Start(), since it may add flags
flag.Parse()
if *help {
flag.PrintDefaults()
os.Exit(0)
} else if *routerVersion {
bi := versioninfo.New(core.Version, core.Commit, core.Date)
fmt.Println(bi.String())
os.Exit(0)
}
// We load this after flag parse so that "OVERRIDE_ENV" can be set by dotenv OR the flag
if *overrideEnvFlag != "" {
_ = godotenv.Overload(*overrideEnvFlag)
}
/*
Config path precedence:
1. Flag
2. Environment variable
3. Dotenv loaded environment variable
4. Default config file
*/
// If not set by flag or normal environment variable, check again for dotenv override loaded envar
if len(*configPathFlag) == 0 {
configPathEnv := os.Getenv("CONFIG_PATH")
err := configPathFlag.Set(configPathEnv)
if err != nil {
// This should be unreachable unless someone returns an non nil err
log.Fatalf("Could not set config path from environment variable: %s", err)
}
}
// If it is still not set, default to config paths
if len(*configPathFlag) == 0 {
*configPathFlag = multipleString{config.DefaultConfigPath}
}
result, err := config.LoadConfig(*configPathFlag)
if err != nil {
log.Fatalf("Could not load config: %s", err)
}
logLevelAtomic := zap.NewAtomicLevelAt(result.Config.LogLevel)
baseLogger := logging.New(!result.Config.JSONLog, result.Config.DevelopmentMode, logLevelAtomic).
With(
zap.String("service", "@wundergraph/router"),
zap.String("service_version", core.Version),
)
// Start pprof server if address is provided
if *pprofListenAddr != "" {
pprofSvr := profile.NewServer(*pprofListenAddr, baseLogger)
defer pprofSvr.Close()
go pprofSvr.Listen()
}
// Start profiling if flags are set
profiler := profile.Start(baseLogger, *cpuProfilePath, *memProfilePath)
defer profiler.Finish()
rs, err := core.NewRouterSupervisor(&core.RouterSupervisorOpts{
BaseLogger: baseLogger,
ConfigFactory: func() (*config.Config, error) {
result, err := config.LoadConfig(*configPathFlag)
if err != nil {
return nil, fmt.Errorf("could not load config: %w", err)
}
if !result.DefaultLoaded {
baseLogger.Info(
"Config file provided. Values in the config file have higher priority than environment variables",
zap.Strings("config_file", *configPathFlag),
)
}
logLevelAtomic.SetLevel(result.Config.LogLevel)
return &result.Config, nil
},
})
if err != nil {
log.Fatalf("Could not create router supervisor: %s", err)
}
rootCtx, rootCancel := context.WithCancel(context.Background())
defer rootCancel()
// Handling shutdown signals
{
killChan := make(chan os.Signal, 1)
signal.Notify(killChan, os.Interrupt,
syscall.SIGTERM, // default for kill
syscall.SIGQUIT, // ctrl + \
syscall.SIGINT, // ctrl+c
)
go func() {
select {
case <-rootCtx.Done():
return
case <-killChan:
rs.Stop()
}
}()
}
// Handling reload signal
{
reloadChan := make(chan os.Signal, 1)
signal.Notify(reloadChan, syscall.SIGHUP)
go func() {
for {
select {
case <-rootCtx.Done():
return
case <-reloadChan:
rs.Reload()
}
}
}()
}
// Setup config file watcher if enabled
if result.Config.WatchConfig.Enabled {
ll := baseLogger.With(zap.String("watcher_label", "router_config"))
startupDelay := 0 * time.Second
// Apply startup delay if configured
if result.Config.WatchConfig.StartupDelay.Enabled {
startupDelay = timex.RandomDuration(result.Config.WatchConfig.StartupDelay.Maximum)
ll.Info("Using startup delay before initializing config watcher",
zap.Duration("delay", startupDelay),
)
}
watchFunc, err := watcher.New(watcher.Options{
Interval: result.Config.WatchConfig.Interval,
Logger: ll,
Paths: *configPathFlag,
Callback: func() {
ll.Info("Configuration changed, triggering reload")
rs.Reload()
},
})
if err != nil {
baseLogger.Error("Could not create watcher", zap.Error(err))
return
}
go func() {
// Sleep for startupDelay to prevent synchronized reloads across
// different instances of the router
time.Sleep(startupDelay)
if err := watchFunc(rootCtx); err != nil {
if !errors.Is(err, context.Canceled) {
ll.Error("Error watching execution config", zap.Error(err))
} else {
ll.Debug("Watcher context cancelled, shutting down")
}
}
}()
ll.Info("Watching router config file",
zap.Strings("config_file", *configPathFlag),
zap.Duration("watch_interval", result.Config.WatchConfig.Interval),
)
} else {
baseLogger.Info("Config file watching is disabled, you can still trigger reloads by sending SIGHUP to the router process")
}
// Start the router supervisor (blocking)
if err := rs.Start(); err != nil {
if errors.Is(err, core.ErrStartupFailed) {
baseLogger.Error("Could not start router", zap.Error(err))
} else {
baseLogger.Error("Could not shutdown router gracefully", zap.Error(err))
}
}
}