376 lines
9.9 KiB
Go
376 lines
9.9 KiB
Go
// Package lumberjack provides a rolling logger.
|
|
//
|
|
// Lumberjack is intended to be one part of a logging infrastructure.
|
|
// It is not an all-in-one solution, but instead is a pluggable
|
|
// component at the bottom of the logging stack that simply controls the files
|
|
// to which logs are written.
|
|
//
|
|
// Lumberjack plays well with any logger that can write to an io.Writer,
|
|
// including the standard library's log package.
|
|
//
|
|
// Lumberjack assumes that only one process is writing to the output files.
|
|
// Using the same lumberjack configuration from multiple processes on the same
|
|
// machine will result in improper behavior.
|
|
package lumberjack
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
// Some helper constants to make your declarations easier to read.
|
|
|
|
Megabyte = 1024 * 1024
|
|
Gigabyte = 1024 * Megabyte
|
|
|
|
// Note that lumberjack days and weeks may not exactly conform to calendar
|
|
// days and weeks due to daylight savings, leap seconds, etc.
|
|
|
|
Day = 24 * time.Hour
|
|
Week = 7 * Day
|
|
|
|
defaultNameFormat = "2006-01-02T15-04-05.000.log"
|
|
defaultMaxSize = 100 * Megabyte
|
|
)
|
|
|
|
// ensure we always implement io.WriteCloser
|
|
var _ io.WriteCloser = (*Logger)(nil)
|
|
|
|
// Logger is an io.WriteCloser that writes to a log file in the given directory
|
|
// with the given NameFormat. NameFormat should include a time formatting
|
|
// layout in it that produces a valid unique filename for the OS. For more
|
|
// about time formatting layouts, read http://golang.org/pkg/time/#pkg-constants.
|
|
//
|
|
// The date encoded in the filename by NameFormat is used to determine which log
|
|
// files are most recent in several situations.
|
|
//
|
|
// Logger opens or creates a logfile on first Write. It looks for files in the
|
|
// directory that match its name format, and if the one with the most recent
|
|
// NameFormat date is less than MaxSize, it will open and append to that file.
|
|
// If no such file exists, or the file is >= MaxSize, a new file is created
|
|
// using the current time with NameFormat to generate the filename.
|
|
//
|
|
// Whenever a write would cause the current log file exceed MaxSize, a new file
|
|
// is created using the current time.
|
|
//
|
|
// Cleaning Up Old Log Files
|
|
//
|
|
// Whenever a new file gets created, old log files may be deleted. The log file
|
|
// directory is scanned for files that match NameFormat. The most recent files
|
|
// according to their NameFormat date will be retained, up to a number equal to
|
|
// MaxBackups (or all of them if MaxBackups is 0). Any files with a last
|
|
// modified time (based on FileInfo.ModTime) older than MaxAge are deleted,
|
|
// regardless of MaxBackups.
|
|
//
|
|
// If MaxBackups and MaxAge are both 0, no old log files will be deleted.
|
|
type Logger struct {
|
|
// Dir determines the directory in which to store log files.
|
|
// It defaults to os.TempDir() if empty.
|
|
Dir string `json:"dir" yaml:"dir"`
|
|
|
|
// NameFormat is the time formatting layout used to generate filenames.
|
|
// It defaults to "2006-01-02T15-04-05.000.log".
|
|
NameFormat string `json:"nameformat" yaml:"nameformat"`
|
|
|
|
// MaxSize is the maximum size in bytes of the log file before it gets
|
|
// rolled. It defaults to 100 megabytes.
|
|
MaxSize int64 `json:"maxsize" yaml:"maxsize"`
|
|
|
|
// MaxAge is the maximum time to retain old log files based on
|
|
// FileInfo.ModTime. The default is not to remove old log files based on
|
|
// age.
|
|
MaxAge time.Duration `json:"maxage" yaml:"maxage"`
|
|
|
|
// MaxBackups is the maximum number of old log files to retain. The default
|
|
// is to retain all old log files (though MaxAge may still cause them to get
|
|
// deleted.)
|
|
MaxBackups int `json:"maxbackups" yaml:"maxbackups"`
|
|
|
|
// LocalTime determines if the time used for formatting the filename is the
|
|
// computer's local time. The default is to use UTC time.
|
|
LocalTime bool `json:"localtime" yaml:"localtime"`
|
|
|
|
size int64
|
|
file *os.File
|
|
mu sync.Mutex
|
|
}
|
|
|
|
// currentTime is only used for testing. Normally it's the time.Now() function.
|
|
var currentTime = time.Now
|
|
|
|
// Write implements io.Writer. If a write would cause the log file to be larger
|
|
// than MaxSize, a new log file is created using the current time formatted with
|
|
// PathFormat. If the length of the write is greater than MaxSize, an error is
|
|
// returned.
|
|
func (l *Logger) Write(p []byte) (n int, err error) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
writeLen := int64(len(p))
|
|
if writeLen > l.max() {
|
|
return 0, fmt.Errorf(
|
|
"write length %d exceeds maximum file size %d", writeLen, l.max(),
|
|
)
|
|
}
|
|
if l.size+writeLen > l.max() {
|
|
if err := l.rotate(); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
if l.file == nil {
|
|
if err = l.openExistingOrNew(len(p)); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
|
|
n, err = l.file.Write(p)
|
|
l.size += int64(n)
|
|
|
|
return n, err
|
|
}
|
|
|
|
// Close implements io.Closer, and closes the current logfile.
|
|
func (l *Logger) Close() error {
|
|
l.mu.Lock()
|
|
err := l.close()
|
|
l.mu.Unlock()
|
|
return err
|
|
}
|
|
|
|
// close closes the file if it is open.
|
|
func (l *Logger) close() error {
|
|
if l.file == nil {
|
|
return nil
|
|
}
|
|
err := l.file.Close()
|
|
l.file = nil
|
|
return err
|
|
}
|
|
|
|
// Rotate causes Logger to close the existing log file and immediately create a
|
|
// new one. This is a helper function for applications that want to initiate
|
|
// rotations outside of the normal rotation rules, such as in response to
|
|
// SIGHUP. After rotating, this initiates a cleanup of old log files according
|
|
// to the normal rules.
|
|
func (l *Logger) Rotate() error {
|
|
l.mu.Lock()
|
|
err := l.rotate()
|
|
l.mu.Unlock()
|
|
return err
|
|
}
|
|
|
|
// rotate closes the current file, if any, opens a new file, and then calls
|
|
// cleanup.
|
|
func (l *Logger) rotate() error {
|
|
if err := l.close(); err != nil {
|
|
return nil
|
|
}
|
|
if err := l.openNew(); err != nil {
|
|
return err
|
|
}
|
|
return l.cleanup()
|
|
}
|
|
|
|
// openNew opens a new log file for writing.
|
|
func (l *Logger) openNew() error {
|
|
err := os.MkdirAll(l.dir(), 0744)
|
|
if err != nil {
|
|
return fmt.Errorf("can't make directories for new logfile: %s", err)
|
|
}
|
|
f, err := os.OpenFile(l.genFilename(), os.O_CREATE|os.O_WRONLY, 0644)
|
|
if err != nil {
|
|
return fmt.Errorf("can't open new logfile: %s", err)
|
|
}
|
|
info, err := f.Stat()
|
|
if err != nil {
|
|
// can't really do anything if close fails here
|
|
_ = f.Close()
|
|
return fmt.Errorf("can't get size of new logfile: %s", err)
|
|
}
|
|
l.size = info.Size()
|
|
l.file = f
|
|
return nil
|
|
}
|
|
|
|
// openExistingOrNew opens the most recently modified logfile in the log
|
|
// directory, if the current write would not put it over MaxSize. If there is
|
|
// no such file or the write would put it over the MaxSize, a new file is
|
|
// created.
|
|
func (l *Logger) openExistingOrNew(writeLen int) error {
|
|
if l.Dir == "" && l.NameFormat == "" {
|
|
return l.openNew()
|
|
}
|
|
files, err := ioutil.ReadDir(l.dir())
|
|
if os.IsNotExist(err) {
|
|
return l.openNew()
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("can't read files in log file directory: %s", err)
|
|
}
|
|
sort.Sort(byFormatTime{files, l.format()})
|
|
for _, info := range files {
|
|
if info.IsDir() {
|
|
continue
|
|
}
|
|
|
|
if !l.isLogFile(info) {
|
|
continue
|
|
}
|
|
|
|
// the first file we find that matches our pattern will be the most
|
|
// recently modified log file.
|
|
if info.Size()+int64(writeLen) < l.max() {
|
|
filename := filepath.Join(l.dir(), info.Name())
|
|
file, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0644)
|
|
if err == nil {
|
|
l.file = file
|
|
return nil
|
|
}
|
|
// if we fail to open the old log file for some reason, just ignore
|
|
// it and open a new log file.
|
|
}
|
|
break
|
|
}
|
|
return l.openNew()
|
|
}
|
|
|
|
// genFilename generates the name of the logfile from the current time.
|
|
func (l *Logger) genFilename() string {
|
|
t := currentTime()
|
|
if !l.LocalTime {
|
|
t = t.UTC()
|
|
}
|
|
return filepath.Join(l.dir(), t.Format(l.format()))
|
|
}
|
|
|
|
// cleanup deletes old log files, keeping at most l.MaxBackups files, as long as
|
|
// none of them are older than MaxAge.
|
|
func (l *Logger) cleanup() error {
|
|
if l.MaxBackups == 0 && l.MaxAge == 0 {
|
|
return nil
|
|
}
|
|
|
|
files, err := l.oldLogFiles()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var deletes []os.FileInfo
|
|
|
|
if l.MaxBackups > 0 {
|
|
deletes = files[l.MaxBackups:]
|
|
files = files[:l.MaxBackups]
|
|
}
|
|
if l.MaxAge > 0 {
|
|
cutoff := currentTime().Add(-1 * l.MaxAge)
|
|
|
|
for _, f := range files {
|
|
if f.ModTime().Before(cutoff) {
|
|
deletes = append(deletes, f)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(deletes) == 0 {
|
|
return nil
|
|
}
|
|
|
|
go deleteAll(l.dir(), deletes)
|
|
|
|
return nil
|
|
}
|
|
|
|
func deleteAll(dir string, files []os.FileInfo) {
|
|
// remove files on a separate goroutine
|
|
for _, f := range files {
|
|
// what am I going to do, log this?
|
|
_ = os.Remove(filepath.Join(dir, f.Name()))
|
|
}
|
|
}
|
|
|
|
// oldLogFiles returns the list of backup log files stored in the same
|
|
// directory as the current log file, sorted by ModTime
|
|
func (l *Logger) oldLogFiles() ([]os.FileInfo, error) {
|
|
files, err := ioutil.ReadDir(l.dir())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("can't read log file directory: %s", err)
|
|
}
|
|
logFiles := []os.FileInfo{}
|
|
|
|
for _, f := range files {
|
|
if f.IsDir() {
|
|
continue
|
|
}
|
|
if filepath.Base(f.Name()) == filepath.Base(l.file.Name()) {
|
|
continue
|
|
}
|
|
|
|
if l.isLogFile(f) {
|
|
logFiles = append(logFiles, f)
|
|
}
|
|
}
|
|
|
|
sort.Sort(byFormatTime{logFiles, l.format()})
|
|
|
|
return logFiles, nil
|
|
}
|
|
|
|
func (l *Logger) isLogFile(f os.FileInfo) bool {
|
|
_, err := time.Parse(l.format(), filepath.Base(f.Name()))
|
|
return err == nil
|
|
}
|
|
|
|
func (l *Logger) max() int64 {
|
|
if l.MaxSize == 0 {
|
|
return defaultMaxSize
|
|
}
|
|
return l.MaxSize
|
|
}
|
|
|
|
func (l *Logger) dir() string {
|
|
if l.Dir != "" {
|
|
return l.Dir
|
|
}
|
|
return os.TempDir()
|
|
}
|
|
|
|
func (l *Logger) format() string {
|
|
if l.NameFormat != "" {
|
|
return l.NameFormat
|
|
}
|
|
return defaultNameFormat
|
|
}
|
|
|
|
// byFormatTime sorts by newest time formatted in the name.
|
|
type byFormatTime struct {
|
|
files []os.FileInfo
|
|
format string
|
|
}
|
|
|
|
func (b byFormatTime) Less(i, j int) bool {
|
|
return b.time(i).After(b.time(j))
|
|
}
|
|
|
|
func (b byFormatTime) Swap(i, j int) {
|
|
b.files[i], b.files[j] = b.files[j], b.files[i]
|
|
}
|
|
|
|
func (b byFormatTime) Len() int {
|
|
return len(b.files)
|
|
}
|
|
|
|
func (b byFormatTime) time(i int) time.Time {
|
|
t, err := time.Parse(b.format, filepath.Base(b.files[i].Name()))
|
|
if err != nil {
|
|
return time.Time{}
|
|
}
|
|
return t
|
|
}
|