gocron/vendor/github.com/cihub/seelog/writers_rollingfilewriter.go

764 lines
21 KiB
Go

// Copyright (c) 2013 - Cloud Instruments Co., Ltd.
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package seelog
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/cihub/seelog/archive"
"github.com/cihub/seelog/archive/gzip"
"github.com/cihub/seelog/archive/tar"
"github.com/cihub/seelog/archive/zip"
)
// Common constants
const (
rollingLogHistoryDelimiter = "."
)
// Types of the rolling writer: roll by date, by time, etc.
type rollingType uint8
const (
rollingTypeSize = iota
rollingTypeTime
)
// Types of the rolled file naming mode: prefix, postfix, etc.
type rollingNameMode uint8
const (
rollingNameModePostfix = iota
rollingNameModePrefix
)
var rollingNameModesStringRepresentation = map[rollingNameMode]string{
rollingNameModePostfix: "postfix",
rollingNameModePrefix: "prefix",
}
func rollingNameModeFromString(rollingNameStr string) (rollingNameMode, bool) {
for tp, tpStr := range rollingNameModesStringRepresentation {
if tpStr == rollingNameStr {
return tp, true
}
}
return 0, false
}
var rollingTypesStringRepresentation = map[rollingType]string{
rollingTypeSize: "size",
rollingTypeTime: "date",
}
func rollingTypeFromString(rollingTypeStr string) (rollingType, bool) {
for tp, tpStr := range rollingTypesStringRepresentation {
if tpStr == rollingTypeStr {
return tp, true
}
}
return 0, false
}
// Old logs archivation type.
type rollingArchiveType uint8
const (
rollingArchiveNone = iota
rollingArchiveZip
rollingArchiveGzip
)
var rollingArchiveTypesStringRepresentation = map[rollingArchiveType]string{
rollingArchiveNone: "none",
rollingArchiveZip: "zip",
rollingArchiveGzip: "gzip",
}
type archiver func(f *os.File, exploded bool) archive.WriteCloser
type unarchiver func(f *os.File) (archive.ReadCloser, error)
type compressionType struct {
extension string
handleMultipleEntries bool
archiver archiver
unarchiver unarchiver
}
var compressionTypes = map[rollingArchiveType]compressionType{
rollingArchiveZip: {
extension: ".zip",
handleMultipleEntries: true,
archiver: func(f *os.File, _ bool) archive.WriteCloser {
return zip.NewWriter(f)
},
unarchiver: func(f *os.File) (archive.ReadCloser, error) {
fi, err := f.Stat()
if err != nil {
return nil, err
}
r, err := zip.NewReader(f, fi.Size())
if err != nil {
return nil, err
}
return archive.NopCloser(r), nil
},
},
rollingArchiveGzip: {
extension: ".gz",
handleMultipleEntries: false,
archiver: func(f *os.File, exploded bool) archive.WriteCloser {
gw := gzip.NewWriter(f)
if exploded {
return gw
}
return tar.NewWriteMultiCloser(gw, gw)
},
unarchiver: func(f *os.File) (archive.ReadCloser, error) {
gr, err := gzip.NewReader(f, f.Name())
if err != nil {
return nil, err
}
// Determine if the gzip is a tar
tr := tar.NewReader(gr)
_, err = tr.Next()
isTar := err == nil
// Reset to beginning of file
if _, err := f.Seek(0, os.SEEK_SET); err != nil {
return nil, err
}
gr.Reset(f)
if isTar {
return archive.NopCloser(tar.NewReader(gr)), nil
}
return gr, nil
},
},
}
func (compressionType *compressionType) rollingArchiveTypeName(name string, exploded bool) string {
if !compressionType.handleMultipleEntries && !exploded {
return name + ".tar" + compressionType.extension
} else {
return name + compressionType.extension
}
}
func rollingArchiveTypeFromString(rollingArchiveTypeStr string) (rollingArchiveType, bool) {
for tp, tpStr := range rollingArchiveTypesStringRepresentation {
if tpStr == rollingArchiveTypeStr {
return tp, true
}
}
return 0, false
}
// Default names for different archive types
var rollingArchiveDefaultExplodedName = "old"
func rollingArchiveTypeDefaultName(archiveType rollingArchiveType, exploded bool) (string, error) {
compressionType, ok := compressionTypes[archiveType]
if !ok {
return "", fmt.Errorf("cannot get default filename for archive type = %v", archiveType)
}
return compressionType.rollingArchiveTypeName("log", exploded), nil
}
// rollerVirtual is an interface that represents all virtual funcs that are
// called in different rolling writer subtypes.
type rollerVirtual interface {
needsToRoll() bool // Returns true if needs to switch to another file.
isFileRollNameValid(rname string) bool // Returns true if logger roll file name (postfix/prefix/etc.) is ok.
sortFileRollNamesAsc(fs []string) ([]string, error) // Sorts logger roll file names in ascending order of their creation by logger.
// getNewHistoryRollFileName is called whenever we are about to roll the
// current log file. It returns the name the current log file should be
// rolled to.
getNewHistoryRollFileName(otherHistoryFiles []string) string
getCurrentFileName() string
}
// rollingFileWriter writes received messages to a file, until time interval passes
// or file exceeds a specified limit. After that the current log file is renamed
// and writer starts to log into a new file. You can set a limit for such renamed
// files count, if you want, and then the rolling writer would delete older ones when
// the files count exceed the specified limit.
type rollingFileWriter struct {
fileName string // log file name
currentDirPath string
currentFile *os.File
currentName string
currentFileSize int64
rollingType rollingType // Rolling mode (Files roll by size/date/...)
archiveType rollingArchiveType
archivePath string
archiveExploded bool
fullName bool
maxRolls int
nameMode rollingNameMode
self rollerVirtual // Used for virtual calls
rollLock sync.Mutex
}
func newRollingFileWriter(fpath string, rtype rollingType, atype rollingArchiveType, apath string, maxr int, namemode rollingNameMode,
archiveExploded bool, fullName bool) (*rollingFileWriter, error) {
rw := new(rollingFileWriter)
rw.currentDirPath, rw.fileName = filepath.Split(fpath)
if len(rw.currentDirPath) == 0 {
rw.currentDirPath = "."
}
rw.rollingType = rtype
rw.archiveType = atype
rw.archivePath = apath
rw.nameMode = namemode
rw.maxRolls = maxr
rw.archiveExploded = archiveExploded
rw.fullName = fullName
return rw, nil
}
func (rw *rollingFileWriter) hasRollName(file string) bool {
switch rw.nameMode {
case rollingNameModePostfix:
rname := rw.fileName + rollingLogHistoryDelimiter
return strings.HasPrefix(file, rname)
case rollingNameModePrefix:
rname := rollingLogHistoryDelimiter + rw.fileName
return strings.HasSuffix(file, rname)
}
return false
}
func (rw *rollingFileWriter) createFullFileName(originalName, rollname string) string {
switch rw.nameMode {
case rollingNameModePostfix:
return originalName + rollingLogHistoryDelimiter + rollname
case rollingNameModePrefix:
return rollname + rollingLogHistoryDelimiter + originalName
}
return ""
}
func (rw *rollingFileWriter) getSortedLogHistory() ([]string, error) {
files, err := getDirFilePaths(rw.currentDirPath, nil, true)
if err != nil {
return nil, err
}
var validRollNames []string
for _, file := range files {
if rw.hasRollName(file) {
rname := rw.getFileRollName(file)
if rw.self.isFileRollNameValid(rname) {
validRollNames = append(validRollNames, rname)
}
}
}
sortedTails, err := rw.self.sortFileRollNamesAsc(validRollNames)
if err != nil {
return nil, err
}
validSortedFiles := make([]string, len(sortedTails))
for i, v := range sortedTails {
validSortedFiles[i] = rw.createFullFileName(rw.fileName, v)
}
return validSortedFiles, nil
}
func (rw *rollingFileWriter) createFileAndFolderIfNeeded(first bool) error {
var err error
if len(rw.currentDirPath) != 0 {
err = os.MkdirAll(rw.currentDirPath, defaultDirectoryPermissions)
if err != nil {
return err
}
}
rw.currentName = rw.self.getCurrentFileName()
filePath := filepath.Join(rw.currentDirPath, rw.currentName)
// This will either open the existing file (without truncating it) or
// create if necessary. Append mode avoids any race conditions.
rw.currentFile, err = os.OpenFile(filePath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, defaultFilePermissions)
if err != nil {
return err
}
stat, err := rw.currentFile.Stat()
if err != nil {
rw.currentFile.Close()
rw.currentFile = nil
return err
}
rw.currentFileSize = stat.Size()
return nil
}
func (rw *rollingFileWriter) archiveExplodedLogs(logFilename string, compressionType compressionType) (err error) {
closeWithError := func(c io.Closer) {
if cerr := c.Close(); cerr != nil && err == nil {
err = cerr
}
}
rollPath := filepath.Join(rw.currentDirPath, logFilename)
src, err := os.Open(rollPath)
if err != nil {
return err
}
defer src.Close() // Read-only
// Buffer to a temporary file on the same partition
// Note: archivePath is a path to a directory when handling exploded logs
dst, err := rw.tempArchiveFile(rw.archivePath)
if err != nil {
return err
}
defer func() {
closeWithError(dst)
if err != nil {
os.Remove(dst.Name()) // Can't do anything when we fail to remove temp file
return
}
// Finalize archive by swapping the buffered archive into place
err = os.Rename(dst.Name(), filepath.Join(rw.archivePath,
compressionType.rollingArchiveTypeName(logFilename, true)))
}()
// archive entry
w := compressionType.archiver(dst, true)
defer closeWithError(w)
fi, err := src.Stat()
if err != nil {
return err
}
if err := w.NextFile(logFilename, fi); err != nil {
return err
}
_, err = io.Copy(w, src)
return err
}
func (rw *rollingFileWriter) archiveUnexplodedLogs(compressionType compressionType, rollsToDelete int, history []string) (err error) {
closeWithError := func(c io.Closer) {
if cerr := c.Close(); cerr != nil && err == nil {
err = cerr
}
}
// Buffer to a temporary file on the same partition
// Note: archivePath is a path to a file when handling unexploded logs
dst, err := rw.tempArchiveFile(filepath.Dir(rw.archivePath))
if err != nil {
return err
}
defer func() {
closeWithError(dst)
if err != nil {
os.Remove(dst.Name()) // Can't do anything when we fail to remove temp file
return
}
// Finalize archive by moving the buffered archive into place
err = os.Rename(dst.Name(), rw.archivePath)
}()
w := compressionType.archiver(dst, false)
defer closeWithError(w)
src, err := os.Open(rw.archivePath)
switch {
// Archive exists
case err == nil:
defer src.Close() // Read-only
r, err := compressionType.unarchiver(src)
if err != nil {
return err
}
defer r.Close() // Read-only
if err := archive.Copy(w, r); err != nil {
return err
}
// Failed to stat
case !os.IsNotExist(err):
return err
}
// Add new files to the archive
for i := 0; i < rollsToDelete; i++ {
rollPath := filepath.Join(rw.currentDirPath, history[i])
src, err := os.Open(rollPath)
if err != nil {
return err
}
defer src.Close() // Read-only
fi, err := src.Stat()
if err != nil {
return err
}
if err := w.NextFile(src.Name(), fi); err != nil {
return err
}
if _, err := io.Copy(w, src); err != nil {
return err
}
}
return nil
}
func (rw *rollingFileWriter) deleteOldRolls(history []string) error {
if rw.maxRolls <= 0 {
return nil
}
rollsToDelete := len(history) - rw.maxRolls
if rollsToDelete <= 0 {
return nil
}
if rw.archiveType != rollingArchiveNone {
if rw.archiveExploded {
os.MkdirAll(rw.archivePath, defaultDirectoryPermissions)
// Archive logs
for i := 0; i < rollsToDelete; i++ {
rw.archiveExplodedLogs(history[i], compressionTypes[rw.archiveType])
}
} else {
os.MkdirAll(filepath.Dir(rw.archivePath), defaultDirectoryPermissions)
rw.archiveUnexplodedLogs(compressionTypes[rw.archiveType], rollsToDelete, history)
}
}
var err error
// In all cases (archive files or not) the files should be deleted.
for i := 0; i < rollsToDelete; i++ {
// Try best to delete files without breaking the loop.
if err = tryRemoveFile(filepath.Join(rw.currentDirPath, history[i])); err != nil {
reportInternalError(err)
}
}
return nil
}
func (rw *rollingFileWriter) getFileRollName(fileName string) string {
switch rw.nameMode {
case rollingNameModePostfix:
return fileName[len(rw.fileName+rollingLogHistoryDelimiter):]
case rollingNameModePrefix:
return fileName[:len(fileName)-len(rw.fileName+rollingLogHistoryDelimiter)]
}
return ""
}
func (rw *rollingFileWriter) roll() error {
// First, close current file.
err := rw.currentFile.Close()
if err != nil {
return err
}
rw.currentFile = nil
// Current history of all previous log files.
// For file roller it may be like this:
// * ...
// * file.log.4
// * file.log.5
// * file.log.6
//
// For date roller it may look like this:
// * ...
// * file.log.11.Aug.13
// * file.log.15.Aug.13
// * file.log.16.Aug.13
// Sorted log history does NOT include current file.
history, err := rw.getSortedLogHistory()
if err != nil {
return err
}
// Renames current file to create a new roll history entry
// For file roller it may be like this:
// * ...
// * file.log.4
// * file.log.5
// * file.log.6
// n file.log.7 <---- RENAMED (from file.log)
newHistoryName := rw.createFullFileName(rw.fileName,
rw.self.getNewHistoryRollFileName(history))
err = os.Rename(filepath.Join(rw.currentDirPath, rw.currentName), filepath.Join(rw.currentDirPath, newHistoryName))
if err != nil {
return err
}
// Finally, add the newly added history file to the history archive
// and, if after that the archive exceeds the allowed max limit, older rolls
// must the removed/archived.
history = append(history, newHistoryName)
if len(history) > rw.maxRolls {
err = rw.deleteOldRolls(history)
if err != nil {
return err
}
}
return nil
}
func (rw *rollingFileWriter) Write(bytes []byte) (n int, err error) {
rw.rollLock.Lock()
defer rw.rollLock.Unlock()
if rw.self.needsToRoll() {
if err := rw.roll(); err != nil {
return 0, err
}
}
if rw.currentFile == nil {
err := rw.createFileAndFolderIfNeeded(true)
if err != nil {
return 0, err
}
}
n, err = rw.currentFile.Write(bytes)
rw.currentFileSize += int64(n)
return n, err
}
func (rw *rollingFileWriter) Close() error {
if rw.currentFile != nil {
e := rw.currentFile.Close()
if e != nil {
return e
}
rw.currentFile = nil
}
return nil
}
func (rw *rollingFileWriter) tempArchiveFile(archiveDir string) (*os.File, error) {
tmp := filepath.Join(archiveDir, ".seelog_tmp")
if err := os.MkdirAll(tmp, defaultDirectoryPermissions); err != nil {
return nil, err
}
return ioutil.TempFile(tmp, "archived_logs")
}
// =============================================================================================
// Different types of rolling writers
// =============================================================================================
// --------------------------------------------------
// Rolling writer by SIZE
// --------------------------------------------------
// rollingFileWriterSize performs roll when file exceeds a specified limit.
type rollingFileWriterSize struct {
*rollingFileWriter
maxFileSize int64
}
func NewRollingFileWriterSize(fpath string, atype rollingArchiveType, apath string, maxSize int64, maxRolls int, namemode rollingNameMode, archiveExploded bool) (*rollingFileWriterSize, error) {
rw, err := newRollingFileWriter(fpath, rollingTypeSize, atype, apath, maxRolls, namemode, archiveExploded, false)
if err != nil {
return nil, err
}
rws := &rollingFileWriterSize{rw, maxSize}
rws.self = rws
return rws, nil
}
func (rws *rollingFileWriterSize) needsToRoll() bool {
return rws.currentFileSize >= rws.maxFileSize
}
func (rws *rollingFileWriterSize) isFileRollNameValid(rname string) bool {
if len(rname) == 0 {
return false
}
_, err := strconv.Atoi(rname)
return err == nil
}
type rollSizeFileTailsSlice []string
func (p rollSizeFileTailsSlice) Len() int {
return len(p)
}
func (p rollSizeFileTailsSlice) Less(i, j int) bool {
v1, _ := strconv.Atoi(p[i])
v2, _ := strconv.Atoi(p[j])
return v1 < v2
}
func (p rollSizeFileTailsSlice) Swap(i, j int) {
p[i], p[j] = p[j], p[i]
}
func (rws *rollingFileWriterSize) sortFileRollNamesAsc(fs []string) ([]string, error) {
ss := rollSizeFileTailsSlice(fs)
sort.Sort(ss)
return ss, nil
}
func (rws *rollingFileWriterSize) getNewHistoryRollFileName(otherLogFiles []string) string {
v := 0
if len(otherLogFiles) != 0 {
latest := otherLogFiles[len(otherLogFiles)-1]
v, _ = strconv.Atoi(rws.getFileRollName(latest))
}
return fmt.Sprintf("%d", v+1)
}
func (rws *rollingFileWriterSize) getCurrentFileName() string {
return rws.fileName
}
func (rws *rollingFileWriterSize) String() string {
return fmt.Sprintf("Rolling file writer (By SIZE): filename: %s, archive: %s, archivefile: %s, maxFileSize: %v, maxRolls: %v",
rws.fileName,
rollingArchiveTypesStringRepresentation[rws.archiveType],
rws.archivePath,
rws.maxFileSize,
rws.maxRolls)
}
// --------------------------------------------------
// Rolling writer by TIME
// --------------------------------------------------
// rollingFileWriterTime performs roll when a specified time interval has passed.
type rollingFileWriterTime struct {
*rollingFileWriter
timePattern string
currentTimeFileName string
}
func NewRollingFileWriterTime(fpath string, atype rollingArchiveType, apath string, maxr int,
timePattern string, namemode rollingNameMode, archiveExploded bool, fullName bool) (*rollingFileWriterTime, error) {
rw, err := newRollingFileWriter(fpath, rollingTypeTime, atype, apath, maxr, namemode, archiveExploded, fullName)
if err != nil {
return nil, err
}
rws := &rollingFileWriterTime{rw, timePattern, ""}
rws.self = rws
return rws, nil
}
func (rwt *rollingFileWriterTime) needsToRoll() bool {
newName := time.Now().Format(rwt.timePattern)
if rwt.currentTimeFileName == "" {
// first run; capture the current name
rwt.currentTimeFileName = newName
return false
}
return newName != rwt.currentTimeFileName
}
func (rwt *rollingFileWriterTime) isFileRollNameValid(rname string) bool {
if len(rname) == 0 {
return false
}
_, err := time.ParseInLocation(rwt.timePattern, rname, time.Local)
return err == nil
}
type rollTimeFileTailsSlice struct {
data []string
pattern string
}
func (p rollTimeFileTailsSlice) Len() int {
return len(p.data)
}
func (p rollTimeFileTailsSlice) Less(i, j int) bool {
t1, _ := time.ParseInLocation(p.pattern, p.data[i], time.Local)
t2, _ := time.ParseInLocation(p.pattern, p.data[j], time.Local)
return t1.Before(t2)
}
func (p rollTimeFileTailsSlice) Swap(i, j int) {
p.data[i], p.data[j] = p.data[j], p.data[i]
}
func (rwt *rollingFileWriterTime) sortFileRollNamesAsc(fs []string) ([]string, error) {
ss := rollTimeFileTailsSlice{data: fs, pattern: rwt.timePattern}
sort.Sort(ss)
return ss.data, nil
}
func (rwt *rollingFileWriterTime) getNewHistoryRollFileName(_ []string) string {
newFileName := rwt.currentTimeFileName
rwt.currentTimeFileName = time.Now().Format(rwt.timePattern)
return newFileName
}
func (rwt *rollingFileWriterTime) getCurrentFileName() string {
if rwt.fullName {
return rwt.createFullFileName(rwt.fileName, time.Now().Format(rwt.timePattern))
}
return rwt.fileName
}
func (rwt *rollingFileWriterTime) String() string {
return fmt.Sprintf("Rolling file writer (By TIME): filename: %s, archive: %s, archivefile: %s, pattern: %s, maxRolls: %v",
rwt.fileName,
rollingArchiveTypesStringRepresentation[rwt.archiveType],
rwt.archivePath,
rwt.timePattern,
rwt.maxRolls)
}