add `docker events --format`

Signed-off-by: Akihiro Suda <suda.akihiro@lab.ntt.co.jp>
This commit is contained in:
Akihiro Suda 2016-09-02 07:40:06 +00:00
parent 3ae023cd22
commit 5af5a1be62
7 changed files with 152 additions and 13 deletions

View File

@ -3,8 +3,10 @@ package system
import ( import (
"fmt" "fmt"
"io" "io"
"io/ioutil"
"sort" "sort"
"strings" "strings"
"text/template"
"time" "time"
"golang.org/x/net/context" "golang.org/x/net/context"
@ -15,6 +17,7 @@ import (
"github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command"
"github.com/docker/docker/opts" "github.com/docker/docker/opts"
"github.com/docker/docker/pkg/jsonlog" "github.com/docker/docker/pkg/jsonlog"
"github.com/docker/docker/utils/templates"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -22,6 +25,7 @@ type eventsOptions struct {
since string since string
until string until string
filter opts.FilterOpt filter opts.FilterOpt
format string
} }
// NewEventsCommand creates a new cobra.Command for `docker events` // NewEventsCommand creates a new cobra.Command for `docker events`
@ -41,11 +45,18 @@ func NewEventsCommand(dockerCli *command.DockerCli) *cobra.Command {
flags.StringVar(&opts.since, "since", "", "Show all events created since timestamp") flags.StringVar(&opts.since, "since", "", "Show all events created since timestamp")
flags.StringVar(&opts.until, "until", "", "Stream events until this timestamp") flags.StringVar(&opts.until, "until", "", "Stream events until this timestamp")
flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
flags.StringVar(&opts.format, "format", "", "Format the output using the given go template")
return cmd return cmd
} }
func runEvents(dockerCli *command.DockerCli, opts *eventsOptions) error { func runEvents(dockerCli *command.DockerCli, opts *eventsOptions) error {
tmpl, err := makeTemplate(opts.format)
if err != nil {
return cli.StatusError{
StatusCode: 64,
Status: "Error parsing format: " + err.Error()}
}
options := types.EventsOptions{ options := types.EventsOptions{
Since: opts.since, Since: opts.since,
Until: opts.until, Until: opts.until,
@ -58,33 +69,48 @@ func runEvents(dockerCli *command.DockerCli, opts *eventsOptions) error {
} }
defer responseBody.Close() defer responseBody.Close()
return streamEvents(responseBody, dockerCli.Out()) return streamEvents(dockerCli.Out(), responseBody, tmpl)
}
func makeTemplate(format string) (*template.Template, error) {
if format == "" {
return nil, nil
}
tmpl, err := templates.Parse(format)
if err != nil {
return tmpl, err
}
// we execute the template for an empty message, so as to validate
// a bad template like "{{.badFieldString}}"
return tmpl, tmpl.Execute(ioutil.Discard, &eventtypes.Message{})
} }
// streamEvents decodes prints the incoming events in the provided output. // streamEvents decodes prints the incoming events in the provided output.
func streamEvents(input io.Reader, output io.Writer) error { func streamEvents(out io.Writer, input io.Reader, tmpl *template.Template) error {
return DecodeEvents(input, func(event eventtypes.Message, err error) error { return DecodeEvents(input, func(event eventtypes.Message, err error) error {
if err != nil { if err != nil {
return err return err
} }
printOutput(event, output) if tmpl == nil {
return nil return prettyPrintEvent(out, event)
}
return formatEvent(out, event, tmpl)
}) })
} }
type eventProcessor func(event eventtypes.Message, err error) error type eventProcessor func(event eventtypes.Message, err error) error
// printOutput prints all types of event information. // prettyPrintEvent prints all types of event information.
// Each output includes the event type, actor id, name and action. // Each output includes the event type, actor id, name and action.
// Actor attributes are printed at the end if the actor has any. // Actor attributes are printed at the end if the actor has any.
func printOutput(event eventtypes.Message, output io.Writer) { func prettyPrintEvent(out io.Writer, event eventtypes.Message) error {
if event.TimeNano != 0 { if event.TimeNano != 0 {
fmt.Fprintf(output, "%s ", time.Unix(0, event.TimeNano).Format(jsonlog.RFC3339NanoFixed)) fmt.Fprintf(out, "%s ", time.Unix(0, event.TimeNano).Format(jsonlog.RFC3339NanoFixed))
} else if event.Time != 0 { } else if event.Time != 0 {
fmt.Fprintf(output, "%s ", time.Unix(event.Time, 0).Format(jsonlog.RFC3339NanoFixed)) fmt.Fprintf(out, "%s ", time.Unix(event.Time, 0).Format(jsonlog.RFC3339NanoFixed))
} }
fmt.Fprintf(output, "%s %s %s", event.Type, event.Action, event.Actor.ID) fmt.Fprintf(out, "%s %s %s", event.Type, event.Action, event.Actor.ID)
if len(event.Actor.Attributes) > 0 { if len(event.Actor.Attributes) > 0 {
var attrs []string var attrs []string
@ -97,7 +123,13 @@ func printOutput(event eventtypes.Message, output io.Writer) {
v := event.Actor.Attributes[k] v := event.Actor.Attributes[k]
attrs = append(attrs, fmt.Sprintf("%s=%s", k, v)) attrs = append(attrs, fmt.Sprintf("%s=%s", k, v))
} }
fmt.Fprintf(output, " (%s)", strings.Join(attrs, ", ")) fmt.Fprintf(out, " (%s)", strings.Join(attrs, ", "))
} }
fmt.Fprint(output, "\n") fmt.Fprint(out, "\n")
return nil
}
func formatEvent(out io.Writer, event eventtypes.Message, tmpl *template.Template) error {
defer out.Write([]byte{'\n'})
return tmpl.Execute(out, event)
} }

View File

@ -1162,7 +1162,7 @@ _docker_events() {
case "$cur" in case "$cur" in
-*) -*)
COMPREPLY=( $( compgen -W "--filter -f --help --since --until" -- "$cur" ) ) COMPREPLY=( $( compgen -W "--filter -f --help --since --until --format" -- "$cur" ) )
;; ;;
esac esac
} }

View File

@ -164,6 +164,7 @@ complete -c docker -A -f -n '__fish_seen_subcommand_from events' -s f -l filter
complete -c docker -A -f -n '__fish_seen_subcommand_from events' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from events' -l help -d 'Print usage'
complete -c docker -A -f -n '__fish_seen_subcommand_from events' -l since -d 'Show all events created since timestamp' complete -c docker -A -f -n '__fish_seen_subcommand_from events' -l since -d 'Show all events created since timestamp'
complete -c docker -A -f -n '__fish_seen_subcommand_from events' -l until -d 'Stream events until this timestamp' complete -c docker -A -f -n '__fish_seen_subcommand_from events' -l until -d 'Stream events until this timestamp'
complete -c docker -A -f -n '__fish_seen_subcommand_from events' -l format -d 'Format the output using the given go template'
# exec # exec
complete -c docker -f -n '__fish_docker_no_subcommand' -a exec -d 'Run a command in a running container' complete -c docker -f -n '__fish_docker_no_subcommand' -a exec -d 'Run a command in a running container'

View File

@ -1660,7 +1660,8 @@ __docker_subcommand() {
$opts_help \ $opts_help \
"($help)*"{-f=,--filter=}"[Filter values]:filter:__docker_complete_events_filter" \ "($help)*"{-f=,--filter=}"[Filter values]:filter:__docker_complete_events_filter" \
"($help)--since=[Events created since this timestamp]:timestamp: " \ "($help)--since=[Events created since this timestamp]:timestamp: " \
"($help)--until=[Events created until this timestamp]:timestamp: " && ret=0 "($help)--until=[Events created until this timestamp]:timestamp: " \
"($help)--format=[Format the output using the given go template]:template: " && ret=0
;; ;;
(exec) (exec)
local state local state

View File

@ -17,6 +17,7 @@ Get real time events from the server
Options: Options:
-f, --filter value Filter output based on conditions provided (default []) -f, --filter value Filter output based on conditions provided (default [])
--format string Format the output using the given go template
--help Print usage --help Print usage
--since string Show all events created since timestamp --since string Show all events created since timestamp
--until string Stream events until this timestamp --until string Stream events until this timestamp
@ -85,6 +86,16 @@ The currently supported filters are:
* network (`network=<name or id>`) * network (`network=<name or id>`)
* daemon (`daemon=<name or id>`) * daemon (`daemon=<name or id>`)
## Format
If a format (`--format`) is specified, the given template will be executed
instead of the default
format. Go's [text/template](http://golang.org/pkg/text/template/) package
describes all the details of the format.
If a format is set to `{{json .}}`, the events are streamed as valid JSON
Lines. For information about JSON Lines, please refer to http://jsonlines.org/ .
## Examples ## Examples
You'll need two shells for this example. You'll need two shells for this example.
@ -180,3 +191,22 @@ relative to the current time on the client machine:
$ docker events --filter 'type=plugin' (experimental) $ docker events --filter 'type=plugin' (experimental)
2016-07-25T17:30:14.825557616Z plugin pull ec7b87f2ce84330fe076e666f17dfc049d2d7ae0b8190763de94e1f2d105993f (name=tiborvass/no-remove:latest) 2016-07-25T17:30:14.825557616Z plugin pull ec7b87f2ce84330fe076e666f17dfc049d2d7ae0b8190763de94e1f2d105993f (name=tiborvass/no-remove:latest)
2016-07-25T17:30:14.888127370Z plugin enable ec7b87f2ce84330fe076e666f17dfc049d2d7ae0b8190763de94e1f2d105993f (name=tiborvass/no-remove:latest) 2016-07-25T17:30:14.888127370Z plugin enable ec7b87f2ce84330fe076e666f17dfc049d2d7ae0b8190763de94e1f2d105993f (name=tiborvass/no-remove:latest)
**Format:**
$ docker events --filter 'type=container' --format 'Type={{.Type}} Status={{.Status}} ID={{.ID}}'
Type=container Status=create ID=2ee349dac409e97974ce8d01b70d250b85e0ba8189299c126a87812311951e26
Type=container Status=attach ID=2ee349dac409e97974ce8d01b70d250b85e0ba8189299c126a87812311951e26
Type=container Status=start ID=2ee349dac409e97974ce8d01b70d250b85e0ba8189299c126a87812311951e26
Type=container Status=resize ID=2ee349dac409e97974ce8d01b70d250b85e0ba8189299c126a87812311951e26
Type=container Status=die ID=2ee349dac409e97974ce8d01b70d250b85e0ba8189299c126a87812311951e26
Type=container Status=destroy ID=2ee349dac409e97974ce8d01b70d250b85e0ba8189299c126a87812311951e26
**Format (as JSON Lines):**
$ docker events --format '{{json .}}'
{"status":"create","id":"196016a57679bf42424484918746a9474cd905dd993c4d0f4..
{"status":"attach","id":"196016a57679bf42424484918746a9474cd905dd993c4d0f4..
{"Type":"network","Action":"connect","Actor":{"ID":"1b50a5bf755f6021dfa78e..
{"status":"start","id":"196016a57679bf42424484918746a9474cd905dd993c4d0f42..
{"status":"resize","id":"196016a57679bf42424484918746a9474cd905dd993c4d0f4..

View File

@ -2,7 +2,9 @@ package main
import ( import (
"bufio" "bufio"
"encoding/json"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"os" "os"
@ -11,6 +13,7 @@ import (
"sync" "sync"
"time" "time"
eventtypes "github.com/docker/docker/api/types/events"
eventstestutils "github.com/docker/docker/daemon/events/testutils" eventstestutils "github.com/docker/docker/daemon/events/testutils"
"github.com/docker/docker/pkg/integration/checker" "github.com/docker/docker/pkg/integration/checker"
icmd "github.com/docker/docker/pkg/integration/cmd" icmd "github.com/docker/docker/pkg/integration/cmd"
@ -745,3 +748,46 @@ func (s *DockerSuite) TestEventsUntilInThePast(c *check.C) {
c.Assert(out, checker.Not(checker.Contains), "test-container2") c.Assert(out, checker.Not(checker.Contains), "test-container2")
c.Assert(out, checker.Contains, "test-container") c.Assert(out, checker.Contains, "test-container")
} }
func (s *DockerSuite) TestEventsFormat(c *check.C) {
since := daemonUnixTime(c)
dockerCmd(c, "run", "--rm", "busybox", "true")
dockerCmd(c, "run", "--rm", "busybox", "true")
out, _ := dockerCmd(c, "events", "--since", since, "--until", daemonUnixTime(c), "--format", "{{json .}}")
dec := json.NewDecoder(strings.NewReader(out))
// make sure we got 2 start events
startCount := 0
for {
var err error
var ev eventtypes.Message
if err = dec.Decode(&ev); err == io.EOF {
break
}
c.Assert(err, checker.IsNil)
if ev.Status == "start" {
startCount++
}
}
c.Assert(startCount, checker.Equals, 2, check.Commentf("should have had 2 start events but had %d, out: %s", startCount, out))
}
func (s *DockerSuite) TestEventsFormatBadFunc(c *check.C) {
// make sure it fails immediately, without receiving any event
result := dockerCmdWithResult("events", "--format", "{{badFuncString .}}")
c.Assert(result, icmd.Matches, icmd.Expected{
Error: "exit status 64",
ExitCode: 64,
Err: "Error parsing format: template: :1: function \"badFuncString\" not defined",
})
}
func (s *DockerSuite) TestEventsFormatBadField(c *check.C) {
// make sure it fails immediately, without receiving any event
result := dockerCmdWithResult("events", "--format", "{{.badFieldString}}")
c.Assert(result, icmd.Matches, icmd.Expected{
Error: "exit status 64",
ExitCode: 64,
Err: "Error parsing format: template: :1:2: executing \"\" at <.badFieldString>: can't evaluate field badFieldString in type *events.Message",
})
}

View File

@ -10,6 +10,7 @@ docker-events - Get real time events from the server
[**-f**|**--filter**[=*[]*]] [**-f**|**--filter**[=*[]*]]
[**--since**[=*SINCE*]] [**--since**[=*SINCE*]]
[**--until**[=*UNTIL*]] [**--until**[=*UNTIL*]]
[**--format**[=*FORMAT*]]
# DESCRIPTION # DESCRIPTION
@ -45,6 +46,9 @@ Docker networks report the following events:
**--until**="" **--until**=""
Stream events until this timestamp Stream events until this timestamp
**--format**=""
Format the output using the given go template
The `--since` and `--until` parameters can be Unix timestamps, date formatted The `--since` and `--until` parameters can be Unix timestamps, date formatted
timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed
relative to the client machine's time. If you do not provide the `--since` option, relative to the client machine's time. If you do not provide the `--since` option,
@ -96,6 +100,31 @@ relative to the current time on the client machine:
If you do not provide the --since option, the command returns only new and/or If you do not provide the --since option, the command returns only new and/or
live events. live events.
## Format
If a format (`--format`) is specified, the given template will be executed
instead of the default format. Go's **text/template** package describes all the
details of the format.
# docker events --filter 'type=container' --format 'Type={{.Type}} Status={{.Status}} ID={{.ID}}'
Type=container Status=create ID=2ee349dac409e97974ce8d01b70d250b85e0ba8189299c126a87812311951e26
Type=container Status=attach ID=2ee349dac409e97974ce8d01b70d250b85e0ba8189299c126a87812311951e26
Type=container Status=start ID=2ee349dac409e97974ce8d01b70d250b85e0ba8189299c126a87812311951e26
Type=container Status=resize ID=2ee349dac409e97974ce8d01b70d250b85e0ba8189299c126a87812311951e26
Type=container Status=die ID=2ee349dac409e97974ce8d01b70d250b85e0ba8189299c126a87812311951e26
Type=container Status=destroy ID=2ee349dac409e97974ce8d01b70d250b85e0ba8189299c126a87812311951e26
If a format is set to `{{json .}}`, the events are streamed as valid JSON
Lines. For information about JSON Lines, please refer to http://jsonlines.org/ .
# docker events --format '{{json .}}'
{"status":"create","id":"196016a57679bf42424484918746a9474cd905dd993c4d0f4..
{"status":"attach","id":"196016a57679bf42424484918746a9474cd905dd993c4d0f4..
{"Type":"network","Action":"connect","Actor":{"ID":"1b50a5bf755f6021dfa78e..
{"status":"start","id":"196016a57679bf42424484918746a9474cd905dd993c4d0f42..
{"status":"resize","id":"196016a57679bf42424484918746a9474cd905dd993c4d0f4..
# HISTORY # HISTORY
April 2014, Originally compiled by William Henry (whenry at redhat dot com) 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.