// 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 }