Merge pull request #12 from ActiveState/better_location
Ability to seek to anywhere in the file before tailing
This commit is contained in:
commit
fdc1517c8b
|
@ -9,9 +9,10 @@ import (
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
func args2config() tail.Config {
|
func args2config() (tail.Config, int64) {
|
||||||
config := tail.Config{Follow: true}
|
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.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.ReOpen, "F", false, "follow, and track file rename/rotation")
|
||||||
flag.BoolVar(&config.Poll, "p", false, "use polling, instead of inotify")
|
flag.BoolVar(&config.Poll, "p", false, "use polling, instead of inotify")
|
||||||
|
@ -19,16 +20,20 @@ func args2config() tail.Config {
|
||||||
if config.ReOpen {
|
if config.ReOpen {
|
||||||
config.Follow = true
|
config.Follow = true
|
||||||
}
|
}
|
||||||
return config
|
return config, n
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
config := args2config()
|
config, n := args2config()
|
||||||
if flag.NFlag() < 1 {
|
if flag.NFlag() < 1 {
|
||||||
fmt.Println("need one or more files as arguments")
|
fmt.Println("need one or more files as arguments")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if n != 0 {
|
||||||
|
config.Location = &tail.SeekInfo{-n, os.SEEK_END}
|
||||||
|
}
|
||||||
|
|
||||||
done := make(chan bool)
|
done := make(chan bool)
|
||||||
for _, filename := range flag.Args() {
|
for _, filename := range flag.Args() {
|
||||||
go tailFile(filename, config, done)
|
go tailFile(filename, config, done)
|
||||||
|
|
28
tail.go
28
tail.go
|
@ -24,15 +24,20 @@ type Line struct {
|
||||||
// log line itself.
|
// log line itself.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SeekInfo struct {
|
||||||
|
Offset int64
|
||||||
|
Whence int // os.SEEK_*
|
||||||
|
}
|
||||||
|
|
||||||
// Config is used to specify how a file must be tailed.
|
// Config is used to specify how a file must be tailed.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Location int // Tail from last N lines (tail -n)
|
Location *SeekInfo // Seek before tailing
|
||||||
Follow bool // Continue looking for new lines (tail -f)
|
Follow bool // Continue looking for new lines (tail -f)
|
||||||
ReOpen bool // Reopen recreated files (tail -F)
|
ReOpen bool // Reopen recreated files (tail -F)
|
||||||
MustExist bool // Fail early if the file does not exist
|
MustExist bool // Fail early if the file does not exist
|
||||||
Poll bool // Poll for file changes instead of using inotify
|
Poll bool // Poll for file changes instead of using inotify
|
||||||
MaxLineSize int // If non-zero, split longer lines into multiple lines
|
MaxLineSize int // If non-zero, split longer lines into multiple lines
|
||||||
LimitRate int64 // If non-zero, limit the rate of read log lines
|
LimitRate int64 // If non-zero, limit the rate of read log lines
|
||||||
// by this much per second.
|
// by this much per second.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,10 +60,6 @@ type Tail struct {
|
||||||
// invoke the `Wait` or `Err` method after finishing reading from the
|
// invoke the `Wait` or `Err` method after finishing reading from the
|
||||||
// `Lines` channel.
|
// `Lines` channel.
|
||||||
func TailFile(filename string, config Config) (*Tail, error) {
|
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 {
|
if config.ReOpen && !config.Follow {
|
||||||
panic("cannot set ReOpen without 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.
|
// Seek to requested location on first open of the file.
|
||||||
if tail.Location == 0 {
|
if tail.Location != nil {
|
||||||
_, err := tail.file.Seek(0, 2) // Seek to the file end
|
_, err := tail.file.Seek(tail.Location.Offset, tail.Location.Whence)
|
||||||
|
// log.Printf("Seeked %s - %+v\n", tail.Filename, tail.Location)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tail.Killf("Seek error on %s: %s", tail.Filename, err)
|
tail.Killf("Seek error on %s: %s", tail.Filename, err)
|
||||||
return
|
return
|
||||||
|
|
30
tail_test.go
30
tail_test.go
|
@ -43,7 +43,7 @@ func TestMustExist(t *testing.T) {
|
||||||
func TestMaxLineSize(_t *testing.T) {
|
func TestMaxLineSize(_t *testing.T) {
|
||||||
t := NewTailTest("maxlinesize", _t)
|
t := NewTailTest("maxlinesize", _t)
|
||||||
t.CreateFile("test.txt", "hello\nworld\nfin\nhe")
|
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"})
|
go t.VerifyTailOutput(tail, []string{"hel", "lo", "wor", "ld", "fin", "he"})
|
||||||
|
|
||||||
// Delete after a reasonable delay, to give tail sufficient time
|
// Delete after a reasonable delay, to give tail sufficient time
|
||||||
|
@ -56,7 +56,7 @@ func TestMaxLineSize(_t *testing.T) {
|
||||||
func TestLocationFull(_t *testing.T) {
|
func TestLocationFull(_t *testing.T) {
|
||||||
t := NewTailTest("location-full", _t)
|
t := NewTailTest("location-full", _t)
|
||||||
t.CreateFile("test.txt", "hello\nworld\n")
|
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"})
|
go t.VerifyTailOutput(tail, []string{"hello", "world"})
|
||||||
|
|
||||||
// Delete after a reasonable delay, to give tail sufficient time
|
// Delete after a reasonable delay, to give tail sufficient time
|
||||||
|
@ -69,7 +69,7 @@ func TestLocationFull(_t *testing.T) {
|
||||||
func TestLocationFullDontFollow(_t *testing.T) {
|
func TestLocationFullDontFollow(_t *testing.T) {
|
||||||
t := NewTailTest("location-full-dontfollow", _t)
|
t := NewTailTest("location-full-dontfollow", _t)
|
||||||
t.CreateFile("test.txt", "hello\nworld\n")
|
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"})
|
go t.VerifyTailOutput(tail, []string{"hello", "world"})
|
||||||
|
|
||||||
// Add more data only after reasonable delay.
|
// Add more data only after reasonable delay.
|
||||||
|
@ -83,7 +83,7 @@ func TestLocationFullDontFollow(_t *testing.T) {
|
||||||
func TestLocationEnd(_t *testing.T) {
|
func TestLocationEnd(_t *testing.T) {
|
||||||
t := NewTailTest("location-end", _t)
|
t := NewTailTest("location-end", _t)
|
||||||
t.CreateFile("test.txt", "hello\nworld\n")
|
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"})
|
go t.VerifyTailOutput(tail, []string{"more", "data"})
|
||||||
|
|
||||||
<-time.After(100 * time.Millisecond)
|
<-time.After(100 * time.Millisecond)
|
||||||
|
@ -96,6 +96,23 @@ func TestLocationEnd(_t *testing.T) {
|
||||||
tail.Stop()
|
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) {
|
func _TestReOpen(_t *testing.T, poll bool) {
|
||||||
var name string
|
var name string
|
||||||
if poll {
|
if poll {
|
||||||
|
@ -107,7 +124,7 @@ func _TestReOpen(_t *testing.T, poll bool) {
|
||||||
t.CreateFile("test.txt", "hello\nworld\n")
|
t.CreateFile("test.txt", "hello\nworld\n")
|
||||||
tail := t.StartTail(
|
tail := t.StartTail(
|
||||||
"test.txt",
|
"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"})
|
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")
|
t.CreateFile("test.txt", "a really long string goes here\nhello\nworld\n")
|
||||||
tail := t.StartTail(
|
tail := t.StartTail(
|
||||||
"test.txt",
|
"test.txt",
|
||||||
Config{Follow: true, ReOpen: false, Poll: poll, Location: -1})
|
Config{Follow: true, ReOpen: false, Poll: poll})
|
||||||
|
|
||||||
go t.VerifyTailOutput(tail, []string{
|
go t.VerifyTailOutput(tail, []string{
|
||||||
"a really long string goes here", "hello", "world", "h311o", "w0r1d", "endofworld"})
|
"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")
|
t.CreateFile("test.txt", "hello\nworld\nagain\n")
|
||||||
config := Config{
|
config := Config{
|
||||||
Follow: true,
|
Follow: true,
|
||||||
Location: -1,
|
|
||||||
LimitRate: 2}
|
LimitRate: 2}
|
||||||
tail := t.StartTail("test.txt", config)
|
tail := t.StartTail("test.txt", config)
|
||||||
// TODO: also verify that tail resumes after the cooloff period.
|
// TODO: also verify that tail resumes after the cooloff period.
|
||||||
|
|
Loading…
Reference in New Issue