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= _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 }