github.com/manicqin/nomad@v0.9.5/client/logmon/logging/rotator.go (about) 1 package logging 2 3 import ( 4 "bufio" 5 "bytes" 6 "fmt" 7 "io/ioutil" 8 "os" 9 "path/filepath" 10 "sort" 11 "sync" 12 "time" 13 14 hclog "github.com/hashicorp/go-hclog" 15 ) 16 17 const ( 18 // logBufferSize is the size of the buffer. 19 logBufferSize = 64 * 1024 20 21 // bufferFlushDuration is the duration at which we flush the buffer. 22 bufferFlushDuration = 100 * time.Millisecond 23 24 // lineScanLimit is the number of bytes we will attempt to scan for new 25 // lines when approaching the end of the file to avoid a log line being 26 // split between two files. Any single line that is greater than this limit 27 // may be split. 28 lineScanLimit = 32 * 1024 29 30 // newLineDelimiter is the delimiter used for new lines. 31 newLineDelimiter = '\n' 32 ) 33 34 // FileRotator writes bytes to a rotated set of files 35 type FileRotator struct { 36 MaxFiles int // MaxFiles is the maximum number of rotated files allowed in a path 37 FileSize int64 // FileSize is the size a rotated file is allowed to grow 38 FileExtension string // FileExtension is appened to the end of the rotated files 39 40 path string // path is the path on the file system where the rotated set of files are opened 41 baseFileName string // baseFileName is the base file name of the rotated files 42 fileNameTemplate string // file name template 43 logFileIdx int // logFileIdx is the current index of the rotated files 44 oldestLogFileIdx int // oldestLogFileIdx is the index of the oldest log file in a path 45 46 currentFile *os.File // currentFile is the file that is currently getting written 47 currentWr int64 // currentWr is the number of bytes written to the current file 48 bufw *bufio.Writer 49 bufLock sync.Mutex 50 51 flushTicker *time.Ticker 52 logger hclog.Logger 53 purgeCh chan struct{} 54 doneCh chan struct{} 55 56 closed bool 57 closedLock sync.Mutex 58 } 59 60 // NewFileRotator returns a new file rotator 61 func NewFileRotator(path string, baseFile string, maxFiles int, 62 fileSize int64, fileExtension string, logger hclog.Logger) (*FileRotator, error) { 63 logger = logger.Named("rotator") 64 template := baseFile + ".%d" 65 if fileExtension != "" { 66 template += "." + fileExtension 67 } 68 rotator := &FileRotator{ 69 MaxFiles: maxFiles, 70 FileSize: fileSize, 71 FileExtension: fileExtension, 72 73 path: path, 74 baseFileName: baseFile, 75 fileNameTemplate: template, 76 77 flushTicker: time.NewTicker(bufferFlushDuration), 78 logger: logger, 79 purgeCh: make(chan struct{}, 1), 80 doneCh: make(chan struct{}, 1), 81 } 82 83 if err := rotator.lastFile(); err != nil { 84 return nil, err 85 } 86 go rotator.purgeOldFiles() 87 go rotator.flushPeriodically() 88 return rotator, nil 89 } 90 91 // Write writes a byte array to a file and rotates the file if it's size becomes 92 // equal to the maximum size the user has defined. 93 func (f *FileRotator) Write(p []byte) (n int, err error) { 94 n = 0 95 var forceRotate bool 96 97 for n < len(p) { 98 // Check if we still have space in the current file, otherwise close and 99 // open the next file 100 if forceRotate || f.currentWr >= f.FileSize { 101 forceRotate = false 102 f.flushBuffer() 103 f.currentFile.Close() 104 if err := f.nextFile(); err != nil { 105 f.logger.Error("error creating next file", "err", err) 106 return 0, err 107 } 108 } 109 // Calculate the remaining size on this file and how much we have left 110 // to write 111 remainingSpace := f.FileSize - f.currentWr 112 remainingToWrite := int64(len(p[n:])) 113 114 // Check if we are near the end of the file. If we are we attempt to 115 // avoid a log line being split between two files. 116 var nw int 117 if (remainingSpace - lineScanLimit) < remainingToWrite { 118 // Scan for new line and if the data up to new line fits in current 119 // file, write to buffer 120 idx := bytes.IndexByte(p[n:], newLineDelimiter) 121 if idx >= 0 && (remainingSpace-int64(idx)-1) >= 0 { 122 // We have space so write it to buffer 123 nw, err = f.writeToBuffer(p[n : n+idx+1]) 124 } else if idx >= 0 { 125 // We found a new line but don't have space so just force rotate 126 forceRotate = true 127 } else if remainingToWrite > f.FileSize || f.FileSize-lineScanLimit < 0 { 128 // There is no new line remaining but there is no point in 129 // rotating since the remaining data will not even fit in the 130 // next file either so just fill this one up. 131 li := int64(n) + remainingSpace 132 if remainingSpace > remainingToWrite { 133 li = int64(n) + remainingToWrite 134 } 135 nw, err = f.writeToBuffer(p[n:li]) 136 } else { 137 // There is no new line in the data remaining for us to write 138 // and it will fit in the next file so rotate. 139 forceRotate = true 140 } 141 } else { 142 // Write all the bytes in the current file 143 nw, err = f.writeToBuffer(p[n:]) 144 } 145 146 // Increment the number of bytes written so far in this method 147 // invocation 148 n += nw 149 150 // Increment the total number of bytes in the file 151 f.currentWr += int64(n) 152 if err != nil { 153 f.logger.Error("error writing to file", "err", err) 154 155 // As bufio writer does not automatically recover in case of any 156 // io error, we need to recover from it manually resetting the 157 // writter. 158 f.createOrResetBuffer() 159 160 return 161 } 162 } 163 return 164 } 165 166 // nextFile opens the next file and purges older files if the number of rotated 167 // files is larger than the maximum files configured by the user 168 func (f *FileRotator) nextFile() error { 169 nextFileIdx := f.logFileIdx 170 for { 171 nextFileIdx += 1 172 logFileName := filepath.Join(f.path, fmt.Sprintf(f.fileNameTemplate, nextFileIdx)) 173 if fi, err := os.Stat(logFileName); err == nil { 174 if fi.IsDir() || fi.Size() >= f.FileSize { 175 continue 176 } 177 } 178 f.logFileIdx = nextFileIdx 179 if err := f.createFile(); err != nil { 180 return err 181 } 182 break 183 } 184 // Purge old files if we have more files than MaxFiles 185 f.closedLock.Lock() 186 defer f.closedLock.Unlock() 187 if f.logFileIdx-f.oldestLogFileIdx >= f.MaxFiles && !f.closed { 188 select { 189 case f.purgeCh <- struct{}{}: 190 default: 191 } 192 } 193 return nil 194 } 195 196 // lastFile finds out the rotated file with the largest index in a path. 197 func (f *FileRotator) lastFile() error { 198 finfos, err := ioutil.ReadDir(f.path) 199 if err != nil { 200 return err 201 } 202 203 for _, fi := range finfos { 204 if fi.IsDir() { 205 continue 206 } 207 var idx int 208 if n, err := fmt.Sscanf(fi.Name(), f.fileNameTemplate, &idx); err != nil || n == 0 { 209 continue 210 } 211 if idx > f.logFileIdx { 212 f.logFileIdx = idx 213 } 214 } 215 if err := f.createFile(); err != nil { 216 return err 217 } 218 return nil 219 } 220 221 // createFile opens a new or existing file for writing 222 func (f *FileRotator) createFile() error { 223 logFileName := filepath.Join(f.path, fmt.Sprintf(f.fileNameTemplate, f.logFileIdx)) 224 cFile, err := os.OpenFile(logFileName, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) 225 if err != nil { 226 return err 227 } 228 229 f.currentFile = cFile 230 fi, err := f.currentFile.Stat() 231 if err != nil { 232 return err 233 } 234 f.currentWr = fi.Size() 235 f.createOrResetBuffer() 236 return nil 237 } 238 239 // flushPeriodically flushes the buffered writer every 100ms to the underlying 240 // file 241 func (f *FileRotator) flushPeriodically() { 242 for range f.flushTicker.C { 243 f.flushBuffer() 244 } 245 } 246 247 // Close flushes and closes the rotator. It never returns an error. 248 func (f *FileRotator) Close() error { 249 f.closedLock.Lock() 250 defer f.closedLock.Unlock() 251 252 // Stop the ticker and flush for one last time 253 f.flushTicker.Stop() 254 f.flushBuffer() 255 256 // Stop the purge go routine 257 if !f.closed { 258 f.doneCh <- struct{}{} 259 close(f.purgeCh) 260 f.closed = true 261 f.currentFile.Close() 262 } 263 264 return nil 265 } 266 267 // purgeOldFiles removes older files and keeps only the last N files rotated for 268 // a file 269 func (f *FileRotator) purgeOldFiles() { 270 for { 271 select { 272 case <-f.purgeCh: 273 var fIndexes []int 274 files, err := ioutil.ReadDir(f.path) 275 if err != nil { 276 f.logger.Error("error getting directory listing", "err", err) 277 return 278 } 279 // Inserting all the rotated files in a slice 280 for _, fi := range files { 281 var idx int 282 if n, err := fmt.Sscanf(fi.Name(), f.fileNameTemplate, &idx); err != nil { 283 f.logger.Error("error extracting file index", "err", err) 284 continue 285 } else if n == 0 { 286 continue 287 } 288 fIndexes = append(fIndexes, idx) 289 } 290 291 // Not continuing to delete files if the number of files is not more 292 // than MaxFiles 293 if len(fIndexes) <= f.MaxFiles { 294 continue 295 } 296 297 // Sorting the file indexes so that we can purge the older files and keep 298 // only the number of files as configured by the user 299 sort.Sort(sort.IntSlice(fIndexes)) 300 toDelete := fIndexes[0 : len(fIndexes)-f.MaxFiles] 301 for _, fIndex := range toDelete { 302 fname := filepath.Join(f.path, fmt.Sprintf(f.fileNameTemplate, fIndex)) 303 err := os.RemoveAll(fname) 304 if err != nil { 305 f.logger.Error("error removing file", "filename", fname, "err", err) 306 } 307 } 308 f.oldestLogFileIdx = fIndexes[0] 309 case <-f.doneCh: 310 return 311 } 312 } 313 } 314 315 // flushBuffer flushes the buffer 316 func (f *FileRotator) flushBuffer() error { 317 f.bufLock.Lock() 318 defer f.bufLock.Unlock() 319 if f.bufw != nil { 320 return f.bufw.Flush() 321 } 322 return nil 323 } 324 325 // writeToBuffer writes the byte array to buffer 326 func (f *FileRotator) writeToBuffer(p []byte) (int, error) { 327 f.bufLock.Lock() 328 defer f.bufLock.Unlock() 329 return f.bufw.Write(p) 330 } 331 332 // createOrResetBuffer creates a new buffer if we don't have one otherwise 333 // resets the buffer 334 func (f *FileRotator) createOrResetBuffer() { 335 f.bufLock.Lock() 336 defer f.bufLock.Unlock() 337 if f.bufw == nil { 338 f.bufw = bufio.NewWriterSize(f.currentFile, logBufferSize) 339 } else { 340 f.bufw.Reset(f.currentFile) 341 } 342 }