Skip to content
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ matrix:
install:
- go get gopkg.in/fsnotify.v1
- go get gopkg.in/tomb.v1
- go get github.com/stoicperlman/fls
3 changes: 2 additions & 1 deletion cmd/gotail/gotail.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package main
import (
"flag"
"fmt"
"io"
"os"

"github.com/hpcloud/tail"
Expand Down Expand Up @@ -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)
Expand Down
46 changes: 40 additions & 6 deletions tail.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nishantroy To make this work just set this LineLocation in the config. It works the the same way Location works, but instead of moving by position it moves by \n's.

Also see the README on the underlying library https://github.com/StoicPerlman/fls. If you have trouble using it just shoot me a message.

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)
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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()
Expand Down
61 changes: 61 additions & 0 deletions tail_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package tail

import (
_ "fmt"
"io"
"io/ioutil"
"os"
"strings"
Expand Down Expand Up @@ -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.

Expand Down