diff options
Diffstat (limited to 'logging/config.go')
| -rw-r--r-- | logging/config.go | 245 |
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 +} |
