Просмотр исходного кода

Merge pull request #26268 from AkihiroSuda/eventsjsonl

add `docker events --format`
Sebastiaan van Stijn 8 лет назад
Родитель
Сommit
bb6fe56e88

+ 43 - 11
cli/command/system/events.go

@@ -3,8 +3,10 @@ package system
 import (
 	"fmt"
 	"io"
+	"io/ioutil"
 	"sort"
 	"strings"
+	"text/template"
 	"time"
 
 	"golang.org/x/net/context"
@@ -15,6 +17,7 @@ import (
 	"github.com/docker/docker/cli/command"
 	"github.com/docker/docker/opts"
 	"github.com/docker/docker/pkg/jsonlog"
+	"github.com/docker/docker/utils/templates"
 	"github.com/spf13/cobra"
 )
 
@@ -22,6 +25,7 @@ type eventsOptions struct {
 	since  string
 	until  string
 	filter opts.FilterOpt
+	format string
 }
 
 // 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.until, "until", "", "Stream events until this timestamp")
 	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
 }
 
 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{
 		Since:   opts.since,
 		Until:   opts.until,
@@ -58,33 +69,48 @@ func runEvents(dockerCli *command.DockerCli, opts *eventsOptions) error {
 	}
 	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.
-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 {
 		if err != nil {
 			return err
 		}
-		printOutput(event, output)
-		return nil
+		if tmpl == nil {
+			return prettyPrintEvent(out, event)
+		}
+		return formatEvent(out, event, tmpl)
 	})
 }
 
 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.
 // 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 {
-		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 {
-		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 {
 		var attrs []string
@@ -97,7 +123,13 @@ func printOutput(event eventtypes.Message, output io.Writer) {
 			v := event.Actor.Attributes[k]
 			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)
 }

+ 1 - 1
contrib/completion/bash/docker

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

+ 1 - 0
contrib/completion/fish/docker.fish

@@ -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 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 format -d 'Format the output using the given go template'
 
 # exec
 complete -c docker -f -n '__fish_docker_no_subcommand' -a exec -d 'Run a command in a running container'

+ 2 - 1
contrib/completion/zsh/_docker

@@ -1660,7 +1660,8 @@ __docker_subcommand() {
                 $opts_help \
                 "($help)*"{-f=,--filter=}"[Filter values]:filter:__docker_complete_events_filter" \
                 "($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)
             local state

+ 30 - 0
docs/reference/commandline/events.md

@@ -17,6 +17,7 @@ Get real time events from the server
 
 Options:
   -f, --filter value   Filter output based on conditions provided (default [])
+      --format string  Format the output using the given go template
       --help           Print usage
       --since string   Show all events created since timestamp
       --until string   Stream events until this timestamp
@@ -85,6 +86,16 @@ The currently supported filters are:
 * network (`network=<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
 
 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)
     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)
+
+**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..

+ 46 - 0
integration-cli/docker_cli_events_test.go

@@ -2,7 +2,9 @@ package main
 
 import (
 	"bufio"
+	"encoding/json"
 	"fmt"
+	"io"
 	"io/ioutil"
 	"net/http"
 	"os"
@@ -11,6 +13,7 @@ import (
 	"sync"
 	"time"
 
+	eventtypes "github.com/docker/docker/api/types/events"
 	eventstestutils "github.com/docker/docker/daemon/events/testutils"
 	"github.com/docker/docker/pkg/integration/checker"
 	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.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",
+	})
+}

+ 29 - 0
man/docker-events.1.md

@@ -10,6 +10,7 @@ docker-events - Get real time events from the server
 [**-f**|**--filter**[=*[]*]]
 [**--since**[=*SINCE*]]
 [**--until**[=*UNTIL*]]
+[**--format**[=*FORMAT*]]
 
 
 # DESCRIPTION
@@ -45,6 +46,9 @@ Docker networks report the following events:
 **--until**=""
    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
 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,
@@ -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
 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
 April 2014, Originally compiled by William Henry (whenry at redhat dot com)
 based on docker.com source material and internal work.