git » log » master » tree

[master] / log.go

// Package log implements a simple logger.
//
// It implements an API somewhat similar to "github.com/google/glog" with a
// focus towards simplicity and integration with standard tools such as
// systemd.
//
// There are command line flags (defined using the flag package) to control
// the behaviour of the default logger. By default, it will write to stderr
// without timestamps; this is suitable for systemd (or equivalent) logging.
//
// Command-line flags:
//
//  -alsologtostderr
//        also log to stderr, in addition to the file
//  -logfile string
//        file to log to (enables logtime)
//  -logtime
//        include the time when writing the log to stderr
//  -logtosyslog string
//        log to syslog, with the given tag
//  -v int
//        verbosity level (1 = debug)
package log // import "blitiri.com.ar/go/log"

import (
	"flag"
	"fmt"
	"io"
	"log/syslog"
	"os"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"sync"
	"time"
)

// Flags that control the default logging.
var (
	vLevel = flag.Int("v", 0, "verbosity level (1 = debug)")

	logFile = flag.String("logfile", "",
		"file to log to (enables logtime)")

	logToSyslog = flag.String("logtosyslog", "",
		"log to syslog, with the given tag")

	logTime = flag.Bool("logtime", false,
		"include the time when writing the log to stderr")

	alsoLogToStderr = flag.Bool("alsologtostderr", false,
		"also log to stderr, in addition to the file")
)

// Level is the type of the logging level constants, to prevent confusion.
type Level int

// Standard logging levels.
const (
	Fatal = Level(-2)
	Error = Level(-1)
	Info  = Level(0)
	Debug = Level(1)
)

var levelToLetter = map[Level]string{
	Fatal: "☠",
	Error: "E",
	Info:  "_",
	Debug: ".",
}

// A Logger represents a logging object that writes logs to a writer.
type Logger struct {
	// Minimum level to log. Messages below this level will be dropped.
	// Note this field is NOT thread safe, if you change it, it is strongly
	// recommended to do so right after creating the logger, and before it is
	// used.
	// The use of this field should be considered EXPERIMENTAL, the API for it
	// could change in the future.
	Level Level

	// Include timestamp in the log message.
	// The use of this field should be considered EXPERIMENTAL, the API for it
	// could change in the future.
	LogTime bool

	// Include the log level in the log message.
	// The use of this field should be considered EXPERIMENTAL, the API for it
	// could change in the future.
	LogLevel bool

	// Include the caller in the log message.
	// The use of this field should be considered EXPERIMENTAL, the API for it
	// could change in the future.
	LogCaller bool

	// File name, if this logger is backed by a file. It's used to implement
	// reopening.
	fname string

	callerSkip int
	w          io.WriteCloser
	sync.Mutex
}

// New creates a new Logger, which writes logs to w.
func New(w io.WriteCloser) *Logger {
	return &Logger{
		w:          w,
		callerSkip: 0,
		Level:      Info,
		LogTime:    true,
		LogLevel:   true,
		LogCaller:  true,
	}
}

// NewFile creates a new Logger, which writes logs to the given file.
func NewFile(path string) (*Logger, error) {
	// Make path absolute, so Reopen continues to work if the program changes
	// its working directory later.
	path, err := filepath.Abs(path)
	if err != nil {
		return nil, err
	}

	f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
	if err != nil {
		return nil, err
	}

	l := New(f)
	l.fname = path
	return l, nil
}

// NewSyslog creates a new Logger, which writes logs to syslog, using the
// given priority and tag.
func NewSyslog(priority syslog.Priority, tag string) (*Logger, error) {
	w, err := syslog.New(priority, tag)
	if err != nil {
		return nil, err
	}

	l := New(w)
	l.LogTime = false
	return l, nil
}

// Close the writer behind the logger.
func (l *Logger) Close() {
	l.w.Close()
}

// Reopen the file behind the logger, if any. This can be used to implement
// log rotation.
//
// Only works for loggers created via NewFile, otherwise it is a no-op.
//
// EXPERIMENTAL, this API could change in the future.
func (l *Logger) Reopen() error {
	if l.fname == "" {
		return nil
	}

	f, err := os.OpenFile(l.fname, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
	if err != nil {
		return err
	}

	l.Lock()
	l.Close()
	l.w = f
	l.Unlock()
	return nil
}

// V returns true if the logger's level is >= the one given, false otherwise.
// It can be used to decide whether to use or gather debugging information
// only at a certain level, to avoid computing it needlessly.
func (l *Logger) V(level Level) bool {
	return level <= l.Level
}

// Log the message into the logger, at the given level. This is low-level and
// should rarely be needed, but it's available to allow the caller to have
// more complex logic if needed. skip is the number of frames to skip when
// computing the file name and line number.
func (l *Logger) Log(level Level, skip int, format string, a ...interface{}) error {
	if !l.V(level) {
		return nil
	}

	b := strings.Builder{}

	// Time.
	if l.LogTime {
		b.WriteString(time.Now().Format("2006-01-02 15:04:05.000000 "))
	}

	// Level.
	if l.LogLevel {
		letter, ok := levelToLetter[level]
		if !ok {
			letter = strconv.Itoa(int(level))
		}
		b.WriteString(letter + " ")
	}

	// Caller.
	if l.LogCaller {
		_, file, line, ok := runtime.Caller(1 + l.callerSkip + skip)
		if !ok {
			file = "unknown"
		}
		fl := fmt.Sprintf("%s:%-4d", filepath.Base(file), line)
		if len(fl) > 18 {
			fl = fl[len(fl)-18:]
		}
		fmt.Fprintf(&b, "%-18s ", fl)
	}

	// Message.
	if !strings.HasSuffix(format, "\n") {
		format += "\n"
	}
	fmt.Fprintf(&b, format, a...)

	l.Lock()
	_, err := l.w.Write([]byte(b.String()))
	l.Unlock()
	return err
}

// Debugf logs information at a Debug level.
func (l *Logger) Debugf(format string, a ...interface{}) {
	l.Log(Debug, 1, format, a...)
}

// Infof logs information at a Info level.
func (l *Logger) Infof(format string, a ...interface{}) {
	l.Log(Info, 1, format, a...)
}

// Errorf logs information at an Error level. It also returns an error
// constructed with the given message, in case it's useful for the caller.
func (l *Logger) Errorf(format string, a ...interface{}) error {
	l.Log(Error, 1, format, a...)
	return fmt.Errorf(format, a...)
}

// Fatalf logs information at a Fatal level, and then exits the program with a
// non-0 exit code.
func (l *Logger) Fatalf(format string, a ...interface{}) {
	l.Log(Fatal, 1, format, a...)
	// TODO: Log traceback?
	os.Exit(1)
}

// Default logger, used by the top-level functions below.
var Default = &Logger{
	w: os.Stderr,

	Level: Info,

	callerSkip: 1,
	LogCaller:  true,
	LogLevel:   true,
	LogTime:    false,
}

// Init the default logger, based on the command-line flags.
func Init() {
	flag.Parse()
	var err error

	if *logToSyslog != "" {
		Default, err = NewSyslog(syslog.LOG_DAEMON|syslog.LOG_INFO, *logToSyslog)
		if err != nil {
			panic(err)
		}
	} else if *logFile != "" {
		Default, err = NewFile(*logFile)
		if err != nil {
			panic(err)
		}
		*logTime = true
	}

	if *alsoLogToStderr && Default.w != os.Stderr {
		Default.w = multiWriteCloser(Default.w, os.Stderr)
	}

	Default.callerSkip = 1
	Default.Level = Level(*vLevel)
	Default.LogTime = *logTime
}

// V is a convenient wrapper to Default.V.
func V(level Level) bool {
	return Default.V(level)
}

// Log is a convenient wrapper to Default.Log.
func Log(level Level, skip int, format string, a ...interface{}) {
	Default.Log(level, skip, format, a...)
}

// Debugf is a convenient wrapper to Default.Debugf.
func Debugf(format string, a ...interface{}) {
	Default.Debugf(format, a...)
}

// Infof is a convenient wrapper to Default.Infof.
func Infof(format string, a ...interface{}) {
	Default.Infof(format, a...)
}

// Errorf is a convenient wrapper to Default.Errorf.
func Errorf(format string, a ...interface{}) error {
	return Default.Errorf(format, a...)
}

// Fatalf is a convenient wrapper to Default.Fatalf.
func Fatalf(format string, a ...interface{}) {
	Default.Fatalf(format, a...)
}

// multiWriteCloser creates a WriteCloser that duplicates its writes and
// closes to all the provided writers.
func multiWriteCloser(wc ...io.WriteCloser) io.WriteCloser {
	return mwc(wc)
}

type mwc []io.WriteCloser

func (m mwc) Write(p []byte) (n int, err error) {
	for _, w := range m {
		if n, err = w.Write(p); err != nil {
			return
		}
	}
	return
}
func (m mwc) Close() error {
	for _, w := range m {
		if err := w.Close(); err != nil {
			return err
		}
	}
	return nil
}