gocommon/log/hooks/file/file_hook.go
Matthieu 'JP' DERASSE bef0d38f1e
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone Build is passing
feat(log): Handle multiple log formatter for file
2022-12-11 12:43:39 +00:00

155 lines
3.6 KiB
Go

package file
import (
"fmt"
"io"
"os"
"sync"
"time"
"github.com/sirupsen/logrus"
)
// BufSize is the number of log that can be put in buffer before make it full
// BufSize= <value> _before_ calling NewFileHook
// Once the buffer is full, logging will start blocking, waiting for slots to
// be available in the queue.
var BufSize uint = 8192
// Hook will write logs to a file.
type Hook struct {
Level logrus.Level
buf chan logrus.Entry
f logrus.Formatter
mu sync.RWMutex
synchronous bool
w io.Writer
wg sync.WaitGroup
}
// XXX: Maybe just take a formatter in input
// NewFileHook creates a hook to be added to an instance of logger.
func NewFileHook(w io.Writer, of OutputFormat) *Hook {
if w == nil {
logrus.Error("Can't create File Hook with an empty writer")
return nil
}
hook := &Hook{
Level: logrus.DebugLevel,
synchronous: true,
f: handleFormat(of),
w: w,
}
return hook
}
// NewAsyncFileHook creates a hook to be added to an instance of logger.
// The hook created will be asynchronous, and it's the responsibility of the user to call the Flush method
// before exiting to empty the log queue.
func NewAsyncFileHook(w io.Writer, of OutputFormat) *Hook {
if w == nil {
logrus.Error("Can't create File Hook with an empty writer")
return nil
}
hook := &Hook{
Level: logrus.DebugLevel,
buf: make(chan logrus.Entry, BufSize),
f: handleFormat(of),
w: w,
}
go hook.fire() // Log in background
return hook
}
// Flush waits for the log queue to be empty.
// This func is meant to be used when the hook was created with NewAsyncFileHook.
func (hook *Hook) Flush() {
if hook.synchronous {
logrus.Error("Can't call Flush on a File Hook in synchronous execution")
}
hook.mu.Lock() // claim the mutex as a Lock - we want exclusive access to it
defer hook.mu.Unlock()
hook.wg.Wait()
}
// handleFormat will take a OutputFormat and will transform it in a formatter.
func handleFormat(of OutputFormat) logrus.Formatter {
if of == OutputFormat_JSON {
return &logrus.JSONFormatter{
PrettyPrint: false,
TimestampFormat: time.RFC3339Nano,
}
}
return &logrus.TextFormatter{
DisableColors: true,
TimestampFormat: time.RFC3339Nano,
QuoteEmptyFields: true,
}
}
// Fire is called when a log event is fired.
// We assume the entry will be altered by another hook,
// otherwise we might be logging something wrong to Graylog.
func (hook *Hook) Fire(entry *logrus.Entry) error {
hook.mu.RLock() // Claim the mutex as a RLock - allowing multiple go routines to log simultaneously
defer hook.mu.RUnlock()
if hook.synchronous {
hook.writeEntry(entry)
} else {
hook.wg.Add(1)
hook.buf <- *entry
}
return nil
}
// fire will loop on the 'buf' channel, and write entries to the writer.
func (hook *Hook) fire() {
for {
entry := <-hook.buf // receive new entry on channel
hook.writeEntry(&entry)
hook.wg.Done()
}
}
// writeEntry write an entry to a file synchronously.
func (hook *Hook) writeEntry(entry *logrus.Entry) {
if hook.w == nil {
fmt.Println("Can't write to a file without a writer")
return
}
message, err := hook.f.Format(entry)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to format log line. Error: %s\n", err)
return
}
_, err = hook.w.Write(message)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to write in log file. Error: %s\nLog Line: %s", err, message)
}
}
// Levels returns the available logging levels.
func (hook *Hook) Levels() []logrus.Level {
levels := []logrus.Level{}
for _, level := range logrus.AllLevels {
if level <= hook.Level {
levels = append(levels, level)
}
}
return levels
}