package logging import ( "context" "log/slog" "os" "github.com/dhao2001/mygo/internal/config" ) // NewLogger creates a slog.Logger with dual output: terminal (stderr) and // optional file. Terminal and file outputs use independent log levels. // On file-open failure, a warning is emitted to stderr and the logger // continues without file output. func NewLogger(cfg config.LogConfig) *slog.Logger { terminalLevel, _ := config.ParseLogLevel(cfg.Level) fileLevel, _ := config.ParseLogLevel(cfg.FileLevel) terminalHandler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ Level: terminalLevel, }) handlers := []slog.Handler{terminalHandler} if cfg.FilePath != "" { f, err := os.OpenFile(cfg.FilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { slog.New(terminalHandler).Warn("failed to open log file, continuing without file output", "path", cfg.FilePath, "error", err) } else { fileHandler := slog.NewTextHandler(f, &slog.HandlerOptions{ Level: fileLevel, }) handlers = append(handlers, fileHandler) } } handler := NewContextHandler(&multiHandler{handlers: handlers}) return slog.New(handler) } // multiHandler fans out to multiple slog.Handler implementations. type multiHandler struct { handlers []slog.Handler } func (m *multiHandler) Enabled(ctx context.Context, level slog.Level) bool { for _, h := range m.handlers { if h.Enabled(ctx, level) { return true } } return false } func (m *multiHandler) Handle(ctx context.Context, r slog.Record) error { for _, h := range m.handlers { if h.Enabled(ctx, r.Level) { if err := h.Handle(ctx, r); err != nil { return err } } } return nil } func (m *multiHandler) WithAttrs(attrs []slog.Attr) slog.Handler { clones := make([]slog.Handler, len(m.handlers)) for i, h := range m.handlers { clones[i] = h.WithAttrs(attrs) } return &multiHandler{handlers: clones} } func (m *multiHandler) WithGroup(name string) slog.Handler { clones := make([]slog.Handler, len(m.handlers)) for i, h := range m.handlers { clones[i] = h.WithGroup(name) } return &multiHandler{handlers: clones} }