diff --git a/.gitignore b/.gitignore index 7ef17d1..c2dc932 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -eng-pad-server +/eng-pad-server /srv/ *.db *.db-wal diff --git a/cmd/eng-pad-server/server.go b/cmd/eng-pad-server/server.go new file mode 100644 index 0000000..10e0e56 --- /dev/null +++ b/cmd/eng-pad-server/server.go @@ -0,0 +1,114 @@ +package main + +import ( + "context" + "fmt" + "log/slog" + "os" + "os/signal" + "syscall" + "time" + + "git.wntrmute.dev/kyle/eng-pad-server/internal/config" + "git.wntrmute.dev/kyle/eng-pad-server/internal/db" + "git.wntrmute.dev/kyle/eng-pad-server/internal/grpcserver" + applog "git.wntrmute.dev/kyle/eng-pad-server/internal/log" + "git.wntrmute.dev/kyle/eng-pad-server/internal/server" + "git.wntrmute.dev/kyle/eng-pad-server/internal/webserver" + "github.com/spf13/cobra" +) + +var serverCmd = &cobra.Command{ + Use: "server", + Short: "Start the eng-pad server", + RunE: runServer, +} + +func init() { + rootCmd.AddCommand(serverCmd) +} + +func runServer(cmd *cobra.Command, args []string) error { + cfg, err := config.Load(cfgFile) + if err != nil { + return err + } + + applog.Init(cfg.Log.Level) + + database, err := db.Open(cfg.Database.Path) + if err != nil { + return err + } + defer func() { _ = database.Close() }() + + if err := db.Migrate(database); err != nil { + return fmt.Errorf("migrate: %w", err) + } + + slog.Info("eng-pad-server starting", "version", version) + + // Start gRPC server + grpcSrv, err := grpcserver.Start(grpcserver.Config{ + Addr: cfg.Server.GRPCAddr, + TLSCert: cfg.Server.TLSCert, + TLSKey: cfg.Server.TLSKey, + DB: database, + BaseURL: cfg.Web.BaseURL, + }) + if err != nil { + return fmt.Errorf("start grpc: %w", err) + } + + // Start REST API server + restSrv, err := server.Start(server.Config{ + Addr: cfg.Server.ListenAddr, + TLSCert: cfg.Server.TLSCert, + TLSKey: cfg.Server.TLSKey, + DB: database, + BaseURL: cfg.Web.BaseURL, + }) + if err != nil { + return fmt.Errorf("start rest: %w", err) + } + + // Start Web UI server + webSrv, err := webserver.Start(webserver.Config{ + Addr: cfg.Web.ListenAddr, + DB: database, + BaseURL: cfg.Web.BaseURL, + }) + if err != nil { + return fmt.Errorf("start web: %w", err) + } + + // Wait for shutdown signal + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + + sig := <-sigCh + slog.Info("received shutdown signal", "signal", sig.String()) + + // Graceful shutdown with 30-second timeout + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // Stop gRPC server gracefully + grpcSrv.GracefulStop() + + // Shutdown REST and web servers + if err := restSrv.Shutdown(ctx); err != nil { + slog.Error("REST server shutdown error", "error", err) + } + if err := webSrv.Shutdown(ctx); err != nil { + slog.Error("web server shutdown error", "error", err) + } + + // Close the database + if err := database.Close(); err != nil { + slog.Error("database close error", "error", err) + } + + slog.Info("shutdown complete") + return nil +} diff --git a/internal/grpcserver/server.go b/internal/grpcserver/server.go index 6f91ad6..e51b62b 100644 --- a/internal/grpcserver/server.go +++ b/internal/grpcserver/server.go @@ -4,6 +4,7 @@ import ( "crypto/tls" "database/sql" "fmt" + "log/slog" "net" pb "git.wntrmute.dev/kyle/eng-pad-server/gen/engpad/v1" @@ -46,7 +47,7 @@ func Start(cfg Config) (*grpc.Server, error) { syncSvc := &SyncService{DB: cfg.DB, BaseURL: cfg.BaseURL} pb.RegisterEngPadSyncServer(srv, syncSvc) - fmt.Printf("gRPC listening on %s\n", cfg.Addr) + slog.Info("gRPC server started", "addr", cfg.Addr) go func() { _ = srv.Serve(lis) }() return srv, nil diff --git a/internal/log/log.go b/internal/log/log.go new file mode 100644 index 0000000..6ac5ec8 --- /dev/null +++ b/internal/log/log.go @@ -0,0 +1,23 @@ +package log + +import ( + "log/slog" + "os" + "strings" +) + +func Init(level string) { + var lvl slog.Level + switch strings.ToLower(level) { + case "debug": + lvl = slog.LevelDebug + case "warn": + lvl = slog.LevelWarn + case "error": + lvl = slog.LevelError + default: + lvl = slog.LevelInfo + } + handler := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: lvl}) + slog.SetDefault(slog.New(handler)) +} diff --git a/internal/server/server.go b/internal/server/server.go index 40170e4..d213b30 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -4,6 +4,7 @@ import ( "crypto/tls" "database/sql" "fmt" + "log/slog" "net/http" "time" @@ -42,7 +43,7 @@ func Start(cfg Config) (*http.Server, error) { IdleTimeout: 120 * time.Second, } - fmt.Printf("REST API listening on %s\n", cfg.Addr) + slog.Info("REST API started", "addr", cfg.Addr) go func() { _ = srv.ListenAndServeTLS("", "") }() return srv, nil diff --git a/internal/webserver/server.go b/internal/webserver/server.go index f875ada..0e97cf9 100644 --- a/internal/webserver/server.go +++ b/internal/webserver/server.go @@ -6,6 +6,7 @@ import ( "fmt" "html/template" "io/fs" + "log/slog" "net/http" "time" @@ -85,10 +86,10 @@ func Start(cfg Config) (*http.Server, error) { Certificates: []tls.Certificate{tlsCert}, MinVersion: tls.VersionTLS13, } - fmt.Printf("Web UI listening on %s (TLS)\n", cfg.Addr) + slog.Info("web UI started", "addr", cfg.Addr, "tls", true) go func() { _ = srv.ListenAndServeTLS("", "") }() } else { - fmt.Printf("Web UI listening on %s\n", cfg.Addr) + slog.Info("web UI started", "addr", cfg.Addr, "tls", false) go func() { _ = srv.ListenAndServe() }() }