github.com/influxdata/telegraf@v1.30.3/internal/rotate/file_writer.go (about) 1 package rotate 2 3 // Rotating things 4 import ( 5 "fmt" 6 "io" 7 "os" 8 "path/filepath" 9 "sort" 10 "strconv" 11 "strings" 12 "sync" 13 "time" 14 ) 15 16 // FilePerm defines the permissions that Writer will use for all 17 // the files it creates. 18 const ( 19 FilePerm = os.FileMode(0644) 20 DateFormat = "2006-01-02" 21 ) 22 23 // FileWriter implements the io.Writer interface and writes to the 24 // filename specified. 25 // Will rotate at the specified interval and/or when the current file size exceeds maxSizeInBytes 26 // At rotation time, current file is renamed and a new file is created. 27 // If the number of archives exceeds maxArchives, older files are deleted. 28 type FileWriter struct { 29 filename string 30 filenameRotationTemplate string 31 current *os.File 32 interval time.Duration 33 maxSizeInBytes int64 34 maxArchives int 35 expireTime time.Time 36 bytesWritten int64 37 sync.Mutex 38 } 39 40 // NewFileWriter creates a new file writer. 41 func NewFileWriter(filename string, interval time.Duration, maxSizeInBytes int64, maxArchives int) (io.WriteCloser, error) { 42 if interval == 0 && maxSizeInBytes <= 0 { 43 // No rotation needed so a basic io.Writer will do the trick 44 return openFile(filename) 45 } 46 47 w := &FileWriter{ 48 filename: filename, 49 interval: interval, 50 maxSizeInBytes: maxSizeInBytes, 51 maxArchives: maxArchives, 52 filenameRotationTemplate: getFilenameRotationTemplate(filename), 53 } 54 55 if err := w.openCurrent(); err != nil { 56 return nil, err 57 } 58 59 return w, nil 60 } 61 62 func openFile(filename string) (*os.File, error) { 63 return os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, FilePerm) 64 } 65 66 func getFilenameRotationTemplate(filename string) string { 67 // Extract the file extension 68 fileExt := filepath.Ext(filename) 69 // Remove the file extension from the filename (if any) 70 stem := strings.TrimSuffix(filename, fileExt) 71 return stem + ".%s-%s" + fileExt 72 } 73 74 // Write writes p to the current file, then checks to see if 75 // rotation is necessary. 76 func (w *FileWriter) Write(p []byte) (n int, err error) { 77 w.Lock() 78 defer w.Unlock() 79 if n, err = w.current.Write(p); err != nil { 80 return 0, err 81 } 82 w.bytesWritten += int64(n) 83 84 if err := w.rotateIfNeeded(); err != nil { 85 return 0, err 86 } 87 88 return n, nil 89 } 90 91 // Close closes the current file. Writer is unusable after this 92 // is called. 93 func (w *FileWriter) Close() (err error) { 94 w.Lock() 95 defer w.Unlock() 96 97 // Rotate before closing 98 if err := w.rotateIfNeeded(); err != nil { 99 return err 100 } 101 102 // Close the file if we did not rotate 103 if err := w.current.Close(); err != nil { 104 return err 105 } 106 107 w.current = nil 108 return nil 109 } 110 111 func (w *FileWriter) openCurrent() (err error) { 112 // In case ModTime() fails, we use time.Now() 113 w.expireTime = time.Now().Add(w.interval) 114 w.bytesWritten = 0 115 w.current, err = openFile(w.filename) 116 117 if err != nil { 118 return err 119 } 120 121 // Goal here is to rotate old pre-existing files. 122 // For that we use fileInfo.ModTime, instead of time.Now(). 123 // Example: telegraf is restarted every 23 hours and 124 // the rotation interval is set to 24 hours. 125 // With time.now() as a reference we'd never rotate the file. 126 if fileInfo, err := w.current.Stat(); err == nil { 127 w.expireTime = fileInfo.ModTime().Add(w.interval) 128 w.bytesWritten = fileInfo.Size() 129 } 130 131 return w.rotateIfNeeded() 132 } 133 134 func (w *FileWriter) rotateIfNeeded() error { 135 if (w.interval > 0 && time.Now().After(w.expireTime)) || 136 (w.maxSizeInBytes > 0 && w.bytesWritten >= w.maxSizeInBytes) { 137 if err := w.rotate(); err != nil { 138 //Ignore rotation errors and keep the log open 139 fmt.Printf("unable to rotate the file %q, %s", w.filename, err.Error()) 140 } 141 return w.openCurrent() 142 } 143 return nil 144 } 145 146 func (w *FileWriter) rotate() (err error) { 147 if err := w.current.Close(); err != nil { 148 return err 149 } 150 151 // Use year-month-date for readability, unix time to make the file name unique with second precision 152 now := time.Now() 153 rotatedFilename := fmt.Sprintf(w.filenameRotationTemplate, now.Format(DateFormat), strconv.FormatInt(now.Unix(), 10)) 154 if err := os.Rename(w.filename, rotatedFilename); err != nil { 155 return err 156 } 157 158 return w.purgeArchivesIfNeeded() 159 } 160 161 func (w *FileWriter) purgeArchivesIfNeeded() (err error) { 162 if w.maxArchives == -1 { 163 //Skip archiving 164 return nil 165 } 166 167 var matches []string 168 if matches, err = filepath.Glob(fmt.Sprintf(w.filenameRotationTemplate, "*", "*")); err != nil { 169 return err 170 } 171 172 //if there are more archives than the configured maximum, then purge older files 173 if len(matches) > w.maxArchives { 174 //sort files alphanumerically to delete older files first 175 sort.Strings(matches) 176 for _, filename := range matches[:len(matches)-w.maxArchives] { 177 if err := os.Remove(filename); err != nil { 178 return err 179 } 180 } 181 } 182 return nil 183 }