package cmd import ( "context" "errors" "fmt" "os" "os/signal" "syscall" "time" "github.com/spf13/cobra" "go.uber.org/zap" "github.com/kanootoko/codevault/internal/config" "github.com/kanootoko/codevault/internal/observability/logging" "github.com/kanootoko/codevault/internal/observability/metrics" "github.com/kanootoko/codevault/internal/runcmd" "github.com/kanootoko/codevault/internal/utils" "github.com/kanootoko/codevault/internal/utils/gshutdown" ) var runCmd = &cobra.Command{ Use: "run", Short: "Start the CodeVault server with the given config", Long: `Start the CodeVault HTTP server using the given config file.`, PreRunE: preRunE, RunE: runE, } func init() { _ = runCmd.Flags().StringP("config", "c", "", "Path to config [CONFIG envvar]") utils.TryFlagFromEnv(runCmd, "config", "CONFIG", true) rootCmd.AddCommand(runCmd) } func preRunE(cmd *cobra.Command, args []string) error { ctx := cmd.Context() ctx, cancelCtx = signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) configPath, _ := cmd.Flags().GetString("config") if configPath == "" { return nil // will fail on RunE() with a proper error } appConfig, err := config.ReadConfig(configPath) if err != nil { return fmt.Errorf("error on reading config: %w", err) } if err = appConfig.Validate(); err != nil { return fmt.Errorf("error on config validation: %w", err) } ctx, err = configureLoggerCtx(ctx, appConfig) if err != nil { return fmt.Errorf("failed to configure logger: %w", err) } logger := logging.LoggerMustFromContext(ctx) ctx = gshutdown.CtxWithGracefulShutdown(ctx, logger.Named("graceful_shutdown")) if err = configureObservability(ctx, appConfig.Observability); err != nil { return fmt.Errorf("failed to configure observability: %w", err) } cmd.SetContext(ctx) return nil } func runE(cmd *cobra.Command, args []string) error { configPath, err := cmd.Flags().GetString("config") if err != nil { return fmt.Errorf("error on getting config option: %w", err) } appConfig, err := config.ReadConfig(configPath) if err != nil { return fmt.Errorf("error on reading config: %w", err) } defer postRun(cmd.Context()) err = runcmd.RunServer(cmd.Context(), appConfig) if err != nil && !errors.Is(err, context.Canceled) { return fmt.Errorf("error on running server: %w", err) } return nil } func postRun(ctx context.Context) { logger := logging.LoggerMustFromContext(ctx) if err := recover(); err != nil { logger.Error("uncaught panic", zap.Any("panic data", err), zap.Stack("stack_trace")) defer panic(err) } cancelCtx() err := gshutdown.AwaitWithTimeout(ctx, time.Second*10) if err != nil { logger.Error("graceful shutdown failed", zap.Error(err)) } loggerCloseFunc() } func configureLoggerCtx(ctx context.Context, appConfig config.Config) (context.Context, error) { logger, closeFunc, err := logging.ConfigureLogger(ctx, appConfig.Observability.Logging, appConfig.Environment == config.ProductionEnvironment) zap.ReplaceGlobals(logger) loggerCloseFunc = closeFunc if err != nil { return ctx, fmt.Errorf("error on logging initialization: %w", err) } return logging.CtxWithLogger(ctx, logger), nil } func configureObservability(ctx context.Context, cfg config.ObservabilityConfig) error { if cfg.Metrics.Host != "" { if err := metrics.Configure(ctx, cfg.Metrics); err != nil { return fmt.Errorf("error on starting Prometheus metrics handler: %w", err) } } return nil } var loggerCloseFunc func() // so the logger is closed after all other context-wide subtasks var cancelCtx context.CancelFunc