5665f27b27
Standardize naming Use stricter assertions and reduce boilerplate to make the intent of the tests more obvious. Also explicitly sort the filenames so that the correct files are pruned, and so that the tests can not flake.
142 lines
3.2 KiB
Go
142 lines
3.2 KiB
Go
package logging
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
now = time.Now
|
|
)
|
|
|
|
//LogFile is used to setup a file based logger that also performs log rotation
|
|
type LogFile struct {
|
|
//Name of the log file
|
|
fileName string
|
|
|
|
//Path to the log file
|
|
logPath string
|
|
|
|
//Duration between each file rotation operation
|
|
duration time.Duration
|
|
|
|
//LastCreated represents the creation time of the latest log
|
|
LastCreated time.Time
|
|
|
|
//FileInfo is the pointer to the current file being written to
|
|
FileInfo *os.File
|
|
|
|
//MaxBytes is the maximum number of desired bytes for a log file
|
|
MaxBytes int
|
|
|
|
//BytesWritten is the number of bytes written in the current log file
|
|
BytesWritten int64
|
|
|
|
// Max rotated files to keep before removing them.
|
|
MaxFiles int
|
|
|
|
//acquire is the mutex utilized to ensure we have no concurrency issues
|
|
acquire sync.Mutex
|
|
}
|
|
|
|
func (l *LogFile) fileNamePattern() string {
|
|
// Extract the file extension
|
|
fileExt := filepath.Ext(l.fileName)
|
|
// If we have no file extension we append .log
|
|
if fileExt == "" {
|
|
fileExt = ".log"
|
|
}
|
|
// Remove the file extension from the filename
|
|
return strings.TrimSuffix(l.fileName, fileExt) + "-%s" + fileExt
|
|
}
|
|
|
|
func (l *LogFile) openNew() error {
|
|
fileNamePattern := l.fileNamePattern()
|
|
|
|
createTime := now()
|
|
newfileName := fmt.Sprintf(fileNamePattern, strconv.FormatInt(createTime.UnixNano(), 10))
|
|
newfilePath := filepath.Join(l.logPath, newfileName)
|
|
|
|
// Try creating a file. We truncate the file because we are the only authority to write the logs
|
|
filePointer, err := os.OpenFile(newfilePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0640)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
l.FileInfo = filePointer
|
|
// New file, new bytes tracker, new creation time :)
|
|
l.LastCreated = createTime
|
|
l.BytesWritten = 0
|
|
return nil
|
|
}
|
|
|
|
func (l *LogFile) rotate() error {
|
|
// Get the time from the last point of contact
|
|
timeElapsed := time.Since(l.LastCreated)
|
|
// Rotate if we hit the byte file limit or the time limit
|
|
if (l.BytesWritten >= int64(l.MaxBytes) && (l.MaxBytes > 0)) || timeElapsed >= l.duration {
|
|
l.FileInfo.Close()
|
|
if err := l.pruneFiles(); err != nil {
|
|
return err
|
|
}
|
|
return l.openNew()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (l *LogFile) pruneFiles() error {
|
|
if l.MaxFiles == 0 {
|
|
return nil
|
|
}
|
|
|
|
pattern := filepath.Join(l.logPath, fmt.Sprintf(l.fileNamePattern(), "*"))
|
|
matches, err := filepath.Glob(pattern)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch {
|
|
case l.MaxFiles < 0:
|
|
return removeFiles(matches)
|
|
case len(matches) < l.MaxFiles:
|
|
return nil
|
|
}
|
|
|
|
sort.Strings(matches)
|
|
last := len(matches) - l.MaxFiles
|
|
return removeFiles(matches[:last])
|
|
}
|
|
|
|
func removeFiles(files []string) error {
|
|
for _, file := range files {
|
|
if err := os.Remove(file); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Write is used to implement io.Writer
|
|
func (l *LogFile) Write(b []byte) (n int, err error) {
|
|
l.acquire.Lock()
|
|
defer l.acquire.Unlock()
|
|
// Create a new file if we have no file to write to
|
|
if l.FileInfo == nil {
|
|
if err := l.openNew(); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
// Check for the last contact and rotate if necessary
|
|
if err := l.rotate(); err != nil {
|
|
return 0, err
|
|
}
|
|
l.BytesWritten += int64(len(b))
|
|
return l.FileInfo.Write(b)
|
|
}
|