From 1e2bc8afe862a21c7d243ba5f151b9250355ca2a Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Fri, 12 Jul 2013 15:30:31 -0700 Subject: [PATCH 1/4] support arbitrary values for `Location` --- cmd/gotail/gotail.go | 2 +- tail.go | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/cmd/gotail/gotail.go b/cmd/gotail/gotail.go index 250af2a..99e6e97 100644 --- a/cmd/gotail/gotail.go +++ b/cmd/gotail/gotail.go @@ -11,7 +11,7 @@ import ( func args2config() tail.Config { config := tail.Config{Follow: true} - flag.IntVar(&config.Location, "n", 0, "tail from the last Nth location") + flag.Int64Var(&config.Location, "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") diff --git a/tail.go b/tail.go index 7c97ba5..29cd4ce 100644 --- a/tail.go +++ b/tail.go @@ -26,7 +26,7 @@ type Line struct { // Config is used to specify how a file must be tailed. type Config struct { - Location int // Tail from last N lines (tail -n) + Location int64 // 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 @@ -55,10 +55,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 +139,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 >= 0 { + // Seek relative to file end + _, err := tail.file.Seek(tail.Location, 2) if err != nil { tail.Killf("Seek error on %s: %s", tail.Filename, err) return From 8d9c6e4ce1a2ce56674a0e1d83a8154b578a6a72 Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Fri, 9 Aug 2013 14:42:51 -0700 Subject: [PATCH 2/4] allow seeking from beginning and end --- tail.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/tail.go b/tail.go index 29cd4ce..d38e540 100644 --- a/tail.go +++ b/tail.go @@ -26,7 +26,9 @@ type Line struct { // Config is used to specify how a file must be tailed. type Config struct { - Location int64 // Tail from last N lines (tail -n) + Location int64 // For positive location, tail after first N + // lines; for negative, tail from last N lines; + // for zero, tail from end. If -1, do not seek. 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 @@ -139,9 +141,16 @@ func (tail *Tail) tailFileSync() { } // Seek to requested location on first open of the file. - if tail.Location >= 0 { - // Seek relative to file end - _, err := tail.file.Seek(tail.Location, 2) + if tail.Location != -1 { + var err error + switch { + case tail.Location <= 0: + // Seek relative to file end + _, err = tail.file.Seek(tail.Location, os.SEEK_END) + default: + // Seek from file beginning + _, err = tail.file.Seek(tail.Location, os.SEEK_SET) + } if err != nil { tail.Killf("Seek error on %s: %s", tail.Filename, err) return From faf14146e7c408c4abca016eb6a3516276c65599 Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Fri, 9 Aug 2013 14:53:37 -0700 Subject: [PATCH 3/4] tail.Location is now the most customizable as it is now a tuple; (offset, whence) from os.Seek --- cmd/gotail/gotail.go | 13 +++++++++---- tail.go | 34 +++++++++++++++------------------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/cmd/gotail/gotail.go b/cmd/gotail/gotail.go index 99e6e97..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.Int64Var(&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 d38e540..91c2600 100644 --- a/tail.go +++ b/tail.go @@ -24,17 +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 int64 // For positive location, tail after first N - // lines; for negative, tail from last N lines; - // for zero, tail from end. If -1, do not seek. - 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. } @@ -141,16 +144,9 @@ func (tail *Tail) tailFileSync() { } // Seek to requested location on first open of the file. - if tail.Location != -1 { - var err error - switch { - case tail.Location <= 0: - // Seek relative to file end - _, err = tail.file.Seek(tail.Location, os.SEEK_END) - default: - // Seek from file beginning - _, err = tail.file.Seek(tail.Location, os.SEEK_SET) - } + 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 From 275e6442bdca9832c29052af67fcbb51925c0dd8 Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Fri, 9 Aug 2013 14:57:38 -0700 Subject: [PATCH 4/4] tests for the new Location spec --- tail_test.go | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) 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.