Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added --follow-retry/-F for docker logs #7663

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 8 additions & 4 deletions api/client/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -1760,10 +1760,11 @@ func (cli *DockerCli) CmdDiff(args ...string) error {

func (cli *DockerCli) CmdLogs(args ...string) error {
var (
cmd = cli.Subcmd("logs", "CONTAINER", "Fetch the logs of a container")
follow = cmd.Bool([]string{"f", "-follow"}, false, "Follow log output")
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)")
cmd = cli.Subcmd("logs", "CONTAINER", "Fetch the logs of a container")
follow = cmd.Bool([]string{"f", "-follow"}, false, "Follow log output")
followRetry = cmd.Bool([]string{"F", "-follow-retry"}, false, "Follow log output and retry to follow on container stop")
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 {
Expand Down Expand Up @@ -1797,6 +1798,9 @@ func (cli *DockerCli) CmdLogs(args ...string) error {
if *follow {
v.Set("follow", "1")
}
if *followRetry {
v.Set("follow_retry", "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)
Expand Down
1 change: 1 addition & 0 deletions api/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ func getContainersLogs(eng *engine.Engine, version version.Version, w http.Respo
return err
}
logsJob.Setenv("follow", r.Form.Get("follow"))
logsJob.Setenv("follow_retry", r.Form.Get("follow_retry"))
logsJob.Setenv("tail", r.Form.Get("tail"))
logsJob.Setenv("stdout", r.Form.Get("stdout"))
logsJob.Setenv("stderr", r.Form.Get("stderr"))
Expand Down
2 changes: 2 additions & 0 deletions daemon/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ func (daemon *Daemon) ContainerRm(job *engine.Job) engine.Status {
if err := daemon.Destroy(container); err != nil {
return job.Errorf("Cannot destroy container %s: %s", name, err)
}
container.stdout.Clean()
container.stderr.Clean()
container.LogEvent("destroy")

if removeVolume {
Expand Down
61 changes: 36 additions & 25 deletions daemon/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@ func (daemon *Daemon) ContainerLogs(job *engine.Job) engine.Status {
}

var (
name = job.Args[0]
stdout = job.GetenvBool("stdout")
stderr = job.GetenvBool("stderr")
tail = job.Getenv("tail")
follow = job.GetenvBool("follow")
times = job.GetenvBool("timestamps")
lines = -1
format string
name = job.Args[0]
stdout = job.GetenvBool("stdout")
stderr = job.GetenvBool("stderr")
tail = job.Getenv("tail")
follow = job.GetenvBool("follow")
follow_retry = job.GetenvBool("follow_retry")
times = job.GetenvBool("timestamps")
lines = -1
format string
)
if !(stdout || stderr) {
return job.Errorf("You must choose at least one stream")
Expand Down Expand Up @@ -111,23 +112,33 @@ func (daemon *Daemon) ContainerLogs(job *engine.Job) engine.Status {
}
}
}
if follow && container.IsRunning() {
errors := make(chan error, 2)
if stdout {
stdoutPipe := container.StdoutLogPipe()
go func() {
errors <- jsonlog.WriteLog(stdoutPipe, job.Stdout, format)
}()
}
if stderr {
stderrPipe := container.StderrLogPipe()
go func() {
errors <- jsonlog.WriteLog(stderrPipe, job.Stderr, format)
}()
}
err := <-errors
if err != nil {
log.Errorf("%s", err)
if (follow && container.IsRunning()) || follow_retry {
var stdoutPipe, stderrPipe io.ReadCloser
for {
errors := make(chan error, 2)
if stdout {
stdoutPipe = container.StdoutLogPipe()
go func() {
errors <- jsonlog.WriteLog(stdoutPipe, job.Stdout, format)
}()
}
if stderr {
stderrPipe = container.StderrLogPipe()
go func() {
errors <- jsonlog.WriteLog(stderrPipe, job.Stderr, format)
}()
}
err := <-errors
if err != nil {
log.Errorf("%s", err)
}
if follow_retry && daemon.Get(name) != nil {
// container still exists, recreate pipes
stdoutPipe.Close()
stderrPipe.Close()
continue
}
break
}
}
return engine.StatusOK
Expand Down
4 changes: 4 additions & 0 deletions docs/man/docker-logs.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ docker-logs - Fetch the logs of a container
# SYNOPSIS
**docker logs**
[**-f**|**--follow**[=*false*]]
[**-F**|**--follow-retry**[=*false*]]
[**-t**|**--timestamps**[=*false*]]
[**--tail**[=*"all"*]]
CONTAINER
Expand All @@ -25,6 +26,9 @@ then continue streaming new output from the container’s stdout and stderr.
**-f**, **--follow**=*true*|*false*
Follow log output. The default is *false*.

**-F**, **--follow-retry**=*true*|*false*
Follow log output and retry to follow on container stop. The default is *false*.

**-t**, **--timestamps**=*true*|*false*
Show timestamps. The default is *false*.

Expand Down
5 changes: 5 additions & 0 deletions docs/sources/reference/api/docker_remote_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ You can still call an old version of the API using

### What's new

`GET /containers/(id)/logs`

**New!**
New parameter `follow_retry` allows to follow logs until container is deleted.

## v1.14

### Full Documentation
Expand Down
4 changes: 3 additions & 1 deletion docs/sources/reference/api/docker_remote_api_v1.15.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ Get stdout and stderr logs from the container ``id``

**Example request**:

GET /containers/4fa6e0f0c678/logs?stderr=1&stdout=1&timestamps=1&follow=1&tail=10 HTTP/1.1
GET /containers/4fa6e0f0c678/logs?stderr=1&stdout=1&timestamps=1&follow=1&tail=10&follow_retry=1 HTTP/1.1

**Example response**:

Expand All @@ -322,6 +322,8 @@ Get stdout and stderr logs from the container ``id``


- **follow** – 1/True/true or 0/False/false, return stream. Default false
- **follow_retry** – 1/True/true or 0/False/false, return stream, which follows
until container destruction. Default false
- **stdout** – 1/True/true or 0/False/false, show stdout log. Default false
- **stderr** – 1/True/true or 0/False/false, show stderr log. Default false
- **timestamps** – 1/True/true or 0/False/false, print timestamps for
Expand Down
3 changes: 3 additions & 0 deletions docs/sources/reference/commandline/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,7 @@ For example:

Fetch the logs of a container

-F, --follow-retry=false Follow log output and retry to follow on container stop
-f, --follow=false Follow log output
-t, --timestamps=false Show timestamps
--tail="all" Output the specified number of lines at the end of logs (defaults to all logs)
Expand All @@ -762,6 +763,8 @@ The `docker logs --timestamp` commands will add an RFC3339Nano
timestamp, for example `2014-05-10T17:42:14.999999999Z07:00`, to each
log entry.

With `-F` logs will be followed until container will be destroyed.

## port

Usage: docker port CONTAINER [PRIVATE_PORT[/PROTO]]
Expand Down
72 changes: 72 additions & 0 deletions integration-cli/docker_cli_logs_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package main

import (
"bufio"
"fmt"
"io/ioutil"
"os/exec"
"regexp"
"strings"
Expand Down Expand Up @@ -245,3 +247,73 @@ func TestLogsFollowStopped(t *testing.T) {
deleteContainer(cleanedContainerID)
logDone("logs - logs follow stopped container")
}

func TestLogsFollowRetry(t *testing.T) {
msg := "follow_retry"
runCmd := exec.Command(dockerBinary, "run", "-d", "busybox", "sh", "-c", fmt.Sprintf("echo %s", msg))
out, _, _, err := runCommandWithStdoutStderr(runCmd)
errorOut(err, t, fmt.Sprintf("run failed with errors: %v", err))

cleanedContainerID := stripTrailingCharacters(out)
exec.Command(dockerBinary, "wait", cleanedContainerID).Run()

// TODO: this should be replaced with waitForContainer
time.Sleep(500 * time.Millisecond)

logsCmd := exec.Command(dockerBinary, "logs", "-F", cleanedContainerID)
stdout, err := logsCmd.StdoutPipe()
if err != nil {
t.Fatal(err)
}
stderr, err := logsCmd.StderrPipe()
if err != nil {
t.Fatal(err)
}
if err := logsCmd.Start(); err != nil {
t.Fatal(err)
}

w := make(chan struct{})
go func() {
s := bufio.NewScanner(stdout)
count := 0
for s.Scan() {
count++
if s.Text() != msg {
t.Fatalf("Log line should be %q, got %q", msg, s.Text())
}
}
if err := s.Err(); err != nil {
t.Fatal(err)
}
if count != 3 {
res, _ := ioutil.ReadAll(stderr)
t.Logf("%s", res)
t.Fatalf("Should be 3 lines in log, got %d", count)
}
if err := logsCmd.Wait(); err != nil {
t.Fatal(err)
}
close(w)
}()

if err := exec.Command(dockerBinary, "start", cleanedContainerID).Run(); err != nil {
t.Fatal(err)
}
exec.Command(dockerBinary, "wait", cleanedContainerID).Run()
if err := exec.Command(dockerBinary, "start", cleanedContainerID).Run(); err != nil {
t.Fatal(err)
}
exec.Command(dockerBinary, "wait", cleanedContainerID).Run()
if err := exec.Command(dockerBinary, "rm", cleanedContainerID).Run(); err != nil {
t.Fatal(err)
}

select {
case <-w:
case <-time.After(5 * time.Second):
t.Fatal("logs -F didn't exit after container remove")
}

logDone("logs - follow-retry")
}