diff --git a/.travis.yml b/.travis.yml index 183f1b7d..f9fc5923 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,3 +17,4 @@ matrix: install: - go get gopkg.in/fsnotify.v1 - go get gopkg.in/tomb.v1 + - go get github.com/stoicperlman/fls diff --git a/cmd/gotail/gotail.go b/cmd/gotail/gotail.go index 3da55f23..fdbb3dc7 100644 --- a/cmd/gotail/gotail.go +++ b/cmd/gotail/gotail.go @@ -6,6 +6,7 @@ package main import ( "flag" "fmt" + "io" "os" "github.com/hpcloud/tail" @@ -36,7 +37,7 @@ func main() { } if n != 0 { - config.Location = &tail.SeekInfo{-n, os.SEEK_END} + config.LineLocation = &tail.SeekInfo{-n, io.SeekEnd} } done := make(chan bool) diff --git a/tail.go b/tail.go index c99cdaa2..2964513d 100644 --- a/tail.go +++ b/tail.go @@ -18,6 +18,7 @@ import ( "github.com/hpcloud/tail/ratelimiter" "github.com/hpcloud/tail/util" "github.com/hpcloud/tail/watch" + "github.com/stoicperlman/fls" "gopkg.in/tomb.v1" ) @@ -57,12 +58,13 @@ type logger interface { // Config is used to specify how a file must be tailed. type Config struct { // File-specifc - Location *SeekInfo // Seek to this location before tailing - ReOpen bool // Reopen recreated files (tail -F) - MustExist bool // Fail early if the file does not exist - Poll bool // Poll for file changes instead of using inotify - Pipe bool // Is a named pipe (mkfifo) - RateLimiter *ratelimiter.LeakyBucket + Location *SeekInfo // Seek to this location before tailing + LineLocation *SeekInfo // Seek to this line number before tailing + ReOpen bool // Reopen recreated files (tail -F) + MustExist bool // Fail early if the file does not exist + Poll bool // Poll for file changes instead of using inotify + Pipe bool // Is a named pipe (mkfifo) + RateLimiter *ratelimiter.LeakyBucket // Generic IO Follow bool // Continue looking for new lines (tail -f) @@ -105,6 +107,10 @@ func TailFile(filename string, config Config) (*Tail, error) { util.Fatal("cannot set ReOpen without Follow.") } + if config.Location != nil && config.LineLocation != nil { + util.Fatal("Location and LineLocation cannot be set at the same time") + } + t := &Tail{ Filename: filename, Lines: make(chan *Line), @@ -246,6 +252,34 @@ func (tail *Tail) tailFileSync() { tail.Killf("Seek error on %s: %s", tail.Filename, err) return } + } else if tail.LineLocation != nil { + lineFile := fls.LineFile(tail.file) + buf := make([]byte, 1) + + _, err := lineFile.Seek(-1, io.SeekEnd) + if err != nil { + tail.Killf("Seek error on %s: %s", tail.Filename, err) + return + } + + _, err = lineFile.Read(buf) + if err != nil { + tail.Killf("Seek error on %s: %s", tail.Filename, err) + return + } + + // if file ends in newline don't count it in lines + // to read from end (mimics unix tail command) + correction := int64(1) + if string(buf) == "\n" { + correction = 0 + } + + _, err = lineFile.SeekLine(tail.LineLocation.Offset+correction, tail.LineLocation.Whence) + if err != nil && err != io.EOF { + tail.Killf("Seek error on %s: %s", tail.Filename, err) + return + } } tail.openReader() diff --git a/tail_test.go b/tail_test.go index 38d6b84b..5d5af7ee 100644 --- a/tail_test.go +++ b/tail_test.go @@ -8,6 +8,7 @@ package tail import ( _ "fmt" + "io" "io/ioutil" "os" "strings" @@ -208,6 +209,66 @@ func TestLocationMiddle(t *testing.T) { tailTest.Cleanup(tail, true) } +func TestLineLocationFull(t *testing.T) { + tailTest := NewTailTest("line-location-full", t) + tailTest.CreateFile("test.txt", "hello\nworld\n") + tail := tailTest.StartTail("test.txt", Config{Follow: true, LineLocation: nil}) + go tailTest.VerifyTailOutput(tail, []string{"hello", "world"}, false) + + // Delete after a reasonable delay, to give tail sufficient time + // to read all lines. + <-time.After(100 * time.Millisecond) + tailTest.RemoveFile("test.txt") + tailTest.Cleanup(tail, true) +} + +func TestLineLocationFullDontFollow(t *testing.T) { + tailTest := NewTailTest("line-location-full-dontfollow", t) + tailTest.CreateFile("test.txt", "hello\nworld\n") + tail := tailTest.StartTail("test.txt", Config{Follow: false, LineLocation: nil}) + go tailTest.VerifyTailOutput(tail, []string{"hello", "world"}, false) + + // Add more data only after reasonable delay. + <-time.After(100 * time.Millisecond) + tailTest.AppendFile("test.txt", "more\ndata\n") + <-time.After(100 * time.Millisecond) + + tailTest.Cleanup(tail, true) +} + +func TestLineLocationEnd(t *testing.T) { + tailTest := NewTailTest("line-location-end", t) + tailTest.CreateFile("test.txt", "hello\nworld\n") + tail := tailTest.StartTail("test.txt", Config{Follow: true, LineLocation: &SeekInfo{0, io.SeekEnd}}) + go tailTest.VerifyTailOutput(tail, []string{"more", "data"}, false) + + <-time.After(100 * time.Millisecond) + tailTest.AppendFile("test.txt", "more\ndata\n") + + // Delete after a reasonable delay, to give tail sufficient time + // to read all lines. + <-time.After(100 * time.Millisecond) + tailTest.RemoveFile("test.txt") + tailTest.Cleanup(tail, true) +} + +func TestLineLocationMiddle(t *testing.T) { + // Test reading from middle. + tailTest := NewTailTest("line-location-middle", t) + tailTest.CreateFile("test.txt", "hello\nworld\n") + tail := tailTest.StartTail("test.txt", Config{Follow: true, LineLocation: &SeekInfo{-1, io.SeekEnd}}) + go tailTest.VerifyTailOutput(tail, []string{"world", "more", "data"}, false) + + <-time.After(100 * time.Millisecond) + tailTest.AppendFile("test.txt", "more\ndata\n") + + // Delete after a reasonable delay, to give tail sufficient time + // to read all lines. + <-time.After(100 * time.Millisecond) + tailTest.RemoveFile("test.txt") + tailTest.Cleanup(tail, true) +} + // The use of polling file watcher could affect file rotation // (detected via renames), so test these explicitly.