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