log API

log

package

API reference for the log package.

T
type

LogLevel

LogLevel represents the severity of a log message.

pkg/log/log.go:13-13
type LogLevel int
I
interface

Logger

Logger defines the interface for logging messages with different severity levels.

Commands receive one through cli.Base. Supply your own with cli.WithLogger
when the application already has a logging stack.

pkg/log/log.go:29-34
type Logger interface

Methods

Info
Method

Parameters

format string
a ...any
func Info(...)
Success
Method

Parameters

format string
a ...any
func Success(...)
Warning
Method

Parameters

format string
a ...any
func Warning(...)
Error
Method

Parameters

format string
a ...any
func Error(...)
S
struct
Implements: Logger

consoleLogger

consoleLogger writes human readable lines: informational output on out,
problems on errOut, so that piping stdout does not swallow errors.

Colors are decided per writer, because stdout and stderr are redirected
independently: “cmd > file” must still colorize the errors on the terminal,
and “cmd 2> file” must not put escape codes in the file.

pkg/log/log.go:42-47
type consoleLogger struct

Methods

Info
Method

Parameters

format string
a ...any
func (*consoleLogger) Info(format string, a ...any)
{
	l.write(l.out, l.colorOut, LogLevelInfo, format, a...)
}
Success
Method

Parameters

format string
a ...any
func (*consoleLogger) Success(format string, a ...any)
{
	l.write(l.out, l.colorOut, LogLevelSuccess, format, a...)
}
Warning
Method

Parameters

format string
a ...any
func (*consoleLogger) Warning(format string, a ...any)
{
	l.write(l.errOut, l.colorErrOut, LogLevelWarning, format, a...)
}
Error
Method

Parameters

format string
a ...any
func (*consoleLogger) Error(format string, a ...any)
{
	l.write(l.errOut, l.colorErrOut, LogLevelError, format, a...)
}
write
Method

Parameters

color bool
level LogLevel
format string
a ...any
func (*consoleLogger) write(w io.Writer, color bool, level LogLevel, format string, a ...any)
{
	msg := format
	if len(a) > 0 {
		msg = fmt.Sprintf(format, a...)
	}

	symbol := levelSymbol(level)
	if color {
		symbol = colorize(symbol, levelColor(level))
	}

	fmt.Fprintf(w, "%s %s\n", symbol, msg)
}

Fields

Name Type Description
out io.Writer
errOut io.Writer
colorOut bool
colorErrOut bool
F
function

New

New creates the default Logger: readable lines on the terminal, colors only
when the terminal can show them.

Returns

pkg/log/log.go:56-63
func New() Logger

{
	return &consoleLogger{
		out:         os.Stdout,
		errOut:      os.Stderr,
		colorOut:    colorEnabled(os.Stdout),
		colorErrOut: colorEnabled(os.Stderr),
	}
}

Example

logger := log.New()
logger.Info("Starting application...")
F
function

NewWriter

NewWriter creates a human readable Logger writing to the given writers,
without colors. Useful in tests and when the output is captured.

Parameters

out
errOut

Returns

pkg/log/log.go:72-80
func NewWriter(out, errOut io.Writer) Logger

{
	if out == nil {
		out = io.Discard
	}
	if errOut == nil {
		errOut = out
	}
	return &consoleLogger{out: out, errOut: errOut}
}

Example

var buf bytes.Buffer
logger := log.NewWriter(&buf, &buf)
F
function

colorEnabled

colorEnabled reports whether ANSI colors should be emitted on w. Colors are
off when the output is redirected to a file or a pipe, when NO_COLOR is set,
and on terminals that declare themselves incapable.

The check is the character device test rather than a real ioctl, to keep the
library free of dependencies. The one case it gets wrong is a redirect to
/dev/null, which is a character device too: colors are emitted there and
then discarded, so nothing observable changes.

Parameters

Returns

bool
pkg/log/log.go:120-139
func colorEnabled(w io.Writer) bool

{
	if os.Getenv("NO_COLOR") != "" {
		return false
	}
	if os.Getenv("TERM") == "dumb" {
		return false
	}

	f, ok := w.(*os.File)
	if !ok {
		return false
	}

	info, err := f.Stat()
	if err != nil {
		return false
	}

	return info.Mode()&os.ModeCharDevice != 0
}
F
function

levelColor

levelColor returns the ANSI color name associated with a log level.

Parameters

level

Returns

string
pkg/log/log.go:142-155
func levelColor(level LogLevel) string

{
	switch level {
	case LogLevelInfo:
		return "blue"
	case LogLevelSuccess:
		return "green"
	case LogLevelWarning:
		return "yellow"
	case LogLevelError:
		return "red"
	default:
		return "reset"
	}
}
F
function

levelSymbol

levelSymbol returns the symbol printed in front of a message.

Parameters

level

Returns

string
pkg/log/log.go:158-171
func levelSymbol(level LogLevel) string

{
	switch level {
	case LogLevelInfo:
		return "ℹ"
	case LogLevelSuccess:
		return "✓"
	case LogLevelWarning:
		return "⚠"
	case LogLevelError:
		return "ø"
	default:
		return " "
	}
}
F
function

colorize

colorize applies the specified ANSI color to the text.

Parameters

text
string
color
string

Returns

string
pkg/log/log.go:190-199
func colorize(text string, color string) string

{
	colors := map[string]string{
		"red":    "\033[31m",
		"green":  "\033[32m",
		"yellow": "\033[33m",
		"blue":   "\033[34m",
		"reset":  "\033[0m",
	}
	return colors[color] + text + colors["reset"]
}
F
function

TestConsoleLogger_WritesReadableLines

Parameters

pkg/log/log_test.go:13-26
func TestConsoleLogger_WritesReadableLines(t *testing.T)

{
	var out bytes.Buffer
	l := NewWriter(&out, &out)

	l.Info("hello %s", "world")

	got := out.String()
	if !strings.Contains(got, "hello world") {
		t.Errorf("message not rendered, got %q", got)
	}
	if strings.Contains(got, "{") || strings.Contains(got, "\"msg\"") {
		t.Errorf("console output must not be JSON, got %q", got)
	}
}
F
function

TestConsoleLogger_SeparatesOutAndErr

Parameters

pkg/log/log_test.go:28-46
func TestConsoleLogger_SeparatesOutAndErr(t *testing.T)

{
	var out, errOut bytes.Buffer
	l := NewWriter(&out, &errOut)

	l.Info("info")
	l.Success("success")
	l.Warning("warning")
	l.Error("error")

	if !strings.Contains(out.String(), "info") || !strings.Contains(out.String(), "success") {
		t.Errorf("info and success belong on stdout, got %q", out.String())
	}
	if strings.Contains(out.String(), "warning") || strings.Contains(out.String(), "error") {
		t.Errorf("warning and error must not reach stdout, got %q", out.String())
	}
	if !strings.Contains(errOut.String(), "warning") || !strings.Contains(errOut.String(), "error") {
		t.Errorf("warning and error belong on stderr, got %q", errOut.String())
	}
}
F
function

TestConsoleLogger_LevelsAreDistinguishable

Parameters

pkg/log/log_test.go:48-61
func TestConsoleLogger_LevelsAreDistinguishable(t *testing.T)

{
	render := func(fn func(Logger)) string {
		var buf bytes.Buffer
		fn(NewWriter(&buf, &buf))
		return buf.String()
	}

	info := render(func(l Logger) { l.Info("same text") })
	success := render(func(l Logger) { l.Success("same text") })

	if info == success {
		t.Error("Success must not be indistinguishable from Info")
	}
}
F
function

TestConsoleLogger_NoColorWhenNotATerminal

Parameters

pkg/log/log_test.go:63-70
func TestConsoleLogger_NoColorWhenNotATerminal(t *testing.T)

{
	var buf bytes.Buffer
	NewWriter(&buf, &buf).Error("boom")

	if strings.Contains(buf.String(), "\033[") {
		t.Errorf("ANSI codes must not be written to a non terminal, got %q", buf.String())
	}
}
F
function

TestConsoleLogger_MessageWithoutArgsIsNotFormatted

Parameters

pkg/log/log_test.go:72-79
func TestConsoleLogger_MessageWithoutArgsIsNotFormatted(t *testing.T)

{
	var buf bytes.Buffer
	NewWriter(&buf, &buf).Info("100% done")

	if !strings.Contains(buf.String(), "100% done") {
		t.Errorf("a lone message must be printed verbatim, got %q", buf.String())
	}
}
F
function

TestLogLevel_String

Parameters

pkg/log/log_test.go:81-93
func TestLogLevel_String(t *testing.T)

{
	cases := map[LogLevel]string{
		LogLevelInfo:    "info",
		LogLevelSuccess: "success",
		LogLevelWarning: "warning",
		LogLevelError:   "error",
	}
	for level, want := range cases {
		if got := level.String(); got != want {
			t.Errorf("level %d: got %q, want %q", level, got, want)
		}
	}
}