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