diff --git a/cmd/gotail/gotail.go b/cmd/gotail/gotail.go index 250af2a..12b2c62 100644 --- a/cmd/gotail/gotail.go +++ b/cmd/gotail/gotail.go @@ -9,9 +9,10 @@ import ( "os" ) -func args2config() tail.Config { +func args2config() (tail.Config, int64) { config := tail.Config{Follow: true} - flag.IntVar(&config.Location, "n", 0, "tail from the last Nth location") + n := int64(0) + flag.Int64Var(&n, "n", 0, "tail from the last Nth location") flag.BoolVar(&config.Follow, "f", false, "wait for additional data to be appended to the file") flag.BoolVar(&config.ReOpen, "F", false, "follow, and track file rename/rotation") flag.BoolVar(&config.Poll, "p", false, "use polling, instead of inotify") @@ -19,16 +20,20 @@ func args2config() tail.Config { if config.ReOpen { config.Follow = true } - return config + return config, n } func main() { - config := args2config() + config, n := args2config() if flag.NFlag() < 1 { fmt.Println("need one or more files as arguments") os.Exit(1) } + if n != 0 { + config.Location = &tail.SeekInfo{-n, os.SEEK_END} + } + done := make(chan bool) for _, filename := range flag.Args() { go tailFile(filename, config, done) diff --git a/tail.go b/tail.go index 7c97ba5..91c2600 100644 --- a/tail.go +++ b/tail.go @@ -24,15 +24,20 @@ type Line struct { // log line itself. } +type SeekInfo struct { + Offset int64 + Whence int // os.SEEK_* +} + // Config is used to specify how a file must be tailed. type Config struct { - Location int // Tail from last N lines (tail -n) - Follow bool // Continue looking for new lines (tail -f) - 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 - MaxLineSize int // If non-zero, split longer lines into multiple lines - LimitRate int64 // If non-zero, limit the rate of read log lines + Location *SeekInfo // Seek before tailing + Follow bool // Continue looking for new lines (tail -f) + 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 + MaxLineSize int // If non-zero, split longer lines into multiple lines + LimitRate int64 // If non-zero, limit the rate of read log lines // by this much per second. } @@ -55,10 +60,6 @@ type Tail struct { // invoke the `Wait` or `Err` method after finishing reading from the // `Lines` channel. func TailFile(filename string, config Config) (*Tail, error) { - if !(config.Location == 0 || config.Location == -1) { - panic("only 0/-1 values are supported for Location.") - } - if config.ReOpen && !config.Follow { panic("cannot set ReOpen without Follow.") } @@ -143,8 +144,9 @@ func (tail *Tail) tailFileSync() { } // Seek to requested location on first open of the file. - if tail.Location == 0 { - _, err := tail.file.Seek(0, 2) // Seek to the file end + if tail.Location != nil { + _, err := tail.file.Seek(tail.Location.Offset, tail.Location.Whence) + // log.Printf("Seeked %s - %+v\n", tail.Filename, tail.Location) if err != nil { tail.Killf("Seek error on %s: %s", tail.Filename, err) return diff --git a/tail_test.go b/tail_test.go index 3bb9d20..e3b298f 100644 --- a/tail_test.go +++ b/tail_test.go @@ -43,7 +43,7 @@ func TestMustExist(t *testing.T) { func TestMaxLineSize(_t *testing.T) { t := NewTailTest("maxlinesize", _t) t.CreateFile("test.txt", "hello\nworld\nfin\nhe") - tail := t.StartTail("test.txt", Config{Follow: true, Location: -1, MaxLineSize: 3}) + tail := t.StartTail("test.txt", Config{Follow: true, Location: nil, MaxLineSize: 3}) go t.VerifyTailOutput(tail, []string{"hel", "lo", "wor", "ld", "fin", "he"}) // Delete after a reasonable delay, to give tail sufficient time @@ -56,7 +56,7 @@ func TestMaxLineSize(_t *testing.T) { func TestLocationFull(_t *testing.T) { t := NewTailTest("location-full", _t) t.CreateFile("test.txt", "hello\nworld\n") - tail := t.StartTail("test.txt", Config{Follow: true, Location: -1}) + tail := t.StartTail("test.txt", Config{Follow: true, Location: nil}) go t.VerifyTailOutput(tail, []string{"hello", "world"}) // Delete after a reasonable delay, to give tail sufficient time @@ -69,7 +69,7 @@ func TestLocationFull(_t *testing.T) { func TestLocationFullDontFollow(_t *testing.T) { t := NewTailTest("location-full-dontfollow", _t) t.CreateFile("test.txt", "hello\nworld\n") - tail := t.StartTail("test.txt", Config{Follow: false, Location: -1}) + tail := t.StartTail("test.txt", Config{Follow: false, Location: nil}) go t.VerifyTailOutput(tail, []string{"hello", "world"}) // Add more data only after reasonable delay. @@ -83,7 +83,7 @@ func TestLocationFullDontFollow(_t *testing.T) { func TestLocationEnd(_t *testing.T) { t := NewTailTest("location-end", _t) t.CreateFile("test.txt", "hello\nworld\n") - tail := t.StartTail("test.txt", Config{Follow: true, Location: 0}) + tail := t.StartTail("test.txt", Config{Follow: true, Location: &SeekInfo{0, os.SEEK_END}}) go t.VerifyTailOutput(tail, []string{"more", "data"}) <-time.After(100 * time.Millisecond) @@ -96,6 +96,23 @@ func TestLocationEnd(_t *testing.T) { tail.Stop() } +func TestLocationMiddle(_t *testing.T) { + // Test reading from middle. + t := NewTailTest("location-end", _t) + t.CreateFile("test.txt", "hello\nworld\n") + tail := t.StartTail("test.txt", Config{Follow: true, Location: &SeekInfo{-6, os.SEEK_END}}) + go t.VerifyTailOutput(tail, []string{"world", "more", "data"}) + + <-time.After(100 * time.Millisecond) + t.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) + t.RemoveFile("test.txt") + tail.Stop() +} + func _TestReOpen(_t *testing.T, poll bool) { var name string if poll { @@ -107,7 +124,7 @@ func _TestReOpen(_t *testing.T, poll bool) { t.CreateFile("test.txt", "hello\nworld\n") tail := t.StartTail( "test.txt", - Config{Follow: true, ReOpen: true, Poll: poll, Location: -1}) + Config{Follow: true, ReOpen: true, Poll: poll}) go t.VerifyTailOutput(tail, []string{"hello", "world", "more", "data", "endofworld"}) @@ -157,7 +174,7 @@ func _TestReSeek(_t *testing.T, poll bool) { t.CreateFile("test.txt", "a really long string goes here\nhello\nworld\n") tail := t.StartTail( "test.txt", - Config{Follow: true, ReOpen: false, Poll: poll, Location: -1}) + Config{Follow: true, ReOpen: false, Poll: poll}) go t.VerifyTailOutput(tail, []string{ "a really long string goes here", "hello", "world", "h311o", "w0r1d", "endofworld"}) @@ -193,7 +210,6 @@ func TestRateLimiting(_t *testing.T) { t.CreateFile("test.txt", "hello\nworld\nagain\n") config := Config{ Follow: true, - Location: -1, LimitRate: 2} tail := t.StartTail("test.txt", config) // TODO: also verify that tail resumes after the cooloff period.