log
packageAPI reference for the log
package.
Imports
(6)LogLevel
LogLevel represents the severity of a log message.
type LogLevel int
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.
type Logger interface
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.
type consoleLogger struct
Methods
Parameters
func (*consoleLogger) Info(format string, a ...any)
{
l.write(l.out, l.colorOut, LogLevelInfo, format, a...)
}
Parameters
func (*consoleLogger) Success(format string, a ...any)
{
l.write(l.out, l.colorOut, LogLevelSuccess, format, a...)
}
Parameters
func (*consoleLogger) Warning(format string, a ...any)
{
l.write(l.errOut, l.colorErrOut, LogLevelWarning, format, a...)
}
Parameters
func (*consoleLogger) Error(format string, a ...any)
{
l.write(l.errOut, l.colorErrOut, LogLevelError, format, a...)
}
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)
}
New
New creates the default Logger: readable lines on the terminal, colors only
when the terminal can show them.
Returns
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...")
Uses
NewWriter
NewWriter creates a human readable Logger writing to the given writers,
without colors. Useful in tests and when the output is captured.
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)
Uses
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
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
}
levelColor
levelColor returns the ANSI color name associated with a log level.
Parameters
Returns
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"
}
}
Uses
levelSymbol
levelSymbol returns the symbol printed in front of a message.
Parameters
Returns
func levelSymbol(level LogLevel) string
{
switch level {
case LogLevelInfo:
return "ℹ"
case LogLevelSuccess:
return "✓"
case LogLevelWarning:
return "⚠"
case LogLevelError:
return "ø"
default:
return " "
}
}
Uses
colorize
colorize applies the specified ANSI color to the text.
Parameters
Returns
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"]
}
TestConsoleLogger_WritesReadableLines
Parameters
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)
}
}
TestConsoleLogger_SeparatesOutAndErr
Parameters
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())
}
}
TestConsoleLogger_LevelsAreDistinguishable
Parameters
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")
}
}
TestConsoleLogger_NoColorWhenNotATerminal
Parameters
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())
}
}
TestConsoleLogger_MessageWithoutArgsIsNotFormatted
Parameters
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())
}
}
TestLogLevel_String
Parameters
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)
}
}
}