aboutsummaryrefslogtreecommitdiffstats
path: root/logging/config.go
diff options
context:
space:
mode:
Diffstat (limited to 'logging/config.go')
-rw-r--r--logging/config.go245
1 files changed, 245 insertions, 0 deletions
diff --git a/logging/config.go b/logging/config.go
new file mode 100644
index 0000000..fdf7a6b
--- /dev/null
+++ b/logging/config.go
@@ -0,0 +1,245 @@
+// Package logging provides helpers for context-aware logging using Go's slog package.
+//
+// It allows attaching loggers to context.Context for structured logging,
+// recovering and logging panics with stack traces,
+// and provides a simple package-level logger setup function.
+package logging
+
+import (
+ "context"
+ "fmt"
+ "log/slog"
+ "os"
+ "strings"
+
+ "go.sudomsg.com/kit/net"
+)
+
+type LogFormat int
+
+const (
+ FormatText LogFormat = iota
+ FormatJSON
+ FormatMessage
+)
+
+func (s LogFormat) String() string {
+ switch s {
+ case FormatJSON:
+ return "json"
+ case FormatText:
+ return "text"
+ case FormatMessage:
+ return "message"
+ default:
+ return fmt.Sprintf("LogFormat(%d)", s)
+ }
+}
+
+func (s *LogFormat) Set(str string) error {
+ switch strings.ToLower(str) {
+ case "text":
+ *s = FormatText
+ case "json":
+ *s = FormatJSON
+ case "message":
+ *s = FormatMessage
+ default:
+ return fmt.Errorf("invalid LogFormat %q", str)
+ }
+ return nil
+}
+
+func (s LogFormat) AppendText(b []byte) ([]byte, error) {
+ return append(b, s.String()...), nil
+}
+
+func (s LogFormat) MarshalText() ([]byte, error) {
+ return s.AppendText(nil)
+}
+
+func (s *LogFormat) UnmarshalText(b []byte) error {
+ return s.Set(string(b))
+}
+
+func newEncoder(s LogFormat) (Encoder, error) {
+ switch s {
+ case FormatText:
+ return LogFmtEncoder, nil
+ case FormatJSON:
+ return JSONEncoder, nil
+ case FormatMessage:
+ return MessageEncoder, nil
+ default:
+ return nil, fmt.Errorf("invalid LogFormat: %q", s)
+ }
+}
+
+// LogSink defines where log output should be written.
+//
+// It is used to configure the log destination in Setup.
+type LogSink int
+
+const (
+ // SinkStderr directs logs to the standard error (os.Stderr).
+ SinkStderr LogSink = iota
+
+ // SinkStdout directs logs to the standard output (os.Stdout).
+ SinkStdout
+
+ // SinkFile directs logs to a file specified by the File field in LogConfig.
+ //
+ // If configured, the File field must be a valid path for writing logs.
+ SinkFile
+
+ SinkSyslog //TODO
+
+ SinkHost //TODO
+)
+
+func (s LogSink) String() string {
+ switch s {
+ case SinkStdout:
+ return "stdout"
+ case SinkStderr:
+ return "stderr"
+ case SinkFile:
+ return "file"
+ case SinkSyslog:
+ return "syslog"
+ default:
+ return fmt.Sprintf("LogSink(%d)", s)
+ }
+}
+
+func (s *LogSink) Set(str string) error {
+ switch strings.ToLower(str) {
+ case "stdout":
+ *s = SinkStdout
+ case "stderr":
+ *s = SinkStderr
+ case "file":
+ *s = SinkFile
+ case "syslog":
+ *s = SinkSyslog
+ default:
+ return fmt.Errorf("invalid LogSink %q", str)
+ }
+ return nil
+}
+
+func (s LogSink) AppendText(b []byte) ([]byte, error) {
+ return append(b, s.String()...), nil
+}
+
+func (s LogSink) MarshalText() ([]byte, error) {
+ return s.AppendText(nil)
+}
+
+func (s *LogSink) UnmarshalText(b []byte) error {
+ return s.Set(string(b))
+}
+
+// LogConfig holds configuration for the logging setup.
+//
+// Level specifies the minimum level for logs (e.g., slog.LevelInfo).
+//
+// Sink specifies the output destination (e.g., SinkStdout, SinkStderr, SinkFile).
+//
+// Format specifies the log format, either "json" or "text".
+// If empty or "json", JSON format is used.
+//
+// File specifies the output file path when SinkFile is selected.
+// It is required if SinkFile is used.
+type LogConfig struct {
+ Level slog.Level `toml:"level"`
+ Sink LogSink `toml:"sink"`
+ Format LogFormat `toml:"format"`
+ File string `toml:"file"`
+ Tag string `toml:"tag"`
+ Network net.NetType `toml:"network"`
+ Address string `toml:"address"`
+ Facility Facility `toml:"facility"`
+ ReplaceAttr func(groups []string, a slog.Attr) slog.Attr // Programmatic only
+}
+
+func init() {
+ Setup(context.Background(), LogConfig{}) // Setup Sensible Defaults
+}
+
+// Setup initializes the default slog.Logger based on the provided LogConfig.
+//
+// It configures the log output sink, log format, and log level.
+//
+// Returns an error if the configuration is invalid or if opening the log file fails.
+//
+// Example:
+//
+// cfg := LogConfig{
+// Level: slog.LevelInfo,
+// Sink: SinkFile,
+// Format: FormatJSON,
+// File: "/var/log/myapp.log",
+// }
+// if err := Setup(cfg); err != nil {
+// log.Fatalf("failed to configure logging: %v", err)
+// }
+//
+// After a successful call, the global slog logger will log accordingly.
+func Setup(ctx context.Context, cfg LogConfig) error {
+ logger, err := New(ctx, cfg)
+ if err != nil {
+ return err
+ }
+ slog.SetDefault(logger)
+ return nil
+}
+
+func newSink(ctx context.Context, cfg LogConfig, encoder Encoder) (Sink, error) {
+ if (cfg.Sink == SinkStdout || cfg.Sink == SinkStderr) && IsDaemonManaged() {
+ return NewHostSink(ctx, cfg.Level, "", encoder, cfg.ReplaceAttr)
+ }
+ switch cfg.Sink {
+ case SinkStdout:
+ return NewWriterSink(os.Stdout, encoder, cfg.Level, cfg.ReplaceAttr), nil
+ case SinkStderr:
+ return NewWriterSink(os.Stderr, encoder, cfg.Level, cfg.ReplaceAttr), nil
+ case SinkFile:
+ if cfg.File == "" {
+ return nil, fmt.Errorf("log sink set to 'file' but no file path provided")
+ }
+ f, err := os.OpenFile(cfg.File, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
+ if err != nil {
+ return nil, fmt.Errorf("failed to open log file: %w", err)
+ }
+ return NewWriterSink(f, encoder, cfg.Level, cfg.ReplaceAttr), nil
+ case SinkSyslog:
+ return DialSyslog(ctx, cfg.Network, cfg.Address, encoder, SyslogOptions{
+ Facility: cfg.Facility,
+ Tag: cfg.Tag,
+ Level: cfg.Level,
+ ReplaceAttr: cfg.ReplaceAttr,
+ })
+ case SinkHost:
+ return NewHostSink(ctx, cfg.Level, cfg.Tag, encoder, cfg.ReplaceAttr)
+ default:
+ return nil, fmt.Errorf("invalid LogSink %v", cfg.Sink)
+ }
+
+}
+
+// New is same as Setup but returns the logger instead of setting up the global logger
+func New(ctx context.Context, cfg LogConfig) (*slog.Logger, error) {
+ encoder, err := newEncoder(cfg.Format)
+ if err != nil {
+ return nil, err
+ }
+
+ sink, err := newSink(ctx, cfg, encoder)
+ if err != nil {
+ return nil, err
+ }
+ handler := NewSinkHandler(sink)
+ logger := slog.New(handler)
+ return logger, nil
+}