gocommon/log/hooks/file/file_hook.go

169 lines
4.2 KiB
Go
Raw Normal View History

package file
import (
"bytes"
"fmt"
"io"
"os"
"strings"
"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
w io.Writer
buf chan logrus.Entry
wg sync.WaitGroup
mu sync.RWMutex
synchronous bool
}
// NewFileHook creates a hook to be added to an instance of logger.
func NewFileHook(w io.Writer) *Hook {
if w == nil {
logrus.Error("Can't create File Hook with an empty writer")
return nil
}
hook := &Hook{
Level: logrus.DebugLevel,
synchronous: true,
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) *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),
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()
}
// 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 := constructMessage(entry)
_, err := hook.w.Write([]byte(fmt.Sprintf("%s\n", 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
}
func constructMessage(entry *logrus.Entry) string {
var messageParts []string
// handle time
messageParts = append(messageParts, fmt.Sprintf("time=\"%s\"", entry.Time.UTC().Format(time.RFC3339)))
// handle log level
messageParts = append(messageParts, fmt.Sprintf("level=%s", entry.Level.String()))
// handle actual message
messageParts = append(messageParts, fmt.Sprintf("msg=\"%s\"", string(bytes.TrimSpace([]byte(entry.Message)))))
// add extra data except err_*
for k, v := range entry.Data {
if !strings.HasPrefix(k, "err_") {
messageParts = append(messageParts, fmt.Sprintf("%s=\"%s\"", k, v))
}
}
message := strings.Join(messageParts, " ")
// handle error stack
// XXX: Is that really how it work ?? I doubt but let see
if _, exist := entry.Data["err_code"]; exist {
// get error infos
//ns := entry.Data["err_ns"]
ctx := entry.Data["err_ctx"]
//id := entry.Data["err_id"]
// get stack and clean it a bit
tSt := entry.Data["err_stack"]
st := strings.Replace(tSt.(string), "\n", "\n\t\t", -1)
message = fmt.Sprintf("%s\nerror:\n\tcontext:%s\n\tstacktrace:\n\t\t%s", message, ctx, st)
}
return message
}