// Package logging configures the application's structured logger (log/slog) // based on the environment: human-readable text in development, JSON in production. package logging import ( "io" "log/slog" "os" "strings" "time" "github.com/gin-gonic/gin" "git/palnet/slack-notifier/internal/config" ) // Setup builds the slog logger from config and installs it as the default // (so package-level slog.Info/Warn/Error calls everywhere use this config). // // poster가 nil이 아니고 cfg.MonitorChannel이 설정돼 있으면, Error 이상 레벨의 로그를 // 해당 Slack 채널로도 알린다. (poster는 보통 *slack.Client) func Setup(cfg config.Config, poster AlertPoster) *slog.Logger { w := resolveWriter(cfg) opts := &slog.HandlerOptions{Level: parseLevel(cfg.LogLevel)} var h slog.Handler if strings.EqualFold(cfg.LogFormat, "json") { h = slog.NewJSONHandler(w, opts) } else { h = slog.NewTextHandler(w, opts) } if poster != nil && cfg.MonitorChannel != "" { h = newSlackAlertHandler(h, poster, cfg.MonitorChannel) } logger := slog.New(h).With("service", "slack-notifier", "env", cfg.Env) slog.SetDefault(logger) return logger } // resolveWriter returns stdout (development), or stdout+날짜별 파일 when LOG_FILE is set // (production defaults to logs/app.log → logs/app-YYYY-MM-DD.log). 날짜가 바뀌면 새 파일로 // 회전하고 LOG_RETENTION_DAYS일치만 보존한다. stdout도 유지해 Docker/journald가 함께 캡처. func resolveWriter(cfg config.Config) io.Writer { if cfg.LogFile == "" { return os.Stdout } dw, err := newDailyWriter(cfg.LogFile, cfg.LogRetentionDays) if err != nil { // 파일 준비 실패 — stdout으로 폴백(기본 로거로 경고). slog.Warn("cannot open log file, using stdout only", "file", cfg.LogFile, "err", err) return os.Stdout } return io.MultiWriter(os.Stdout, dw) } func parseLevel(s string) slog.Level { switch strings.ToLower(s) { case "debug": return slog.LevelDebug case "warn", "warning": return slog.LevelWarn case "error": return slog.LevelError default: return slog.LevelInfo } } // GinMiddleware logs each HTTP request through slog, replacing gin.Logger() // so access logs share the same env-based format. func GinMiddleware() gin.HandlerFunc { return func(c *gin.Context) { start := time.Now() c.Next() level := slog.LevelInfo if c.Writer.Status() >= 500 { level = slog.LevelError } else if c.Writer.Status() >= 400 { level = slog.LevelWarn } slog.LogAttrs(c.Request.Context(), level, "http_request", slog.String("method", c.Request.Method), slog.String("path", c.Request.URL.Path), slog.Int("status", c.Writer.Status()), slog.Int64("latency_ms", time.Since(start).Milliseconds()), slog.String("client_ip", c.ClientIP()), ) } }