package logger import ( "fmt" "os" "path/filepath" ) // FileRollingWriter maintains a set of log files numbered in order // to keep a subset of log data to ensure it doesn't grow pass defined limits type FileRollingWriter struct { baseFileName string directory string maxFileSize int64 maxFileCount uint fileHandle *os.File } // NewFileRollingWriter creates a new rolling file writer. // directory is the working directory for the files // baseFileName is the log file name. This writer appends .log to the name for the file name // maxFileSize is the size in bytes of how large each file can be. Not a hard limit, general limit based after each write // maxFileCount is the number of rolled files to keep. func NewFileRollingWriter(directory, baseFileName string, maxFileSize int64, maxFileCount uint) *FileRollingWriter { return &FileRollingWriter{ directory: directory, baseFileName: baseFileName, maxFileSize: maxFileSize, maxFileCount: maxFileCount, } } // Write is an implementation of io.writer the rolls the file once it reaches its max size // It is expected the caller to Write is doing so in a thread safe manner (as WriteManager does). func (w *FileRollingWriter) Write(p []byte) (n int, err error) { logFile, isSingleFile := buildPath(w.directory, w.baseFileName) if w.fileHandle == nil { h, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0664) if err != nil { return 0, err } w.fileHandle = h } // get size for rolling check info, err := w.fileHandle.Stat() if err != nil { // failed to stat the file. Close the file handle and attempt to open a new handle on the next write w.Close() w.fileHandle = nil return 0, err } // write to the file written, err := w.fileHandle.Write(p) // check if the file needs to be rolled if err == nil && info.Size()+int64(written) > w.maxFileSize && !isSingleFile { // close the file handle than do the renaming. A new one will be opened on the next write w.Close() w.rename(logFile, 1) } return written, err } // Close closes the file handle if it is open func (w *FileRollingWriter) Close() { if w.fileHandle != nil { w.fileHandle.Close() w.fileHandle = nil } } // rename is how the files are rolled. It works recursively to move the base log file to the rolled ones // e.g. cloudflared.log -> cloudflared-1.log, // but if cloudflared-1.log already exists, it is renamed to cloudflared-2.log, // then the other files move in to their postion func (w *FileRollingWriter) rename(sourcePath string, index uint) { destinationPath, isSingleFile := buildPath(w.directory, fmt.Sprintf("%s-%d", w.baseFileName, index)) if isSingleFile { return //don't need to rename anything, it is a single file } // rolled to the max amount of files allowed on disk if index >= w.maxFileCount { os.Remove(destinationPath) } // if the rolled path already exist, rename it to cloudflared-2.log, then do this one. // recursive call since the oldest one needs to be renamed, before the newer ones can be moved if exists(destinationPath) { w.rename(destinationPath, index+1) } os.Rename(sourcePath, destinationPath) } // return the path to the log file and if it is a single file or not. // true means a single file. false means a rolled file func buildPath(directory, fileName string) (string, bool) { if !isDirectory(directory) { // not a directory, so try and treat it as a single file for backwards compatibility sake return directory, true } return filepath.Join(directory, fileName+".log"), false } func exists(filePath string) bool { if _, err := os.Stat(filePath); os.IsNotExist(err) { return false } return true } func isDirectory(path string) bool { if path == "" { return true } fileInfo, err := os.Stat(path) if err != nil { return false } return fileInfo.IsDir() }