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