169 lines
4.2 KiB
Go
169 lines
4.2 KiB
Go
|
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
|
||
|
}
|