Implement tail for docker logs
Fixes #4330 Docker-DCO-1.1-Signed-off-by: Alexandr Morozov <lk4d4math@gmail.com> (github: LK4D4)
This commit is contained in:
parent
1d8231b230
commit
bb7ecbd92c
2 changed files with 209 additions and 0 deletions
61
tailfile/tailfile.go
Normal file
61
tailfile/tailfile.go
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
package tailfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
const blockSize = 1024
|
||||||
|
|
||||||
|
var eol = []byte("\n")
|
||||||
|
var ErrNonPositiveLinesNumber = errors.New("Lines number must be positive")
|
||||||
|
|
||||||
|
//TailFile returns last n lines of file f
|
||||||
|
func TailFile(f *os.File, n int) ([][]byte, error) {
|
||||||
|
if n <= 0 {
|
||||||
|
return nil, ErrNonPositiveLinesNumber
|
||||||
|
}
|
||||||
|
size, err := f.Seek(0, os.SEEK_END)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
block := -1
|
||||||
|
var data []byte
|
||||||
|
var cnt int
|
||||||
|
for {
|
||||||
|
var b []byte
|
||||||
|
step := int64(block * blockSize)
|
||||||
|
left := size + step // how many bytes to beginning
|
||||||
|
if left < 0 {
|
||||||
|
if _, err := f.Seek(0, os.SEEK_SET); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
b = make([]byte, blockSize+left)
|
||||||
|
if _, err := f.Read(b); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data = append(b, data...)
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
b = make([]byte, blockSize)
|
||||||
|
if _, err := f.Seek(step, os.SEEK_END); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := f.Read(b); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data = append(b, data...)
|
||||||
|
}
|
||||||
|
cnt += bytes.Count(b, eol)
|
||||||
|
if cnt > n {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
block--
|
||||||
|
}
|
||||||
|
lines := bytes.Split(data, eol)
|
||||||
|
if n < len(lines) {
|
||||||
|
return lines[len(lines)-n-1 : len(lines)-1], nil
|
||||||
|
}
|
||||||
|
return lines[:len(lines)-1], nil
|
||||||
|
}
|
148
tailfile/tailfile_test.go
Normal file
148
tailfile/tailfile_test.go
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
package tailfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTailFile(t *testing.T) {
|
||||||
|
f, err := ioutil.TempFile("", "tail-test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
defer os.RemoveAll(f.Name())
|
||||||
|
testFile := []byte(`first line
|
||||||
|
second line
|
||||||
|
third line
|
||||||
|
fourth line
|
||||||
|
fifth line
|
||||||
|
next first line
|
||||||
|
next second line
|
||||||
|
next third line
|
||||||
|
next fourth line
|
||||||
|
next fifth line
|
||||||
|
last first line
|
||||||
|
next first line
|
||||||
|
next second line
|
||||||
|
next third line
|
||||||
|
next fourth line
|
||||||
|
next fifth line
|
||||||
|
next first line
|
||||||
|
next second line
|
||||||
|
next third line
|
||||||
|
next fourth line
|
||||||
|
next fifth line
|
||||||
|
last second line
|
||||||
|
last third line
|
||||||
|
last fourth line
|
||||||
|
last fifth line
|
||||||
|
truncated line`)
|
||||||
|
if _, err := f.Write(testFile); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := f.Seek(0, os.SEEK_SET); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
expected := []string{"last fourth line", "last fifth line"}
|
||||||
|
res, err := TailFile(f, 2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for i, l := range res {
|
||||||
|
t.Logf("%s", l)
|
||||||
|
if expected[i] != string(l) {
|
||||||
|
t.Fatalf("Expected line %s, got %s", expected[i], l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTailFileManyLines(t *testing.T) {
|
||||||
|
f, err := ioutil.TempFile("", "tail-test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
defer os.RemoveAll(f.Name())
|
||||||
|
testFile := []byte(`first line
|
||||||
|
second line
|
||||||
|
truncated line`)
|
||||||
|
if _, err := f.Write(testFile); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := f.Seek(0, os.SEEK_SET); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
expected := []string{"first line", "second line"}
|
||||||
|
res, err := TailFile(f, 10000)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for i, l := range res {
|
||||||
|
t.Logf("%s", l)
|
||||||
|
if expected[i] != string(l) {
|
||||||
|
t.Fatalf("Expected line %s, got %s", expected[i], l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTailEmptyFile(t *testing.T) {
|
||||||
|
f, err := ioutil.TempFile("", "tail-test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
defer os.RemoveAll(f.Name())
|
||||||
|
res, err := TailFile(f, 10000)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(res) != 0 {
|
||||||
|
t.Fatal("Must be empty slice from empty file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTailNegativeN(t *testing.T) {
|
||||||
|
f, err := ioutil.TempFile("", "tail-test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
defer os.RemoveAll(f.Name())
|
||||||
|
testFile := []byte(`first line
|
||||||
|
second line
|
||||||
|
truncated line`)
|
||||||
|
if _, err := f.Write(testFile); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := f.Seek(0, os.SEEK_SET); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := TailFile(f, -1); err != ErrNonPositiveLinesNumber {
|
||||||
|
t.Fatalf("Expected ErrNonPositiveLinesNumber, got %s", err)
|
||||||
|
}
|
||||||
|
if _, err := TailFile(f, 0); err != ErrNonPositiveLinesNumber {
|
||||||
|
t.Fatalf("Expected ErrNonPositiveLinesNumber, got %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkTail(b *testing.B) {
|
||||||
|
f, err := ioutil.TempFile("", "tail-test")
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
defer os.RemoveAll(f.Name())
|
||||||
|
for i := 0; i < 10000; i++ {
|
||||||
|
if _, err := f.Write([]byte("tailfile pretty interesting line\n")); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
if _, err := TailFile(f, 1000); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue