diff --git a/README.md b/README.md index 4906a5e..d68afa3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,138 @@ -# ScuroLogger (slog) +# slog +Small structured logger for Go with text and JSON output, multiple writers, and optional traceback metadata. + +Russian version: [README_ru.md](README_ru.md) + +## Features + +- Fan out the same record to multiple destinations. +- Text and JSON writers. +- `stdout`, files, and arbitrary external `io.Writer` values. +- Optional timestamps for text output. +- Compact traceback metadata for text writers and full traceback slices for JSON. +- Explicit ownership rules for writer closing. + +## Installation + +```bash +go get git.nix13.pw/scuroneko/slog +``` + +## Quick start + +```go +package main + +import ( + "log" + + "git.nix13.pw/scuroneko/slog" +) + +func main() { + logger := slog.CreateLogger(). + Prefix("API"). + Level(slog.DEBUG). + PrintTraceback(true). + JsonPretty(true) + + text := logger.CreateTextStdoutWriter() + jsonFile, err := logger.CreateJsonFileWriter("logs/app.json") + if err != nil { + log.Fatal(err) + } + + logger.AddWriters(text, jsonFile) + + logger.Infoln("service started") + logger.Warnln("cache miss") + logger.Errorln("request failed") + logger.Debugln("debug details") + + if err := logger.Close(); err != nil { + log.Fatal(err) + } +} +``` + +## Defaults + +`CreateLogger()` starts with: + +- `Prefix("LOG")` +- `Level(slog.FATAL)` +- `PrintTime(true)` +- `PrintTraceback(false)` +- `JsonPretty(false)` + +Important: with the current level ordering, `Level(slog.FATAL)` allows `INFO`, `WARN`, `ERROR`, and `FATAL`, but not `DEBUG`. Use `Level(slog.DEBUG)` to enable every level. + +## Writers and ownership + +`Logger.Close()` only closes writers created by the logger itself: + +- `CreateTextFileWriter(...)` +- `CreateJsonFileWriter(...)` + +The following writers remain owned by the caller and are not closed by `Logger.Close()`: + +- `CreateTextWriter(existingWriter)` +- `CreateJsonWriter(existingWriter)` +- `CreateTextStdoutWriter()` +- `CreateJsonStdoutWriter()` + +This makes it safe to plug in `bytes.Buffer`, network writers, and other externally managed resources. + +## Output formats + +Text writers render records like: + +```text +[API] [INFO] [main.go:main:27] [17.03.26 14:05:09] service started +``` + +The traceback field is included only when `PrintTraceback(true)` is enabled. + +JSON writers emit objects with this shape: + +```json +{ + "time": "2026-03-17T14:05:09.123456789+03:00", + "level": "info", + "prefix": "API", + "message": "service started", + "traceback": [ + { + "method": "main", + "filename": "main.go", + "line": 27, + "signature": "main.main", + "fullPath": "/path/to/main.go" + } + ] +} +``` + +When `JsonPretty(true)` is enabled, JSON is indented. + +## API summary + +- `Info`, `Warn`, `Error`, `Debug`, and `Fatal` accept a list of values. +- `Infof`, `Warnf`, `Errorf`, `Debugf`, and `Fatalf` use `fmt.Sprintf`. +- The `*ln` methods preserve newline semantics, which is useful for `stdout`, Docker, and line-based collectors. +- `Fatal`, `Fatalf`, and `Fatalln` call `os.Exit(1)` after writing the message. + +## Traceback behavior + +- Text writers use the nearest user stack frame. +- JSON writers receive the full traceback slice. +- Internal `slog` frames and `runtime` frames are filtered out. + +## Repository example + +See [examples/main.go](examples/main.go). + +## License + +This project is licensed under GNU GPLv3. See [LICENSE](LICENSE). diff --git a/README_ru.md b/README_ru.md new file mode 100644 index 0000000..7692425 --- /dev/null +++ b/README_ru.md @@ -0,0 +1,138 @@ +# slog + +Небольшой структурированный логгер для Go с текстовым и JSON-выводом, несколькими writer'ами и настраиваемыми traceback-метаданными. + +English version: [README.md](README.md) + +## Возможности + +- Одновременная запись в несколько destinations. +- Текстовый и JSON-форматы. +- `stdout`, файлы и любые внешние `io.Writer`. +- Опциональные timestamp'ы для текстового вывода. +- Компактный traceback для текстовых writer'ов и полный traceback для JSON. +- Явные правила владения writer'ами при `Close()`. + +## Установка + +```bash +go get git.nix13.pw/scuroneko/slog +``` + +## Быстрый старт + +```go +package main + +import ( + "log" + + "git.nix13.pw/scuroneko/slog" +) + +func main() { + logger := slog.CreateLogger(). + Prefix("API"). + Level(slog.DEBUG). + PrintTraceback(true). + JsonPretty(true) + + text := logger.CreateTextStdoutWriter() + jsonFile, err := logger.CreateJsonFileWriter("logs/app.json") + if err != nil { + log.Fatal(err) + } + + logger.AddWriters(text, jsonFile) + + logger.Infoln("service started") + logger.Warnln("cache miss") + logger.Errorln("request failed") + logger.Debugln("debug details") + + if err := logger.Close(); err != nil { + log.Fatal(err) + } +} +``` + +## Значения по умолчанию + +`CreateLogger()` создает логгер со следующими настройками: + +- `Prefix("LOG")` +- `Level(slog.FATAL)` +- `PrintTime(true)` +- `PrintTraceback(false)` +- `JsonPretty(false)` + +Важно: в текущей модели уровней `Level(slog.FATAL)` пропускает `INFO`, `WARN`, `ERROR` и `FATAL`, но не `DEBUG`. Чтобы включить все сообщения, используйте `Level(slog.DEBUG)`. + +## Writer'ы и владение + +`Logger.Close()` закрывает только writer'ы, которые логгер создал сам: + +- `CreateTextFileWriter(...)` +- `CreateJsonFileWriter(...)` + +Внешние writer'ы не закрываются: + +- `CreateTextWriter(existingWriter)` +- `CreateJsonWriter(existingWriter)` +- `CreateTextStdoutWriter()` +- `CreateJsonStdoutWriter()` + +Это позволяет безопасно подключать `bytes.Buffer`, сетевые writer'ы и другие уже управляемые ресурсы. + +## Форматы вывода + +Текстовый writer формирует записи вида: + +```text +[API] [INFO] [main.go:main:27] [17.03.26 14:05:09] service started +``` + +Поле traceback появляется только если включен `PrintTraceback(true)`. + +JSON writer записывает объект со следующими полями: + +```json +{ + "time": "2026-03-17T14:05:09.123456789+03:00", + "level": "info", + "prefix": "API", + "message": "service started", + "traceback": [ + { + "method": "main", + "filename": "main.go", + "line": 27, + "signature": "main.main", + "fullPath": "/path/to/main.go" + } + ] +} +``` + +Если включен `JsonPretty(true)`, JSON выводится с отступами. + +## API кратко + +- `Info`, `Warn`, `Error`, `Debug`, `Fatal` принимают список значений. +- `Infof`, `Warnf`, `Errorf`, `Debugf`, `Fatalf` используют `fmt.Sprintf`. +- Методы `*ln` добавляют семантику перевода строки, что удобно для `stdout`, Docker и line-based collectors. +- `Fatal`, `Fatalf` и `Fatalln` вызывают `os.Exit(1)` после записи сообщения. + +## Поведение traceback + +- Текстовые writer'ы используют ближайший пользовательский stack frame. +- JSON writer'ы получают полный traceback. +- Внутренние frame'ы `slog` и `runtime` фильтруются из traceback. + +## Пример из репозитория + +См. [examples/main.go](examples/main.go). + +## Лицензия + +Проект распространяется под GNU GPLv3. См. [LICENSE](LICENSE). diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..474fce5 --- /dev/null +++ b/doc.go @@ -0,0 +1,42 @@ +// Package slog provides a small structured logger that can write the same +// record to text and JSON outputs. +// +// A Logger can fan out records to stdout, files, or any external io.Writer. +// Text writers can include timestamps and a compact traceback frame. JSON +// writers always emit a structured traceback slice. +// +// CreateLogger uses the following defaults: +// - prefix: "LOG" +// - level: FATAL, which enables info, warn, error, and fatal records +// - text timestamps: enabled +// - text traceback: disabled +// - pretty JSON: disabled +// +// Call Level(DEBUG) to enable all records, including debug messages. +// +// Basic usage: +// +// logger := slog.CreateLogger(). +// Prefix("API"). +// Level(slog.DEBUG). +// PrintTraceback(true). +// JsonPretty(true) +// +// text := logger.CreateTextStdoutWriter() +// jsonFile, err := logger.CreateJsonFileWriter("logs/app.json") +// if err != nil { +// panic(err) +// } +// +// logger.AddWriters(text, jsonFile) +// logger.Infoln("service started") +// +// if err := logger.Close(); err != nil { +// panic(err) +// } +// +// Writers created with CreateTextFileWriter or CreateJsonFileWriter are owned by +// the logger and are closed by Logger.Close. Writers created from existing +// io.Writer values through CreateTextWriter or CreateJsonWriter remain owned by +// the caller and are not closed by the logger. +package slog diff --git a/examples/main.go b/examples/main.go index 99a03c6..4806170 100644 --- a/examples/main.go +++ b/examples/main.go @@ -1,21 +1,53 @@ package main -import "git.nix13.pw/scuroneko/slog" +import ( + "bytes" + "fmt" + + "git.nix13.pw/scuroneko/slog" +) func main() { - logger := slog.CreateLogger().JsonPretty(false) - defer logger.Close() - logger.AddWriter(logger.CreateTextStdoutWriter()) - f, err := logger.CreateJsonFileWriter("main.log") + logger := slog.CreateLogger(). + Prefix("EXAMPLE"). + Level(slog.DEBUG). + JsonPretty(true) + + textStdout := logger.CreateTextStdoutWriter() + jsonStdout := logger.CreateJsonStdoutWriter() + + textFile, err := logger.CreateTextFileWriter("logs/text.log") if err != nil { panic(err) } - logger.AddWriter(f) - logger.Infoln("Test") - logger.Warnln("Test") - logger.Errorln("Test") - logger.Debugln("Test") // No output - logger.Level(slog.DEBUG) - logger.Debugln("Test") // Now we have output - logger.Fatalln("Test") // Exit code 1 + + jsonFile, err := logger.CreateJsonFileWriter("logs/json.log") + if err != nil { + panic(err) + } + + var externalBuffer bytes.Buffer + externalText := logger.CreateTextWriter(&externalBuffer) + externalJSON := logger.CreateJsonWriter(&externalBuffer) + + logger.AddWriters( + textStdout, + jsonStdout, + textFile, + jsonFile, + externalText, + externalJSON, + ) + + logger.Infoln("service started") + logger.Warnln("cache miss") + logger.Errorln("request failed") + logger.Debugln("debug details") + + if err := logger.Close(); err != nil { + panic(err) + } + + fmt.Println("external buffer contents:") + fmt.Println(externalBuffer.String()) } diff --git a/io.go b/io.go index 9c0948c..bc19419 100644 --- a/io.go +++ b/io.go @@ -5,55 +5,70 @@ import ( "os" ) +// Infof logs a formatted info message. func (l *Logger) Infof(format string, args ...any) { l.print(INFO, fmt.Sprintf(format, args...)) } +// Info logs an info message. func (l *Logger) Info(m ...any) { l.print(INFO, m...) } +// Infoln logs an info message and appends a newline semantic for writers that need it. func (l *Logger) Infoln(m ...any) { l.println(INFO, m...) } +// Warnf logs a formatted warning message. func (l *Logger) Warnf(format string, args ...any) { l.print(WARN, fmt.Sprintf(format, args...)) } +// Warn logs a warning message. func (l *Logger) Warn(m ...any) { l.print(WARN, m...) } +// Warnln logs a warning message with newline semantic. func (l *Logger) Warnln(m ...any) { l.println(WARN, m...) } +// Errorf logs a formatted error message. func (l *Logger) Errorf(format string, args ...any) { l.print(ERROR, fmt.Sprintf(format, args...)) } +// Error logs an error message. func (l *Logger) Error(m ...any) { l.print(ERROR, m...) } +// Errorln logs an error message with newline semantic. func (l *Logger) Errorln(m ...any) { l.println(ERROR, m...) } +// Fatalf logs a formatted fatal message and exits the process with code 1. func (l *Logger) Fatalf(format string, args ...any) { l.print(FATAL, fmt.Sprintf(format, args...)) os.Exit(1) } +// Fatal logs a fatal message and exits the process with code 1. func (l *Logger) Fatal(m ...any) { l.print(FATAL, m...) os.Exit(1) } +// Fatalln logs a fatal message with newline semantic and exits the process with code 1. func (l *Logger) Fatalln(m ...any) { l.println(FATAL, m...) os.Exit(1) } +// Debugf logs a formatted debug message. func (l *Logger) Debugf(format string, args ...any) { l.print(DEBUG, fmt.Sprintf(format, args...)) } +// Debug logs a debug message. func (l *Logger) Debug(m ...any) { l.print(DEBUG, m...) } +// Debugln logs a debug message with newline semantic. func (l *Logger) Debugln(m ...any) { l.println(DEBUG, m...) } diff --git a/logger.go b/logger.go index 51a7057..d008f07 100644 --- a/logger.go +++ b/logger.go @@ -10,6 +10,7 @@ import ( "github.com/fatih/color" ) +// Logger routes log records to one or more configured writers. type Logger struct { prefix string level LogLevel @@ -20,16 +21,19 @@ type Logger struct { jsonPretty bool } +// LogLevel describes a logging severity. type LogLevel struct { n uint8 t string c color.Attribute } +// GetName returns the lowercase textual representation of the level. func (l *LogLevel) GetName() string { return l.t } +// MethodTraceback describes a single stack frame attached to a log entry. type MethodTraceback struct { Method string `json:"method"` Filename string `json:"filename"` @@ -38,6 +42,7 @@ type MethodTraceback struct { FullPath string `json:"fullPath"` } +// Predefined log levels. var ( INFO = LogLevel{n: 0, t: "info", c: color.FgWhite} WARN = LogLevel{n: 1, t: "warn", c: color.FgHiYellow} @@ -46,6 +51,7 @@ var ( DEBUG = LogLevel{n: 4, t: "debug", c: color.FgGreen} ) +// CreateLogger creates a logger with default settings. func CreateLogger() *Logger { return &Logger{ prefix: "LOG", @@ -55,34 +61,42 @@ func CreateLogger() *Logger { } } +// Prefix sets the record prefix and returns the logger for chaining. func (l *Logger) Prefix(prefix string) *Logger { l.prefix = prefix return l } +// Level sets the maximum enabled level and returns the logger for chaining. func (l *Logger) Level(level LogLevel) *Logger { l.level = level return l } +// PrintTraceback enables traceback output for text writers. func (l *Logger) PrintTraceback(b bool) *Logger { l.printTraceback = b return l } +// PrintTime enables timestamps for text writers. func (l *Logger) PrintTime(b bool) *Logger { l.printTime = b return l } +// JsonPretty enables indented JSON output for JSON writers. func (l *Logger) JsonPretty(b bool) *Logger { l.jsonPretty = b return l } +// AddWriters appends multiple writers to the logger. func (l *Logger) AddWriters(writers ...LoggerWriter) *Logger { l.writers = append(l.writers, writers...) return l } +// AddWriter appends a single writer to the logger. func (l *Logger) AddWriter(writer LoggerWriter) *Logger { l.writers = append(l.writers, writer) return l } +// Close closes all owned writers and returns a joined error, if any. func (l *Logger) Close() error { var errs []error for _, writer := range l.writers { @@ -98,31 +112,40 @@ func (l *Logger) Close() error { return errors.Join(errs...) } +// CreateTextWriter wraps an external writer with the logger text settings. func (l *Logger) CreateTextWriter(w io.Writer) *LoggerTextWriter { return CreateTextWriter(w, l.printTraceback, l.printTime) } +// CreateTextStdoutWriter creates a non-owning text writer for os.Stdout. func (l *Logger) CreateTextStdoutWriter() *LoggerTextWriter { return CreateTextStdoutWriter(l.printTraceback, l.printTime) } +// CreateTextFileWriter creates an owning text writer for a file. func (l *Logger) CreateTextFileWriter(filename string) (*LoggerTextWriter, error) { return CreateTextFileWriter(filename, l.printTraceback, l.printTime) } +// CreateJsonWriter wraps an external writer with the logger JSON settings. func (l *Logger) CreateJsonWriter(w io.Writer) *LoggerJsonWriter { return CreateJsonWriter(w, l.jsonPretty) } +// CreateJsonStdoutWriter creates a non-owning JSON writer for os.Stdout. func (l *Logger) CreateJsonStdoutWriter() *LoggerJsonWriter { return CreateJsonStdoutWriter(l.jsonPretty) } +// CreateJsonFileWriter creates an owning JSON writer for a file. func (l *Logger) CreateJsonFileWriter(filename string) (*LoggerJsonWriter, error) { return CreateJsonFileWriter(filename, l.jsonPretty) } +// FormatTime converts time to the package text log timestamp format. func FormatTime(t time.Time) string { return fmt.Sprintf("%02d.%02d.%02d %02d:%02d:%02d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(), t.Second()) } +// FormatTraceback converts a traceback frame to a compact string. func FormatTraceback(mt *MethodTraceback) string { return fmt.Sprintf("%s:%s:%d", mt.Filename, mt.Method, mt.Line) } +// FormatFullTraceback joins multiple traceback frames into one string. func FormatFullTraceback(tracebacks []*MethodTraceback) string { formatted := make([]string, 0) for _, tb := range tracebacks { @@ -130,6 +153,7 @@ func FormatFullTraceback(tracebacks []*MethodTraceback) string { } return strings.Join(formatted, "->") } +// BuildString renders a text log record using the provided settings. func BuildString(level LogLevel, prefix string, printTime, printTraceback bool, m ...any) string { args := []string{ fmt.Sprintf("[%s]", prefix), diff --git a/logger_test.go b/logger_test.go index 0330220..ff19b4b 100644 --- a/logger_test.go +++ b/logger_test.go @@ -4,6 +4,7 @@ import ( "bytes" "errors" "os" + "strings" "testing" ) @@ -153,6 +154,42 @@ func TestCreateJsonStdoutWriterDoesNotCloseStdout(t *testing.T) { } } +func TestGetFullTracebackSkipsRuntimeAndSlogFrames(t *testing.T) { + tracebacks := captureTracebackForTest() + if len(tracebacks) == 0 { + t.Fatal("expected at least one traceback frame") + } + + first := tracebacks[0] + if first.Method != "captureTracebackForTest" { + t.Fatalf("first frame should be the nearest user frame, got %s", first.Method) + } + for _, tb := range tracebacks { + if strings.HasPrefix(tb.Signature, "runtime.") { + t.Fatalf("runtime frame should be filtered out, got %s", tb.Signature) + } + if _, ok := internalTracebackMethods[tb.Method]; ok { + t.Fatalf("internal slog frame should be filtered out, got %s", tb.Signature) + } + } +} + +func TestGetTracebackReturnsNearestUserFrame(t *testing.T) { + tb := captureSingleTracebackForTest() + if tb == nil { + t.Fatal("expected traceback frame") + } + if tb.Method != "captureSingleTracebackForTest" { + t.Fatalf("expected nearest user frame, got %s", tb.Method) + } + if strings.HasPrefix(tb.Signature, "runtime.") { + t.Fatalf("runtime frame should be filtered out, got %s", tb.Signature) + } + if _, ok := internalTracebackMethods[tb.Method]; ok { + t.Fatalf("internal slog frame should be filtered out, got %s", tb.Signature) + } +} + func swapStdout(t *testing.T) *os.File { t.Helper() @@ -168,3 +205,11 @@ func swapStdout(t *testing.T) *os.File { }) return stdoutFile } + +func captureTracebackForTest() []*MethodTraceback { + return getFullTraceback(0) +} + +func captureSingleTracebackForTest() *MethodTraceback { + return getTraceback() +} diff --git a/traceback.go b/traceback.go index 699345a..2c218ec 100644 --- a/traceback.go +++ b/traceback.go @@ -2,59 +2,83 @@ package slog import ( "runtime" - "sort" "strings" ) -func getTraceback() *MethodTraceback { - caller, _, _, _ := runtime.Caller(5) - details := runtime.FuncForPC(caller) - signature := details.Name() - path, line := details.FileLine(caller) - splitPath := strings.Split(path, "/") +var internalTracebackMethods = map[string]struct{}{ + "Infof": {}, + "Info": {}, + "Infoln": {}, + "Warnf": {}, + "Warn": {}, + "Warnln": {}, + "Errorf": {}, + "Error": {}, + "Errorln": {}, + "Fatalf": {}, + "Fatal": {}, + "Fatalln": {}, + "Debugf": {}, + "Debug": {}, + "Debugln": {}, + "print": {}, + "println": {}, + "reportWriterError": {}, + "getTraceback": {}, + "getFullTraceback": {}, +} +func getTraceback() *MethodTraceback { + tracebacks := getFullTraceback(0) + if len(tracebacks) == 0 { + return nil + } + return tracebacks[0] +} + +func getFullTraceback(skip int) []*MethodTraceback { + pc := make([]uintptr, 15) + n := runtime.Callers(skip+2, pc) + list := make([]*MethodTraceback, 0) + frames := runtime.CallersFrames(pc[:n]) + for { + frame, more := frames.Next() + tb := buildMethodTraceback(frame.PC) + if tb != nil && !isInternalTraceback(tb) { + list = append(list, tb) + } + if !more { + break + } + } + return list +} + +func buildMethodTraceback(pc uintptr) *MethodTraceback { + details := runtime.FuncForPC(pc) + if details == nil { + return nil + } + + signature := details.Name() + path, line := details.FileLine(pc) + splitPath := strings.Split(path, "/") splitSignature := strings.Split(signature, ".") method := splitSignature[len(splitSignature)-1] - tb := &MethodTraceback{ + return &MethodTraceback{ Filename: splitPath[len(splitPath)-1], FullPath: path, Line: line, Signature: signature, Method: method, } - - return tb } -func getFullTraceback(skip int) []*MethodTraceback { - pc := make([]uintptr, 15) - runtime.Callers(skip, pc) - list := make([]*MethodTraceback, 0) - frames := runtime.CallersFrames(pc) - for { - frame, more := frames.Next() - if !more { - break - } - details := runtime.FuncForPC(frame.PC) - signature := details.Name() - path, line := details.FileLine(frame.PC) - splitPath := strings.Split(path, "/") - splitSignature := strings.Split(signature, ".") - method := splitSignature[len(splitSignature)-1] - - tb := &MethodTraceback{ - Filename: splitPath[len(splitPath)-1], - FullPath: path, - Line: line, - Signature: signature, - Method: method, - } - list = append(list, tb) +func isInternalTraceback(tb *MethodTraceback) bool { + if strings.HasPrefix(tb.Signature, "runtime.") { + return true } - sort.Slice(list, func(i, j int) bool { - return j < i - }) - return list + _, ok := internalTracebackMethods[tb.Method] + return ok } diff --git a/utils.go b/utils.go index 2235817..e4831cf 100644 --- a/utils.go +++ b/utils.go @@ -1,5 +1,6 @@ package slog +// Map applies f to each element of s and returns the resulting slice. func Map[T, R any](s []T, f func(T) R) []R { out := make([]R, len(s)) for i, el := range s { diff --git a/writers.go b/writers.go index e916c0d..fe082d5 100644 --- a/writers.go +++ b/writers.go @@ -10,12 +10,14 @@ import ( "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 } +// LoggerTextWriter writes human-readable log records to an io.Writer. type LoggerTextWriter struct { LoggerWriter writer io.Writer @@ -24,14 +26,19 @@ type LoggerTextWriter struct { printTime bool } +// 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, _ []*MethodTraceback, messages ...any) error { s := BuildString(level, prefix, w.printTime, w.printTraceback, messages...) _, err := w.Write([]byte(s)) return err } + +// Close closes the owned writer, if any. func (w *LoggerTextWriter) Close() error { if w.closer == nil { return nil @@ -39,13 +46,15 @@ func (w *LoggerTextWriter) Close() error { return w.closer.Close() } -// LoggerJsonWriter write into writer JSON +// LoggerJsonWriter writes structured log records as JSON. type LoggerJsonWriter struct { LoggerWriter writer io.Writer closer io.Closer pretty bool } + +// LoggerJsonMessage is the JSON payload emitted by LoggerJsonWriter. type LoggerJsonMessage struct { Time time.Time `json:"time"` Level string `json:"level"` @@ -54,9 +63,12 @@ type LoggerJsonMessage struct { 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 { msg := Map(messages, func(el any) string { return fmt.Sprintf("%v", el) @@ -91,6 +103,8 @@ func (w *LoggerJsonWriter) Print(level LogLevel, prefix string, traceback []*Met _, err = w.Write(data) return err } + +// Close closes the owned writer, if any. func (w *LoggerJsonWriter) Close() error { if w.closer == nil { return nil @@ -98,12 +112,17 @@ func (w *LoggerJsonWriter) Close() error { return w.closer.Close() } +// CreateTextWriter wraps an external writer for text output without taking +// ownership of it. func CreateTextWriter(w io.Writer, printTraceback, printTime bool) *LoggerTextWriter { writer := &LoggerTextWriter{ writer: w, printTraceback: printTraceback, printTime: printTime, } 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, printTraceback, printTime bool) (*LoggerTextWriter, error) { err := os.MkdirAll(filepath.Dir(path), os.ModePerm) if err != nil { @@ -117,23 +136,34 @@ func CreateTextFileWriter(path string, printTraceback, printTime bool) (*LoggerT writer.closer = file return writer, nil } + +// CreateTextStdoutWriter creates a text writer for os.Stdout without taking +// ownership of stdout. func CreateTextStdoutWriter(printTraceback, printTime bool) *LoggerTextWriter { writer := CreateTextWriter(os.Stdout, printTraceback, printTime) 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 {