diff --git a/api/client/commands.go b/api/client/commands.go index b2561104a7..c0ff64b284 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -1782,6 +1782,10 @@ func (cli *DockerCli) CmdEvents(args ...string) error { cmd := cli.Subcmd("events", "", "Get real time events from the server") since := cmd.String([]string{"#since", "-since"}, "", "Show all events created since timestamp") until := cmd.String([]string{"-until"}, "", "Stream events until this timestamp") + + flFilter := opts.NewListOpts(nil) + cmd.Var(&flFilter, []string{"f", "-filter"}, "Provide filter values (i.e. 'event=stop')") + if err := cmd.Parse(args); err != nil { return nil } @@ -1791,9 +1795,20 @@ func (cli *DockerCli) CmdEvents(args ...string) error { return nil } var ( - v = url.Values{} - loc = time.FixedZone(time.Now().Zone()) + v = url.Values{} + loc = time.FixedZone(time.Now().Zone()) + eventFilterArgs = filters.Args{} ) + + // Consolidate all filter flags, and sanity check them early. + // They'll get process in the daemon/server. + for _, f := range flFilter.GetAll() { + var err error + eventFilterArgs, err = filters.ParseFlag(f, eventFilterArgs) + if err != nil { + return err + } + } var setTime = func(key, value string) { format := timeutils.RFC3339NanoFixed if len(value) < len(format) { @@ -1811,6 +1826,13 @@ func (cli *DockerCli) CmdEvents(args ...string) error { if *until != "" { setTime("until", *until) } + if len(eventFilterArgs) > 0 { + filterJson, err := filters.ToParam(eventFilterArgs) + if err != nil { + return err + } + v.Set("filters", filterJson) + } if err := cli.stream("GET", "/events?"+v.Encode(), nil, cli.out, nil); err != nil { return err } diff --git a/api/server/server.go b/api/server/server.go index b3cf0603bb..3cdb9edf1e 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -315,6 +315,7 @@ func getEvents(eng *engine.Engine, version version.Version, w http.ResponseWrite streamJSON(job, w, true) job.Setenv("since", r.Form.Get("since")) job.Setenv("until", r.Form.Get("until")) + job.Setenv("filters", r.Form.Get("filters")) return job.Run() } diff --git a/docs/sources/reference/api/docker_remote_api_v1.16.md b/docs/sources/reference/api/docker_remote_api_v1.16.md index 11ba333801..cddcd1ff2c 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.16.md +++ b/docs/sources/reference/api/docker_remote_api_v1.16.md @@ -1388,6 +1388,7 @@ Query Parameters: - **since** – timestamp used for polling - **until** – timestamp used for polling +- **filters** – a json encoded value of the filters (a map[string][]string) to process on the event list. Status Codes: diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index b9c2945707..9868424b7e 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -614,7 +614,10 @@ For example: Usage: docker events [OPTIONS] Get real time events from the server - + -f, --filter=[] Provide filter values. Valid filters: + event= - event to filter + image= - image to filter + container= - container to filter --since="" Show all events created since timestamp --until="" Stream events until this timestamp @@ -626,6 +629,24 @@ and Docker images will report: untag, delete +#### Filtering + +The filtering flag (`-f` or `--filter`) format is of "key=value". If you would like to use +multiple filters, pass multiple flags (e.g., `--filter "foo=bar" --filter "bif=baz"`) + +Using the same filter multiple times will be handled as a *OR*; for example +`--filter container=588a23dac085 --filter container=a8f7720b8c22` will display events for +container 588a23dac085 *OR* container a8f7720b8c22 + +Using multiple filters will be handled as a *AND*; for example +`--filter container=588a23dac085 --filter event=start` will display events for container +container 588a23dac085 *AND* the event type is *start* + +Current filters: + * event + * image + * container + #### Examples You'll need two shells for this example. @@ -634,31 +655,64 @@ You'll need two shells for this example. $ sudo docker events -**Shell 2: Start and Stop a Container:** +**Shell 2: Start and Stop containers:** $ sudo docker start 4386fb97867d $ sudo docker stop 4386fb97867d + $ sudo docker stop 7805c1d35632 **Shell 1: (Again .. now showing events):** - 2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from 12de384bfb10) start - 2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from 12de384bfb10) die - 2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from 12de384bfb10) stop + 2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) start + 2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) die + 2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) stop + 2014-05-10T17:42:14.999999999Z07:00 7805c1d35632: (from redis:2.8) die + 2014-05-10T17:42:14.999999999Z07:00 7805c1d35632: (from redis:2.8) stop **Show events in the past from a specified time:** $ sudo docker events --since 1378216169 - 2014-03-10T17:42:14.999999999Z07:00 4386fb97867d: (from 12de384bfb10) die - 2014-03-10T17:42:14.999999999Z07:00 4386fb97867d: (from 12de384bfb10) stop + 2014-03-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) die + 2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) stop + 2014-05-10T17:42:14.999999999Z07:00 7805c1d35632: (from redis:2.8) die + 2014-03-10T17:42:14.999999999Z07:00 7805c1d35632: (from redis:2.8) stop $ sudo docker events --since '2013-09-03' - 2014-09-03T17:42:14.999999999Z07:00 4386fb97867d: (from 12de384bfb10) start - 2014-09-03T17:42:14.999999999Z07:00 4386fb97867d: (from 12de384bfb10) die - 2014-09-03T17:42:14.999999999Z07:00 4386fb97867d: (from 12de384bfb10) stop + 2014-09-03T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) start + 2014-09-03T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) die + 2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) stop + 2014-05-10T17:42:14.999999999Z07:00 7805c1d35632: (from redis:2.8) die + 2014-09-03T17:42:14.999999999Z07:00 7805c1d35632: (from redis:2.8) stop $ sudo docker events --since '2013-09-03 15:49:29 +0200 CEST' - 2014-09-03T15:49:29.999999999Z07:00 4386fb97867d: (from 12de384bfb10) die - 2014-09-03T15:49:29.999999999Z07:00 4386fb97867d: (from 12de384bfb10) stop + 2014-09-03T15:49:29.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) die + 2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) stop + 2014-05-10T17:42:14.999999999Z07:00 7805c1d35632: (from redis:2.8) die + 2014-09-03T15:49:29.999999999Z07:00 7805c1d35632: (from redis:2.8) stop + +**Filter events:** + + $ sudo docker events --filter 'event=stop' + 2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) stop + 2014-09-03T17:42:14.999999999Z07:00 7805c1d35632: (from redis:2.8) stop + + $ sudo docker events --filter 'image=ubuntu-1:14.04' + 2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) start + 2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) die + 2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) stop + + $ sudo docker events --filter 'container=7805c1d35632' + 2014-05-10T17:42:14.999999999Z07:00 7805c1d35632: (from redis:2.8) die + 2014-09-03T15:49:29.999999999Z07:00 7805c1d35632: (from redis:2.8) stop + + $ sudo docker events --filter 'container=7805c1d35632' --filter 'container=4386fb97867d' + 2014-09-03T15:49:29.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) die + 2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) stop + 2014-05-10T17:42:14.999999999Z07:00 7805c1d35632: (from redis:2.8) die + 2014-09-03T15:49:29.999999999Z07:00 7805c1d35632: (from redis:2.8) stop + + $ sudo docker events --filter 'container=7805c1d35632' --filter 'event=stop' + 2014-09-03T15:49:29.999999999Z07:00 7805c1d35632: (from redis:2.8) stop ## exec @@ -777,7 +831,7 @@ uses up the `VIRTUAL SIZE` listed only once. #### Filtering -The filtering flag (`-f` or `--filter`) format is of "key=value". If there are more +The filtering flag (`-f` or `--filter`) format is of "key=value". If there is more than one filter, then pass multiple flags (e.g., `--filter "foo=bar" --filter "bif=baz"`) Current filters: diff --git a/events/events.go b/events/events.go index 57a82cada0..0951f7099d 100644 --- a/events/events.go +++ b/events/events.go @@ -6,6 +6,7 @@ import ( "time" "github.com/docker/docker/engine" + "github.com/docker/docker/pkg/parsers/filters" "github.com/docker/docker/utils" ) @@ -48,6 +49,11 @@ func (e *Events) Get(job *engine.Job) engine.Status { timeout = time.NewTimer(time.Unix(until, 0).Sub(time.Now())) ) + eventFilters, err := filters.FromParam(job.Getenv("filters")) + if err != nil { + return job.Error(err) + } + // If no until, disable timeout if until == 0 { timeout.Stop() @@ -61,7 +67,7 @@ func (e *Events) Get(job *engine.Job) engine.Status { // Resend every event in the [since, until] time interval. if since != 0 { - if err := e.writeCurrent(job, since, until); err != nil { + if err := e.writeCurrent(job, since, until, eventFilters); err != nil { return job.Error(err) } } @@ -72,7 +78,7 @@ func (e *Events) Get(job *engine.Job) engine.Status { if !ok { return engine.StatusOK } - if err := writeEvent(job, event); err != nil { + if err := writeEvent(job, event, eventFilters); err != nil { return job.Error(err) } case <-timeout.C: @@ -97,7 +103,23 @@ func (e *Events) SubscribersCount(job *engine.Job) engine.Status { return engine.StatusOK } -func writeEvent(job *engine.Job, event *utils.JSONMessage) error { +func writeEvent(job *engine.Job, event *utils.JSONMessage, eventFilters filters.Args) error { + isFiltered := func(field string, filter []string) bool { + if len(filter) == 0 { + return false + } + for _, v := range filter { + if v == field { + return false + } + } + return true + } + + if isFiltered(event.Status, eventFilters["event"]) || isFiltered(event.From, eventFilters["image"]) || isFiltered(event.ID, eventFilters["container"]) { + return nil + } + // When sending an event JSON serialization errors are ignored, but all // other errors lead to the eviction of the listener. if b, err := json.Marshal(event); err == nil { @@ -108,11 +130,11 @@ func writeEvent(job *engine.Job, event *utils.JSONMessage) error { return nil } -func (e *Events) writeCurrent(job *engine.Job, since, until int64) error { +func (e *Events) writeCurrent(job *engine.Job, since, until int64, eventFilters filters.Args) error { e.mu.RLock() for _, event := range e.events { if event.Time >= since && (event.Time <= until || until == 0) { - if err := writeEvent(job, event); err != nil { + if err := writeEvent(job, event, eventFilters); err != nil { e.mu.RUnlock() return err } diff --git a/integration-cli/docker_cli_events_test.go b/integration-cli/docker_cli_events_test.go index 600a3fa72f..82c685f2a2 100644 --- a/integration-cli/docker_cli_events_test.go +++ b/integration-cli/docker_cli_events_test.go @@ -61,7 +61,7 @@ func TestEventsPause(t *testing.T) { t.Fatalf("event should be pause, not %#v", pauseEvent) } if unpauseEvent[len(unpauseEvent)-1] != "unpause" { - t.Fatalf("event should be pause, not %#v", unpauseEvent) + t.Fatalf("event should be unpause, not %#v", unpauseEvent) } waitCmd := exec.Command(dockerBinary, "wait", name) @@ -138,13 +138,13 @@ func TestEventsContainerEvents(t *testing.T) { t.Fatalf("event should be create, not %#v", createEvent) } if startEvent[len(startEvent)-1] != "start" { - t.Fatalf("event should be pause, not %#v", startEvent) + t.Fatalf("event should be start, not %#v", startEvent) } if dieEvent[len(dieEvent)-1] != "die" { - t.Fatalf("event should be pause, not %#v", dieEvent) + t.Fatalf("event should be die, not %#v", dieEvent) } if destroyEvent[len(destroyEvent)-1] != "destroy" { - t.Fatalf("event should be pause, not %#v", destroyEvent) + t.Fatalf("event should be destroy, not %#v", destroyEvent) } logDone("events - container create, start, die, destroy is logged") @@ -283,3 +283,57 @@ func TestEventsImageImport(t *testing.T) { logDone("events - image import is logged") } + +func TestEventsFilters(t *testing.T) { + since := time.Now().Unix() + cmd(t, "run", "--rm", "busybox", "true") + cmd(t, "run", "--rm", "busybox", "true") + eventsCmd := exec.Command(dockerBinary, "events", fmt.Sprintf("--since=%d", since), fmt.Sprintf("--until=%d", time.Now().Unix()), "--filter", "event=die") + out, exitCode, err := runCommandWithOutput(eventsCmd) + if exitCode != 0 || err != nil { + t.Fatalf("Failed to get events with exit code %d: %s", exitCode, err) + } + events := strings.Split(out, "\n") + events = events[:len(events)-1] + if len(events) != 2 { + t.Fatalf("Expected 2 events, got %d: %v", len(events), events) + } + dieEvent := strings.Fields(events[len(events)-1]) + if dieEvent[len(dieEvent)-1] != "die" { + t.Fatalf("event should be die, not %#v", dieEvent) + } + + dieEvent = strings.Fields(events[len(events)-2]) + if dieEvent[len(dieEvent)-1] != "die" { + t.Fatalf("event should be die, not %#v", dieEvent) + } + + eventsCmd = exec.Command(dockerBinary, "events", fmt.Sprintf("--since=%d", since), fmt.Sprintf("--until=%d", time.Now().Unix()), "--filter", "event=die", "--filter", "event=start") + out, exitCode, err = runCommandWithOutput(eventsCmd) + if exitCode != 0 || err != nil { + t.Fatalf("Failed to get events with exit code %d: %s", exitCode, err) + } + events = strings.Split(out, "\n") + events = events[:len(events)-1] + if len(events) != 4 { + t.Fatalf("Expected 4 events, got %d: %v", len(events), events) + } + startEvent := strings.Fields(events[len(events)-4]) + if startEvent[len(startEvent)-1] != "start" { + t.Fatalf("event should be start, not %#v", startEvent) + } + dieEvent = strings.Fields(events[len(events)-3]) + if dieEvent[len(dieEvent)-1] != "die" { + t.Fatalf("event should be die, not %#v", dieEvent) + } + startEvent = strings.Fields(events[len(events)-2]) + if startEvent[len(startEvent)-1] != "start" { + t.Fatalf("event should be start, not %#v", startEvent) + } + dieEvent = strings.Fields(events[len(events)-1]) + if dieEvent[len(dieEvent)-1] != "die" { + t.Fatalf("event should be die, not %#v", dieEvent) + } + + logDone("events - filters") +}