184 lines
4.7 KiB
Go
184 lines
4.7 KiB
Go
package rotate
|
|
|
|
// Rotating things
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// FilePerm defines the permissions that Writer will use for all
|
|
// the files it creates.
|
|
const (
|
|
FilePerm = os.FileMode(0644)
|
|
DateFormat = "2006-01-02"
|
|
)
|
|
|
|
// FileWriter implements the io.Writer interface and writes to the
|
|
// filename specified.
|
|
// Will rotate at the specified interval and/or when the current file size exceeds maxSizeInBytes
|
|
// At rotation time, current file is renamed and a new file is created.
|
|
// If the number of archives exceeds maxArchives, older files are deleted.
|
|
type FileWriter struct {
|
|
filename string
|
|
filenameRotationTemplate string
|
|
current *os.File
|
|
interval time.Duration
|
|
maxSizeInBytes int64
|
|
maxArchives int
|
|
expireTime time.Time
|
|
bytesWritten int64
|
|
sync.Mutex
|
|
}
|
|
|
|
// NewFileWriter creates a new file writer.
|
|
func NewFileWriter(filename string, interval time.Duration, maxSizeInBytes int64, maxArchives int) (io.WriteCloser, error) {
|
|
if interval == 0 && maxSizeInBytes <= 0 {
|
|
// No rotation needed so a basic io.Writer will do the trick
|
|
return openFile(filename)
|
|
}
|
|
|
|
w := &FileWriter{
|
|
filename: filename,
|
|
interval: interval,
|
|
maxSizeInBytes: maxSizeInBytes,
|
|
maxArchives: maxArchives,
|
|
filenameRotationTemplate: getFilenameRotationTemplate(filename),
|
|
}
|
|
|
|
if err := w.openCurrent(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return w, nil
|
|
}
|
|
|
|
func openFile(filename string) (*os.File, error) {
|
|
return os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, FilePerm)
|
|
}
|
|
|
|
func getFilenameRotationTemplate(filename string) string {
|
|
// Extract the file extension
|
|
fileExt := filepath.Ext(filename)
|
|
// Remove the file extension from the filename (if any)
|
|
stem := strings.TrimSuffix(filename, fileExt)
|
|
return stem + ".%s-%s" + fileExt
|
|
}
|
|
|
|
// Write writes p to the current file, then checks to see if
|
|
// rotation is necessary.
|
|
func (w *FileWriter) Write(p []byte) (n int, err error) {
|
|
w.Lock()
|
|
defer w.Unlock()
|
|
if n, err = w.current.Write(p); err != nil {
|
|
return 0, err
|
|
}
|
|
w.bytesWritten += int64(n)
|
|
|
|
if err = w.rotateIfNeeded(); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return n, nil
|
|
}
|
|
|
|
// Close closes the current file. Writer is unusable after this
|
|
// is called.
|
|
func (w *FileWriter) Close() (err error) {
|
|
w.Lock()
|
|
defer w.Unlock()
|
|
|
|
// Rotate before closing
|
|
if err = w.rotate(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = w.current.Close(); err != nil {
|
|
return err
|
|
}
|
|
w.current = nil
|
|
return nil
|
|
}
|
|
|
|
func (w *FileWriter) openCurrent() (err error) {
|
|
// In case ModTime() fails, we use time.Now()
|
|
w.expireTime = time.Now().Add(w.interval)
|
|
w.bytesWritten = 0
|
|
w.current, err = openFile(w.filename)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Goal here is to rotate old pre-existing files.
|
|
// For that we use fileInfo.ModTime, instead of time.Now().
|
|
// Example: telegraf is restarted every 23 hours and
|
|
// the rotation interval is set to 24 hours.
|
|
// With time.now() as a reference we'd never rotate the file.
|
|
if fileInfo, err := w.current.Stat(); err == nil {
|
|
w.expireTime = fileInfo.ModTime().Add(w.interval)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (w *FileWriter) rotateIfNeeded() error {
|
|
if (w.interval > 0 && time.Now().After(w.expireTime)) ||
|
|
(w.maxSizeInBytes > 0 && w.bytesWritten >= w.maxSizeInBytes) {
|
|
if err := w.rotate(); err != nil {
|
|
//Ignore rotation errors and keep the log open
|
|
fmt.Printf("unable to rotate the file '%s', %s", w.filename, err.Error())
|
|
}
|
|
return w.openCurrent()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (w *FileWriter) rotate() (err error) {
|
|
if err = w.current.Close(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Use year-month-date for readability, unix time to make the file name unique with second precision
|
|
now := time.Now()
|
|
rotatedFilename := fmt.Sprintf(w.filenameRotationTemplate, now.Format(DateFormat), strconv.FormatInt(now.Unix(), 10))
|
|
if err = os.Rename(w.filename, rotatedFilename); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = w.purgeArchivesIfNeeded(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (w *FileWriter) purgeArchivesIfNeeded() (err error) {
|
|
if w.maxArchives == -1 {
|
|
//Skip archiving
|
|
return nil
|
|
}
|
|
|
|
var matches []string
|
|
if matches, err = filepath.Glob(fmt.Sprintf(w.filenameRotationTemplate, "*", "*")); err != nil {
|
|
return err
|
|
}
|
|
|
|
//if there are more archives than the configured maximum, then purge older files
|
|
if len(matches) > w.maxArchives {
|
|
//sort files alphanumerically to delete older files first
|
|
sort.Strings(matches)
|
|
for _, filename := range matches[:len(matches)-w.maxArchives] {
|
|
if err = os.Remove(filename); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|