better app log options with improved output & level control

This commit is contained in:
Elijah Duffy
2026-01-28 17:42:45 -08:00
parent ad155bab43
commit 974c3db329
2 changed files with 37 additions and 33 deletions

View File

@@ -1,3 +1,5 @@
# applog # applog
applog is a opinionated logger configuration. applog is a opinionated logger configuration.
WARNING: do not use this library, the API is not stable and I make breaking changes at random. There's a reason it's not on GitHub yet, or possibly ever tbh.

View File

@@ -22,21 +22,27 @@ var (
// AppLogOpts contains options for configuring the application logger. // AppLogOpts contains options for configuring the application logger.
type AppLogOpts struct { type AppLogOpts struct {
// Verbose enables verbose logging, which will use the LogOutput writer // ConsoleOutput is the writer for pretty-printed console logs. If nil,
// for pretty-printed console output. If false, console output is disabled. // console output is disabled. Generally recommended to set this to
Verbose bool // os.Stderr, leaving os.Stdout for application output (usually via
// fmt.Print*).
ConsoleOutput io.Writer
// LogOutput is the output writer for the pretty-printed slog console // ConsoleLevel is the minimum log level for console output. Defaults to
// logger that is enabled if Verbose is set to true. If nil, console // slog.LevelInfo.
// output will be disabled. ConsoleLevel slog.Level
LogOutput io.Writer
// LogFile is the file where JSON formatted logs will be written. If // FileOutput is the path to a file where JSON formatted logs will be
// empty, log file output will be disabled. // written. If empty, file output will be disabled.
LogFile string 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 // SetDefault indicates whether to set the logger as the default logger
// for slog. Generally not recommended. // for slog. Generally recommended to manage logger via app lifecycle
// instead of relying on globals.
SetDefault bool SetDefault bool
} }
@@ -44,23 +50,14 @@ type AppLogOpts struct {
func (opts AppLogOpts) Module() *app.Module { func (opts AppLogOpts) Module() *app.Module {
return app.NewModule(ModuleName, app.ModuleOpts{ return app.NewModule(ModuleName, app.ModuleOpts{
Setup: func(m *app.Module) error { Setup: func(m *app.Module) error {
if opts.LogFile == "" && (opts.LogOutput == nil || !opts.Verbose) { if opts.FileOutput == "" && opts.ConsoleOutput == nil {
return fmt.Errorf("no logging output configured") return fmt.Errorf("no logging output configured")
} }
output := opts.LogOutput if err := setupLogger(m.Lifecycle(), opts); err != nil {
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) return fmt.Errorf("failed to set up logger: %w", err)
} }
if opts.SetDefault {
slog.SetDefault(m.Logger())
}
return nil return nil
}, },
Teardown: func(m *app.Module) error { Teardown: func(m *app.Module) error {
@@ -80,32 +77,37 @@ func Module(opts AppLogOpts) *app.Module {
// and a tint handler for pretty-printed console output. May return an error // 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 // if the log file cannot be opened. Log file should be created if it does not
// exist and appended to if it does. // exist and appended to if it does.
func setupLogger(lifecycle *app.Lifecycle, logFilePath string, logoutput io.Writer) error { func setupLogger(lifecycle *app.Lifecycle, opts AppLogOpts) error {
handlers := make([]slog.Handler, 0) handlers := make([]slog.Handler, 0)
// If log file is specified, set up JSON file logging // If log file is specified, set up JSON file logging
if logFilePath != "" { if opts.FileOutput != "" {
var err error var err error
logfile, err = os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) logfile, err = os.OpenFile(opts.FileOutput, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil { if err != nil {
return err return err
} }
handlers = append(handlers, slog.NewJSONHandler(logfile, &slog.HandlerOptions{})) handlers = append(handlers, slog.NewJSONHandler(logfile, &slog.HandlerOptions{
Level: opts.FileLevel,
}))
} }
// If log output is specified, set up pretty-printed console logging // If log output is specified, set up pretty-printed console logging
if logoutput != nil { if opts.ConsoleOutput != nil {
handlers = append(handlers, tint.NewHandler(logoutput, &tint.Options{ handlers = append(handlers, tint.NewHandler(opts.ConsoleOutput, &tint.Options{
Level: slog.LevelDebug, Level: opts.ConsoleLevel,
TimeFormat: time.Kitchen, TimeFormat: time.Kitchen,
})) }))
} }
logger := slog.New( logger := slog.New(slogmulti.Fanout(handlers...))
slogmulti.Fanout(handlers...), lifecycle.WithLogger(logger) // set logger on lifecycle
)
lifecycle.WithLogger(logger) // optionally set logger as slog default (not recommended)
if opts.SetDefault {
slog.SetDefault(logger)
}
return nil return nil
} }