Elichai Turkel 1f69f9eed9
Cleanup the logger and make it asynchronous (#1524)
* Remove Subsystems map and replace with RegisterSubSystem

* Clean up the logger

* Fix LOGFLAGS and make LongFile work correctly

* Parallelize the logger backend

* More logger cleanup

* Initialize and close the logger backend wherever it's needed

* Move the location where the backend is closed, also print the log if it panics while writing

* Add TestMain to reachability manager tests to preserve the same log level

* Fix review comments

Co-authored-by: Svarog <feanorr@gmail.com>
2021-03-01 14:04:40 +02:00

192 lines
5.7 KiB
Go

package logger
import (
"fmt"
"github.com/jrick/logrotate/rotator"
"github.com/pkg/errors"
"io"
"os"
"path/filepath"
"runtime/debug"
"strings"
"sync"
"sync/atomic"
)
const normalLogSize = 512
// defaultFlags specifies changes to the default logger behavior. It is set
// during package init and configured using the LOGFLAGS environment variable.
// New logger backends can override these default flags using WithFlags.
// We're using this instead of `init()` function because variables are initialized before init functions,
// and this variable is used inside other variable intializations, so runs before them
var defaultFlags = getDefaultFlags()
// Flags to modify Backend's behavior.
const (
// LogFlagLongFile modifies the logger output to include full path and line number
// of the logging callsite, e.g. /a/b/c/main.go:123.
LogFlagLongFile uint32 = 1 << iota
// LogFlagShortFile modifies the logger output to include filename and line number
// of the logging callsite, e.g. main.go:123. takes precedence over LogFlagLongFile.
LogFlagShortFile
)
// Read logger flags from the LOGFLAGS environment variable. Multiple flags can
// be set at once, separated by commas.
func getDefaultFlags() (flags uint32) {
for _, f := range strings.Split(os.Getenv("LOGFLAGS"), ",") {
switch f {
case "longfile":
flags |= LogFlagLongFile
case "shortfile":
flags |= LogFlagShortFile
}
}
return
}
const logsBuffer = 0
// Backend is a logging backend. Subsystems created from the backend write to
// the backend's Writer. Backend provides atomic writes to the Writer from all
// subsystems.
type Backend struct {
flag uint32
isRunning uint32
writers []logWriter
writeChan chan logEntry
syncClose sync.Mutex // used to sync that the logger finished writing everything
}
// NewBackendWithFlags configures a Backend to use the specified flags rather than using
// the package's defaults as determined through the LOGFLAGS environment
// variable.
func NewBackendWithFlags(flags uint32) *Backend {
return &Backend{flag: flags, writeChan: make(chan logEntry, logsBuffer)}
}
// NewBackend creates a new logger backend.
func NewBackend() *Backend {
return NewBackendWithFlags(defaultFlags)
}
const (
defaultThresholdKB = 100 * 1000 // 100 MB logs by default.
defaultMaxRolls = 8 // keep 8 last logs by default.
)
type logWriter interface {
io.WriteCloser
LogLevel() Level
}
type logWriterWrap struct {
io.WriteCloser
logLevel Level
}
func (lw logWriterWrap) LogLevel() Level {
return lw.logLevel
}
// AddLogFile adds a file which the log will write into on a certain
// log level with the default log rotation settings. It'll create the file if it doesn't exist.
func (b *Backend) AddLogFile(logFile string, logLevel Level) error {
return b.AddLogFileWithCustomRotator(logFile, logLevel, defaultThresholdKB, defaultMaxRolls)
}
// AddLogWriter adds a type implementing io.WriteCloser which the log will write into on a certain
// log level with the default log rotation settings. It'll create the file if it doesn't exist.
func (b *Backend) AddLogWriter(logWriter io.WriteCloser, logLevel Level) error {
if b.IsRunning() {
return errors.New("The logger is already running")
}
b.writers = append(b.writers, logWriterWrap{
WriteCloser: logWriter,
logLevel: logLevel,
})
return nil
}
// AddLogFileWithCustomRotator adds a file which the log will write into on a certain
// log level, with the specified log rotation settings.
// It'll create the file if it doesn't exist.
func (b *Backend) AddLogFileWithCustomRotator(logFile string, logLevel Level, thresholdKB int64, maxRolls int) error {
if b.IsRunning() {
return errors.New("The logger is already running")
}
logDir, _ := filepath.Split(logFile)
// if the logDir is empty then `logFile` is in the cwd and there's no need to create any directory.
if logDir != "" {
err := os.MkdirAll(logDir, 0700)
if err != nil {
return errors.Errorf("failed to create log directory: %+v", err)
}
}
r, err := rotator.New(logFile, thresholdKB, false, maxRolls)
if err != nil {
return errors.Errorf("failed to create file rotator: %s", err)
}
b.writers = append(b.writers, logWriterWrap{
WriteCloser: r,
logLevel: logLevel,
})
return nil
}
// Run launches the logger backend in a separate go-routine. should only be called once.
func (b *Backend) Run() error {
if !atomic.CompareAndSwapUint32(&b.isRunning, 0, 1) {
return errors.New("The logger is already running")
}
go func() {
defer func() {
if err := recover(); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Fatal error in logger.Backend goroutine: %+v\n", err)
_, _ = fmt.Fprintf(os.Stderr, "Goroutine stacktrace: %s\n", debug.Stack())
}
}()
b.runBlocking()
}()
return nil
}
func (b *Backend) runBlocking() {
defer atomic.StoreUint32(&b.isRunning, 0)
b.syncClose.Lock()
defer b.syncClose.Unlock()
for log := range b.writeChan {
for _, writer := range b.writers {
if log.level >= writer.LogLevel() {
_, _ = writer.Write(log.log)
}
}
}
}
// IsRunning returns true if backend.Run() has been called and false if it hasn't.
func (b *Backend) IsRunning() bool {
return atomic.LoadUint32(&b.isRunning) != 0
}
// Close finalizes all log rotators for this backend
func (b *Backend) Close() {
close(b.writeChan)
// Wait for it to finish writing using the syncClose mutex.
b.syncClose.Lock()
defer b.syncClose.Unlock()
for _, writer := range b.writers {
_ = writer.Close()
}
}
// Logger returns a new logger for a particular subsystem that writes to the
// Backend b. A tag describes the subsystem and is included in all log
// messages. The logger uses the info verbosity level by default.
func (b *Backend) Logger(subsystemTag string) *Logger {
return &Logger{LevelOff, subsystemTag, b, b.writeChan}
}