Signed-off-by: Akihiro Suda <suda.akihiro@lab.ntt.co.jp>
| ... | ... |
@@ -3,8 +3,10 @@ package system |
| 3 | 3 |
import ( |
| 4 | 4 |
"fmt" |
| 5 | 5 |
"io" |
| 6 |
+ "io/ioutil" |
|
| 6 | 7 |
"sort" |
| 7 | 8 |
"strings" |
| 9 |
+ "text/template" |
|
| 8 | 10 |
"time" |
| 9 | 11 |
|
| 10 | 12 |
"golang.org/x/net/context" |
| ... | ... |
@@ -15,6 +17,7 @@ import ( |
| 15 | 15 |
"github.com/docker/docker/cli/command" |
| 16 | 16 |
"github.com/docker/docker/opts" |
| 17 | 17 |
"github.com/docker/docker/pkg/jsonlog" |
| 18 |
+ "github.com/docker/docker/utils/templates" |
|
| 18 | 19 |
"github.com/spf13/cobra" |
| 19 | 20 |
) |
| 20 | 21 |
|
| ... | ... |
@@ -22,6 +25,7 @@ type eventsOptions struct {
|
| 22 | 22 |
since string |
| 23 | 23 |
until string |
| 24 | 24 |
filter opts.FilterOpt |
| 25 |
+ format string |
|
| 25 | 26 |
} |
| 26 | 27 |
|
| 27 | 28 |
// NewEventsCommand creates a new cobra.Command for `docker events` |
| ... | ... |
@@ -41,11 +45,18 @@ func NewEventsCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 41 | 41 |
flags.StringVar(&opts.since, "since", "", "Show all events created since timestamp") |
| 42 | 42 |
flags.StringVar(&opts.until, "until", "", "Stream events until this timestamp") |
| 43 | 43 |
flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") |
| 44 |
+ flags.StringVar(&opts.format, "format", "", "Format the output using the given go template") |
|
| 44 | 45 |
|
| 45 | 46 |
return cmd |
| 46 | 47 |
} |
| 47 | 48 |
|
| 48 | 49 |
func runEvents(dockerCli *command.DockerCli, opts *eventsOptions) error {
|
| 50 |
+ tmpl, err := makeTemplate(opts.format) |
|
| 51 |
+ if err != nil {
|
|
| 52 |
+ return cli.StatusError{
|
|
| 53 |
+ StatusCode: 64, |
|
| 54 |
+ Status: "Error parsing format: " + err.Error()} |
|
| 55 |
+ } |
|
| 49 | 56 |
options := types.EventsOptions{
|
| 50 | 57 |
Since: opts.since, |
| 51 | 58 |
Until: opts.until, |
| ... | ... |
@@ -58,33 +69,48 @@ func runEvents(dockerCli *command.DockerCli, opts *eventsOptions) error {
|
| 58 | 58 |
} |
| 59 | 59 |
defer responseBody.Close() |
| 60 | 60 |
|
| 61 |
- return streamEvents(responseBody, dockerCli.Out()) |
|
| 61 |
+ return streamEvents(dockerCli.Out(), responseBody, tmpl) |
|
| 62 |
+} |
|
| 63 |
+ |
|
| 64 |
+func makeTemplate(format string) (*template.Template, error) {
|
|
| 65 |
+ if format == "" {
|
|
| 66 |
+ return nil, nil |
|
| 67 |
+ } |
|
| 68 |
+ tmpl, err := templates.Parse(format) |
|
| 69 |
+ if err != nil {
|
|
| 70 |
+ return tmpl, err |
|
| 71 |
+ } |
|
| 72 |
+ // we execute the template for an empty message, so as to validate |
|
| 73 |
+ // a bad template like "{{.badFieldString}}"
|
|
| 74 |
+ return tmpl, tmpl.Execute(ioutil.Discard, &eventtypes.Message{})
|
|
| 62 | 75 |
} |
| 63 | 76 |
|
| 64 | 77 |
// streamEvents decodes prints the incoming events in the provided output. |
| 65 |
-func streamEvents(input io.Reader, output io.Writer) error {
|
|
| 78 |
+func streamEvents(out io.Writer, input io.Reader, tmpl *template.Template) error {
|
|
| 66 | 79 |
return DecodeEvents(input, func(event eventtypes.Message, err error) error {
|
| 67 | 80 |
if err != nil {
|
| 68 | 81 |
return err |
| 69 | 82 |
} |
| 70 |
- printOutput(event, output) |
|
| 71 |
- return nil |
|
| 83 |
+ if tmpl == nil {
|
|
| 84 |
+ return prettyPrintEvent(out, event) |
|
| 85 |
+ } |
|
| 86 |
+ return formatEvent(out, event, tmpl) |
|
| 72 | 87 |
}) |
| 73 | 88 |
} |
| 74 | 89 |
|
| 75 | 90 |
type eventProcessor func(event eventtypes.Message, err error) error |
| 76 | 91 |
|
| 77 |
-// printOutput prints all types of event information. |
|
| 92 |
+// prettyPrintEvent prints all types of event information. |
|
| 78 | 93 |
// Each output includes the event type, actor id, name and action. |
| 79 | 94 |
// Actor attributes are printed at the end if the actor has any. |
| 80 |
-func printOutput(event eventtypes.Message, output io.Writer) {
|
|
| 95 |
+func prettyPrintEvent(out io.Writer, event eventtypes.Message) error {
|
|
| 81 | 96 |
if event.TimeNano != 0 {
|
| 82 |
- fmt.Fprintf(output, "%s ", time.Unix(0, event.TimeNano).Format(jsonlog.RFC3339NanoFixed)) |
|
| 97 |
+ fmt.Fprintf(out, "%s ", time.Unix(0, event.TimeNano).Format(jsonlog.RFC3339NanoFixed)) |
|
| 83 | 98 |
} else if event.Time != 0 {
|
| 84 |
- fmt.Fprintf(output, "%s ", time.Unix(event.Time, 0).Format(jsonlog.RFC3339NanoFixed)) |
|
| 99 |
+ fmt.Fprintf(out, "%s ", time.Unix(event.Time, 0).Format(jsonlog.RFC3339NanoFixed)) |
|
| 85 | 100 |
} |
| 86 | 101 |
|
| 87 |
- fmt.Fprintf(output, "%s %s %s", event.Type, event.Action, event.Actor.ID) |
|
| 102 |
+ fmt.Fprintf(out, "%s %s %s", event.Type, event.Action, event.Actor.ID) |
|
| 88 | 103 |
|
| 89 | 104 |
if len(event.Actor.Attributes) > 0 {
|
| 90 | 105 |
var attrs []string |
| ... | ... |
@@ -97,7 +123,13 @@ func printOutput(event eventtypes.Message, output io.Writer) {
|
| 97 | 97 |
v := event.Actor.Attributes[k] |
| 98 | 98 |
attrs = append(attrs, fmt.Sprintf("%s=%s", k, v))
|
| 99 | 99 |
} |
| 100 |
- fmt.Fprintf(output, " (%s)", strings.Join(attrs, ", ")) |
|
| 100 |
+ fmt.Fprintf(out, " (%s)", strings.Join(attrs, ", ")) |
|
| 101 | 101 |
} |
| 102 |
- fmt.Fprint(output, "\n") |
|
| 102 |
+ fmt.Fprint(out, "\n") |
|
| 103 |
+ return nil |
|
| 104 |
+} |
|
| 105 |
+ |
|
| 106 |
+func formatEvent(out io.Writer, event eventtypes.Message, tmpl *template.Template) error {
|
|
| 107 |
+ defer out.Write([]byte{'\n'})
|
|
| 108 |
+ return tmpl.Execute(out, event) |
|
| 103 | 109 |
} |
| ... | ... |
@@ -1162,7 +1162,7 @@ _docker_events() {
|
| 1162 | 1162 |
|
| 1163 | 1163 |
case "$cur" in |
| 1164 | 1164 |
-*) |
| 1165 |
- COMPREPLY=( $( compgen -W "--filter -f --help --since --until" -- "$cur" ) ) |
|
| 1165 |
+ COMPREPLY=( $( compgen -W "--filter -f --help --since --until --format" -- "$cur" ) ) |
|
| 1166 | 1166 |
;; |
| 1167 | 1167 |
esac |
| 1168 | 1168 |
} |
| ... | ... |
@@ -164,6 +164,7 @@ complete -c docker -A -f -n '__fish_seen_subcommand_from events' -s f -l filter |
| 164 | 164 |
complete -c docker -A -f -n '__fish_seen_subcommand_from events' -l help -d 'Print usage' |
| 165 | 165 |
complete -c docker -A -f -n '__fish_seen_subcommand_from events' -l since -d 'Show all events created since timestamp' |
| 166 | 166 |
complete -c docker -A -f -n '__fish_seen_subcommand_from events' -l until -d 'Stream events until this timestamp' |
| 167 |
+complete -c docker -A -f -n '__fish_seen_subcommand_from events' -l format -d 'Format the output using the given go template' |
|
| 167 | 168 |
|
| 168 | 169 |
# exec |
| 169 | 170 |
complete -c docker -f -n '__fish_docker_no_subcommand' -a exec -d 'Run a command in a running container' |
| ... | ... |
@@ -1660,7 +1660,8 @@ __docker_subcommand() {
|
| 1660 | 1660 |
$opts_help \ |
| 1661 | 1661 |
"($help)*"{-f=,--filter=}"[Filter values]:filter:__docker_complete_events_filter" \
|
| 1662 | 1662 |
"($help)--since=[Events created since this timestamp]:timestamp: " \ |
| 1663 |
- "($help)--until=[Events created until this timestamp]:timestamp: " && ret=0 |
|
| 1663 |
+ "($help)--until=[Events created until this timestamp]:timestamp: " \ |
|
| 1664 |
+ "($help)--format=[Format the output using the given go template]:template: " && ret=0 |
|
| 1664 | 1665 |
;; |
| 1665 | 1666 |
(exec) |
| 1666 | 1667 |
local state |
| ... | ... |
@@ -17,6 +17,7 @@ Get real time events from the server |
| 17 | 17 |
|
| 18 | 18 |
Options: |
| 19 | 19 |
-f, --filter value Filter output based on conditions provided (default []) |
| 20 |
+ --format string Format the output using the given go template |
|
| 20 | 21 |
--help Print usage |
| 21 | 22 |
--since string Show all events created since timestamp |
| 22 | 23 |
--until string Stream events until this timestamp |
| ... | ... |
@@ -85,6 +86,16 @@ The currently supported filters are: |
| 85 | 85 |
* network (`network=<name or id>`) |
| 86 | 86 |
* daemon (`daemon=<name or id>`) |
| 87 | 87 |
|
| 88 |
+## Format |
|
| 89 |
+ |
|
| 90 |
+If a format (`--format`) is specified, the given template will be executed |
|
| 91 |
+instead of the default |
|
| 92 |
+format. Go's [text/template](http://golang.org/pkg/text/template/) package |
|
| 93 |
+describes all the details of the format. |
|
| 94 |
+ |
|
| 95 |
+If a format is set to `{{json .}}`, the events are streamed as valid JSON
|
|
| 96 |
+Lines. For information about JSON Lines, please refer to http://jsonlines.org/ . |
|
| 97 |
+ |
|
| 88 | 98 |
## Examples |
| 89 | 99 |
|
| 90 | 100 |
You'll need two shells for this example. |
| ... | ... |
@@ -180,3 +191,22 @@ relative to the current time on the client machine: |
| 180 | 180 |
$ docker events --filter 'type=plugin' (experimental) |
| 181 | 181 |
2016-07-25T17:30:14.825557616Z plugin pull ec7b87f2ce84330fe076e666f17dfc049d2d7ae0b8190763de94e1f2d105993f (name=tiborvass/no-remove:latest) |
| 182 | 182 |
2016-07-25T17:30:14.888127370Z plugin enable ec7b87f2ce84330fe076e666f17dfc049d2d7ae0b8190763de94e1f2d105993f (name=tiborvass/no-remove:latest) |
| 183 |
+ |
|
| 184 |
+**Format:** |
|
| 185 |
+ |
|
| 186 |
+ $ docker events --filter 'type=container' --format 'Type={{.Type}} Status={{.Status}} ID={{.ID}}'
|
|
| 187 |
+ Type=container Status=create ID=2ee349dac409e97974ce8d01b70d250b85e0ba8189299c126a87812311951e26 |
|
| 188 |
+ Type=container Status=attach ID=2ee349dac409e97974ce8d01b70d250b85e0ba8189299c126a87812311951e26 |
|
| 189 |
+ Type=container Status=start ID=2ee349dac409e97974ce8d01b70d250b85e0ba8189299c126a87812311951e26 |
|
| 190 |
+ Type=container Status=resize ID=2ee349dac409e97974ce8d01b70d250b85e0ba8189299c126a87812311951e26 |
|
| 191 |
+ Type=container Status=die ID=2ee349dac409e97974ce8d01b70d250b85e0ba8189299c126a87812311951e26 |
|
| 192 |
+ Type=container Status=destroy ID=2ee349dac409e97974ce8d01b70d250b85e0ba8189299c126a87812311951e26 |
|
| 193 |
+ |
|
| 194 |
+**Format (as JSON Lines):** |
|
| 195 |
+ |
|
| 196 |
+ $ docker events --format '{{json .}}'
|
|
| 197 |
+ {"status":"create","id":"196016a57679bf42424484918746a9474cd905dd993c4d0f4..
|
|
| 198 |
+ {"status":"attach","id":"196016a57679bf42424484918746a9474cd905dd993c4d0f4..
|
|
| 199 |
+ {"Type":"network","Action":"connect","Actor":{"ID":"1b50a5bf755f6021dfa78e..
|
|
| 200 |
+ {"status":"start","id":"196016a57679bf42424484918746a9474cd905dd993c4d0f42..
|
|
| 201 |
+ {"status":"resize","id":"196016a57679bf42424484918746a9474cd905dd993c4d0f4..
|
| ... | ... |
@@ -2,7 +2,9 @@ package main |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"bufio" |
| 5 |
+ "encoding/json" |
|
| 5 | 6 |
"fmt" |
| 7 |
+ "io" |
|
| 6 | 8 |
"io/ioutil" |
| 7 | 9 |
"net/http" |
| 8 | 10 |
"os" |
| ... | ... |
@@ -11,6 +13,7 @@ import ( |
| 11 | 11 |
"sync" |
| 12 | 12 |
"time" |
| 13 | 13 |
|
| 14 |
+ eventtypes "github.com/docker/docker/api/types/events" |
|
| 14 | 15 |
eventstestutils "github.com/docker/docker/daemon/events/testutils" |
| 15 | 16 |
"github.com/docker/docker/pkg/integration/checker" |
| 16 | 17 |
icmd "github.com/docker/docker/pkg/integration/cmd" |
| ... | ... |
@@ -745,3 +748,46 @@ func (s *DockerSuite) TestEventsUntilInThePast(c *check.C) {
|
| 745 | 745 |
c.Assert(out, checker.Not(checker.Contains), "test-container2") |
| 746 | 746 |
c.Assert(out, checker.Contains, "test-container") |
| 747 | 747 |
} |
| 748 |
+ |
|
| 749 |
+func (s *DockerSuite) TestEventsFormat(c *check.C) {
|
|
| 750 |
+ since := daemonUnixTime(c) |
|
| 751 |
+ dockerCmd(c, "run", "--rm", "busybox", "true") |
|
| 752 |
+ dockerCmd(c, "run", "--rm", "busybox", "true") |
|
| 753 |
+ out, _ := dockerCmd(c, "events", "--since", since, "--until", daemonUnixTime(c), "--format", "{{json .}}")
|
|
| 754 |
+ dec := json.NewDecoder(strings.NewReader(out)) |
|
| 755 |
+ // make sure we got 2 start events |
|
| 756 |
+ startCount := 0 |
|
| 757 |
+ for {
|
|
| 758 |
+ var err error |
|
| 759 |
+ var ev eventtypes.Message |
|
| 760 |
+ if err = dec.Decode(&ev); err == io.EOF {
|
|
| 761 |
+ break |
|
| 762 |
+ } |
|
| 763 |
+ c.Assert(err, checker.IsNil) |
|
| 764 |
+ if ev.Status == "start" {
|
|
| 765 |
+ startCount++ |
|
| 766 |
+ } |
|
| 767 |
+ } |
|
| 768 |
+ |
|
| 769 |
+ c.Assert(startCount, checker.Equals, 2, check.Commentf("should have had 2 start events but had %d, out: %s", startCount, out))
|
|
| 770 |
+} |
|
| 771 |
+ |
|
| 772 |
+func (s *DockerSuite) TestEventsFormatBadFunc(c *check.C) {
|
|
| 773 |
+ // make sure it fails immediately, without receiving any event |
|
| 774 |
+ result := dockerCmdWithResult("events", "--format", "{{badFuncString .}}")
|
|
| 775 |
+ c.Assert(result, icmd.Matches, icmd.Expected{
|
|
| 776 |
+ Error: "exit status 64", |
|
| 777 |
+ ExitCode: 64, |
|
| 778 |
+ Err: "Error parsing format: template: :1: function \"badFuncString\" not defined", |
|
| 779 |
+ }) |
|
| 780 |
+} |
|
| 781 |
+ |
|
| 782 |
+func (s *DockerSuite) TestEventsFormatBadField(c *check.C) {
|
|
| 783 |
+ // make sure it fails immediately, without receiving any event |
|
| 784 |
+ result := dockerCmdWithResult("events", "--format", "{{.badFieldString}}")
|
|
| 785 |
+ c.Assert(result, icmd.Matches, icmd.Expected{
|
|
| 786 |
+ Error: "exit status 64", |
|
| 787 |
+ ExitCode: 64, |
|
| 788 |
+ Err: "Error parsing format: template: :1:2: executing \"\" at <.badFieldString>: can't evaluate field badFieldString in type *events.Message", |
|
| 789 |
+ }) |
|
| 790 |
+} |
| ... | ... |
@@ -10,6 +10,7 @@ docker-events - Get real time events from the server |
| 10 | 10 |
[**-f**|**--filter**[=*[]*]] |
| 11 | 11 |
[**--since**[=*SINCE*]] |
| 12 | 12 |
[**--until**[=*UNTIL*]] |
| 13 |
+[**--format**[=*FORMAT*]] |
|
| 13 | 14 |
|
| 14 | 15 |
|
| 15 | 16 |
# DESCRIPTION |
| ... | ... |
@@ -45,6 +46,9 @@ Docker networks report the following events: |
| 45 | 45 |
**--until**="" |
| 46 | 46 |
Stream events until this timestamp |
| 47 | 47 |
|
| 48 |
+**--format**="" |
|
| 49 |
+ Format the output using the given go template |
|
| 50 |
+ |
|
| 48 | 51 |
The `--since` and `--until` parameters can be Unix timestamps, date formatted |
| 49 | 52 |
timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed |
| 50 | 53 |
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: |
| 96 | 96 |
If you do not provide the --since option, the command returns only new and/or |
| 97 | 97 |
live events. |
| 98 | 98 |
|
| 99 |
+## Format |
|
| 100 |
+ |
|
| 101 |
+If a format (`--format`) is specified, the given template will be executed |
|
| 102 |
+instead of the default format. Go's **text/template** package describes all the |
|
| 103 |
+details of the format. |
|
| 104 |
+ |
|
| 105 |
+ # docker events --filter 'type=container' --format 'Type={{.Type}} Status={{.Status}} ID={{.ID}}'
|
|
| 106 |
+ Type=container Status=create ID=2ee349dac409e97974ce8d01b70d250b85e0ba8189299c126a87812311951e26 |
|
| 107 |
+ Type=container Status=attach ID=2ee349dac409e97974ce8d01b70d250b85e0ba8189299c126a87812311951e26 |
|
| 108 |
+ Type=container Status=start ID=2ee349dac409e97974ce8d01b70d250b85e0ba8189299c126a87812311951e26 |
|
| 109 |
+ Type=container Status=resize ID=2ee349dac409e97974ce8d01b70d250b85e0ba8189299c126a87812311951e26 |
|
| 110 |
+ Type=container Status=die ID=2ee349dac409e97974ce8d01b70d250b85e0ba8189299c126a87812311951e26 |
|
| 111 |
+ Type=container Status=destroy ID=2ee349dac409e97974ce8d01b70d250b85e0ba8189299c126a87812311951e26 |
|
| 112 |
+ |
|
| 113 |
+If a format is set to `{{json .}}`, the events are streamed as valid JSON
|
|
| 114 |
+Lines. For information about JSON Lines, please refer to http://jsonlines.org/ . |
|
| 115 |
+ |
|
| 116 |
+ # docker events --format '{{json .}}'
|
|
| 117 |
+ {"status":"create","id":"196016a57679bf42424484918746a9474cd905dd993c4d0f4..
|
|
| 118 |
+ {"status":"attach","id":"196016a57679bf42424484918746a9474cd905dd993c4d0f4..
|
|
| 119 |
+ {"Type":"network","Action":"connect","Actor":{"ID":"1b50a5bf755f6021dfa78e..
|
|
| 120 |
+ {"status":"start","id":"196016a57679bf42424484918746a9474cd905dd993c4d0f42..
|
|
| 121 |
+ {"status":"resize","id":"196016a57679bf42424484918746a9474cd905dd993c4d0f4..
|
|
| 122 |
+ |
|
| 123 |
+ |
|
| 99 | 124 |
# HISTORY |
| 100 | 125 |
April 2014, Originally compiled by William Henry (whenry at redhat dot com) |
| 101 | 126 |
based on docker.com source material and internal work. |