Skip to content

Commit eeb5e22

Browse files
committed
feat(io): add task aware logger
1 parent 8a65daf commit eeb5e22

7 files changed

Lines changed: 620 additions & 37 deletions

File tree

io/ci.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package io
2+
3+
import "os"
4+
5+
// isCI returns true if running in a CI environment.
6+
func isCI() bool {
7+
return os.Getenv("GITHUB_ACTIONS") == "true" || os.Getenv("CI") == "true"
8+
}

io/logger.go

Lines changed: 133 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"os"
66
"time"
77

8+
"charm.land/lipgloss/v2"
89
"charm.land/log/v2"
910
"github.com/charmbracelet/colorprofile"
1011
"github.com/muesli/termenv"
@@ -193,14 +194,14 @@ func (l *StandardLogger) Infof(msg string, args ...any) {
193194
l.syncLoggerFormat()
194195
switch l.mode {
195196
case Text:
196-
l.PlainTextInfo(fmt.Sprintf(msg, args...))
197+
l.PlainTextInfo(safeSprintf(msg, args...))
197198
return
198199
case Hidden:
199200
return
200201
case JSON, Logfmt:
201-
l.outHandler.Infof(msg, args...)
202+
l.outHandler.Info(safeSprintf(msg, args...))
202203
if l.archiveHandler != nil {
203-
l.archiveHandler.Infof(msg, args...)
204+
l.archiveHandler.Info(safeSprintf(msg, args...))
204205
}
205206
}
206207
}
@@ -209,14 +210,14 @@ func (l *StandardLogger) Noticef(msg string, args ...any) {
209210
l.syncLoggerFormat()
210211
switch l.mode {
211212
case Text:
212-
l.PlainTextNotice(fmt.Sprintf(msg, args...))
213+
l.PlainTextNotice(safeSprintf(msg, args...))
213214
return
214215
case Hidden:
215216
return
216217
case JSON, Logfmt:
217-
l.outHandler.With().Log(themes.LogNoticeLevel, msg, args...)
218+
l.outHandler.With().Log(themes.LogNoticeLevel, safeSprintf(msg, args...))
218219
if l.archiveHandler != nil {
219-
l.archiveHandler.Errorf(msg, args...)
220+
l.archiveHandler.Error(safeSprintf(msg, args...))
220221
}
221222
}
222223
}
@@ -225,14 +226,14 @@ func (l *StandardLogger) Debugf(msg string, args ...any) {
225226
l.syncLoggerFormat()
226227
switch l.mode {
227228
case Text:
228-
l.PlainTextDebug(fmt.Sprintf(msg, args...))
229+
l.PlainTextDebug(safeSprintf(msg, args...))
229230
return
230231
case Hidden:
231232
return
232233
case JSON, Logfmt:
233-
l.outHandler.Debugf(msg, args...)
234+
l.outHandler.Debug(safeSprintf(msg, args...))
234235
if l.archiveHandler != nil {
235-
l.archiveHandler.Debugf(msg, args...)
236+
l.archiveHandler.Debug(safeSprintf(msg, args...))
236237
}
237238
}
238239
}
@@ -251,14 +252,14 @@ func (l *StandardLogger) Errorf(msg string, args ...any) {
251252
l.syncLoggerFormat()
252253
switch l.mode {
253254
case Text:
254-
l.PlainTextError(fmt.Sprintf(msg, args...))
255+
l.PlainTextError(safeSprintf(msg, args...))
255256
return
256257
case Hidden:
257258
return
258259
case JSON, Logfmt:
259-
l.outHandler.Errorf(msg, args...)
260+
l.outHandler.Error(safeSprintf(msg, args...))
260261
if l.archiveHandler != nil {
261-
l.archiveHandler.Errorf(msg, args...)
262+
l.archiveHandler.Error(safeSprintf(msg, args...))
262263
}
263264
}
264265
}
@@ -267,14 +268,14 @@ func (l *StandardLogger) Warnf(msg string, args ...any) {
267268
l.syncLoggerFormat()
268269
switch l.mode {
269270
case Text:
270-
l.PlainTextWarn(fmt.Sprintf(msg, args...))
271+
l.PlainTextWarn(safeSprintf(msg, args...))
271272
return
272273
case Hidden:
273274
return
274275
case JSON, Logfmt:
275-
l.outHandler.Warnf(msg, args...)
276+
l.outHandler.Warn(safeSprintf(msg, args...))
276277
if l.archiveHandler != nil {
277-
l.archiveHandler.Warnf(msg, args...)
278+
l.archiveHandler.Warn(safeSprintf(msg, args...))
278279
}
279280
}
280281
}
@@ -285,18 +286,19 @@ func (l *StandardLogger) FatalErr(err error) {
285286

286287
func (l *StandardLogger) Fatalf(msg string, args ...any) {
287288
l.syncLoggerFormat()
289+
formatted := safeSprintf(msg, args...)
288290
switch l.mode {
289291
case Text:
290-
l.PlainTextError(fmt.Sprintf(msg, args...))
291-
l.exitFunc(msg, args...)
292+
l.PlainTextError(formatted)
293+
l.exitFunc(formatted)
292294
return
293295
case Hidden:
294296
return
295297
case JSON, Logfmt:
296298
if l.archiveHandler != nil {
297-
l.archiveHandler.Errorf(msg, args...)
299+
l.archiveHandler.Error(formatted)
298300
}
299-
l.outHandler.Fatalf(msg, args...)
301+
l.outHandler.Fatal(formatted)
300302
}
301303
}
302304

@@ -450,6 +452,118 @@ func (l *StandardLogger) syncLoggerFormat() {
450452
}
451453
}
452454

455+
// safeSprintf applies fmt.Sprintf only when args are provided.
456+
// When called with no args, it returns msg as-is, avoiding
457+
// misinterpretation of % characters in the message.
458+
func safeSprintf(msg string, args ...any) string {
459+
if len(args) == 0 {
460+
return msg
461+
}
462+
return fmt.Sprintf(msg, args...)
463+
}
464+
453465
func defaultExit(_ string, _ ...any) {
454466
os.Exit(1)
455467
}
468+
469+
// --- TaskAwareLogger implementation ---
470+
471+
func (l *StandardLogger) PrintWithTask(task *TaskContext, line string) {
472+
if l.mode == Hidden {
473+
return
474+
}
475+
prefix := taskPrefix(task, l.theme.ColorPalette())
476+
formatted := fmt.Sprintf("%s %s", prefix, line)
477+
_, _ = fmt.Fprintln(l.outFile, formatted)
478+
479+
if l.archiveHandler != nil {
480+
l.archiveHandler.Info(line, "task", task.Name)
481+
}
482+
}
483+
484+
func (l *StandardLogger) PrintErrWithTask(task *TaskContext, line string) {
485+
if l.mode == Hidden {
486+
return
487+
}
488+
prefix := taskPrefix(task, l.theme.ColorPalette())
489+
errLine := l.theme.RenderError(line)
490+
formatted := fmt.Sprintf("%s %s", prefix, errLine)
491+
_, _ = fmt.Fprintln(l.outFile, formatted)
492+
493+
if l.archiveHandler != nil {
494+
l.archiveHandler.Error(line, "task", task.Name)
495+
}
496+
}
497+
498+
func (l *StandardLogger) PrintTaskSummary(tasks []*TaskContext) {
499+
if l.mode == Hidden || len(tasks) == 0 {
500+
return
501+
}
502+
503+
_, _ = fmt.Fprintln(l.outFile)
504+
505+
palette := l.theme.ColorPalette()
506+
headerStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color(palette.Primary))
507+
_, _ = fmt.Fprintln(l.outFile, headerStyle.Render("Task Summary"))
508+
509+
maxLen := 0
510+
for _, t := range tasks {
511+
if len(t.Name) > maxLen {
512+
maxLen = len(t.Name)
513+
}
514+
}
515+
516+
for _, t := range tasks {
517+
var statusColor string
518+
switch t.Status {
519+
case TaskSuccess:
520+
statusColor = palette.Success
521+
case TaskFailed:
522+
statusColor = palette.Error
523+
case TaskSkipped:
524+
statusColor = palette.Warning
525+
case TaskRunning:
526+
statusColor = palette.Info
527+
}
528+
529+
nameStyle := lipgloss.NewStyle().Width(maxLen + 2)
530+
statusStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(statusColor)).Bold(true)
531+
durationStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(palette.Gray))
532+
533+
line := fmt.Sprintf(" %s %s %s",
534+
nameStyle.Render(t.Name),
535+
statusStyle.Render(fmt.Sprintf("%-4s", t.Status.Icon())),
536+
durationStyle.Render(t.Duration().Truncate(time.Millisecond).String()),
537+
)
538+
_, _ = fmt.Fprintln(l.outFile, line)
539+
}
540+
_, _ = fmt.Fprintln(l.outFile)
541+
542+
if l.archiveHandler != nil {
543+
for _, t := range tasks {
544+
l.archiveHandler.Info("task_summary",
545+
"task", t.Name,
546+
"status", t.Status.String(),
547+
"duration", t.Duration().String(),
548+
)
549+
}
550+
}
551+
}
552+
553+
func (l *StandardLogger) BeginGroup(name string) {
554+
if isCI() {
555+
_, _ = fmt.Fprintf(l.outFile, "::group::%s\n", name)
556+
} else {
557+
palette := l.theme.ColorPalette()
558+
style := lipgloss.NewStyle().
559+
Foreground(lipgloss.Color(palette.Secondary)).
560+
Bold(true)
561+
_, _ = fmt.Fprintf(l.outFile, "\n%s\n", style.Render("--- "+name+" ---"))
562+
}
563+
}
564+
565+
func (l *StandardLogger) EndGroup() {
566+
if isCI() {
567+
_, _ = fmt.Fprintln(l.outFile, "::endgroup::")
568+
}
569+
}

io/output.go

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ type StdOutWriter struct {
99
LogFields []any
1010
Logger Logger
1111
LogMode *LogMode
12+
Task *TaskContext
1213
}
1314

1415
func (w StdOutWriter) Write(p []byte) (n int, err error) {
@@ -29,17 +30,16 @@ func (w StdOutWriter) Write(p []byte) (n int, err error) {
2930
case Text:
3031
w.Logger.Print(string(p))
3132
return len(p), nil
32-
case JSON, Logfmt:
33-
if strings.TrimSpace(string(p)) == "" {
34-
return len(p), nil
35-
}
36-
splitP := strings.Split(string(p), "\n")
37-
for _, line := range splitP {
38-
if strings.TrimSpace(line) == "" {
39-
continue
33+
case Logfmt:
34+
if w.Task != nil {
35+
if tal, ok := w.Logger.(TaskAwareLogger); ok {
36+
writeLines(p, func(line string) { tal.PrintWithTask(w.Task, line) })
37+
return len(p), nil
4038
}
41-
w.Logger.Info(line, w.LogFields...)
4239
}
40+
writeLines(p, func(line string) { w.Logger.Info(line, w.LogFields...) })
41+
case JSON:
42+
writeLines(p, func(line string) { w.Logger.Info(line, w.LogFields...) })
4343
default:
4444
return len(p), fmt.Errorf("unknown log mode %v", curMode)
4545
}
@@ -51,6 +51,7 @@ type StdErrWriter struct {
5151
LogFields []any
5252
Logger Logger
5353
LogMode *LogMode
54+
Task *TaskContext
5455
}
5556

5657
func (w StdErrWriter) Write(p []byte) (n int, err error) {
@@ -70,20 +71,33 @@ func (w StdErrWriter) Write(p []byte) (n int, err error) {
7071
case Text:
7172
w.Logger.Print(string(p))
7273
return len(p), nil
73-
case JSON, Logfmt:
74-
if strings.TrimSpace(string(p)) == "" {
75-
return len(p), nil
76-
}
77-
splitP := strings.Split(string(p), "\n")
78-
for _, line := range splitP {
79-
if strings.TrimSpace(line) == "" {
80-
continue
74+
case Logfmt:
75+
if w.Task != nil {
76+
if tal, ok := w.Logger.(TaskAwareLogger); ok {
77+
writeLines(p, func(line string) { tal.PrintErrWithTask(w.Task, line) })
78+
return len(p), nil
8179
}
82-
w.Logger.Notice(line, w.LogFields...)
8380
}
81+
writeLines(p, func(line string) { w.Logger.Notice(line, w.LogFields...) })
82+
case JSON:
83+
writeLines(p, func(line string) { w.Logger.Notice(line, w.LogFields...) })
8484
default:
8585
return len(p), fmt.Errorf("unknown log mode %v", w.LogMode)
8686
}
8787

8888
return len(p), nil
8989
}
90+
91+
// writeLines splits p into non-empty lines and calls fn for each.
92+
func writeLines(p []byte, fn func(string)) {
93+
s := string(p)
94+
if strings.TrimSpace(s) == "" {
95+
return
96+
}
97+
for _, line := range strings.Split(s, "\n") {
98+
if strings.TrimSpace(line) == "" {
99+
continue
100+
}
101+
fn(line)
102+
}
103+
}

0 commit comments

Comments
 (0)