Merge pull request #6803 from vieux/pr_6176

Implement tail for docker logs
This commit is contained in:
Victor Vieux 2014-07-01 14:59:37 -07:00
commit 49d49ac98f
8 changed files with 317 additions and 32 deletions

View File

@ -1693,6 +1693,7 @@ func (cli *DockerCli) CmdLogs(args ...string) error {
cmd = cli.Subcmd("logs", "CONTAINER", "Fetch the logs of a container") cmd = cli.Subcmd("logs", "CONTAINER", "Fetch the logs of a container")
follow = cmd.Bool([]string{"f", "-follow"}, false, "Follow log output") follow = cmd.Bool([]string{"f", "-follow"}, false, "Follow log output")
times = cmd.Bool([]string{"t", "-timestamps"}, false, "Show timestamps") times = cmd.Bool([]string{"t", "-timestamps"}, false, "Show timestamps")
tail = cmd.String([]string{"-tail"}, "all", "Output the specified number of lines at the end of logs (defaults to all logs)")
) )
if err := cmd.Parse(args); err != nil { if err := cmd.Parse(args); err != nil {
@ -1726,6 +1727,7 @@ func (cli *DockerCli) CmdLogs(args ...string) error {
if *follow { if *follow {
v.Set("follow", "1") v.Set("follow", "1")
} }
v.Set("tail", *tail)
return cli.streamHelper("GET", "/containers/"+name+"/logs?"+v.Encode(), env.GetSubEnv("Config").GetBool("Tty"), nil, cli.out, cli.err, nil) return cli.streamHelper("GET", "/containers/"+name+"/logs?"+v.Encode(), env.GetSubEnv("Config").GetBool("Tty"), nil, cli.out, cli.err, nil)
} }

View File

@ -378,6 +378,7 @@ func getContainersLogs(eng *engine.Engine, version version.Version, w http.Respo
return err return err
} }
logsJob.Setenv("follow", r.Form.Get("follow")) logsJob.Setenv("follow", r.Form.Get("follow"))
logsJob.Setenv("tail", r.Form.Get("tail"))
logsJob.Setenv("stdout", r.Form.Get("stdout")) logsJob.Setenv("stdout", r.Form.Get("stdout"))
logsJob.Setenv("stderr", r.Form.Get("stderr")) logsJob.Setenv("stderr", r.Form.Get("stderr"))
logsJob.Setenv("timestamps", r.Form.Get("timestamps")) logsJob.Setenv("timestamps", r.Form.Get("timestamps"))

View File

@ -306,7 +306,7 @@ Get stdout and stderr logs from the container ``id``
**Example request**: **Example request**:
GET /containers/4fa6e0f0c678/logs?stderr=1&stdout=1&timestamps=1&follow=1 HTTP/1.1 GET /containers/4fa6e0f0c678/logs?stderr=1&stdout=1&timestamps=1&follow=1&tail=10 HTTP/1.1
**Example response**: **Example response**:
@ -319,14 +319,12 @@ Get stdout and stderr logs from the container ``id``
   
- **follow** 1/True/true or 0/False/false, return stream. - **follow** 1/True/true or 0/False/false, return stream. Default false
Default false - **stdout** 1/True/true or 0/False/false, show stdout log. Default false
- **stdout** 1/True/true or 0/False/false, if logs=true, return - **stderr** 1/True/true or 0/False/false, show stderr log. Default false
stdout log. Default false - **timestamps** 1/True/true or 0/False/false, print timestamps for
- **stderr** 1/True/true or 0/False/false, if logs=true, return every log line. Default false
stderr log. Default false - **tail** Output specified number of lines at the end of logs: `all` or `<number>`. Default all
- **timestamps** 1/True/true or 0/False/false, if logs=true, print
timestamps for every log line. Default false
Status Codes: Status Codes:

View File

@ -738,13 +738,15 @@ specify this by adding the server name.
-f, --follow=false Follow log output -f, --follow=false Follow log output
-t, --timestamps=false Show timestamps -t, --timestamps=false Show timestamps
--tail="all" Output the specified number of lines at the end of logs (defaults to all logs)
The `docker logs` command batch-retrieves all logs The `docker logs` command batch-retrieves logs present at the time of execution.
present at the time of execution.
The ``docker logs --follow`` command will first return all logs from the The `docker logs --follow` command will continue streaming the new output from
beginning and then continue streaming new output from the container's `STDOUT` the container's `STDOUT` and `STDERR`.
and `STDERR`.
Passing a negative number or a non-integer to `--tail` is invalid and the
value is set to `all` in that case. This behavior may change in the future.
## port ## port

View File

@ -169,3 +169,47 @@ func TestLogsStderrInStdout(t *testing.T) {
logDone("logs - stderr in stdout (with pseudo-tty)") logDone("logs - stderr in stdout (with pseudo-tty)")
} }
func TestLogsTail(t *testing.T) {
testLen := 100
runCmd := exec.Command(dockerBinary, "run", "-d", "busybox", "sh", "-c", fmt.Sprintf("for i in $(seq 1 %d); do echo =; done;", testLen))
out, _, _, err := runCommandWithStdoutStderr(runCmd)
errorOut(err, t, fmt.Sprintf("run failed with errors: %v", err))
cleanedContainerID := stripTrailingCharacters(out)
exec.Command(dockerBinary, "wait", cleanedContainerID).Run()
logsCmd := exec.Command(dockerBinary, "logs", "--tail", "5", cleanedContainerID)
out, _, _, err = runCommandWithStdoutStderr(logsCmd)
errorOut(err, t, fmt.Sprintf("failed to log container: %v %v", out, err))
lines := strings.Split(out, "\n")
if len(lines) != 6 {
t.Fatalf("Expected log %d lines, received %d\n", 6, len(lines))
}
logsCmd = exec.Command(dockerBinary, "logs", "--tail", "all", cleanedContainerID)
out, _, _, err = runCommandWithStdoutStderr(logsCmd)
errorOut(err, t, fmt.Sprintf("failed to log container: %v %v", out, err))
lines = strings.Split(out, "\n")
if len(lines) != testLen+1 {
t.Fatalf("Expected log %d lines, received %d\n", testLen+1, len(lines))
}
logsCmd = exec.Command(dockerBinary, "logs", "--tail", "random", cleanedContainerID)
out, _, _, err = runCommandWithStdoutStderr(logsCmd)
errorOut(err, t, fmt.Sprintf("failed to log container: %v %v", out, err))
lines = strings.Split(out, "\n")
if len(lines) != testLen+1 {
t.Fatalf("Expected log %d lines, received %d\n", testLen+1, len(lines))
}
deleteContainer(cleanedContainerID)
logDone("logs - logs tail")
}

61
pkg/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
}

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)
}
}
}

View File

@ -22,6 +22,7 @@
package server package server
import ( import (
"bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@ -52,6 +53,7 @@ import (
"github.com/dotcloud/docker/image" "github.com/dotcloud/docker/image"
"github.com/dotcloud/docker/pkg/graphdb" "github.com/dotcloud/docker/pkg/graphdb"
"github.com/dotcloud/docker/pkg/signal" "github.com/dotcloud/docker/pkg/signal"
"github.com/dotcloud/docker/pkg/tailfile"
"github.com/dotcloud/docker/registry" "github.com/dotcloud/docker/registry"
"github.com/dotcloud/docker/runconfig" "github.com/dotcloud/docker/runconfig"
"github.com/dotcloud/docker/utils" "github.com/dotcloud/docker/utils"
@ -2153,8 +2155,10 @@ func (srv *Server) ContainerLogs(job *engine.Job) engine.Status {
name = job.Args[0] name = job.Args[0]
stdout = job.GetenvBool("stdout") stdout = job.GetenvBool("stdout")
stderr = job.GetenvBool("stderr") stderr = job.GetenvBool("stderr")
tail = job.Getenv("tail")
follow = job.GetenvBool("follow") follow = job.GetenvBool("follow")
times = job.GetenvBool("timestamps") times = job.GetenvBool("timestamps")
lines = -1
format string format string
) )
if !(stdout || stderr) { if !(stdout || stderr) {
@ -2163,6 +2167,9 @@ func (srv *Server) ContainerLogs(job *engine.Job) engine.Status {
if times { if times {
format = time.StampMilli format = time.StampMilli
} }
if tail == "" {
tail = "all"
}
container := srv.daemon.Get(name) container := srv.daemon.Get(name)
if container == nil { if container == nil {
return job.Errorf("No such container: %s", name) return job.Errorf("No such container: %s", name)
@ -2190,6 +2197,27 @@ func (srv *Server) ContainerLogs(job *engine.Job) engine.Status {
} else if err != nil { } else if err != nil {
utils.Errorf("Error reading logs (json): %s", err) utils.Errorf("Error reading logs (json): %s", err)
} else { } else {
if tail != "all" {
var err error
lines, err = strconv.Atoi(tail)
if err != nil {
utils.Errorf("Failed to parse tail %s, error: %v, show all logs", err)
lines = -1
}
}
if lines != 0 {
if lines > 0 {
f := cLog.(*os.File)
ls, err := tailfile.TailFile(f, lines)
if err != nil {
return job.Error(err)
}
tmp := bytes.NewBuffer([]byte{})
for _, l := range ls {
fmt.Fprintf(tmp, "%s\n", l)
}
cLog = tmp
}
dec := json.NewDecoder(cLog) dec := json.NewDecoder(cLog)
for { for {
l := &utils.JSONLog{} l := &utils.JSONLog{}
@ -2212,6 +2240,7 @@ func (srv *Server) ContainerLogs(job *engine.Job) engine.Status {
} }
} }
} }
}
if follow { if follow {
errors := make(chan error, 2) errors := make(chan error, 2)
if stdout { if stdout {