From 9a10e4f9c52b7a6654688a331f2275b804c8ba09 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 17 Sep 2024 18:58:59 +0200 Subject: [PATCH] handler: add styled output option --- handler.go | 46 +++++++++++++++++++++++++++++++++++++++++++--- level.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/handler.go b/handler.go index 4672460..293287e 100644 --- a/handler.go +++ b/handler.go @@ -6,6 +6,7 @@ import ( "io" "log/slog" "strconv" + "strings" "sync" "sync/atomic" "time" @@ -13,6 +14,15 @@ import ( "unicode/utf8" ) +const ( + resetSeq = "0" + boldSeq = "1" + faintSeq = "2" + + esc = '\x1b' + csi = string(esc) + "[" +) + // HandlerOption is the signature of a functional option that can be used to // modify the behaviour of the DefaultHandler. type HandlerOption func(*handlerOpts) @@ -49,6 +59,15 @@ func WithTimeSource(fn func() time.Time) HandlerOption { } } +// WithStyledOutput can be used to add additional styling to the logs. This +// currently includes colored & bold tags and faint fonts for attribute keys and +// callsites. +func WithStyledOutput() HandlerOption { + return func(opts *handlerOpts) { + opts.styled = true + } +} + // WithNoTimestamp is an option that can be used to omit timestamps from the log // lines. func WithNoTimestamp() HandlerOption { @@ -228,6 +247,23 @@ func (d *DefaultHandler) with(tag string, withCallstackOffset bool, return &sl } +func (d *DefaultHandler) styleString(s string, styles ...string) string { + if !d.opts.styled { + return s + } + + if len(styles) == 0 { + return s + } + + seq := strings.Join(styles, ";") + if seq == "" { + return s + } + + return fmt.Sprintf("%s%sm%s%sm", csi, seq, s, csi+resetSeq) +} + func (d *DefaultHandler) appendAttr(buf *buffer, a slog.Attr) { // Resolve the Attr's value before doing anything else. a.Value = a.Value.Resolve() @@ -244,7 +280,9 @@ func (d *DefaultHandler) appendAttr(buf *buffer, a slog.Attr) { func (d *DefaultHandler) writeLevel(buf *buffer, level Level) { str := fmt.Sprintf("[%s]", level) - buf.writeString(str) + buf.writeString(d.styleString( + str, boldSeq, string(level.ansiColoSeq())), + ) } func (d *DefaultHandler) writeCallSite(buf *buffer, file string, line int) { @@ -252,7 +290,9 @@ func (d *DefaultHandler) writeCallSite(buf *buffer, file string, line int) { return } - buf.writeString(fmt.Sprintf(" %s:%d", file, line)) + buf.writeString( + d.styleString(fmt.Sprintf(" %s:%d", file, line), faintSeq), + ) } func appendString(buf *buffer, str string) { @@ -270,7 +310,7 @@ func (d *DefaultHandler) appendKey(buf *buffer, key string) { } key += "=" - buf.writeString(key) + buf.writeString(d.styleString(key, faintSeq)) } func appendValue(buf *buffer, v slog.Value) { diff --git a/level.go b/level.go index db84295..b840ebb 100644 --- a/level.go +++ b/level.go @@ -80,3 +80,31 @@ func (l Level) String() string { return str("CRT", l-LevelCritical) } } + +type ansiColorSeq string + +const ( + ansiColorSeqDarkTeal ansiColorSeq = "38;5;30" + ansiColorSeqDarkBlue ansiColorSeq = "38;5;63" + ansiColorSeqLightBlue ansiColorSeq = "38;5;86" + ansiColorSeqYellow ansiColorSeq = "38;5;192" + ansiColorSeqRed ansiColorSeq = "38;5;204" + ansiColorSeqPink ansiColorSeq = "38;5;134" +) + +func (l Level) ansiColoSeq() ansiColorSeq { + switch l { + case LevelTrace: + return ansiColorSeqDarkTeal + case LevelDebug: + return ansiColorSeqDarkBlue + case LevelWarn: + return ansiColorSeqYellow + case LevelError: + return ansiColorSeqRed + case LevelCritical: + return ansiColorSeqPink + default: + return ansiColorSeqLightBlue + } +}