package applog import ( "fmt" "io" "log/slog" "os" "time" "gitea.auvem.com/go-toolkit/app" "github.com/lmittmann/tint" slogmulti "github.com/samber/slog-multi" ) // ModuleName is the name of the module provided by applog. const ModuleName = "app logger" var ( // logfile is the file handle for the log file, if any. logfile *os.File ) // AppLogOpts contains options for configuring the application logger. type AppLogOpts struct { // ConsoleOutput is the writer for pretty-printed console logs. If nil, // console output is disabled. Generally recommended to set this to // os.Stderr, leaving os.Stdout for application output (usually via // fmt.Print*). ConsoleOutput io.Writer // ConsoleLevel is the minimum log level for console output. Defaults to // slog.LevelInfo. ConsoleLevel slog.Level // FileOutput is the path to a file where JSON formatted logs will be // written. If empty, file output will be disabled. FileOutput string // FileLevel is the minimum log level for file output. Defaults to // slog.LevelInfo. FileLevel slog.Level // SetDefault indicates whether to set the logger as the default logger // for slog. Generally recommended to manage logger via app lifecycle // instead of relying on globals. SetDefault bool } // Module creates a new Module instance. func (opts AppLogOpts) Module() *app.Module { return app.NewModule(ModuleName, app.ModuleOpts{ Setup: func(m *app.Module) error { if opts.FileOutput == "" && opts.ConsoleOutput == nil { return fmt.Errorf("no logging output configured") } if err := setupLogger(m.Lifecycle(), opts); err != nil { return fmt.Errorf("failed to set up logger: %w", err) } return nil }, Teardown: func(m *app.Module) error { teardownLogger() return nil }, }) } // Module creates a new Module instance for the application logger with the // provided options. func Module(opts AppLogOpts) *app.Module { return opts.Module() } // setupLogger initializes the multi logger with a JSON handler for file output // and a tint handler for pretty-printed console output. May return an error // if the log file cannot be opened. Log file should be created if it does not // exist and appended to if it does. func setupLogger(lifecycle *app.Lifecycle, opts AppLogOpts) error { handlers := make([]slog.Handler, 0) // If log file is specified, set up JSON file logging if opts.FileOutput != "" { var err error logfile, err = os.OpenFile(opts.FileOutput, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return err } handlers = append(handlers, slog.NewJSONHandler(logfile, &slog.HandlerOptions{ Level: opts.FileLevel, })) } // If log output is specified, set up pretty-printed console logging if opts.ConsoleOutput != nil { handlers = append(handlers, tint.NewHandler(opts.ConsoleOutput, &tint.Options{ Level: opts.ConsoleLevel, TimeFormat: time.Kitchen, })) } logger := slog.New(slogmulti.Fanout(handlers...)) lifecycle.WithLogger(logger) // set logger on lifecycle // optionally set logger as slog default (not recommended) if opts.SetDefault { slog.SetDefault(logger) } return nil } // teardownLogger flushes and closes the log file handle. func teardownLogger() { // If logfile is nil, nothing to do if logfile == nil { return } // Flush the logger to ensure all logs are written if err := logfile.Sync(); err != nil { slog.Error("Error flushing log file", "err", err) } // Close log file handle if err := logfile.Close(); err != nil { slog.Error("Error closing log file", "err", err) } }