diff --git a/README.md b/README.md index 193e151..cf47cca 100644 --- a/README.md +++ b/README.md @@ -72,4 +72,74 @@ BenchmarkLoggers/dummy-4 1239873 893.2 ns/op BenchmarkLoggers/console-4 483354 2338 ns/op 128 B/op 1 allocs/op BenchmarkLoggers/std-text-4 368828 3141 ns/op 132 B/op 3 allocs/op BenchmarkLoggers/std-json-4 393322 2909 ns/op 248 B/op 4 allocs/op -``` \ No newline at end of file +``` + +## Indentation +`console.Handler` also implements the custom interface: +```go +type Indenter interface { + // SetIndentation configures the prefix and indent strings. + // The prefix, if any, is added first, + // then the indent string is added as many times as the current depth. + SetIndentation(prefix, indent string) + + // Increment the indentation depth. + Increment() + + // Decrement the indentation depth. + // If the current depth is already zero there is no change. + Decrement() +} +``` +Indentation of functions can occasionally be useful for delineating +calls by indenting callees more than callers. + +This isn't something that `log.slog` provides. +It is necessary to provide the handler pointer as a global variable or function +in order to use the `Indenter` interface. + +Because the current `depth` is stored within the `console.Handler` this functionality is not thread-safe. +In order to use it in a multi-thread environment there must be a handler (and logger) for each thread, +and these items must be passed down through all intervening function calls (in lieu of thread variables). + +### Example + +```go +package main + +import ( + "log/slog" + "os" + + "github.com/phsym/console-slog" +) + +var hdlr *console.Handler + +func main() { + hdlr = console.NewHandler(os.Stderr, &console.HandlerOptions{Level: slog.LevelDebug}) + hdlr.SetIndentation("", " ") + slog.SetDefault(slog.New(hdlr)) + slog.Info("factorial", "result", factorial(7)) +} + +func factorial(number uint) uint { + hdlr.Increment() + defer hdlr.Decrement() + + slog.Debug("factorial", "number", number) + var result uint + if number < 2 { + result = 1 + } else { + result = number * factorial(number-1) + } + slog.Debug("factorial", "result", result) + return result +} +``` + +![factorial-indent](./doc/img/factorial-indent.png) + +The specific case of runaway recursion shows up clearly as the messages +march repeatedly across the screen. \ No newline at end of file diff --git a/doc/img/factorial-indent.png b/doc/img/factorial-indent.png new file mode 100644 index 0000000..575aea2 Binary files /dev/null and b/doc/img/factorial-indent.png differ diff --git a/example/main.go b/examples/basic/main.go similarity index 100% rename from example/main.go rename to examples/basic/main.go diff --git a/examples/indent/main.go b/examples/indent/main.go new file mode 100644 index 0000000..5317233 --- /dev/null +++ b/examples/indent/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "log/slog" + "os" + + "github.com/phsym/console-slog" +) + +var hdlr *console.Handler + +func main() { + hdlr = console.NewHandler(os.Stderr, &console.HandlerOptions{Level: slog.LevelDebug}) + hdlr.SetIndentation("", " ") + slog.SetDefault(slog.New(hdlr)) + slog.Info("factorial", "result", factorial(7)) +} + +func factorial(number uint) uint { + hdlr.Increment() + defer hdlr.Decrement() + + slog.Debug("factorial", "number", number) + var result uint + if number < 2 { + result = 1 + } else { + result = number * factorial(number-1) + } + slog.Debug("factorial", "result", result) + return result +} diff --git a/handler.go b/handler.go index 82ce3ea..f7ed79b 100644 --- a/handler.go +++ b/handler.go @@ -40,12 +40,60 @@ type HandlerOptions struct { Theme Theme } +type Indenter interface { + // SetIndentation configures the prefix and indent strings. + // The prefix, if any, is added first, + // then the indent string is added as many times as the current depth. + SetIndentation(prefix, indent string) + + // Increment the indentation depth. + Increment() + + // Decrement the indentation depth. + // If the current depth is already zero there is no change. + Decrement() +} + +// indentation defines support data for enhanced message and arg indentation by depth. +type indentation struct { + // Prefix for indentation string. + prefix string + + // Indent string for each depth level. + indent string + + // Depth is the current indentation depth + depth uint +} + +func (hd *indentation) indentMsg(msg string) string { + if hd.prefix == "" && (hd.indent == "" || hd.depth < 1) { + // No indentation. + return msg + } + + // Build indentation string. + builder := strings.Builder{} + if hd.prefix != "" { + builder.WriteString(hd.prefix) + } + if hd.indent != "" && hd.depth > 0 { + var i uint + for i = 0; i < hd.depth; i++ { + builder.WriteString(hd.indent) + } + } + builder.WriteString(msg) + return builder.String() +} + type Handler struct { opts HandlerOptions out io.Writer group string context buffer enc *encoder + indentation } var _ slog.Handler = (*Handler)(nil) @@ -89,7 +137,7 @@ func (h *Handler) Handle(_ context.Context, rec slog.Record) error { if h.opts.AddSource && rec.PC > 0 { h.enc.writeSource(buf, rec.PC, cwd) } - h.enc.writeMessage(buf, rec.Level, rec.Message) + h.enc.writeMessage(buf, rec.Level, h.indentMsg(rec.Message)) buf.copy(&h.context) rec.Attrs(func(a slog.Attr) bool { h.enc.writeAttr(buf, a, h.group) @@ -135,3 +183,21 @@ func (h *Handler) WithGroup(name string) slog.Handler { enc: h.enc, } } + +var _ Indenter = (*Handler)(nil) + +func (h *Handler) SetIndentation(prefix, indent string) { + h.prefix = prefix + h.indent = indent + h.depth = 0 +} + +func (h *Handler) Increment() { + h.depth++ +} + +func (h *Handler) Decrement() { + if h.depth > 0 { + h.depth-- + } +}