Add --since argument to docker logs cmd

Added --since argument to `docker logs` command. Accept unix
timestamps and shows logs only created after the specified date.

Default value is 0 and passing default value or not specifying
the value in the request causes parameter to be ignored (behavior
prior to this change).

Signed-off-by: Ahmet Alp Balkan <ahmetalpbalkan@gmail.com>
This commit is contained in:
Ahmet Alp Balkan 2015-04-14 04:36:12 +00:00
parent 340fd140e6
commit cb9a6b9aed
15 changed files with 154 additions and 25 deletions

View File

@ -2,8 +2,6 @@ package client
import ( import (
"net/url" "net/url"
"strconv"
"time"
"github.com/docker/docker/opts" "github.com/docker/docker/opts"
flag "github.com/docker/docker/pkg/mflag" flag "github.com/docker/docker/pkg/mflag"
@ -26,7 +24,6 @@ func (cli *DockerCli) CmdEvents(args ...string) error {
var ( var (
v = url.Values{} v = url.Values{}
loc = time.FixedZone(time.Now().Zone())
eventFilterArgs = filters.Args{} eventFilterArgs = filters.Args{}
) )
@ -39,22 +36,11 @@ func (cli *DockerCli) CmdEvents(args ...string) error {
return err return err
} }
} }
var setTime = func(key, value string) {
format := timeutils.RFC3339NanoFixed
if len(value) < len(format) {
format = format[:len(value)]
}
if t, err := time.ParseInLocation(format, value, loc); err == nil {
v.Set(key, strconv.FormatInt(t.Unix(), 10))
} else {
v.Set(key, value)
}
}
if *since != "" { if *since != "" {
setTime("since", *since) v.Set("since", timeutils.GetTimestamp(*since))
} }
if *until != "" { if *until != "" {
setTime("until", *until) v.Set("until", timeutils.GetTimestamp(*until))
} }
if len(eventFilterArgs) > 0 { if len(eventFilterArgs) > 0 {
filterJSON, err := filters.ToParam(eventFilterArgs) filterJSON, err := filters.ToParam(eventFilterArgs)

View File

@ -7,6 +7,7 @@ import (
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
flag "github.com/docker/docker/pkg/mflag" flag "github.com/docker/docker/pkg/mflag"
"github.com/docker/docker/pkg/timeutils"
) )
// CmdLogs fetches the logs of a given container. // CmdLogs fetches the logs of a given container.
@ -16,6 +17,7 @@ func (cli *DockerCli) CmdLogs(args ...string) error {
var ( var (
cmd = cli.Subcmd("logs", "CONTAINER", "Fetch the logs of a container", true) cmd = cli.Subcmd("logs", "CONTAINER", "Fetch the logs of a container", true)
follow = cmd.Bool([]string{"f", "-follow"}, false, "Follow log output") follow = cmd.Bool([]string{"f", "-follow"}, false, "Follow log output")
since = cmd.String([]string{"-since"}, "", "Show logs since timestamp")
times = cmd.Bool([]string{"t", "-timestamps"}, false, "Show timestamps") times = cmd.Bool([]string{"t", "-timestamps"}, false, "Show timestamps")
tail = cmd.String([]string{"-tail"}, "all", "Number of lines to show from the end of the logs") tail = cmd.String([]string{"-tail"}, "all", "Number of lines to show from the end of the logs")
) )
@ -43,6 +45,10 @@ func (cli *DockerCli) CmdLogs(args ...string) error {
v.Set("stdout", "1") v.Set("stdout", "1")
v.Set("stderr", "1") v.Set("stderr", "1")
if *since != "" {
v.Set("since", timeutils.GetTimestamp(*since))
}
if *times { if *times {
v.Set("timestamps", "1") v.Set("timestamps", "1")
} }

View File

@ -594,9 +594,19 @@ func (s *Server) getContainersLogs(version version.Version, w http.ResponseWrite
return fmt.Errorf("Bad parameters: you must choose at least one stream") return fmt.Errorf("Bad parameters: you must choose at least one stream")
} }
var since time.Time
if r.Form.Get("since") != "" {
s, err := strconv.ParseInt(r.Form.Get("since"), 10, 64)
if err != nil {
return err
}
since = time.Unix(s, 0)
}
logsConfig := &daemon.ContainerLogsConfig{ logsConfig := &daemon.ContainerLogsConfig{
Follow: boolValue(r, "follow"), Follow: boolValue(r, "follow"),
Timestamps: boolValue(r, "timestamps"), Timestamps: boolValue(r, "timestamps"),
Since: since,
Tail: r.Form.Get("tail"), Tail: r.Form.Get("tail"),
UseStdout: stdout, UseStdout: stdout,
UseStderr: stderr, UseStderr: stderr,

View File

@ -593,7 +593,7 @@ _docker_logs() {
case "$cur" in case "$cur" in
-*) -*)
COMPREPLY=( $( compgen -W "--follow -f --help --tail --timestamps -t" -- "$cur" ) ) COMPREPLY=( $( compgen -W "--follow -f --help --since --tail --timestamps -t" -- "$cur" ) )
;; ;;
*) *)
local counter=$(__docker_pos_first_nonflag '--tail') local counter=$(__docker_pos_first_nonflag '--tail')

View File

@ -233,6 +233,7 @@ complete -c docker -f -n '__fish_docker_no_subcommand' -a logs -d 'Fetch the log
complete -c docker -A -f -n '__fish_seen_subcommand_from logs' -s f -l follow -d 'Follow log output' complete -c docker -A -f -n '__fish_seen_subcommand_from logs' -s f -l follow -d 'Follow log output'
complete -c docker -A -f -n '__fish_seen_subcommand_from logs' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from logs' -l help -d 'Print usage'
complete -c docker -A -f -n '__fish_seen_subcommand_from logs' -s t -l timestamps -d 'Show timestamps' complete -c docker -A -f -n '__fish_seen_subcommand_from logs' -s t -l timestamps -d 'Show timestamps'
complete -c docker -A -f -n '__fish_seen_subcommand_from logs' -l since -d 'Show logs since timestamp'
complete -c docker -A -f -n '__fish_seen_subcommand_from logs' -l tail -d 'Output the specified number of lines at the end of logs (defaults to all logs)' complete -c docker -A -f -n '__fish_seen_subcommand_from logs' -l tail -d 'Output the specified number of lines at the end of logs (defaults to all logs)'
complete -c docker -A -f -n '__fish_seen_subcommand_from logs' -a '(__fish_print_docker_containers running)' -d "Container" complete -c docker -A -f -n '__fish_seen_subcommand_from logs' -a '(__fish_print_docker_containers running)' -d "Container"

View File

@ -305,6 +305,7 @@ __docker_subcommand () {
(logs) (logs)
_arguments \ _arguments \
{-f,--follow}'[Follow log output]' \ {-f,--follow}'[Follow log output]' \
'-s,--since[Show logs since timestamp]' \
{-t,--timestamps}'[Show timestamps]' \ {-t,--timestamps}'[Show timestamps]' \
'--tail=-[Output the last K lines]:lines:(1 10 20 50 all)' \ '--tail=-[Output the last K lines]:lines:(1 10 20 50 all)' \
'*:containers:__docker_containers' '*:containers:__docker_containers'

View File

@ -8,6 +8,7 @@ import (
"os" "os"
"strconv" "strconv"
"sync" "sync"
"time"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/docker/docker/pkg/jsonlog" "github.com/docker/docker/pkg/jsonlog"
@ -19,6 +20,7 @@ import (
type ContainerLogsConfig struct { type ContainerLogsConfig struct {
Follow, Timestamps bool Follow, Timestamps bool
Tail string Tail string
Since time.Time
UseStdout, UseStderr bool UseStdout, UseStderr bool
OutStream io.Writer OutStream io.Writer
} }
@ -88,6 +90,7 @@ func (daemon *Daemon) ContainerLogs(name string, config *ContainerLogsConfig) er
lines = -1 lines = -1
} }
} }
if lines != 0 { if lines != 0 {
if lines > 0 { if lines > 0 {
f := cLog.(*os.File) f := cLog.(*os.File)
@ -101,9 +104,11 @@ func (daemon *Daemon) ContainerLogs(name string, config *ContainerLogsConfig) er
} }
cLog = tmp cLog = tmp
} }
dec := json.NewDecoder(cLog) dec := json.NewDecoder(cLog)
l := &jsonlog.JSONLog{} l := &jsonlog.JSONLog{}
for { for {
l.Reset()
if err := dec.Decode(l); err == io.EOF { if err := dec.Decode(l); err == io.EOF {
break break
} else if err != nil { } else if err != nil {
@ -111,6 +116,9 @@ func (daemon *Daemon) ContainerLogs(name string, config *ContainerLogsConfig) er
break break
} }
logLine := l.Log logLine := l.Log
if !config.Since.IsZero() && l.Created.Before(config.Since) {
continue
}
if config.Timestamps { if config.Timestamps {
// format can be "" or time format, so here can't be error // format can be "" or time format, so here can't be error
logLine, _ = l.Format(format) logLine, _ = l.Format(format)
@ -121,7 +129,6 @@ func (daemon *Daemon) ContainerLogs(name string, config *ContainerLogsConfig) er
if l.Stream == "stderr" && config.UseStderr { if l.Stream == "stderr" && config.UseStderr {
io.WriteString(errStream, logLine) io.WriteString(errStream, logLine)
} }
l.Reset()
} }
} }
} }
@ -139,7 +146,7 @@ func (daemon *Daemon) ContainerLogs(name string, config *ContainerLogsConfig) er
stdoutPipe := container.StdoutLogPipe() stdoutPipe := container.StdoutLogPipe()
defer stdoutPipe.Close() defer stdoutPipe.Close()
go func() { go func() {
errors <- jsonlog.WriteLog(stdoutPipe, outStream, format) errors <- jsonlog.WriteLog(stdoutPipe, outStream, format, config.Since)
wg.Done() wg.Done()
}() }()
} }
@ -148,7 +155,7 @@ func (daemon *Daemon) ContainerLogs(name string, config *ContainerLogsConfig) er
stderrPipe := container.StderrLogPipe() stderrPipe := container.StderrLogPipe()
defer stderrPipe.Close() defer stderrPipe.Close()
go func() { go func() {
errors <- jsonlog.WriteLog(stderrPipe, errStream, format) errors <- jsonlog.WriteLog(stderrPipe, errStream, format, config.Since)
wg.Done() wg.Done()
}() }()
} }

View File

@ -8,6 +8,7 @@ docker-logs - Fetch the logs of a container
**docker logs** **docker logs**
[**-f**|**--follow**[=*false*]] [**-f**|**--follow**[=*false*]]
[**--help**] [**--help**]
[**--since**[=*SINCE*]]
[**-t**|**--timestamps**[=*false*]] [**-t**|**--timestamps**[=*false*]]
[**--tail**[=*"all"*]] [**--tail**[=*"all"*]]
CONTAINER CONTAINER
@ -31,6 +32,9 @@ then continue streaming new output from the containers stdout and stderr.
**-f**, **--follow**=*true*|*false* **-f**, **--follow**=*true*|*false*
Follow log output. The default is *false*. Follow log output. The default is *false*.
**--since**=""
Show logs since timestamp
**-t**, **--timestamps**=*true*|*false* **-t**, **--timestamps**=*true*|*false*
Show timestamps. The default is *false*. Show timestamps. The default is *false*.
@ -42,3 +46,4 @@ April 2014, Originally compiled by William Henry (whenry at redhat dot com)
based on docker.com source material and internal work. based on docker.com source material and internal work.
June 2014, updated by Sven Dowideit <SvenDowideit@home.org.au> June 2014, updated by Sven Dowideit <SvenDowideit@home.org.au>
July 2014, updated by Sven Dowideit <SvenDowideit@home.org.au> July 2014, updated by Sven Dowideit <SvenDowideit@home.org.au>
April 2015, updated by Ahmet Alp Balkan <ahmetalpbalkan@gmail.com>

View File

@ -52,6 +52,12 @@ You can still call an old version of the API using
You can now supply a `stream` bool to get only one set of stats and You can now supply a `stream` bool to get only one set of stats and
disconnect disconnect
`GET /containers(id)/logs`
**New!**
This endpoint now accepts a `since` timestamp parameter.
## v1.18 ## v1.18
### Full documentation ### Full documentation

View File

@ -477,7 +477,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&tail=10 HTTP/1.1 GET /containers/4fa6e0f0c678/logs?stderr=1&stdout=1&timestamps=1&follow=1&tail=10&since=1428990821 HTTP/1.1
**Example response**: **Example response**:
@ -493,6 +493,8 @@ Query Parameters:
- **follow** 1/True/true or 0/False/false, return stream. Default false - **follow** 1/True/true or 0/False/false, return stream. Default false
- **stdout** 1/True/true or 0/False/false, show stdout log. 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 - **stderr** 1/True/true or 0/False/false, show stderr log. Default false
- **since** UNIX timestamp (integer) to filter logs. Specifying a timestamp
will only output log-entries since that timestamp. Default: 0 (unfiltered)
- **timestamps** 1/True/true or 0/False/false, print timestamps for - **timestamps** 1/True/true or 0/False/false, print timestamps for
every log line. Default false every log line. Default false
- **tail** Output specified number of lines at the end of logs: `all` or `<number>`. Default all - **tail** Output specified number of lines at the end of logs: `all` or `<number>`. Default all

View File

@ -1616,6 +1616,7 @@ For example:
Fetch the logs of a container Fetch the logs of a container
-f, --follow=false Follow log output -f, --follow=false Follow log output
--since="" Show logs since timestamp
-t, --timestamps=false Show timestamps -t, --timestamps=false Show timestamps
--tail="all" Number of lines to show from the end of the logs --tail="all" Number of lines to show from the end of the logs
@ -1635,6 +1636,10 @@ timestamp, for example `2014-09-16T06:17:46.000000000Z`, to each
log entry. To ensure that the timestamps for are aligned the log entry. To ensure that the timestamps for are aligned the
nano-second part of the timestamp will be padded with zero when necessary. nano-second part of the timestamp will be padded with zero when necessary.
The `--since` option shows logs of a container generated only after
the given date, specified as RFC 3339 or UNIX timestamp. The `--since` option
can be combined with the `--follow` and `--tail` options.
## pause ## pause
Usage: docker pause CONTAINER [CONTAINER...] Usage: docker pause CONTAINER [CONTAINER...]

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"os/exec" "os/exec"
"regexp" "regexp"
"strconv"
"strings" "strings"
"time" "time"
@ -276,6 +277,80 @@ func (s *DockerSuite) TestLogsFollowStopped(c *check.C) {
deleteContainer(cleanedContainerID) deleteContainer(cleanedContainerID)
} }
func (s *DockerSuite) TestLogsSince(c *check.C) {
name := "testlogssince"
runCmd := exec.Command(dockerBinary, "run", "--name="+name, "busybox", "/bin/sh", "-c", `date +%s; for i in $(seq 1 5); do sleep 1; echo log$i; done`)
out, _, err := runCommandWithOutput(runCmd)
if err != nil {
c.Fatalf("run failed with errors: %s, %v", out, err)
}
outLines := strings.Split(out, "\n")
startUnix, _ := strconv.ParseInt(outLines[0], 10, 64)
since := startUnix + 3
logsCmd := exec.Command(dockerBinary, "logs", "-t", fmt.Sprintf("--since=%v", since), name)
out, _, err = runCommandWithOutput(logsCmd)
if err != nil {
c.Fatalf("failed to log container: %s, %v", out, err)
}
// Skip 2 seconds
unexpected := []string{"log1", "log2"}
for _, v := range unexpected {
if strings.Contains(out, v) {
c.Fatalf("unexpected log message returned=%v, since=%v\nout=%v", v, since, out)
}
}
// Test with default value specified and parameter omitted
expected := []string{"log1", "log2", "log3", "log4", "log5"}
for _, cmd := range []*exec.Cmd{
exec.Command(dockerBinary, "logs", "-t", name),
exec.Command(dockerBinary, "logs", "-t", "--since=0", name),
} {
out, _, err = runCommandWithOutput(cmd)
if err != nil {
c.Fatalf("failed to log container: %s, %v", out, err)
}
for _, v := range expected {
if !strings.Contains(out, v) {
c.Fatalf("'%v' does not contain=%v\nout=%s", cmd.Args, v, out)
}
}
}
}
func (s *DockerSuite) TestLogsSinceFutureFollow(c *check.C) {
runCmd := exec.Command(dockerBinary, "run", "-d", "busybox", "/bin/sh", "-c", `for i in $(seq 1 5); do date +%s; sleep 1; done`)
out, _, err := runCommandWithOutput(runCmd)
if err != nil {
c.Fatalf("run failed with errors: %s, %v", out, err)
}
cleanedContainerID := strings.TrimSpace(out)
now := daemonTime(c).Unix()
since := now + 2
logCmd := exec.Command(dockerBinary, "logs", "-f", fmt.Sprintf("--since=%v", since), cleanedContainerID)
out, _, err = runCommandWithOutput(logCmd)
if err != nil {
c.Fatalf("failed to log container: %s, %v", out, err)
}
lines := strings.Split(strings.TrimSpace(out), "\n")
if len(lines) == 0 {
c.Fatal("got no log lines")
}
for _, v := range lines {
ts, err := strconv.ParseInt(v, 10, 64)
if err != nil {
c.Fatalf("cannot parse timestamp output from log: '%v'\nout=%s", v, out)
}
if ts < since {
c.Fatalf("earlier log found. since=%v logdate=%v", since, ts)
}
}
}
// Regression test for #8832 // Regression test for #8832
func (s *DockerSuite) TestLogsFollowSlowStdoutConsumer(c *check.C) { func (s *DockerSuite) TestLogsFollowSlowStdoutConsumer(c *check.C) {
runCmd := exec.Command(dockerBinary, "run", "-d", "busybox", "/bin/sh", "-c", `usleep 200000;yes X | head -c 200000`) runCmd := exec.Command(dockerBinary, "run", "-d", "busybox", "/bin/sh", "-c", `usleep 200000;yes X | head -c 200000`)

View File

@ -32,16 +32,20 @@ func (jl *JSONLog) Reset() {
jl.Created = time.Time{} jl.Created = time.Time{}
} }
func WriteLog(src io.Reader, dst io.Writer, format string) error { func WriteLog(src io.Reader, dst io.Writer, format string, since time.Time) error {
dec := json.NewDecoder(src) dec := json.NewDecoder(src)
l := &JSONLog{} l := &JSONLog{}
for { for {
l.Reset()
if err := dec.Decode(l); err == io.EOF { if err := dec.Decode(l); err == io.EOF {
return nil return nil
} else if err != nil { } else if err != nil {
logrus.Printf("Error streaming logs: %s", err) logrus.Printf("Error streaming logs: %s", err)
return err return err
} }
if !since.IsZero() && l.Created.Before(since) {
continue
}
line, err := l.Format(format) line, err := l.Format(format)
if err != nil { if err != nil {
return err return err
@ -49,6 +53,5 @@ func WriteLog(src io.Reader, dst io.Writer, format string) error {
if _, err := io.WriteString(dst, line); err != nil { if _, err := io.WriteString(dst, line); err != nil {
return err return err
} }
l.Reset()
} }
} }

View File

@ -21,7 +21,7 @@ func TestWriteLog(t *testing.T) {
} }
w := bytes.NewBuffer(nil) w := bytes.NewBuffer(nil)
format := timeutils.RFC3339NanoFixed format := timeutils.RFC3339NanoFixed
if err := WriteLog(&buf, w, format); err != nil { if err := WriteLog(&buf, w, format, time.Time{}); err != nil {
t.Fatal(err) t.Fatal(err)
} }
res := w.String() res := w.String()
@ -52,7 +52,7 @@ func BenchmarkWriteLog(b *testing.B) {
b.SetBytes(int64(r.Len())) b.SetBytes(int64(r.Len()))
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
if err := WriteLog(r, w, format); err != nil { if err := WriteLog(r, w, format, time.Time{}); err != nil {
b.Fatal(err) b.Fatal(err)
} }
b.StopTimer() b.StopTimer()

22
pkg/timeutils/utils.go Normal file
View File

@ -0,0 +1,22 @@
package timeutils
import (
"strconv"
"time"
)
// GetTimestamp tries to parse given string as RFC3339 time
// or Unix timestamp, if successful returns a Unix timestamp
// as string otherwise returns value back.
func GetTimestamp(value string) string {
format := RFC3339NanoFixed
loc := time.FixedZone(time.Now().Zone())
if len(value) < len(format) {
format = format[:len(value)]
}
t, err := time.ParseInLocation(format, value, loc)
if err != nil {
return value
}
return strconv.FormatInt(t.Unix(), 10)
}