github.com/dayvar14/tail@v0.0.0-20240222011302-f530c4fa0c9f/tail.go (about) 1 // Copyright (c) 2019 FOSS contributors of https://github.com/nxadm/tail 2 // Copyright (c) 2015 HPE Software Inc. All rights reserved. 3 // Copyright (c) 2013 ActiveState Software Inc. All rights reserved. 4 5 //nxadm/tail provides a Go library that emulates the features of the BSD `tail` 6 //program. The library comes with full support for truncation/move detection as 7 //it is designed to work with log rotation tools. The library works on all 8 //operating systems supported by Go, including POSIX systems like Linux and 9 //*BSD, and MS Windows. Go 1.9 is the oldest compiler release supported. 10 package tail 11 12 import ( 13 "bufio" 14 "errors" 15 "fmt" 16 "io" 17 "io/ioutil" 18 "log" 19 "os" 20 "strings" 21 "sync" 22 "time" 23 24 "github.com/dayvar14/tail/ratelimiter" 25 "github.com/dayvar14/tail/util" 26 "github.com/dayvar14/tail/watch" 27 "gopkg.in/tomb.v1" 28 ) 29 30 var ( 31 // ErrStop is returned when the tail of a file has been marked to be stopped. 32 ErrStop = errors.New("tail should now stop") 33 ) 34 35 type Line struct { 36 Text string // The contents of the file 37 Num int // The line number 38 SeekInfo SeekInfo // SeekInfo 39 Time time.Time // Present time 40 Err error // Error from tail 41 } 42 43 // Deprecated: this function is no longer used internally and it has little of no 44 // use in the API. As such, it will be removed from the API in a future major 45 // release. 46 // 47 // NewLine returns a * pointer to a Line struct. 48 func NewLine(text string, lineNum int) *Line { 49 return &Line{text, lineNum, SeekInfo{}, time.Now(), nil} 50 } 51 52 // SeekInfo represents arguments to io.Seek. See: https://golang.org/pkg/io/#SectionReader.Seek 53 type SeekInfo struct { 54 Offset int64 55 Whence int 56 } 57 58 type logger interface { 59 Fatal(v ...interface{}) 60 Fatalf(format string, v ...interface{}) 61 Fatalln(v ...interface{}) 62 Panic(v ...interface{}) 63 Panicf(format string, v ...interface{}) 64 Panicln(v ...interface{}) 65 Print(v ...interface{}) 66 Printf(format string, v ...interface{}) 67 Println(v ...interface{}) 68 } 69 70 // Config is used to specify how a file must be tailed. 71 type Config struct { 72 // File-specifc 73 Location *SeekInfo // Tail from this location. If nil, start at the beginning of the file 74 ReOpen bool // Reopen recreated files (tail -F) 75 MustExist bool // Fail early if the file does not exist 76 Poll bool // Poll for file changes instead of using the default inotify 77 Pipe bool // The file is a named pipe (mkfifo) 78 OpenReaderFunc func(rd io.Reader) io.Reader 79 80 // Generic IO 81 Follow bool // Continue looking for new lines (tail -f) 82 MaxLineSize int // If non-zero, split longer lines into multiple lines 83 CompleteLines bool // Only return complete lines (that end with "\n" or EOF when Follow is false) 84 85 // Optionally, use a ratelimiter (e.g. created by the ratelimiter/NewLeakyBucket function) 86 RateLimiter *ratelimiter.LeakyBucket 87 88 // Optionally use a Logger. When nil, the Logger is set to tail.DefaultLogger. 89 // To disable logging, set it to tail.DiscardingLogger 90 Logger logger 91 } 92 93 type Tail struct { 94 Filename string // The filename 95 Lines chan *Line // A consumable channel of *Line 96 Config // Tail.Configuration 97 98 file *os.File 99 reader *bufio.Reader 100 lineNum int 101 102 lineBuf *strings.Builder 103 104 watcher watch.FileWatcher 105 changes *watch.FileChanges 106 107 tomb.Tomb // provides: Done, Kill, Dying 108 109 lk sync.Mutex 110 } 111 112 var ( 113 // DefaultLogger logs to os.Stderr and it is used when Config.Logger == nil 114 DefaultLogger = log.New(os.Stderr, "", log.LstdFlags) 115 // DiscardingLogger can be used to disable logging output 116 DiscardingLogger = log.New(ioutil.Discard, "", 0) 117 ) 118 119 // TailFile begins tailing the file. And returns a pointer to a Tail struct 120 // and an error. An output stream is made available via the Tail.Lines 121 // channel (e.g. to be looped and printed). To handle errors during tailing, 122 // after finishing reading from the Lines channel, invoke the `Wait` or `Err` 123 // method on the returned *Tail. 124 func TailFile(filename string, config Config) (*Tail, error) { 125 if config.ReOpen && !config.Follow { 126 util.Fatal("cannot set ReOpen without Follow.") 127 } 128 129 t := &Tail{ 130 Filename: filename, 131 Lines: make(chan *Line), 132 Config: config, 133 } 134 135 if config.CompleteLines { 136 t.lineBuf = new(strings.Builder) 137 } 138 139 // when Logger was not specified in config, use default logger 140 if t.Logger == nil { 141 t.Logger = DefaultLogger 142 } 143 144 if t.Poll { 145 t.watcher = watch.NewPollingFileWatcher(filename) 146 } else { 147 t.watcher = watch.NewInotifyFileWatcher(filename) 148 } 149 150 if t.MustExist { 151 var err error 152 t.file, err = OpenFile(t.Filename) 153 if err != nil { 154 return nil, err 155 } 156 } 157 158 go t.tailFileSync() 159 160 return t, nil 161 } 162 163 // Tell returns the file's current position, like stdio's ftell() and an error. 164 // Beware that this value may not be completely accurate because one line from 165 // the chan(tail.Lines) may have been read already. 166 func (tail *Tail) Tell() (offset int64, err error) { 167 if tail.file == nil { 168 return 169 } 170 offset, err = tail.file.Seek(0, io.SeekCurrent) 171 if err != nil { 172 return 173 } 174 175 tail.lk.Lock() 176 defer tail.lk.Unlock() 177 if tail.reader == nil { 178 return 179 } 180 181 offset -= int64(tail.reader.Buffered()) 182 return 183 } 184 185 // Stop stops the tailing activity. 186 func (tail *Tail) Stop() error { 187 tail.Kill(nil) 188 return tail.Wait() 189 } 190 191 // StopAtEOF stops tailing as soon as the end of the file is reached. The function 192 // returns an error, 193 func (tail *Tail) StopAtEOF() error { 194 tail.Kill(errStopAtEOF) 195 return tail.Wait() 196 } 197 198 var errStopAtEOF = errors.New("tail: stop at eof") 199 200 func (tail *Tail) close() { 201 close(tail.Lines) 202 tail.closeFile() 203 } 204 205 func (tail *Tail) closeFile() { 206 if tail.file != nil { 207 tail.file.Close() 208 tail.file = nil 209 } 210 } 211 212 func (tail *Tail) reopen() error { 213 if tail.lineBuf != nil { 214 tail.lineBuf.Reset() 215 } 216 tail.closeFile() 217 tail.lineNum = 0 218 for { 219 var err error 220 tail.file, err = OpenFile(tail.Filename) 221 if err != nil { 222 if os.IsNotExist(err) { 223 tail.Logger.Printf("Waiting for %s to appear...", tail.Filename) 224 if err := tail.watcher.BlockUntilExists(&tail.Tomb); err != nil { 225 if err == tomb.ErrDying { 226 return err 227 } 228 return fmt.Errorf("Failed to detect creation of %s: %s", tail.Filename, err) 229 } 230 continue 231 } 232 return fmt.Errorf("Unable to open file %s: %s", tail.Filename, err) 233 } 234 break 235 } 236 return nil 237 } 238 239 func (tail *Tail) readLine() (string, error) { 240 tail.lk.Lock() 241 line, err := tail.reader.ReadString('\n') 242 tail.lk.Unlock() 243 244 newlineEnding := strings.HasSuffix(line, "\n") 245 line = strings.TrimRight(line, "\n") 246 247 // if we don't have to handle incomplete lines, we can return the line as-is 248 if !tail.Config.CompleteLines { 249 // Note ReadString "returns the data read before the error" in 250 // case of an error, including EOF, so we return it as is. The 251 // caller is expected to process it if err is EOF. 252 return line, err 253 } 254 255 if _, err := tail.lineBuf.WriteString(line); err != nil { 256 return line, err 257 } 258 259 if newlineEnding { 260 line = tail.lineBuf.String() 261 tail.lineBuf.Reset() 262 return line, nil 263 } else { 264 if tail.Config.Follow { 265 line = "" 266 } 267 return line, io.EOF 268 } 269 } 270 271 func (tail *Tail) tailFileSync() { 272 defer tail.Done() 273 defer tail.close() 274 275 if !tail.MustExist { 276 // deferred first open. 277 err := tail.reopen() 278 if err != nil { 279 if err != tomb.ErrDying { 280 tail.Kill(err) 281 } 282 return 283 } 284 } 285 286 // Seek to requested location on first open of the file. 287 if tail.Location != nil { 288 _, err := tail.file.Seek(tail.Location.Offset, tail.Location.Whence) 289 if err != nil { 290 tail.Killf("Seek error on %s: %s", tail.Filename, err) 291 return 292 } 293 } 294 295 tail.openReader() 296 297 if err := tail.watchChanges(); err != nil { 298 tail.Killf("Error watching for changes on %s: %s", tail.Filename, err) 299 return 300 } 301 302 // Read line by line. 303 for { 304 // do not seek in named pipes 305 if !tail.Pipe { 306 // grab the position in case we need to back up in the event of a half-line 307 if _, err := tail.Tell(); err != nil { 308 tail.Kill(err) 309 return 310 } 311 } 312 313 line, err := tail.readLine() 314 315 // Process `line` even if err is EOF. 316 if err == nil { 317 cooloff := !tail.sendLine(line) 318 if cooloff { 319 // Wait a second before seeking till the end of 320 // file when rate limit is reached. 321 msg := ("Too much log activity; waiting a second before resuming tailing") 322 offset, _ := tail.Tell() 323 tail.Lines <- &Line{msg, tail.lineNum, SeekInfo{Offset: offset}, time.Now(), errors.New(msg)} 324 select { 325 case <-time.After(time.Second): 326 case <-tail.Dying(): 327 return 328 } 329 if err := tail.seekEnd(); err != nil { 330 tail.Kill(err) 331 return 332 } 333 } 334 } else if err == io.EOF { 335 if !tail.Follow { 336 if line != "" { 337 tail.sendLine(line) 338 } 339 return 340 } 341 342 if tail.Follow && line != "" { 343 tail.sendLine(line) 344 if err := tail.seekEnd(); err != nil { 345 tail.Kill(err) 346 return 347 } 348 } 349 350 // When EOF is reached, wait for more data to become 351 // available. Wait strategy is based on the `tail.watcher` 352 // implementation (inotify or polling). 353 err := tail.waitForChanges() 354 if err != nil { 355 if err != ErrStop { 356 tail.Kill(err) 357 } 358 return 359 } 360 } else { 361 // non-EOF error 362 tail.Killf("Error reading %s: %s", tail.Filename, err) 363 return 364 } 365 366 select { 367 case <-tail.Dying(): 368 if tail.Err() == errStopAtEOF { 369 continue 370 } 371 return 372 default: 373 } 374 } 375 } 376 377 // watchChanges ensures the watcher is running. 378 func (tail *Tail) watchChanges() error { 379 if tail.changes != nil { 380 return nil 381 } 382 var pos int64 383 var err error 384 if !tail.Pipe { 385 pos, err = tail.file.Seek(0, io.SeekCurrent) 386 if err != nil { 387 return err 388 } 389 } 390 tail.changes, err = tail.watcher.ChangeEvents(&tail.Tomb, pos) 391 return err 392 } 393 394 // waitForChanges waits until the file has been appended, deleted, 395 // moved or truncated. When moved or deleted - the file will be 396 // reopened if ReOpen is true. Truncated files are always reopened. 397 func (tail *Tail) waitForChanges() error { 398 if err := tail.watchChanges(); err != nil { 399 return err 400 } 401 402 select { 403 case <-tail.changes.Modified: 404 return nil 405 case <-tail.changes.Deleted: 406 tail.changes = nil 407 if tail.ReOpen { 408 // XXX: we must not log from a library. 409 tail.Logger.Printf("Re-opening moved/deleted file %s ...", tail.Filename) 410 if err := tail.reopen(); err != nil { 411 return err 412 } 413 tail.Logger.Printf("Successfully reopened %s", tail.Filename) 414 tail.openReader() 415 return nil 416 } 417 tail.Logger.Printf("Stopping tail as file no longer exists: %s", tail.Filename) 418 return ErrStop 419 case <-tail.changes.Truncated: 420 // Always reopen truncated files (Follow is true) 421 tail.Logger.Printf("Re-opening truncated file %s ...", tail.Filename) 422 if err := tail.reopen(); err != nil { 423 return err 424 } 425 tail.Logger.Printf("Successfully reopened truncated %s", tail.Filename) 426 tail.openReader() 427 return nil 428 case <-tail.Dying(): 429 return ErrStop 430 } 431 } 432 433 func (tail *Tail) openReader() { 434 tail.lk.Lock() 435 var rd io.Reader = tail.file 436 if tail.OpenReaderFunc != nil { 437 rd = tail.OpenReaderFunc(rd) 438 } 439 440 if tail.MaxLineSize > 0 { 441 // add 2 to account for newline characters 442 tail.reader = bufio.NewReaderSize(rd, tail.MaxLineSize+2) 443 } else { 444 tail.reader = bufio.NewReader(rd) 445 } 446 tail.lk.Unlock() 447 } 448 449 func (tail *Tail) seekEnd() error { 450 return tail.seekTo(SeekInfo{Offset: 0, Whence: io.SeekEnd}) 451 } 452 453 func (tail *Tail) seekTo(pos SeekInfo) error { 454 _, err := tail.file.Seek(pos.Offset, pos.Whence) 455 if err != nil { 456 return fmt.Errorf("Seek error on %s: %s", tail.Filename, err) 457 } 458 // Reset the read buffer whenever the file is re-seek'ed 459 tail.reader.Reset(tail.file) 460 return nil 461 } 462 463 // sendLine sends the line(s) to Lines channel, splitting longer lines 464 // if necessary. Return false if rate limit is reached. 465 func (tail *Tail) sendLine(line string) bool { 466 now := time.Now() 467 lines := []string{line} 468 469 // Split longer lines 470 if tail.MaxLineSize > 0 && len(line) > tail.MaxLineSize { 471 lines = util.PartitionString(line, tail.MaxLineSize) 472 } 473 474 for _, line := range lines { 475 tail.lineNum++ 476 offset, _ := tail.Tell() 477 select { 478 case tail.Lines <- &Line{line, tail.lineNum, SeekInfo{Offset: offset}, now, nil}: 479 case <-tail.Dying(): 480 return true 481 } 482 } 483 484 if tail.Config.RateLimiter != nil { 485 ok := tail.Config.RateLimiter.Pour(uint16(len(lines))) 486 if !ok { 487 tail.Logger.Printf("Leaky bucket full (%v); entering 1s cooloff period.", 488 tail.Filename) 489 return false 490 } 491 } 492 493 return true 494 } 495 496 // Cleanup removes inotify watches added by the tail package. This function is 497 // meant to be invoked from a process's exit handler. Linux kernel may not 498 // automatically remove inotify watches after the process exits. 499 // If you plan to re-read a file, don't call Cleanup in between. 500 func (tail *Tail) Cleanup() { 501 watch.Cleanup(tail.Filename) 502 }