diff --git a/cmd/root/logger.go b/cmd/root/logger.go index 37a05337..1a815632 100644 --- a/cmd/root/logger.go +++ b/cmd/root/logger.go @@ -3,10 +3,13 @@ package root import ( "context" "fmt" + "io" "os" + "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/flags" "github.com/databricks/cli/libs/log" + "github.com/fatih/color" "golang.org/x/exp/slog" ) @@ -16,6 +19,72 @@ const ( envLogFormat = "DATABRICKS_LOG_FORMAT" ) +type friendlyHandler struct { + slog.Handler + w io.Writer +} + +var ( + levelTrace = color.New(color.FgYellow).Sprint("TRACE") + levelDebug = color.New(color.FgYellow).Sprint("DEBUG") + levelInfo = color.New(color.FgGreen).Sprintf("%5s", "INFO") + levelWarn = color.New(color.FgMagenta).Sprintf("%5s", "WARN") + levelError = color.New(color.FgRed).Sprint("ERROR") +) + +func (l *friendlyHandler) coloredLevel(rec slog.Record) string { + switch rec.Level { + case log.LevelTrace: + return levelTrace + case slog.LevelDebug: + return levelDebug + case slog.LevelInfo: + return levelInfo + case slog.LevelWarn: + return levelWarn + case log.LevelError: + return levelError + } + return "" +} + +func (l *friendlyHandler) Handle(ctx context.Context, rec slog.Record) error { + t := fmt.Sprintf("%02d:%02d", rec.Time.Hour(), rec.Time.Minute()) + attrs := "" + rec.Attrs(func(a slog.Attr) { + attrs += fmt.Sprintf(" %s%s%s", + color.CyanString(a.Key), + color.CyanString("="), + color.YellowString(a.Value.String())) + }) + msg := fmt.Sprintf("%s %s %s%s\n", + color.MagentaString(t), + l.coloredLevel(rec), + color.HiWhiteString(rec.Message), + attrs) + _, err := l.w.Write([]byte(msg)) + return err +} + +func makeLogHandler(opts slog.HandlerOptions) (slog.Handler, error) { + switch logOutput { + case flags.OutputJSON: + return opts.NewJSONHandler(logFile.Writer()), nil + case flags.OutputText: + w := logFile.Writer() + if cmdio.IsTTY(w) { + return &friendlyHandler{ + Handler: opts.NewTextHandler(w), + w: w, + }, nil + } + return opts.NewTextHandler(w), nil + + default: + return nil, fmt.Errorf("invalid log output mode: %s", logOutput) + } +} + func initializeLogger(ctx context.Context) (context.Context, error) { opts := slog.HandlerOptions{} opts.Level = logLevel.Level() @@ -31,14 +100,9 @@ func initializeLogger(ctx context.Context) (context.Context, error) { return nil, err } - var handler slog.Handler - switch logOutput { - case flags.OutputJSON: - handler = opts.NewJSONHandler(logFile.Writer()) - case flags.OutputText: - handler = opts.NewTextHandler(logFile.Writer()) - default: - return nil, fmt.Errorf("invalid log output: %s", logOutput) + handler, err := makeLogHandler(opts) + if err != nil { + return nil, err } slog.SetDefault(slog.New(handler)) diff --git a/libs/cmdio/io.go b/libs/cmdio/io.go index 223610b2..beaa8571 100644 --- a/libs/cmdio/io.go +++ b/libs/cmdio/io.go @@ -46,6 +46,16 @@ func IsInteractive(ctx context.Context) bool { return c.interactive } +// IsTTY detects if io.Writer is a terminal. +func IsTTY(w io.Writer) bool { + f, ok := w.(*os.File) + if !ok { + return false + } + fd := f.Fd() + return isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd) +} + // IsTTY detects if stdout is a terminal. It assumes that stderr is terminal as well func (c *cmdIO) IsTTY() bool { f, ok := c.out.(*os.File)