From afe4ee10564ea7d6f3b7ac1cebcd88cd5db7b128 Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Wed, 25 Mar 2026 13:18:06 +0900 Subject: [PATCH] feat: add structured logging with log/slog and panic recovery Replace log.Printf/Println with slog.Info/Error/Warn across the codebase. Initialize slog with JSON handler in main.go. Add panic recovery defer in SSH session handler. Add structured game event logging (room created, player joined, game started, game over, player inactive removed). Co-Authored-By: Claude Opus 4.6 (1M context) --- game/lobby.go | 4 ++++ game/session.go | 3 +++ main.go | 10 ++++++++-- server/ssh.go | 12 ++++++++++-- web/server.go | 18 +++++++++--------- 5 files changed, 34 insertions(+), 13 deletions(-) diff --git a/game/lobby.go b/game/lobby.go index bfb5c47..0c19d93 100644 --- a/game/lobby.go +++ b/game/lobby.go @@ -2,6 +2,7 @@ package game import ( "fmt" + "log/slog" "math/rand" "sync" @@ -138,6 +139,7 @@ func (l *Lobby) CreateRoom(name string) string { Name: name, Status: RoomWaiting, } + slog.Info("room created", "code", code, "name", name) return code } @@ -155,6 +157,7 @@ func (l *Lobby) JoinRoom(code, playerName, fingerprint string) error { return fmt.Errorf("room %s already in progress", code) } room.Players = append(room.Players, LobbyPlayer{Name: playerName, Fingerprint: fingerprint}) + slog.Info("player joined", "room", code, "player", playerName) return nil } @@ -218,6 +221,7 @@ func (l *Lobby) StartRoom(code string) { defer l.mu.Unlock() if room, ok := l.rooms[code]; ok { room.Status = RoomPlaying + slog.Info("game started", "room", code, "players", len(room.Players)) } } diff --git a/game/session.go b/game/session.go index 9082abc..0deb957 100644 --- a/game/session.go +++ b/game/session.go @@ -2,6 +2,7 @@ package game import ( "fmt" + "log/slog" "sync" "time" @@ -143,6 +144,7 @@ func (s *GameSession) combatLoop() { s.mu.Unlock() if gameOver { + slog.Info("game over", "floor", s.state.FloorNum, "victory", s.state.Victory) return } @@ -155,6 +157,7 @@ func (s *GameSession) combatLoop() { if p.Fingerprint != "" && !p.IsOut() { if last, ok := s.lastActivity[p.Fingerprint]; ok { if now.Sub(last) > 60*time.Second { + slog.Warn("player inactive removed", "fingerprint", p.Fingerprint, "name", p.Name) s.addLog(fmt.Sprintf("%s removed (disconnected)", p.Name)) changed = true continue diff --git a/main.go b/main.go index 271b788..d9c5222 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" "log" + "log/slog" "os" "github.com/tolelom/catacombs/config" @@ -15,6 +16,11 @@ import ( func main() { os.MkdirAll("data", 0755) + logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelInfo, + })) + slog.SetDefault(logger) + cfg, err := config.Load("config.yaml") if err != nil { if os.IsNotExist(err) { @@ -38,11 +44,11 @@ func main() { // Start web terminal server in background go func() { if err := web.Start(webAddr, cfg.Server.SSHPort); err != nil { - log.Printf("Web server error: %v", err) + slog.Error("web server error", "error", err) } }() - log.Printf("Catacombs server starting — SSH :%d, Web :%d", cfg.Server.SSHPort, cfg.Server.HTTPPort) + slog.Info("server starting", "ssh_port", cfg.Server.SSHPort, "http_port", cfg.Server.HTTPPort) if err := server.Start(sshAddr, lobby, db); err != nil { log.Fatal(err) } diff --git a/server/ssh.go b/server/ssh.go index 0fb3e17..8187dd1 100644 --- a/server/ssh.go +++ b/server/ssh.go @@ -2,7 +2,7 @@ package server import ( "fmt" - "log" + "log/slog" "github.com/charmbracelet/ssh" "github.com/charmbracelet/wish" @@ -31,6 +31,14 @@ func Start(addr string, lobby *game.Lobby, db *store.DB) error { if s.PublicKey() != nil { fingerprint = gossh.FingerprintSHA256(s.PublicKey()) } + + defer func() { + if r := recover(); r != nil { + slog.Error("session panic recovered", "error", r, "fingerprint", fingerprint) + } + }() + + slog.Info("new SSH session", "fingerprint", fingerprint, "width", pty.Window.Width, "height", pty.Window.Height) m := ui.NewModel(pty.Window.Width, pty.Window.Height, fingerprint, lobby, db) return m, []tea.ProgramOption{tea.WithAltScreen()} }), @@ -40,6 +48,6 @@ func Start(addr string, lobby *game.Lobby, db *store.DB) error { return fmt.Errorf("could not create server: %w", err) } - log.Printf("Starting SSH server on %s", addr) + slog.Info("starting SSH server", "addr", addr) return s.ListenAndServe() } diff --git a/web/server.go b/web/server.go index d15f902..e54f910 100644 --- a/web/server.go +++ b/web/server.go @@ -5,7 +5,7 @@ import ( "encoding/json" "fmt" "io" - "log" + "log/slog" "net/http" "sync" @@ -38,14 +38,14 @@ func Start(addr string, sshPort int) error { handleWS(w, r, sshPort) }) - log.Printf("Starting web terminal on %s", addr) + slog.Info("starting web terminal", "addr", addr) return http.ListenAndServe(addr, mux) } func handleWS(w http.ResponseWriter, r *http.Request, sshPort int) { ws, err := upgrader.Upgrade(w, r, nil) if err != nil { - log.Printf("WebSocket upgrade error: %v", err) + slog.Error("WebSocket upgrade error", "error", err) return } defer ws.Close() @@ -62,7 +62,7 @@ func handleWS(w http.ResponseWriter, r *http.Request, sshPort int) { sshAddr := fmt.Sprintf("localhost:%d", sshPort) client, err := ssh.Dial("tcp", sshAddr, sshConfig) if err != nil { - log.Printf("SSH dial error: %v", err) + slog.Error("SSH dial error", "error", err) ws.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("Failed to connect to game server: %v\r\n", err))) return } @@ -70,7 +70,7 @@ func handleWS(w http.ResponseWriter, r *http.Request, sshPort int) { session, err := client.NewSession() if err != nil { - log.Printf("SSH session error: %v", err) + slog.Error("SSH session error", "error", err) return } defer session.Close() @@ -81,24 +81,24 @@ func handleWS(w http.ResponseWriter, r *http.Request, sshPort int) { ssh.TTY_OP_ISPEED: 14400, ssh.TTY_OP_OSPEED: 14400, }); err != nil { - log.Printf("PTY request error: %v", err) + slog.Error("PTY request error", "error", err) return } stdin, err := session.StdinPipe() if err != nil { - log.Printf("stdin pipe error: %v", err) + slog.Error("stdin pipe error", "error", err) return } stdout, err := session.StdoutPipe() if err != nil { - log.Printf("stdout pipe error: %v", err) + slog.Error("stdout pipe error", "error", err) return } if err := session.Shell(); err != nil { - log.Printf("shell error: %v", err) + slog.Error("shell error", "error", err) return }