diff --git a/backend.go b/backend.go index b813438..6b08155 100644 --- a/backend.go +++ b/backend.go @@ -1,12 +1,13 @@ package logging import ( - "errors" "fmt" "io" "os" ) +// Backend is the interface that specifies the methods that a backend must +// implement type Backend interface { Write(*Record) error SetFormatter(*Formatter) @@ -19,6 +20,7 @@ type Backend interface { // Backend to write in file-like objects // +// FileBackend is a backend that writes to a file. type FileBackend struct { l io.Writer formatter *Formatter @@ -26,7 +28,8 @@ type FileBackend struct { filepath string } -// Creates a new backend to write the logs on the standard output +// NewStdoutBackend creates a new backend to write the logs on the standard +// output func NewStdoutBackend() (b *FileBackend) { b = &FileBackend{ l: os.Stdout, @@ -35,7 +38,7 @@ func NewStdoutBackend() (b *FileBackend) { return } -// Creates a new backend to write the logs on the error output +// NewStderrBackend creates a new backend to write the logs on the error output func NewStderrBackend() (b *FileBackend) { b = &FileBackend{ l: os.Stderr, @@ -44,21 +47,23 @@ func NewStderrBackend() (b *FileBackend) { return } -// Creates a new backend to write the logs in a given file -func NewFileBackend(filename string) (b *FileBackend, e error) { - filename_fd, err := os.OpenFile(filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644) +// NewFileBackend creates a new backend to write the logs in a given file +func NewFileBackend(filename string) (*FileBackend, error) { + fd, err := os.OpenFile(filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644) //nolint: gosec if err != nil { - e = errors.New(fmt.Sprintf("Cannot open log file %s (%s)", filename, err.Error())) + return nil, fmt.Errorf("Cannot open log file %s: %w", filename, err) } - b = &FileBackend{ - l: filename_fd, + + b := &FileBackend{ + l: fd, formatter: &defaultFormatter, filepath: filename, } - return + + return b, nil } -// Creates a new backend to write the logs in a given io.Writer +// NewIoBackend creates a new backend to write the logs in a given io.Writer func NewIoBackend(buf io.Writer) (b *FileBackend) { return &FileBackend{ l: buf, @@ -72,18 +77,23 @@ func (b FileBackend) Write(r *Record) error { return err } +// SetLevel changes the log level of the backend func (b *FileBackend) SetLevel(l Level) { b.level = l } +// Level returns the log level set for this backend func (b *FileBackend) Level() Level { return b.level } +// SetFormatter defines the formatter for this backend func (b *FileBackend) SetFormatter(f *Formatter) { b.formatter = f } +// Reopen closes and reopens the file it writes to. It should be used after log +// rotation func (b *FileBackend) Reopen() error { if b.filepath == "" { return nil @@ -95,7 +105,7 @@ func (b *FileBackend) Reopen() error { } } - fd, err := os.OpenFile(b.filepath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644) + fd, err := os.OpenFile(b.filepath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644) //nolint: gosec if err != nil { return fmt.Errorf("Cannot open log file %s: %w", b.filepath, err) } @@ -109,24 +119,31 @@ func (b *FileBackend) Reopen() error { // Noop Backend // +// NoopBackend does nothing and discards all log entries without writing them anywhere type NoopBackend struct{} +// NewNoopBackend creates a noop backend func NewNoopBackend() (Backend, error) { return &NoopBackend{}, nil } +// Write is a noop func (nb *NoopBackend) Write(r *Record) error { return nil } +// SetFormatter is a noop func (nb *NoopBackend) SetFormatter(f *Formatter) {} +// SetLevel is a noop func (nb *NoopBackend) SetLevel(level Level) {} +// Level always returns DefeultLevel func (nb *NoopBackend) Level() Level { return DefaultLevel } +// Reopen is a noop func (nb *NoopBackend) Reopen() error { return nil } diff --git a/backend_syslog_linux.go b/backend_syslog_linux.go index 5408803..8d6eb27 100644 --- a/backend_syslog_linux.go +++ b/backend_syslog_linux.go @@ -12,12 +12,16 @@ import ( // Syslog Backend // +// SyslogBackend writes the logs to a syslog system type SyslogBackend struct { w *syslog.Writer formatter *Formatter level Level } +// NewSyslogBackend initializes a new connection to a syslog server with the +// given facility. +// tag can contain an identifier for the log stream. It defaults to os.Arg[0]. func NewSyslogBackend(facilityName string, tag string) (Backend, error) { f, err := facility(facilityName) if err != nil { @@ -34,37 +38,42 @@ func NewSyslogBackend(facilityName string, tag string) (Backend, error) { return sb, nil } +// Write sends an entry to the syslog server func (sb *SyslogBackend) Write(r *Record) (err error) { text := (*sb.formatter)(r) switch r.Level { - case DEBUG: + case Debug: err = sb.w.Debug(text) - case INFO: + case Info: err = sb.w.Info(text) - case WARNING: + case Warning: err = sb.w.Warning(text) - case ERROR: + case Error: err = sb.w.Err(text) - case CRITICAL: + case Critical: err = sb.w.Crit(text) - case FATAL: + case Fatal: err = sb.w.Emerg(text) } return err } +// SetFormatter defines the formatter for this backend func (sb *SyslogBackend) SetFormatter(f *Formatter) { sb.formatter = f } +// SetLevel changes the log level of the backend func (sb *SyslogBackend) SetLevel(level Level) { sb.level = level } +// Level returns the log level set for this backend func (sb *SyslogBackend) Level() Level { return sb.level } +// Reopen is a no-op func (sb *SyslogBackend) Reopen() error { return nil } @@ -93,9 +102,10 @@ var facilities = map[string]syslog.Priority{ } func facility(name string) (syslog.Priority, error) { - if p, ok := facilities[strings.ToLower(name)]; !ok { + p, ok := facilities[strings.ToLower(name)] + if !ok { return 0, fmt.Errorf("Facility '%s' does not exist", name) - } else { - return p, nil } + + return p, nil } diff --git a/logger.go b/logger.go index 9ffee73..a39e043 100644 --- a/logger.go +++ b/logger.go @@ -7,35 +7,38 @@ import ( "sync" ) +// Logger is a facility that writes logs to one or more backands (files, +// stdout/stderr, syslog, etc.) which can be configured independently +// +// Loggers are concurrent-safe. type Logger struct { - // added to avoid concurrent writes sync.Mutex name string - level Level backends []Backend } +// NewLogger initializes a new Logger with no backend and with the default log level. func NewLogger(name string) (l *Logger) { l = &Logger{ - name: name, - level: DEFAULT_LEVEL, + name: name, } return } -// Add a new Backend to the logger +// AddBackend add a new Backend to the logger. All set backends are kept. func (l *Logger) AddBackend(b Backend) { l.backends = append(l.backends, b) } -// Sets the backend list to the logger. Any existing backend will be lost +// SetBackend sets the backend list to the logger. Any existing backend will be lost. +// FIXME must close file backends to avoid fd leaks func (l *Logger) SetBackend(b ...Backend) { l.backends = b } +// SetLevel changes the log level of all regisered backends to the given level. func (l *Logger) SetLevel(level Level) { - l.level = level for _, backend := range l.backends { backend.SetLevel(level) } @@ -51,12 +54,18 @@ func (b *buffer) Write(p []byte) (n int, err error) { return len(p), nil } +// AsStdLog encapsulate the logger in an instance of lof.Logger from the +// standard library and returns it. +// +// It is there for interoperability reasons. func (l *Logger) AsStdLog(level Level) *log.Logger { stdLogger := log.New(&buffer{logger: l, level: level}, "", 0) return stdLogger } +// Log sends a record containing the message `m` to the registered backends +// whose level is at least `level` func (l *Logger) Log(level Level, m string) { l.Lock() defer l.Unlock() @@ -64,56 +73,74 @@ func (l *Logger) Log(level Level, m string) { r := NewRecord(l.name, level, m) for _, backend := range l.backends { if r.Level >= backend.Level() { - backend.Write(r) + _ = backend.Write(r) } } } +// Debug logs a message with the Debug level func (l *Logger) Debug(text string) { - l.Log(DEBUG, text) + l.Log(Debug, text) } +// Debugf formats the message with given args and logs the result with the +// Debug level func (l *Logger) Debugf(text string, args ...interface{}) { l.Debug(fmt.Sprintf(text, args...)) } +// Info logs a message with the Info level func (l *Logger) Info(text string) { - l.Log(INFO, text) + l.Log(Info, text) } +// Infof formats the message with given args and logs the result with the +// Info level func (l *Logger) Infof(text string, args ...interface{}) { l.Info(fmt.Sprintf(text, args...)) } +// Warning logs a message with the Warning level func (l *Logger) Warning(text string) { - l.Log(WARNING, text) + l.Log(Warning, text) } +// Warningf formats the message with given args and logs the result with the +// Warning level func (l *Logger) Warningf(text string, args ...interface{}) { l.Warning(fmt.Sprintf(text, args...)) } +// Error logs a message with the Error level func (l *Logger) Error(text string) { - l.Log(ERROR, text) + l.Log(Error, text) } +// Errorf formats the message with given args and logs the result with the +// Error level func (l *Logger) Errorf(text string, args ...interface{}) { l.Error(fmt.Sprintf(text, args...)) } +// Critical logs a message with the Critical level func (l *Logger) Critical(text string) { - l.Log(CRITICAL, text) + l.Log(Critical, text) } +// Criticalf formats the message with given args and logs the result with the +// Critical level func (l *Logger) Criticalf(text string, args ...interface{}) { l.Critical(fmt.Sprintf(text, args...)) } +// Fatal logs a message with the Fatal level func (l *Logger) Fatal(text string) { - l.Log(DEBUG, text) + l.Log(Debug, text) os.Exit(100) } +// Fatalf formats the message with given args and logs the result with the +// Fatal level func (l *Logger) Fatalf(text string, args ...interface{}) { l.Fatal(fmt.Sprintf(text, args...)) } diff --git a/logging.go b/logging.go index d71d4ec..137f2c2 100644 --- a/logging.go +++ b/logging.go @@ -1,7 +1,6 @@ package logging import ( - "errors" "fmt" "strings" "sync" @@ -12,8 +11,10 @@ var ( lock sync.Mutex ) +// Level is the type of log levels type Level byte +//nolint: golint const ( Debug Level = iota Info @@ -26,21 +27,28 @@ const ( var levelNames = [6]string{"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "FATAL"} +// Name returns the name of the log level func (l Level) Name() string { return levelNames[l] } +// LevelByName creates a log level given its name. It returns an error if the +// name does not match any level. func LevelByName(l string) (Level, error) { for pos, name := range levelNames { if name == l { return Level(pos), nil } } - return DEBUG, errors.New(fmt.Sprintf("Invalid log level %s", l)) + return Debug, fmt.Errorf("Invalid log level %s", l) } +// Formatter is the types of the functions that can be used to format a log +// entry. They take a pointer to a record and return a formatted string. type Formatter func(*Record) string +// GetLogger returns a logger given its name. if the logger does not exist, it +// initializes one with the defaults (it logs to stdout with level debug) func GetLogger(name string) (l *Logger) { lock.Lock() defer lock.Unlock() diff --git a/record.go b/record.go index 56a3768..d0b9e77 100644 --- a/record.go +++ b/record.go @@ -4,6 +4,8 @@ import ( "time" ) +// Record contains the data to be logged. It is passed to a formatter to +// generate the logged message type Record struct { Logger string Timestamp time.Time @@ -11,6 +13,7 @@ type Record struct { Message string } +// NewRecord creates a new record, setting its timestamp to time.Now() func NewRecord(name string, l Level, m string) (r *Record) { r = &Record{ Logger: name,