Add structured logging with log/slog
Replace fmt.Printf logging calls with slog.Info/slog.Error for structured JSON output to stderr. Add internal/log package to initialize the default slog handler from the config log level. Fix .gitignore to only ignore the binary at the repo root, not the cmd/eng-pad-server directory. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,4 +1,4 @@
|
|||||||
eng-pad-server
|
/eng-pad-server
|
||||||
/srv/
|
/srv/
|
||||||
*.db
|
*.db
|
||||||
*.db-wal
|
*.db-wal
|
||||||
|
|||||||
114
cmd/eng-pad-server/server.go
Normal file
114
cmd/eng-pad-server/server.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
pb "git.wntrmute.dev/kyle/eng-pad-server/gen/engpad/v1"
|
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}
|
syncSvc := &SyncService{DB: cfg.DB, BaseURL: cfg.BaseURL}
|
||||||
pb.RegisterEngPadSyncServer(srv, syncSvc)
|
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) }()
|
go func() { _ = srv.Serve(lis) }()
|
||||||
|
|
||||||
return srv, nil
|
return srv, nil
|
||||||
|
|||||||
23
internal/log/log.go
Normal file
23
internal/log/log.go
Normal file
@@ -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))
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ func Start(cfg Config) (*http.Server, error) {
|
|||||||
IdleTimeout: 120 * time.Second,
|
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("", "") }()
|
go func() { _ = srv.ListenAndServeTLS("", "") }()
|
||||||
|
|
||||||
return srv, nil
|
return srv, nil
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -85,10 +86,10 @@ func Start(cfg Config) (*http.Server, error) {
|
|||||||
Certificates: []tls.Certificate{tlsCert},
|
Certificates: []tls.Certificate{tlsCert},
|
||||||
MinVersion: tls.VersionTLS13,
|
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("", "") }()
|
go func() { _ = srv.ListenAndServeTLS("", "") }()
|
||||||
} else {
|
} 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() }()
|
go func() { _ = srv.ListenAndServe() }()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user