Skip to content

Commit 4472660

Browse files
kokesnxadm
andauthored
Config setting to only yield complete lines (#26)
* first draft of CompleteLines * tests * refactor * CompleteLines should return the last line if we don't follow * rename temp dirs to avoid races * Conditional allocation of strings.Builder * Fix tests by using the new cleanup() Co-authored-by: nxadm <[email protected]>
1 parent 6abd9f9 commit 4472660

File tree

2 files changed

+139
-6
lines changed

2 files changed

+139
-6
lines changed

tail.go

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,9 @@ type Config struct {
7777
Pipe bool // The file is a named pipe (mkfifo)
7878

7979
// Generic IO
80-
Follow bool // Continue looking for new lines (tail -f)
81-
MaxLineSize int // If non-zero, split longer lines into multiple lines
80+
Follow bool // Continue looking for new lines (tail -f)
81+
MaxLineSize int // If non-zero, split longer lines into multiple lines
82+
CompleteLines bool // Only return complete lines (that end with "\n" or EOF when Follow is false)
8283

8384
// Optionally, use a ratelimiter (e.g. created by the ratelimiter/NewLeakyBucket function)
8485
RateLimiter *ratelimiter.LeakyBucket
@@ -97,6 +98,8 @@ type Tail struct {
9798
reader *bufio.Reader
9899
lineNum int
99100

101+
lineBuf *strings.Builder
102+
100103
watcher watch.FileWatcher
101104
changes *watch.FileChanges
102105

@@ -128,6 +131,10 @@ func TailFile(filename string, config Config) (*Tail, error) {
128131
Config: config,
129132
}
130133

134+
if config.CompleteLines {
135+
t.lineBuf = new(strings.Builder)
136+
}
137+
131138
// when Logger was not specified in config, use default logger
132139
if t.Logger == nil {
133140
t.Logger = DefaultLogger
@@ -202,6 +209,9 @@ func (tail *Tail) closeFile() {
202209
}
203210

204211
func (tail *Tail) reopen() error {
212+
if tail.lineBuf != nil {
213+
tail.lineBuf.Reset()
214+
}
205215
tail.closeFile()
206216
tail.lineNum = 0
207217
for {
@@ -229,16 +239,32 @@ func (tail *Tail) readLine() (string, error) {
229239
tail.lk.Lock()
230240
line, err := tail.reader.ReadString('\n')
231241
tail.lk.Unlock()
232-
if err != nil {
242+
243+
newlineEnding := strings.HasSuffix(line, "\n")
244+
line = strings.TrimRight(line, "\n")
245+
246+
// if we don't have to handle incomplete lines, we can return the line as-is
247+
if !tail.Config.CompleteLines {
233248
// Note ReadString "returns the data read before the error" in
234249
// case of an error, including EOF, so we return it as is. The
235250
// caller is expected to process it if err is EOF.
236251
return line, err
237252
}
238253

239-
line = strings.TrimRight(line, "\n")
254+
if _, err := tail.lineBuf.WriteString(line); err != nil {
255+
return line, err
256+
}
240257

241-
return line, err
258+
if newlineEnding {
259+
line = tail.lineBuf.String()
260+
tail.lineBuf.Reset()
261+
return line, nil
262+
} else {
263+
if tail.Config.Follow {
264+
line = ""
265+
}
266+
return line, io.EOF
267+
}
242268
}
243269

244270
func (tail *Tail) tailFileSync() {

tail_test.go

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,113 @@ func TestInotify_WaitForCreateThenMove(t *testing.T) {
515515
tailTest.Cleanup(tail, false)
516516
}
517517

518+
func TestIncompleteLines(t *testing.T) {
519+
tailTest, cleanup := NewTailTest("incomplete-lines", t)
520+
defer cleanup()
521+
filename := "test.txt"
522+
config := Config{
523+
Follow: true,
524+
CompleteLines: true,
525+
}
526+
tail := tailTest.StartTail(filename, config)
527+
go func() {
528+
time.Sleep(100 * time.Millisecond)
529+
tailTest.CreateFile(filename, "hello world\n")
530+
time.Sleep(100 * time.Millisecond)
531+
// here we intentially write a partial line to see if `Tail` contains
532+
// information that it's incomplete
533+
tailTest.AppendFile(filename, "hello")
534+
time.Sleep(100 * time.Millisecond)
535+
tailTest.AppendFile(filename, " again\n")
536+
}()
537+
538+
lines := []string{"hello world", "hello again"}
539+
540+
tailTest.ReadLines(tail, lines, false)
541+
542+
tailTest.RemoveFile(filename)
543+
tail.Stop()
544+
tail.Cleanup()
545+
}
546+
547+
func TestIncompleteLongLines(t *testing.T) {
548+
tailTest, cleanup := NewTailTest("incomplete-lines-long", t)
549+
defer cleanup()
550+
filename := "test.txt"
551+
config := Config{
552+
Follow: true,
553+
MaxLineSize: 3,
554+
CompleteLines: true,
555+
}
556+
tail := tailTest.StartTail(filename, config)
557+
go func() {
558+
time.Sleep(100 * time.Millisecond)
559+
tailTest.CreateFile(filename, "hello world\n")
560+
time.Sleep(100 * time.Millisecond)
561+
tailTest.AppendFile(filename, "hello")
562+
time.Sleep(100 * time.Millisecond)
563+
tailTest.AppendFile(filename, "again\n")
564+
}()
565+
566+
lines := []string{"hel", "lo ", "wor", "ld", "hel", "loa", "gai", "n"}
567+
568+
tailTest.ReadLines(tail, lines, false)
569+
570+
tailTest.RemoveFile(filename)
571+
tail.Stop()
572+
tail.Cleanup()
573+
}
574+
575+
func TestIncompleteLinesWithReopens(t *testing.T) {
576+
tailTest, cleanup := NewTailTest("incomplete-lines-reopens", t)
577+
defer cleanup()
578+
filename := "test.txt"
579+
config := Config{
580+
Follow: true,
581+
CompleteLines: true,
582+
}
583+
tail := tailTest.StartTail(filename, config)
584+
go func() {
585+
time.Sleep(100 * time.Millisecond)
586+
tailTest.CreateFile(filename, "hello world\nhi")
587+
time.Sleep(100 * time.Millisecond)
588+
tailTest.TruncateFile(filename, "rewriting\n")
589+
}()
590+
591+
// not that the "hi" gets lost, because it was never a complete line
592+
lines := []string{"hello world", "rewriting"}
593+
594+
tailTest.ReadLines(tail, lines, false)
595+
596+
tailTest.RemoveFile(filename)
597+
tail.Stop()
598+
tail.Cleanup()
599+
}
600+
601+
func TestIncompleteLinesWithoutFollow(t *testing.T) {
602+
tailTest, cleanup := NewTailTest("incomplete-lines-no-follow", t)
603+
defer cleanup()
604+
filename := "test.txt"
605+
config := Config{
606+
Follow: false,
607+
CompleteLines: true,
608+
}
609+
tail := tailTest.StartTail(filename, config)
610+
go func() {
611+
time.Sleep(100 * time.Millisecond)
612+
// intentionally missing a newline at the end
613+
tailTest.CreateFile(filename, "foo\nbar\nbaz")
614+
}()
615+
616+
lines := []string{"foo", "bar", "baz"}
617+
618+
tailTest.VerifyTailOutput(tail, lines, true)
619+
620+
tailTest.RemoveFile(filename)
621+
tail.Stop()
622+
tail.Cleanup()
623+
}
624+
518625
func reSeek(t *testing.T, poll bool) {
519626
var name string
520627
if poll {
@@ -557,7 +664,7 @@ type TailTest struct {
557664
}
558665

559666
func NewTailTest(name string, t *testing.T) (TailTest, func()) {
560-
testdir, err := ioutil.TempDir("", "tail-test-" + name)
667+
testdir, err := ioutil.TempDir("", "tail-test-"+name)
561668
if err != nil {
562669
t.Fatal(err)
563670
}

0 commit comments

Comments
 (0)