From 257d985d0a10e893f08ac0e347e8674cd94bfdd1 Mon Sep 17 00:00:00 2001 From: Elijah Duffy Date: Wed, 4 Jun 2025 15:17:45 -0700 Subject: [PATCH] initial commit --- README.md | 3 ++ applog.go | 129 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 14 ++++++ go.sum | 18 ++++++++ 4 files changed, 164 insertions(+) create mode 100644 README.md create mode 100644 applog.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/README.md b/README.md new file mode 100644 index 0000000..e0f71db --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# applog + +applog is a opinionated logger configuration. diff --git a/applog.go b/applog.go new file mode 100644 index 0000000..ea96aef --- /dev/null +++ b/applog.go @@ -0,0 +1,129 @@ +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) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..da7ca14 --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module gitea.auvem.com/go-toolkit/applog + +go 1.24.0 + +require ( + gitea.auvem.com/go-toolkit/app v0.0.0-20250603235859-6f9e3731acf9 + github.com/lmittmann/tint v1.1.1 + github.com/samber/slog-multi v1.4.0 +) + +require ( + github.com/samber/lo v1.49.1 // indirect + golang.org/x/text v0.21.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d5ec4bb --- /dev/null +++ b/go.sum @@ -0,0 +1,18 @@ +gitea.auvem.com/go-toolkit/app v0.0.0-20250603235859-6f9e3731acf9 h1:MYOI+bB4IBAqoL1tyIUFnu0S+NSq0OX88J3K/PUR7lI= +gitea.auvem.com/go-toolkit/app v0.0.0-20250603235859-6f9e3731acf9/go.mod h1:a7ENpOxndUdONE6oZ9MZAvG1ba2uq01x/LtcnDkpOj8= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/lmittmann/tint v1.1.1 h1:xmmGuinUsCSxWdwH1OqMUQ4tzQsq3BdjJLAAmVKJ9Dw= +github.com/lmittmann/tint v1.1.1/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/samber/slog-multi v1.4.0 h1:pwlPMIE7PrbTHQyKWDU+RIoxP1+HKTNOujk3/kdkbdg= +github.com/samber/slog-multi v1.4.0/go.mod h1:FsQ4Uv2L+E/8TZt+/BVgYZ1LoDWCbfCU21wVIoMMrO8= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=