aboutsummaryrefslogtreecommitdiffstats
path: root/client/go/script-utils/logfmt/tail_unix.go
diff options
context:
space:
mode:
Diffstat (limited to 'client/go/script-utils/logfmt/tail_unix.go')
-rw-r--r--client/go/script-utils/logfmt/tail_unix.go170
1 files changed, 170 insertions, 0 deletions
diff --git a/client/go/script-utils/logfmt/tail_unix.go b/client/go/script-utils/logfmt/tail_unix.go
new file mode 100644
index 00000000000..7703844da48
--- /dev/null
+++ b/client/go/script-utils/logfmt/tail_unix.go
@@ -0,0 +1,170 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+// vespa logfmt command
+// Author: arnej
+
+//go:build !windows
+
+package logfmt
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "os"
+ "time"
+
+ "golang.org/x/sys/unix"
+)
+
+const lastLinesSize = 4 * 1024
+
+// an active "tail -f" like object
+
+type unixTail struct {
+ lines chan Line
+ lineBuf []byte
+ curFile *os.File
+ fn string
+ reader *bufio.Reader
+ curStat unix.Stat_t
+}
+
+func (t *unixTail) Lines() chan Line { return t.lines }
+
+// API for starting to follow a log file
+
+func FollowFile(fn string) (Tail, error) {
+ res := unixTail{}
+ res.fn = fn
+ res.lineBuf = make([]byte, lastLinesSize)
+ res.openTail()
+ res.lines = make(chan Line, 20)
+ res.lineBuf = res.lineBuf[:0]
+ go runTailWith(&res)
+ return &res, nil
+}
+
+func (t *unixTail) setFile(f *os.File) {
+ if t.curFile != nil {
+ t.curFile.Close()
+ }
+ t.curFile = f
+ if f != nil {
+ err := unix.Fstat(int(f.Fd()), &t.curStat)
+ if err != nil {
+ f.Close()
+ fmt.Fprintf(os.Stderr, "unexpected failure: %v\n", err)
+ return
+ }
+ t.reader = bufio.NewReaderSize(f, 1024*1024)
+ } else {
+ t.reader = nil
+ }
+}
+
+// open log file and seek to the start of a line near the end, if possible.
+func (t *unixTail) openTail() {
+ file, err := os.Open(t.fn)
+ if err != nil {
+ return
+ }
+ sz, err := file.Seek(0, os.SEEK_END)
+ if err != nil {
+ return
+ }
+ if sz < lastLinesSize {
+ sz, err = file.Seek(0, os.SEEK_SET)
+ if err == nil {
+ // just read from start of file, all OK
+ t.setFile(file)
+ }
+ return
+ }
+ sz, _ = file.Seek(-lastLinesSize, os.SEEK_END)
+ n, err := file.Read(t.lineBuf)
+ if err != nil {
+ return
+ }
+ for i := 0; i < n; i++ {
+ if t.lineBuf[i] == '\n' {
+ sz, err = file.Seek(sz+int64(i+1), os.SEEK_SET)
+ if err == nil {
+ t.setFile(file)
+ }
+ return
+ }
+ }
+}
+
+func (t *unixTail) reopen(cur *unix.Stat_t) {
+ for cnt := 0; cnt < 100; cnt++ {
+ file, err := os.Open(t.fn)
+ if err != nil {
+ t.setFile(nil)
+ if cnt == 0 {
+ fmt.Fprintf(os.Stderr, "%v (waiting for log file to appear)\n", err)
+ }
+ time.Sleep(1000 * time.Millisecond)
+ continue
+ }
+ var stat unix.Stat_t
+ err = unix.Fstat(int(file.Fd()), &stat)
+ if err != nil {
+ file.Close()
+ fmt.Fprintf(os.Stderr, "unexpected failure: %v\n", err)
+ time.Sleep(5000 * time.Millisecond)
+ continue
+ }
+ if cur != nil && cur.Dev == stat.Dev && cur.Ino == stat.Ino {
+ // same file, continue following it
+ file.Close()
+ return
+ }
+ // new file, start following it
+ t.setFile(file)
+ return
+ }
+}
+
+// runs as a goroutine
+func runTailWith(t *unixTail) {
+ defer t.setFile(nil)
+loop:
+ for {
+ for t.curFile == nil {
+ t.reopen(nil)
+ }
+ bytes, err := t.reader.ReadSlice('\n')
+ t.lineBuf = append(t.lineBuf, bytes...)
+ if err == bufio.ErrBufferFull {
+ continue
+ }
+ if err == nil {
+ ll := len(t.lineBuf) - 1
+ t.lines <- Line{Text: string(t.lineBuf[:ll])}
+ t.lineBuf = t.lineBuf[:0]
+ continue
+ }
+ if err == io.EOF {
+ pos, _ := t.curFile.Seek(0, os.SEEK_CUR)
+ for cnt := 0; cnt < 100; cnt++ {
+ time.Sleep(10 * time.Millisecond)
+ sz, _ := t.curFile.Seek(0, os.SEEK_END)
+ if sz != pos {
+ if sz < pos {
+ // truncation case
+ pos = 0
+ }
+ t.curFile.Seek(pos, os.SEEK_SET)
+ continue loop
+ }
+ }
+ // no change in file size, try reopening
+ t.reopen(&t.curStat)
+ } else {
+ fmt.Fprintf(os.Stderr, "error tailing '%s': %v\n", t.fn, err)
+ close(t.lines)
+ return
+ }
+ }
+}