• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

fornellas / resonance / 14681039432

26 Apr 2025 12:05PM UTC coverage: 57.102% (+0.3%) from 56.807%
14681039432

Pull #262

github

web-flow
Merge d5a56d147 into 194d32297
Pull Request #262: Refactor Logger

662 of 831 new or added lines in 29 files covered. (79.66%)

8 existing lines in 5 files now uncovered.

3791 of 6639 relevant lines covered (57.1%)

22.76 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.87
/log/console_handler.go
1
package log
2

3
import (
4
        "bytes"
5
        "context"
6
        "fmt"
7
        "io"
8
        "log/slog"
9
        "os"
10
        "runtime"
11
        "slices"
12
        "strconv"
13
        "strings"
14
        "sync"
15
        "unicode/utf8"
16

17
        "golang.org/x/term"
18

19
        "github.com/fornellas/resonance/unicode"
20
)
21

22
// ANSI color codes for console output
23
const (
24
        reset     = "\033[0m"
25
        bold      = "\033[1m"
26
        dim       = "\033[2m"
27
        italic    = "\033[3m"
28
        underline = "\033[4m"
29
        blink     = "\033[5m"
30
        reverse   = "\033[7m"
31
        hidden    = "\033[8m"
32
        strike    = "\033[9m"
33

34
        // Foreground colors (3-bit)
35
        black   = "\033[30m"
36
        red     = "\033[31m"
37
        green   = "\033[32m"
38
        yellow  = "\033[33m"
39
        blue    = "\033[34m"
40
        magenta = "\033[35m"
41
        cyan    = "\033[36m"
42
        white   = "\033[37m"
43

44
        // Background colors (3-bit)
45
        // blackBg   = "\033[40m"
46
        // redBg     = "\033[41m"
47
        // greenBg   = "\033[42m"
48
        // yellowBg  = "\033[43m"
49
        // blueBg    = "\033[44m"
50
        // magentaBg = "\033[45m"
51
        // cyanBg    = "\033[46m"
52
        // whiteBg   = "\033[47m"
53

54
        // // Bright foreground colors (4-bit)
55
        // darkGray     = "\033[90m"
56
        // lightRed     = "\033[91m"
57
        // lightGreen   = "\033[92m"
58
        // lightYellow  = "\033[93m"
59
        // lightBlue    = "\033[94m"
60
        // lightMagenta = "\033[95m"
61
        // lightCyan    = "\033[96m"
62
        // lightWhite   = "\033[97m"
63

64
        // // Bright background colors (4-bit)
65
        // darkGrayBg     = "\033[100m"
66
        // lightRedBg     = "\033[101m"
67
        // lightGreenBg   = "\033[102m"
68
        // lightYellowBg  = "\033[103m"
69
        // lightBlueBg    = "\033[104m"
70
        // lightMagentaBg = "\033[105m"
71
        // lightCyanBg    = "\033[106m"
72
        // lightWhiteBg   = "\033[107m"
73
)
74

75
// currHandlerChain tracks the chain of ConsoleHandler instances to avoid
76
// duplicating output of handler attributes. It maintains the last written
77
// handler chain to determine which parts of the chain need to be written
78
// for subsequent log entries.
79
type currHandlerChain struct {
80
        chain []*ConsoleHandler
81
        m     sync.Mutex
82
}
83

84
func newCurrHandlerChain() *currHandlerChain {
103✔
85
        return &currHandlerChain{
103✔
86
                chain: []*ConsoleHandler{},
103✔
87
        }
103✔
88
}
103✔
89

90
func (s *currHandlerChain) writeHandlerGroupAttrs(writer io.Writer, handlerChain []*ConsoleHandler) {
139✔
91
        s.m.Lock()
139✔
92
        defer s.m.Unlock()
139✔
93

139✔
94
        for i, h := range handlerChain {
461✔
95
                if i+1 > len(s.chain) {
471✔
96
                        h.writeHandlerGroupAttrs(writer, nil)
149✔
97
                } else {
332✔
98
                        ch := s.chain[i]
183✔
99
                        if h != ch {
232✔
100
                                h.writeHandlerGroupAttrs(writer, ch)
49✔
101
                        }
49✔
102
                }
103
        }
104

105
        s.chain = make([]*ConsoleHandler, len(handlerChain))
139✔
106
        copy(s.chain, handlerChain)
139✔
107
}
108

109
// ConsoleHandlerOptions extends HandlerOptions with ConsoleHandler specific options.
110
type ConsoleHandlerOptions struct {
111
        slog.HandlerOptions
112
        // Time layout for timestamps; if empty, time is not included in output.
113
        TimeLayout string
114
        // If true, force ANSI escape sequences for color, even when no TTY detected.
115
        ForceColor bool
116
        // If true, disable color, even when TTY detected; takes precedence over ForceColor.
117
        NoColor bool
118
}
119

120
// ConsoleHandler implements slog.Handler interface with enhanced console output features.
121
// It provides colorized logging with level-appropriate colors, proper indentation for
122
// nested groups, and smart handling of multiline content. ConsoleHandler automatically
123
// detects terminal capabilities and enables or disables ANSI color codes accordingly,
124
// though this behavior can be overridden through options. The handler also supports
125
// customizable timestamp formats and sophisticated attribute formatting.
126
type ConsoleHandler struct {
127
        opts             *ConsoleHandlerOptions
128
        writer           io.Writer
129
        writerMutex      *sync.Mutex
130
        color            bool
131
        groups           []string
132
        attrs            []slog.Attr
133
        handlerChain     []*ConsoleHandler
134
        currHandlerChain *currHandlerChain
135
}
136

137
// NewConsoleHandler creates a new ConsoleHandler
138
func NewConsoleHandler(w io.Writer, opts *ConsoleHandlerOptions) *ConsoleHandler {
103✔
139
        if opts == nil {
108✔
140
                opts = &ConsoleHandlerOptions{}
5✔
141
        }
5✔
142

143
        isTTY := false
103✔
144
        if f, ok := w.(*os.File); ok {
135✔
145
                isTTY = term.IsTerminal(int(f.Fd()))
32✔
146
        }
32✔
147

148
        h := &ConsoleHandler{
103✔
149
                opts:             opts,
103✔
150
                writer:           w,
103✔
151
                writerMutex:      &sync.Mutex{},
103✔
152
                color:            !opts.NoColor && (opts.ForceColor || isTTY),
103✔
153
                groups:           []string{},
103✔
154
                attrs:            []slog.Attr{},
103✔
155
                currHandlerChain: newCurrHandlerChain(),
103✔
156
        }
103✔
157
        h.handlerChain = []*ConsoleHandler{h}
103✔
158
        return h
103✔
159
}
160

161
func (h *ConsoleHandler) clone() *ConsoleHandler {
309✔
162
        h2 := *h
309✔
163
        h2.groups = slices.Clip(h.groups)
309✔
164
        h2.attrs = slices.Clip(h.attrs)
309✔
165
        h2.handlerChain = slices.Clip(h.handlerChain)
309✔
166
        return &h2
309✔
167
}
309✔
168

169
// Enabled implements slog.Handler.Enabled
170
func (h *ConsoleHandler) Enabled(_ context.Context, level slog.Level) bool {
222✔
171
        minLevel := slog.LevelInfo
222✔
172
        if h.opts.Level != nil {
378✔
173
                minLevel = h.opts.Level.Level()
156✔
174
        }
156✔
175
        return level >= minLevel
222✔
176
}
177

178
// WithAttrs implements slog.Handler.WithAttrs
179
func (h *ConsoleHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
140✔
180
        h2 := h.clone()
140✔
181
        h2.attrs = append(h2.attrs, attrs...)
140✔
182
        h2.handlerChain[len(h2.handlerChain)-1] = h2
140✔
183
        return h2
140✔
184
}
140✔
185

186
// WithGroup implements slog.Handler.WithGroup
187
func (h *ConsoleHandler) WithGroup(name string) slog.Handler {
185✔
188
        if len(name) == 0 {
193✔
189
                return h
8✔
190
        }
8✔
191

192
        h2 := h.clone()
179✔
193
        h2.groups = append(h2.groups, name)
179✔
194
        h2.attrs = []slog.Attr{}
179✔
195
        h2.handlerChain = append(h2.handlerChain, h2)
179✔
196
        return h2
179✔
197
}
198

199
func (h *ConsoleHandler) escape(s string) string {
602✔
200
        rs := []rune{}
602✔
201
        for _, r := range s {
8,441✔
202
                if r == '\t' || strconv.IsPrint(r) {
15,678✔
203
                        rs = append(rs, r)
7,839✔
204
                } else {
7,839✔
NEW
205
                        e := strconv.QuoteRune(r)
×
NEW
206
                        e = e[1 : len(e)-1]
×
NEW
207
                        rs = append(rs, []rune(e)...)
×
NEW
208
                }
×
209
        }
210
        return string(rs)
602✔
211
}
212

213
func (h *ConsoleHandler) colorize(s string, color string) string {
530✔
214
        if h.color {
538✔
215
                return color + s + reset
8✔
216
        }
8✔
217
        return s
524✔
218
}
219

220
func (h *ConsoleHandler) writeAttr(writer io.Writer, indent int, attr slog.Attr) {
287✔
221
        attr.Value = attr.Value.Resolve()
287✔
222
        if h.opts.ReplaceAttr != nil && attr.Value.Kind() != slog.KindGroup {
331✔
223
                attr = h.opts.ReplaceAttr(h.groups, attr)
44✔
224
                attr.Value = attr.Value.Resolve()
44✔
225
        }
44✔
226

227
        if attr.Equal(slog.Attr{}) {
287✔
NEW
228
                return
×
NEW
229
        }
×
230

231
        indentStr := strings.Repeat("  ", indent)
287✔
232

287✔
233
        if attr.Value.Kind() == slog.KindGroup {
404✔
234
                groupAttrs := attr.Value.Group()
117✔
235
                if len(attr.Key) == 0 {
117✔
NEW
236
                        for _, groupAttr := range groupAttrs {
×
NEW
237
                                h.writeAttr(writer, indent, groupAttr)
×
NEW
238
                        }
×
239
                } else {
117✔
240
                        emoji := ""
117✔
241
                        r, _ := utf8.DecodeRuneInString(attr.Key)
117✔
242
                        if !unicode.IsEmojiStartCodePoint(r) {
152✔
243
                                emoji = "🏷️ "
35✔
244
                        }
35✔
245
                        fmt.Fprintf(writer, "%s%s%s\n", indentStr, emoji, h.colorize(h.escape(attr.Key), bold))
117✔
246
                        for _, groupAttr := range groupAttrs {
230✔
247
                                h.writeAttr(writer, indent+1, groupAttr)
113✔
248
                        }
113✔
249
                }
250
        } else {
180✔
251
                fmt.Fprintf(writer, "%s%s:", indentStr, h.colorize(h.escape(attr.Key), cyan+dim))
180✔
252
                valueStr := attr.Value.String()
180✔
253
                if len(valueStr) > 0 && bytes.ContainsRune([]byte(valueStr), '\n') {
189✔
254
                        strings.SplitSeq(valueStr, "\n")(func(line string) bool {
34✔
255
                                fmt.Fprintf(writer, "\n  %s%s", indentStr, h.colorize(h.escape(line), dim))
25✔
256
                                return true
25✔
257
                        })
25✔
258
                        writer.Write([]byte("\n"))
9✔
259
                } else {
175✔
260
                        fmt.Fprintf(writer, " %s\n", h.colorize(h.escape(valueStr), dim))
175✔
261
                }
175✔
262
        }
263
}
264

265
func (h *ConsoleHandler) sameGroups(h2 *ConsoleHandler) bool {
49✔
266
        if len(h.groups) != len(h2.groups) {
49✔
NEW
267
                return false
×
NEW
268
        }
×
269
        for i, v := range h.groups {
129✔
270
                if v != h2.groups[i] {
112✔
271
                        return false
32✔
272
                }
32✔
273
        }
274
        return true
19✔
275
}
276

277
// write handler group & attrs, as a function of the current handler at the chain, preventing
278
// duplicate attrs
279
func (h *ConsoleHandler) writeHandlerGroupAttrs(writer io.Writer, ch *ConsoleHandler) {
190✔
280
        var attrs []slog.Attr
190✔
281
        var sameGroups bool
190✔
282
        if ch != nil {
239✔
283
                if sameGroups = h.sameGroups(ch); sameGroups {
68✔
284
                        attrs = []slog.Attr{}
19✔
285
                        for i, attr := range h.attrs {
42✔
286
                                if i+1 <= len(ch.attrs) && attr.Equal(ch.attrs[i]) {
34✔
287
                                        continue
11✔
288
                                }
289
                                attrs = append(attrs, attr)
14✔
290
                        }
291
                } else {
32✔
292
                        attrs = h.attrs
32✔
293
                }
32✔
294
        } else {
149✔
295
                attrs = h.attrs
149✔
296
        }
149✔
297
        if len(h.groups) > 0 {
314✔
298
                if sameGroups {
140✔
299
                        for _, attr := range attrs {
27✔
300
                                h.writeAttr(writer, len(h.groups), attr)
11✔
301
                        }
11✔
302
                } else {
114✔
303
                        attrAny := make([]any, len(attrs))
114✔
304
                        for i, attr := range attrs {
221✔
305
                                attrAny[i] = attr
107✔
306
                        }
107✔
307
                        h.writeAttr(writer, len(h.groups)-1, slog.Group(h.groups[len(h.groups)-1], attrAny...))
114✔
308
                }
309
        } else {
76✔
310
                for _, attr := range attrs {
96✔
311
                        h.writeAttr(writer, 0, attr)
20✔
312
                }
20✔
313
        }
314
}
315

316
// Handle implements slog.Handler.Handle
317
func (h *ConsoleHandler) Handle(_ context.Context, record slog.Record) error {
139✔
318
        var buff bytes.Buffer
139✔
319

139✔
320
        h.currHandlerChain.writeHandlerGroupAttrs(&buff, h.handlerChain)
139✔
321

139✔
322
        indentStr := strings.Repeat("  ", len(h.groups))
139✔
323
        buff.Write([]byte(indentStr))
139✔
324

139✔
325
        if h.opts.TimeLayout != "" && !record.Time.IsZero() {
144✔
326
                timeStr := record.Time.Round(0).Format(h.opts.TimeLayout)
5✔
327
                fmt.Fprintf(&buff, "%s ", h.colorize(timeStr, dim))
5✔
328
        }
5✔
329

330
        message := h.escape(record.Message)
139✔
331
        if h.color {
144✔
332
                if record.Level >= slog.LevelError {
10✔
333
                        fmt.Fprintf(&buff, "%s %s\n", h.colorize(record.Level.String(), red+bold), h.colorize(message, bold))
5✔
334
                } else if record.Level >= slog.LevelWarn {
5✔
NEW
335
                        fmt.Fprintf(&buff, "%s %s\n", h.colorize(record.Level.String(), yellow+bold), h.colorize(message, bold))
×
NEW
336
                } else if record.Level >= slog.LevelInfo {
×
NEW
337
                        fmt.Fprintf(&buff, "%s\n", h.colorize(message, bold))
×
NEW
338
                } else if record.Level >= slog.LevelDebug {
×
NEW
339
                        fmt.Fprintf(&buff, "%s\n", message)
×
NEW
340
                } else {
×
NEW
341
                        fmt.Fprintf(&buff, "%s\n", h.colorize(message, dim))
×
NEW
342
                }
×
343
        } else {
136✔
344
                fmt.Fprintf(&buff, "%s %s\n", record.Level.String(), message)
136✔
345
        }
136✔
346

347
        if h.opts.HandlerOptions.AddSource && record.PC != 0 {
171✔
348
                frames := runtime.CallersFrames([]uintptr{record.PC})
32✔
349
                frame, _ := frames.Next()
32✔
350
                fileInfo := fmt.Sprintf("%s:%d", frame.File, frame.Line)
32✔
351
                fmt.Fprintf(&buff, "%s  %s", indentStr, h.colorize(fileInfo, dim))
32✔
352
                if len(frame.Function) > 0 {
64✔
353
                        fmt.Fprintf(&buff, " (%s)", h.colorize(frame.Function, dim))
32✔
354
                }
32✔
355
                buff.Write([]byte("\n"))
32✔
356
        }
357

358
        if record.NumAttrs() > 0 {
185✔
359
                record.Attrs(func(attr slog.Attr) bool {
101✔
360
                        h.writeAttr(&buff, len(h.groups)+1, attr)
55✔
361
                        return true
55✔
362
                })
55✔
363
        }
364

365
        h.writerMutex.Lock()
139✔
366
        defer h.writerMutex.Unlock()
139✔
367
        h.writer.Write(buff.Bytes())
139✔
368

139✔
369
        return nil
139✔
370
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc