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  }