package sneklog import ( "bytes" "encoding/json" "fmt" "io" "os" "path/filepath" "strings" "time" ) // LoggerWriter is the common sink interface used by Logger. type LoggerWriter interface { Close() error Write(p []byte) (n int, err error) Print(level LogLevel, prefix string, traceback []*MethodTraceback, messages ...any) error } // PrepareMessages normalizes message parts and extracts a trailing newline marker. func PrepareMessages(messages ...any) (bool, []any) { msg := Map(messages, func(el any) string { return fmt.Sprint(el) }) newline := false if len(msg) > 0 && strings.HasSuffix(msg[len(msg)-1], "\n") { newline = true msg[len(msg)-1] = strings.TrimSuffix(msg[len(msg)-1], "\n") if msg[len(msg)-1] == "" { msg = msg[:len(msg)-1] } } return newline, Map(msg, func(el string) any { return any(el) }) } // LoggerTextWriter writes human-readable log records to an io.Writer. type LoggerTextWriter struct { LoggerWriter writer io.Writer closer io.Closer formatter *Formatter } // SetFormatter replaces the formatter used by the text writer. func (w *LoggerTextWriter) SetFormatter(formatter *Formatter) *LoggerTextWriter { w.formatter = formatter return w } // Formatter returns the effective formatter for the text writer. func (w *LoggerTextWriter) Formatter() *Formatter { if w.formatter == nil { return DefaultTextFormatter } return w.formatter } // Write forwards raw bytes to the underlying writer. func (w *LoggerTextWriter) Write(p []byte) (n int, err error) { return w.writer.Write(p) } // Print formats the provided record as text and writes it to the underlying writer. func (w *LoggerTextWriter) Print(level LogLevel, prefix string, traceback []*MethodTraceback, messages ...any) error { newline, messages := PrepareMessages(messages...) f := w.Formatter() s := f.FormatMessage(level, prefix, traceback, messages...) if f.ColorOutput && (!f.ColorOnlyStdout || w.writer == os.Stdout || w.writer == os.Stderr) { s = f.ColorizeString(s, level) } _, err := w.Write([]byte(s)) if err != nil { return err } if newline { _, err = w.Write([]byte("\n")) if err != nil { return err } } return nil } // Close closes the owned writer, if any. func (w *LoggerTextWriter) Close() error { if w.closer == nil { return nil } return w.closer.Close() } // LoggerJsonWriter writes structured log records as JSON. type LoggerJsonWriter struct { LoggerWriter writer io.Writer closer io.Closer pretty bool formatter *Formatter } // Formatter returns the effective formatter for the JSON writer. func (w *LoggerJsonWriter) Formatter() *Formatter { if w.formatter == nil { return DefaultJsonFormatter } return w.formatter } // SetFormatter replaces the formatter used by the JSON writer. func (w *LoggerJsonWriter) SetFormatter(f *Formatter) *LoggerJsonWriter { w.formatter = f return w } // LoggerJsonMessage is the JSON payload emitted by LoggerJsonWriter. type LoggerJsonMessage struct { Time string `json:"time"` Level string `json:"level"` Prefix string `json:"prefix"` Message string `json:"message"` Traceback []*MethodTraceback `json:"traceback"` } // Write forwards raw bytes to the underlying writer. func (w *LoggerJsonWriter) Write(data []byte) (int, error) { return w.writer.Write(data) } // Print encodes the provided record as JSON and writes it to the underlying writer. func (w *LoggerJsonWriter) Print(level LogLevel, prefix string, traceback []*MethodTraceback, messages ...any) error { newline, messages := PrepareMessages(messages...) f := w.Formatter() s := f.FormatMessage(level, prefix, traceback, messages...) if f.ColorOutput && (!f.ColorOnlyStdout || w.writer == os.Stdout || w.writer == os.Stderr) { s = f.ColorizeString(s, level) } m := LoggerJsonMessage{ Time: f.FormatTime(time.Now()), Level: level.GetName(), Prefix: prefix, Message: s, Traceback: traceback, } buf := bytes.NewBuffer(nil) enc := json.NewEncoder(buf) enc.SetEscapeHTML(f.JSONEscapeHTML) if w.pretty { enc.SetIndent("", " ") } err := enc.Encode(m) if err != nil { return err } data := buf.Bytes() if !newline { data = bytes.TrimSuffix(data, []byte("\n")) } _, err = w.writer.Write(data) if err != nil { return err } return nil } // Close closes the owned writer, if any. func (w *LoggerJsonWriter) Close() error { if w.closer == nil { return nil } return w.closer.Close() } // CreateTextWriter wraps an external writer for text output without taking // ownership of it. func CreateTextWriter(w io.Writer) *LoggerTextWriter { writer := &LoggerTextWriter{ writer: w, } return writer } // CreateTextFileWriter creates a text writer for path, creating parent // directories as needed. The returned writer owns the opened file. func CreateTextFileWriter(path string) (*LoggerTextWriter, error) { err := os.MkdirAll(filepath.Dir(path), os.ModePerm) if err != nil { return nil, err } file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return nil, err } writer := CreateTextWriter(file) writer.closer = file return writer, nil } // CreateTextStdoutWriter creates a text writer for os.Stdout without taking // ownership of stdout. func CreateTextStdoutWriter() *LoggerTextWriter { writer := CreateTextWriter(os.Stdout) writer.closer = nil return writer } // CreateJsonWriter wraps an external writer for JSON output without taking // ownership of it. func CreateJsonWriter(w io.Writer, pretty bool) *LoggerJsonWriter { writer := &LoggerJsonWriter{ writer: w, pretty: pretty, } return writer } // CreateJsonStdoutWriter creates a JSON writer for os.Stdout without taking // ownership of stdout. func CreateJsonStdoutWriter(pretty bool) *LoggerJsonWriter { writer := CreateJsonWriter(os.Stdout, pretty) writer.closer = nil return writer } // CreateJsonFileWriter creates a JSON writer for path, creating parent // directories as needed. The returned writer owns the opened file. func CreateJsonFileWriter(path string, pretty bool) (*LoggerJsonWriter, error) { err := os.MkdirAll(filepath.Dir(path), os.ModePerm) if err != nil { return nil, err } file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return nil, err } writer := CreateJsonWriter(file, pretty) writer.closer = file return writer, nil }