1
0
mirror of https://github.com/strongdm/comply synced 2024-11-12 19:04:54 +00:00
comply/vendor/github.com/jcelliott/lumber/filelog.go
2018-05-09 18:02:33 -07:00

363 lines
9.5 KiB
Go

package lumber
import (
"bufio"
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
)
const (
// mode constants
APPEND = iota
TRUNC
BACKUP
ROTATE
)
const (
BUFSIZE = 100
)
type FileLogger struct {
queue chan *Message
done chan bool
out *os.File
timeFormat, prefix string
outLevel, maxLines, curLines, maxRotate, mode int
closed, errored bool
levels []string
}
// Convenience function to create a new append-only logger
func NewAppendLogger(f string) (*FileLogger, error) {
return NewFileLogger(f, INFO, APPEND, 0, 0, BUFSIZE)
}
// Convenience function to create a new truncating logger
func NewTruncateLogger(f string) (*FileLogger, error) {
return NewFileLogger(f, INFO, TRUNC, 0, 0, BUFSIZE)
}
// Convenience function to create a new backup logger
func NewBackupLogger(f string, maxBackup int) (*FileLogger, error) {
return NewFileLogger(f, INFO, BACKUP, 0, maxBackup, BUFSIZE)
}
// Convenience function to create a new rotating logger
func NewRotateLogger(f string, maxLines, maxRotate int) (*FileLogger, error) {
return NewFileLogger(f, INFO, ROTATE, maxLines, maxRotate, BUFSIZE)
}
// Creates a new FileLogger with filename f, output level o, and an empty prefix.
// Modes are described in the documentation; maxLines and maxRotate are only significant
// for some modes.
func NewFileLogger(f string, o, mode, maxLines, maxRotate, bufsize int) (*FileLogger, error) {
var file *os.File
var err error
switch mode {
case APPEND:
// open log file, append if it already exists
file, err = os.OpenFile(f, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
case TRUNC:
// just truncate file and start logging
file, err = os.OpenFile(f, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
case BACKUP:
// rotate every time a new logger is created
file, err = openBackup(f, 0, maxRotate)
case ROTATE:
// "normal" rotation, when file reaches line limit
file, err = openBackup(f, maxLines, maxRotate)
default:
return nil, fmt.Errorf("Invalid mode parameter: %d", mode)
}
if err != nil {
return nil, fmt.Errorf("Error creating logger: %s", err)
}
return newFileLogger(file, o, mode, maxLines, maxRotate, bufsize), nil
}
func NewBasicFileLogger(f *os.File, level int) (l *FileLogger) {
return newFileLogger(f, level, 0, 0, 0, BUFSIZE)
}
func newFileLogger(f *os.File, o, mode, maxLines, maxRotate, bufsize int) (l *FileLogger) {
l = &FileLogger{
queue: make(chan *Message, bufsize),
done: make(chan bool),
out: f,
outLevel: o,
timeFormat: TIMEFORMAT,
prefix: "",
maxLines: maxLines,
maxRotate: maxRotate,
mode: mode,
levels: levels,
}
if mode == ROTATE {
// get the current line count if relevant
l.curLines = countLines(l.out)
}
go l.startOutput()
return
}
func (l *FileLogger) startOutput() {
for {
m, ok := <-l.queue
if !ok {
// the channel is closed and empty
l.printLog(&Message{len(l.levels) - 1, fmt.Sprintf("Closing log now"), time.Now()})
l.out.Sync()
if err := l.out.Close(); err != nil {
l.printLog(&Message{len(l.levels) - 1, fmt.Sprintf("Error closing log file: %s", err), time.Now()})
}
l.done <- true
return
}
l.output(m)
}
}
// Attempt to create new log. Specific behavior depends on the maxLines setting
func openBackup(f string, maxLines, maxRotate int) (*os.File, error) {
// first try to open the file with O_EXCL (file must not already exist)
file, err := os.OpenFile(f, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0644)
// if there are no errors (it's a new file), we can just use this file
if err == nil {
return file, nil
}
// if the error wasn't an 'Exist' error, we've got a problem
if !os.IsExist(err) {
return nil, fmt.Errorf("Error opening file for logging: %s", err)
}
if maxLines == 0 {
// we're in backup mode, rotate and return the new file
return doRotate(f, maxRotate)
}
// the file already exists, open it
return os.OpenFile(f, os.O_RDWR|os.O_APPEND, 0644)
}
// Rotate the logs
func (l *FileLogger) rotate() error {
oldFile := l.out
file, err := doRotate(l.out.Name(), l.maxRotate)
if err != nil {
return fmt.Errorf("Error rotating logs: %s", err)
}
l.curLines = 0
l.out = file
oldFile.Close()
return nil
}
// Rotate all the logs and return a file with newly vacated filename
// Rename 'log.name' to 'log.name.1' and 'log.name.1' to 'log.name.2' etc
func doRotate(f string, limit int) (*os.File, error) {
// create a format string with the correct amount of zero-padding for the limit
numFmt := fmt.Sprintf(".%%0%dd", len(fmt.Sprintf("%d", limit)))
// get all rotated files and sort them in reverse order
list, err := filepath.Glob(fmt.Sprintf("%s.*", f))
if err != nil {
return nil, fmt.Errorf("Error rotating logs: %s", err)
}
sort.Sort(sort.Reverse(sort.StringSlice(list)))
for _, file := range list {
parts := strings.Split(file, ".")
numPart := parts[len(parts)-1]
num, err := strconv.Atoi(numPart)
if err != nil {
// not a number, don't rotate it
continue
}
if num >= limit {
// we're at the limit, don't rotate it
continue
}
newName := fmt.Sprintf(strings.Join(parts[:len(parts)-1], ".")+numFmt, num+1)
// don't check error because there's nothing we can do
os.Rename(file, newName)
}
if err = os.Rename(f, fmt.Sprintf(f+numFmt, 1)); err != nil {
if !os.IsNotExist(err) {
return nil, fmt.Errorf("Error rotating logs: %s", err)
}
}
return os.OpenFile(f, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
}
// Generic output function. Outputs messages if they are higher level than outLevel for this
// specific logger. If msg does not end with a newline, one will be appended.
func (l *FileLogger) output(msg *Message) {
if l.mode == ROTATE && l.curLines >= l.maxLines && !l.errored {
err := l.rotate()
if err != nil {
// if we can't rotate the logs, we should stop logging to prevent the log file from growing
// past the limit and continuously retrying the rotate operation (but log current msg first)
l.printLog(msg)
l.printLog(&Message{len(l.levels) - 1, fmt.Sprintf("Error rotating logs: %s. Closing log."), time.Now()})
l.errored = true
l.close()
}
}
l.printLog(msg)
}
func (l *FileLogger) printLog(msg *Message) {
buf := []byte{}
buf = append(buf, msg.time.Format(l.timeFormat)...)
if l.prefix != "" {
buf = append(buf, ' ')
buf = append(buf, l.prefix...)
}
buf = append(buf, ' ')
buf = append(buf, l.levels[msg.level]...)
buf = append(buf, ' ')
buf = append(buf, msg.m...)
if len(msg.m) > 0 && msg.m[len(msg.m)-1] != '\n' {
buf = append(buf, '\n')
}
l.curLines += 1
l.out.Write(buf)
}
// Sets the available levels for this logger
// TODO: append a *LOG* level
func (l *FileLogger) SetLevels(lvls []string) {
if lvls[len(lvls)-1] != "*LOG*" {
lvls = append(lvls, "*LOG*")
}
l.levels = lvls
}
// Sets the output level for this logger
func (l *FileLogger) Level(o int) {
if o >= 0 && o <= len(l.levels)-1 {
l.outLevel = o
}
}
// Sets the prefix for this logger
func (l *FileLogger) Prefix(p string) {
l.prefix = p
}
// Sets the time format for this logger
func (l *FileLogger) TimeFormat(f string) {
l.timeFormat = f
}
// Flush the messages in the queue and shut down the logger.
func (l *FileLogger) close() {
l.closed = true
// closing the channel will signal the goroutine to finish writing messages in the queue
// and then shut down by sync'ing and close'ing the file.
close(l.queue)
}
// Flush the messages in the queue and shut down the logger.
func (l *FileLogger) Close() {
l.close()
<-l.done
}
// return the number of lines in the given file
func countLines(f *os.File) int {
r := bufio.NewReader(f)
count := 0
var err error = nil
for err == nil {
prefix := true
_, prefix, err = r.ReadLine()
if err != nil {
}
// sometimes we don't get the whole line at once
if !prefix && err == nil {
count++
}
}
return count
}
func (l *FileLogger) log(lvl int, format string, v ...interface{}) {
if lvl < l.outLevel || l.closed {
return
}
// recover in case the channel has already been closed (unlikely race condition)
// this could also be solved with a lock, but would cause a performance hit
defer recover()
l.queue <- &Message{lvl, fmt.Sprintf(format, v...), time.Now()}
}
// Logging functions
func (l *FileLogger) Fatal(format string, v ...interface{}) {
l.log(FATAL, format, v...)
}
func (l *FileLogger) Error(format string, v ...interface{}) {
l.log(ERROR, format, v...)
}
func (l *FileLogger) Warn(format string, v ...interface{}) {
l.log(WARN, format, v...)
}
func (l *FileLogger) Info(format string, v ...interface{}) {
l.log(INFO, format, v...)
}
func (l *FileLogger) Debug(format string, v ...interface{}) {
l.log(DEBUG, format, v...)
}
func (l *FileLogger) Trace(format string, v ...interface{}) {
l.log(TRACE, format, v...)
}
func (l *FileLogger) Print(lvl int, v ...interface{}) {
l.output(&Message{lvl, fmt.Sprint(v...), time.Now()})
}
func (l *FileLogger) Printf(lvl int, format string, v ...interface{}) {
l.output(&Message{lvl, fmt.Sprintf(format, v...), time.Now()})
}
func (l *FileLogger) GetLevel() int {
return l.outLevel
}
func (l *FileLogger) IsFatal() bool {
return l.outLevel <= FATAL
}
func (l *FileLogger) IsError() bool {
return l.outLevel <= ERROR
}
func (l *FileLogger) IsWarn() bool {
return l.outLevel <= WARN
}
func (l *FileLogger) IsInfo() bool {
return l.outLevel <= INFO
}
func (l *FileLogger) IsDebug() bool {
return l.outLevel <= DEBUG
}
func (l *FileLogger) IsTrace() bool {
return l.outLevel <= TRACE
}