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:
Alexandr Morozov 2014-06-03 15:09:33 +04:00 committed by LK4D4
parent 1d8231b230
commit bb7ecbd92c
2 changed files with 209 additions and 0 deletions

61
tailfile/tailfile.go Normal file
View 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
View 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)
}
}
}