Files
codevault/cmd/run.go

124 lines
3.5 KiB
Go

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