This repository has been archived on 2024-04-08. You can view files and clone it, but cannot push or open issues or pull requests.
dagger/cmd/dagger/logger/plain.go
Marcos Nils 27d878456f
Improve tty error logging when buildkit vertex is unknown (#2188)
* Improve tty error logging when buildkit vertex is unknown

Creates a generic "system" group in the tty output which captures
buildkit events that report a non-dagger vertex name. This happens
currently when using core.#Dockerfile actions since Dagger delegates the
LLB generation to buildkit through it's frontend and we don't get
meaningful events that we can correlate from Dagger's side

Signed-off-by: Marcos Lilljedahl <marcosnils@gmail.com>
2022-04-18 21:50:34 -03:00

200 lines
4.6 KiB
Go

package logger
import (
"bytes"
"encoding/json"
"fmt"
"hash/adler32"
"io"
"sort"
"strings"
"time"
"github.com/mitchellh/colorstring"
"github.com/rs/zerolog"
)
var colorize = colorstring.Colorize{
Colors: colorstring.DefaultColors,
Reset: true,
}
type PlainOutput struct {
Out io.Writer
}
const systemGroup = "system"
func (c *PlainOutput) Write(p []byte) (int, error) {
event := map[string]interface{}{}
d := json.NewDecoder(bytes.NewReader(p))
if err := d.Decode(&event); err != nil {
return 0, fmt.Errorf("cannot decode event: %s", err)
}
source := parseSource(event)
return fmt.Fprintln(c.Out, colorize.Color(fmt.Sprintf("%s %s %s%s%s",
formatTimestamp(event),
formatLevel(event),
formatSource(source),
formatMessage(event),
formatFields(event),
)))
}
func formatLevel(event map[string]interface{}) string {
level := zerolog.DebugLevel
if l, ok := event[zerolog.LevelFieldName].(string); ok {
level, _ = zerolog.ParseLevel(l)
}
switch level {
case zerolog.TraceLevel:
return "[magenta]TRC[reset]"
case zerolog.DebugLevel:
return "[yellow]DBG[reset]"
case zerolog.InfoLevel:
return "[green]INF[reset]"
case zerolog.WarnLevel:
return "[red]WRN[reset]"
case zerolog.ErrorLevel:
return "[red]ERR[reset]"
case zerolog.FatalLevel:
return "[red]FTL[reset]"
case zerolog.PanicLevel:
return "[red]PNC[reset]"
default:
return "[bold]???[reset]"
}
}
func formatTimestamp(event map[string]interface{}) string {
ts, ok := event[zerolog.TimestampFieldName].(string)
if !ok {
return "???"
}
t, err := time.Parse(zerolog.TimeFieldFormat, ts)
if err != nil {
panic(err)
}
return fmt.Sprintf("[dark_gray]%s[reset]", t.Format(time.Kitchen))
}
func formatMessage(event map[string]interface{}) string {
message, ok := event[zerolog.MessageFieldName].(string)
if !ok {
return ""
}
message = strings.TrimSpace(message)
if err, ok := event[zerolog.ErrorFieldName].(string); ok && err != "" {
message = message + ": " + err
}
level := zerolog.DebugLevel
if l, ok := event[zerolog.LevelFieldName].(string); ok {
level, _ = zerolog.ParseLevel(l)
}
switch level {
case zerolog.TraceLevel:
return fmt.Sprintf("[dim]%s[reset]", message)
case zerolog.DebugLevel:
return fmt.Sprintf("[dim]%s[reset]", message)
case zerolog.InfoLevel:
return message
case zerolog.WarnLevel:
return fmt.Sprintf("[yellow]%s[reset]", message)
case zerolog.ErrorLevel:
return fmt.Sprintf("[red]%s[reset]", message)
case zerolog.FatalLevel:
return fmt.Sprintf("[red]%s[reset]", message)
case zerolog.PanicLevel:
return fmt.Sprintf("[red]%s[reset]", message)
default:
return message
}
}
func parseSource(event map[string]interface{}) string {
source := systemGroup
if task, ok := event["task"].(string); ok && task != "" {
source = task
}
return source
}
func formatSource(source string) string {
return fmt.Sprintf("[%s]%s | [reset]",
hashColor(source),
source,
)
}
func formatFields(entry map[string]interface{}) string {
// these are the fields we don't want to expose, either because they're
// already part of the Log structure or because they're internal
fieldSkipList := map[string]struct{}{
zerolog.MessageFieldName: {},
zerolog.LevelFieldName: {},
zerolog.TimestampFieldName: {},
zerolog.ErrorFieldName: {},
zerolog.CallerFieldName: {},
"environment": {},
"task": {},
"state": {},
}
fields := []string{}
for key, value := range entry {
if _, ok := fieldSkipList[key]; ok {
continue
}
switch v := value.(type) {
case string:
fields = append(fields, fmt.Sprintf("%s=%s", key, v))
case int:
fields = append(fields, fmt.Sprintf("%s=%v", key, v))
case float64:
dur := time.Duration(v) * time.Millisecond
s := dur.Round(100 * time.Millisecond).String()
fields = append(fields, fmt.Sprintf("%s=%s", key, s))
case nil:
fields = append(fields, fmt.Sprintf("%s=null", key))
default:
o, err := json.MarshalIndent(v, "", " ")
if err != nil {
panic(err)
}
fields = append(fields, fmt.Sprintf("%s=%s", key, o))
}
}
if len(fields) == 0 {
return ""
}
sort.SliceStable(fields, func(i, j int) bool {
return fields[i] < fields[j]
})
return fmt.Sprintf(" [bold]%s[reset]", strings.Join(fields, " "))
}
// hashColor returns a consistent color for a given string
func hashColor(text string) string {
colors := []string{
"green",
"light_green",
"light_blue",
"blue",
"magenta",
"light_magenta",
"light_yellow",
"cyan",
"light_cyan",
}
h := adler32.Checksum([]byte(text))
return colors[int(h)%len(colors)]
}