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 { // Verbose enables verbose logging, which will use the LogOutput writer // for pretty-printed console output. If false, console output is disabled. Verbose bool // LogOutput is the output writer for the pretty-printed slog console // logger that is enabled if Verbose is set to true. If nil, console // output will be disabled. LogOutput io.Writer // LogFile is the file where JSON formatted logs will be written. If // empty, log file output will be disabled. LogFile string // SetDefault indicates whether to set the logger as the default logger // for slog. Generally not recommended. 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.LogFile == "" && (opts.LogOutput == nil || !opts.Verbose) { return fmt.Errorf("no logging output configured") } output := opts.LogOutput if !opts.Verbose { output = nil } if err := setupLogger(m.Lifecycle(), opts.LogFile, output); err != nil { return fmt.Errorf("failed to set up logger: %w", err) } if opts.SetDefault { slog.SetDefault(m.Logger()) } 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, logFilePath string, logoutput io.Writer) error { handlers := make([]slog.Handler, 0) // If log file is specified, set up JSON file logging if logFilePath != "" { var err error logfile, err = os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return err } handlers = append(handlers, slog.NewJSONHandler(logfile, &slog.HandlerOptions{})) } // If log output is specified, set up pretty-printed console logging if logoutput != nil { handlers = append(handlers, tint.NewHandler(logoutput, &tint.Options{ Level: slog.LevelDebug, TimeFormat: time.Kitchen, })) } logger := slog.New( slogmulti.Fanout(handlers...), ) lifecycle.WithLogger(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) } }