package cmd import ( "fmt" "log/slog" "net" "net/http" "os" "os/signal" "syscall" "time" application "git.oblat.lv/alex/triggerssmith/internal/app" "git.oblat.lv/alex/triggerssmith/internal/config" "git.oblat.lv/alex/triggerssmith/internal/vars" "github.com/spf13/cobra" ) var optsServeCmd = struct { ConfigPath *string Debug *bool }{} // simple middleware for request logging func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() slog.Info("HTTP request", slog.String("method", r.Method), slog.String("path", r.URL.Path), slog.String("remote", r.RemoteAddr), ) next.ServeHTTP(w, r) slog.Debug("HTTP request finished", slog.String("method", r.Method), slog.String("path", r.URL.Path), slog.Duration("latency", time.Since(start)), ) }) } func writePID(path string) error { pid := os.Getpid() f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { return err } defer f.Close() _, err = fmt.Fprintf(f, "%d\n", pid) return err } var serveCmd = &cobra.Command{ Use: "serve", Short: "Start the server", Run: func(cmd *cobra.Command, args []string) { defer func() { if r := recover(); r != nil { slog.Error("Application panicked", slog.Any("error", r)) os.Exit(-1) } }() // configure logger if *optsServeCmd.Debug { slog.SetDefault(slog.New(slog.NewTextHandler(cmd.OutOrStdout(), &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true}))) } else { slog.SetDefault(slog.New(slog.NewTextHandler(cmd.OutOrStdout(), &slog.HandlerOptions{Level: slog.LevelInfo}))) } pid := os.Getpid() slog.Debug("Starting server", slog.Int("pid", pid)) if err := writePID(vars.PID_PATH); err != nil { panic(err) } slog.Debug("created pid file", slog.String("path", vars.PID_PATH)) defer os.Remove(vars.PID_PATH) // load config slog.Debug("Reading configuration", slog.String("path", *optsServeCmd.ConfigPath)) cfg, err := config.LoadConfig(*optsServeCmd.ConfigPath) if err != nil { slog.Error("Failed to load configuration", slog.String("path", *optsServeCmd.ConfigPath), slog.String("error", err.Error())) return } slog.Debug("Configuration loaded", slog.Any("config", cfg)) // init app app, err := application.NewApp() if err != application.ErrNilPointerWarn && err != nil { slog.Error("Failed to create app instance", slog.String("error", err.Error())) return } app.LoadConfiguration(cfg) server := app.Server() mux := http.NewServeMux() // static files staticPath := cfg.Server.StaticFilesPath slog.Debug("Setting up static file server", slog.String("path", staticPath)) fs := http.FileServer(http.Dir(staticPath)) mux.Handle("/static/", http.StripPrefix("/static/", fs)) handler := loggingMiddleware(mux) server.SetHandler(handler) server.Init() var addr = net.JoinHostPort(cfg.Server.Addr, fmt.Sprintf("%d", cfg.Server.Port)) slog.Debug("Binding listener", slog.String("address", addr)) err = server.Start(addr) if err != nil { slog.Error("Failed to start server", slog.String("error", err.Error())) return } else { slog.Info("Server started", slog.String("address", net.JoinHostPort(cfg.Server.Addr, fmt.Sprintf("%d", cfg.Server.Port)))) } sigch := make(chan os.Signal, 1) signal.Notify(sigch, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM) for true { sig := <-sigch slog.Debug("got signal", slog.Any("os.Signal", sig)) switch sig { case syscall.SIGHUP: if err := config.ReloadConfig(cfg); err != nil { slog.Error("Failed to reload configuration", slog.String("error", err.Error())) } else { slog.Info("Configuration reloaded") var addr = net.JoinHostPort(cfg.Server.Addr, fmt.Sprintf("%d", cfg.Server.Port)) slog.Debug("New configuration", slog.Any("config", cfg)) err = server.Reload(addr) if err != nil { slog.Error("Failed to restart server with new configuration", slog.String("error", err.Error())) } } case syscall.SIGINT: slog.Info("Stopping server by SIGINT") err := server.Stop() if err != nil { slog.Error("Failed to stop server", slog.String("err", err.Error())) os.Exit(1) } os.Remove(vars.PID_PATH) return case syscall.SIGTERM: slog.Info("Stopping server by SIGTERM") err := server.Stop() if err != nil { slog.Error("Failed to stop server", slog.String("err", err.Error())) os.Exit(1) } os.Remove(vars.PID_PATH) return } } }, } func init() { optsServeCmd.Debug = serveCmd.Flags().BoolP("debug", "d", false, "Enable debug logs") optsServeCmd.ConfigPath = serveCmd.Flags().StringP("config", "c", "config.yaml", "Path to configuration file") rootCmd.AddCommand(serveCmd) }