Signed-off-by: Victor Vieux <vieux@docker.com>
| ... | ... |
@@ -1760,6 +1760,10 @@ func (cli *DockerCli) CmdEvents(args ...string) error {
|
| 1760 | 1760 |
cmd := cli.Subcmd("events", "", "Get real time events from the server")
|
| 1761 | 1761 |
since := cmd.String([]string{"#since", "-since"}, "", "Show all events created since timestamp")
|
| 1762 | 1762 |
until := cmd.String([]string{"-until"}, "", "Stream events until this timestamp")
|
| 1763 |
+ |
|
| 1764 |
+ flFilter := opts.NewListOpts(nil) |
|
| 1765 |
+ cmd.Var(&flFilter, []string{"f", "-filter"}, "Provide filter values (i.e. 'event=stop')")
|
|
| 1766 |
+ |
|
| 1763 | 1767 |
if err := cmd.Parse(args); err != nil {
|
| 1764 | 1768 |
return nil |
| 1765 | 1769 |
} |
| ... | ... |
@@ -1769,9 +1773,20 @@ func (cli *DockerCli) CmdEvents(args ...string) error {
|
| 1769 | 1769 |
return nil |
| 1770 | 1770 |
} |
| 1771 | 1771 |
var ( |
| 1772 |
- v = url.Values{}
|
|
| 1773 |
- loc = time.FixedZone(time.Now().Zone()) |
|
| 1772 |
+ v = url.Values{}
|
|
| 1773 |
+ loc = time.FixedZone(time.Now().Zone()) |
|
| 1774 |
+ eventFilterArgs = filters.Args{}
|
|
| 1774 | 1775 |
) |
| 1776 |
+ |
|
| 1777 |
+ // Consolidate all filter flags, and sanity check them early. |
|
| 1778 |
+ // They'll get process in the daemon/server. |
|
| 1779 |
+ for _, f := range flFilter.GetAll() {
|
|
| 1780 |
+ var err error |
|
| 1781 |
+ eventFilterArgs, err = filters.ParseFlag(f, eventFilterArgs) |
|
| 1782 |
+ if err != nil {
|
|
| 1783 |
+ return err |
|
| 1784 |
+ } |
|
| 1785 |
+ } |
|
| 1775 | 1786 |
var setTime = func(key, value string) {
|
| 1776 | 1787 |
format := timeutils.RFC3339NanoFixed |
| 1777 | 1788 |
if len(value) < len(format) {
|
| ... | ... |
@@ -1789,6 +1804,13 @@ func (cli *DockerCli) CmdEvents(args ...string) error {
|
| 1789 | 1789 |
if *until != "" {
|
| 1790 | 1790 |
setTime("until", *until)
|
| 1791 | 1791 |
} |
| 1792 |
+ if len(eventFilterArgs) > 0 {
|
|
| 1793 |
+ filterJson, err := filters.ToParam(eventFilterArgs) |
|
| 1794 |
+ if err != nil {
|
|
| 1795 |
+ return err |
|
| 1796 |
+ } |
|
| 1797 |
+ v.Set("filters", filterJson)
|
|
| 1798 |
+ } |
|
| 1792 | 1799 |
if err := cli.stream("GET", "/events?"+v.Encode(), nil, cli.out, nil); err != nil {
|
| 1793 | 1800 |
return err |
| 1794 | 1801 |
} |
| ... | ... |
@@ -315,6 +315,7 @@ func getEvents(eng *engine.Engine, version version.Version, w http.ResponseWrite |
| 315 | 315 |
streamJSON(job, w, true) |
| 316 | 316 |
job.Setenv("since", r.Form.Get("since"))
|
| 317 | 317 |
job.Setenv("until", r.Form.Get("until"))
|
| 318 |
+ job.Setenv("filters", r.Form.Get("filters"))
|
|
| 318 | 319 |
return job.Run() |
| 319 | 320 |
} |
| 320 | 321 |
|
| ... | ... |
@@ -1386,6 +1386,7 @@ Query Parameters: |
| 1386 | 1386 |
|
| 1387 | 1387 |
- **since** – timestamp used for polling |
| 1388 | 1388 |
- **until** – timestamp used for polling |
| 1389 |
+- **filters** – a json encoded value of the filters (a map[string][]string) to process on the event list. |
|
| 1389 | 1390 |
|
| 1390 | 1391 |
Status Codes: |
| 1391 | 1392 |
|
| ... | ... |
@@ -610,7 +610,10 @@ For example: |
| 610 | 610 |
Usage: docker events [OPTIONS] |
| 611 | 611 |
|
| 612 | 612 |
Get real time events from the server |
| 613 |
- |
|
| 613 |
+ -f, --filter=[] Provide filter values. Valid filters: |
|
| 614 |
+ event=<string> - event to filter |
|
| 615 |
+ image=<string> - image to filter |
|
| 616 |
+ container=<string> - container to filter |
|
| 614 | 617 |
--since="" Show all events created since timestamp |
| 615 | 618 |
--until="" Stream events until this timestamp |
| 616 | 619 |
|
| ... | ... |
@@ -622,6 +625,24 @@ and Docker images will report: |
| 622 | 622 |
|
| 623 | 623 |
untag, delete |
| 624 | 624 |
|
| 625 |
+#### Filtering |
|
| 626 |
+ |
|
| 627 |
+The filtering flag (`-f` or `--filter`) format is of "key=value". If you would like to use |
|
| 628 |
+multiple filters, pass multiple flags (e.g., `--filter "foo=bar" --filter "bif=baz"`) |
|
| 629 |
+ |
|
| 630 |
+Using the same filter multiple times will be handled as a `OR`; for example |
|
| 631 |
+`--filter container=588a23dac085 --filter container=a8f7720b8c22` will display events for |
|
| 632 |
+container 588a23dac085 `OR` container a8f7720b8c22 |
|
| 633 |
+ |
|
| 634 |
+Using multiple filters will be handled as a `AND`; for example |
|
| 635 |
+`--filter container=588a23dac085 --filter event=start` will display events for container |
|
| 636 |
+container 588a23dac085 `AND` only when the event type is `start` |
|
| 637 |
+ |
|
| 638 |
+Current filters: |
|
| 639 |
+ * event |
|
| 640 |
+ * image |
|
| 641 |
+ * container |
|
| 642 |
+ |
|
| 625 | 643 |
#### Examples |
| 626 | 644 |
|
| 627 | 645 |
You'll need two shells for this example. |
| ... | ... |
@@ -630,31 +651,64 @@ You'll need two shells for this example. |
| 630 | 630 |
|
| 631 | 631 |
$ sudo docker events |
| 632 | 632 |
|
| 633 |
-**Shell 2: Start and Stop a Container:** |
|
| 633 |
+**Shell 2: Start and Stop containers:** |
|
| 634 | 634 |
|
| 635 | 635 |
$ sudo docker start 4386fb97867d |
| 636 | 636 |
$ sudo docker stop 4386fb97867d |
| 637 |
+ $ sudo docker stop 7805c1d35632 |
|
| 637 | 638 |
|
| 638 | 639 |
**Shell 1: (Again .. now showing events):** |
| 639 | 640 |
|
| 640 |
- 2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from 12de384bfb10) start |
|
| 641 |
- 2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from 12de384bfb10) die |
|
| 642 |
- 2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from 12de384bfb10) stop |
|
| 641 |
+ 2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) start |
|
| 642 |
+ 2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) die |
|
| 643 |
+ 2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) stop |
|
| 644 |
+ 2014-05-10T17:42:14.999999999Z07:00 7805c1d35632: (from redis:2.8) die |
|
| 645 |
+ 2014-05-10T17:42:14.999999999Z07:00 7805c1d35632: (from redis:2.8) stop |
|
| 643 | 646 |
|
| 644 | 647 |
**Show events in the past from a specified time:** |
| 645 | 648 |
|
| 646 | 649 |
$ sudo docker events --since 1378216169 |
| 647 |
- 2014-03-10T17:42:14.999999999Z07:00 4386fb97867d: (from 12de384bfb10) die |
|
| 648 |
- 2014-03-10T17:42:14.999999999Z07:00 4386fb97867d: (from 12de384bfb10) stop |
|
| 650 |
+ 2014-03-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) die |
|
| 651 |
+ 2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) stop |
|
| 652 |
+ 2014-05-10T17:42:14.999999999Z07:00 7805c1d35632: (from redis:2.8) die |
|
| 653 |
+ 2014-03-10T17:42:14.999999999Z07:00 7805c1d35632: (from redis:2.8) stop |
|
| 649 | 654 |
|
| 650 | 655 |
$ sudo docker events --since '2013-09-03' |
| 651 |
- 2014-09-03T17:42:14.999999999Z07:00 4386fb97867d: (from 12de384bfb10) start |
|
| 652 |
- 2014-09-03T17:42:14.999999999Z07:00 4386fb97867d: (from 12de384bfb10) die |
|
| 653 |
- 2014-09-03T17:42:14.999999999Z07:00 4386fb97867d: (from 12de384bfb10) stop |
|
| 656 |
+ 2014-09-03T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) start |
|
| 657 |
+ 2014-09-03T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) die |
|
| 658 |
+ 2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) stop |
|
| 659 |
+ 2014-05-10T17:42:14.999999999Z07:00 7805c1d35632: (from redis:2.8) die |
|
| 660 |
+ 2014-09-03T17:42:14.999999999Z07:00 7805c1d35632: (from redis:2.8) stop |
|
| 654 | 661 |
|
| 655 | 662 |
$ sudo docker events --since '2013-09-03 15:49:29 +0200 CEST' |
| 656 |
- 2014-09-03T15:49:29.999999999Z07:00 4386fb97867d: (from 12de384bfb10) die |
|
| 657 |
- 2014-09-03T15:49:29.999999999Z07:00 4386fb97867d: (from 12de384bfb10) stop |
|
| 663 |
+ 2014-09-03T15:49:29.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) die |
|
| 664 |
+ 2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) stop |
|
| 665 |
+ 2014-05-10T17:42:14.999999999Z07:00 7805c1d35632: (from redis:2.8) die |
|
| 666 |
+ 2014-09-03T15:49:29.999999999Z07:00 7805c1d35632: (from redis:2.8) stop |
|
| 667 |
+ |
|
| 668 |
+**Filter events:** |
|
| 669 |
+ |
|
| 670 |
+ $ sudo docker events --filter 'event=stop' |
|
| 671 |
+ 2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) stop |
|
| 672 |
+ 2014-09-03T17:42:14.999999999Z07:00 7805c1d35632: (from redis:2.8) stop |
|
| 673 |
+ |
|
| 674 |
+ $ sudo docker events --filter 'image=ubuntu-1:14.04' |
|
| 675 |
+ 2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) start |
|
| 676 |
+ 2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) die |
|
| 677 |
+ 2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) stop |
|
| 678 |
+ |
|
| 679 |
+ $ sudo docker events --filter 'container=7805c1d35632' |
|
| 680 |
+ 2014-05-10T17:42:14.999999999Z07:00 7805c1d35632: (from redis:2.8) die |
|
| 681 |
+ 2014-09-03T15:49:29.999999999Z07:00 7805c1d35632: (from redis:2.8) stop |
|
| 682 |
+ |
|
| 683 |
+ $ sudo docker events --filter 'container=7805c1d35632' --filter 'container=4386fb97867d' |
|
| 684 |
+ 2014-09-03T15:49:29.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) die |
|
| 685 |
+ 2014-05-10T17:42:14.999999999Z07:00 4386fb97867d: (from ubuntu-1:14.04) stop |
|
| 686 |
+ 2014-05-10T17:42:14.999999999Z07:00 7805c1d35632: (from redis:2.8) die |
|
| 687 |
+ 2014-09-03T15:49:29.999999999Z07:00 7805c1d35632: (from redis:2.8) stop |
|
| 688 |
+ |
|
| 689 |
+ $ sudo docker events --filter 'container=7805c1d35632' --filter 'event=stop' |
|
| 690 |
+ 2014-09-03T15:49:29.999999999Z07:00 7805c1d35632: (from redis:2.8) stop |
|
| 658 | 691 |
|
| 659 | 692 |
## exec |
| 660 | 693 |
|
| ... | ... |
@@ -768,7 +822,7 @@ by default. |
| 768 | 768 |
|
| 769 | 769 |
#### Filtering |
| 770 | 770 |
|
| 771 |
-The filtering flag (`-f` or `--filter`) format is of "key=value". If there are more |
|
| 771 |
+The filtering flag (`-f` or `--filter`) format is of "key=value". If there is more |
|
| 772 | 772 |
than one filter, then pass multiple flags (e.g., `--filter "foo=bar" --filter "bif=baz"`) |
| 773 | 773 |
|
| 774 | 774 |
Current filters: |
| ... | ... |
@@ -6,6 +6,7 @@ import ( |
| 6 | 6 |
"time" |
| 7 | 7 |
|
| 8 | 8 |
"github.com/docker/docker/engine" |
| 9 |
+ "github.com/docker/docker/pkg/parsers/filters" |
|
| 9 | 10 |
"github.com/docker/docker/utils" |
| 10 | 11 |
) |
| 11 | 12 |
|
| ... | ... |
@@ -48,6 +49,11 @@ func (e *Events) Get(job *engine.Job) engine.Status {
|
| 48 | 48 |
timeout = time.NewTimer(time.Unix(until, 0).Sub(time.Now())) |
| 49 | 49 |
) |
| 50 | 50 |
|
| 51 |
+ eventFilters, err := filters.FromParam(job.Getenv("filters"))
|
|
| 52 |
+ if err != nil {
|
|
| 53 |
+ return job.Error(err) |
|
| 54 |
+ } |
|
| 55 |
+ |
|
| 51 | 56 |
// If no until, disable timeout |
| 52 | 57 |
if until == 0 {
|
| 53 | 58 |
timeout.Stop() |
| ... | ... |
@@ -61,7 +67,7 @@ func (e *Events) Get(job *engine.Job) engine.Status {
|
| 61 | 61 |
|
| 62 | 62 |
// Resend every event in the [since, until] time interval. |
| 63 | 63 |
if since != 0 {
|
| 64 |
- if err := e.writeCurrent(job, since, until); err != nil {
|
|
| 64 |
+ if err := e.writeCurrent(job, since, until, eventFilters); err != nil {
|
|
| 65 | 65 |
return job.Error(err) |
| 66 | 66 |
} |
| 67 | 67 |
} |
| ... | ... |
@@ -72,7 +78,7 @@ func (e *Events) Get(job *engine.Job) engine.Status {
|
| 72 | 72 |
if !ok {
|
| 73 | 73 |
return engine.StatusOK |
| 74 | 74 |
} |
| 75 |
- if err := writeEvent(job, event); err != nil {
|
|
| 75 |
+ if err := writeEvent(job, event, eventFilters); err != nil {
|
|
| 76 | 76 |
return job.Error(err) |
| 77 | 77 |
} |
| 78 | 78 |
case <-timeout.C: |
| ... | ... |
@@ -97,7 +103,23 @@ func (e *Events) SubscribersCount(job *engine.Job) engine.Status {
|
| 97 | 97 |
return engine.StatusOK |
| 98 | 98 |
} |
| 99 | 99 |
|
| 100 |
-func writeEvent(job *engine.Job, event *utils.JSONMessage) error {
|
|
| 100 |
+func writeEvent(job *engine.Job, event *utils.JSONMessage, eventFilters filters.Args) error {
|
|
| 101 |
+ isFiltered := func(field string, filter []string) bool {
|
|
| 102 |
+ if len(filter) == 0 {
|
|
| 103 |
+ return false |
|
| 104 |
+ } |
|
| 105 |
+ for _, v := range filter {
|
|
| 106 |
+ if v == field {
|
|
| 107 |
+ return false |
|
| 108 |
+ } |
|
| 109 |
+ } |
|
| 110 |
+ return true |
|
| 111 |
+ } |
|
| 112 |
+ |
|
| 113 |
+ if isFiltered(event.Status, eventFilters["event"]) || isFiltered(event.From, eventFilters["image"]) || isFiltered(event.ID, eventFilters["container"]) {
|
|
| 114 |
+ return nil |
|
| 115 |
+ } |
|
| 116 |
+ |
|
| 101 | 117 |
// When sending an event JSON serialization errors are ignored, but all |
| 102 | 118 |
// other errors lead to the eviction of the listener. |
| 103 | 119 |
if b, err := json.Marshal(event); err == nil {
|
| ... | ... |
@@ -108,11 +130,11 @@ func writeEvent(job *engine.Job, event *utils.JSONMessage) error {
|
| 108 | 108 |
return nil |
| 109 | 109 |
} |
| 110 | 110 |
|
| 111 |
-func (e *Events) writeCurrent(job *engine.Job, since, until int64) error {
|
|
| 111 |
+func (e *Events) writeCurrent(job *engine.Job, since, until int64, eventFilters filters.Args) error {
|
|
| 112 | 112 |
e.mu.RLock() |
| 113 | 113 |
for _, event := range e.events {
|
| 114 | 114 |
if event.Time >= since && (event.Time <= until || until == 0) {
|
| 115 |
- if err := writeEvent(job, event); err != nil {
|
|
| 115 |
+ if err := writeEvent(job, event, eventFilters); err != nil {
|
|
| 116 | 116 |
e.mu.RUnlock() |
| 117 | 117 |
return err |
| 118 | 118 |
} |
| ... | ... |
@@ -61,7 +61,7 @@ func TestEventsPause(t *testing.T) {
|
| 61 | 61 |
t.Fatalf("event should be pause, not %#v", pauseEvent)
|
| 62 | 62 |
} |
| 63 | 63 |
if unpauseEvent[len(unpauseEvent)-1] != "unpause" {
|
| 64 |
- t.Fatalf("event should be pause, not %#v", unpauseEvent)
|
|
| 64 |
+ t.Fatalf("event should be unpause, not %#v", unpauseEvent)
|
|
| 65 | 65 |
} |
| 66 | 66 |
|
| 67 | 67 |
waitCmd := exec.Command(dockerBinary, "wait", name) |
| ... | ... |
@@ -138,13 +138,13 @@ func TestEventsContainerEvents(t *testing.T) {
|
| 138 | 138 |
t.Fatalf("event should be create, not %#v", createEvent)
|
| 139 | 139 |
} |
| 140 | 140 |
if startEvent[len(startEvent)-1] != "start" {
|
| 141 |
- t.Fatalf("event should be pause, not %#v", startEvent)
|
|
| 141 |
+ t.Fatalf("event should be start, not %#v", startEvent)
|
|
| 142 | 142 |
} |
| 143 | 143 |
if dieEvent[len(dieEvent)-1] != "die" {
|
| 144 |
- t.Fatalf("event should be pause, not %#v", dieEvent)
|
|
| 144 |
+ t.Fatalf("event should be die, not %#v", dieEvent)
|
|
| 145 | 145 |
} |
| 146 | 146 |
if destroyEvent[len(destroyEvent)-1] != "destroy" {
|
| 147 |
- t.Fatalf("event should be pause, not %#v", destroyEvent)
|
|
| 147 |
+ t.Fatalf("event should be destroy, not %#v", destroyEvent)
|
|
| 148 | 148 |
} |
| 149 | 149 |
|
| 150 | 150 |
logDone("events - container create, start, die, destroy is logged")
|
| ... | ... |
@@ -283,3 +283,58 @@ func TestEventsImageImport(t *testing.T) {
|
| 283 | 283 |
|
| 284 | 284 |
logDone("events - image import is logged")
|
| 285 | 285 |
} |
| 286 |
+ |
|
| 287 |
+func TestEventsFilters(t *testing.T) {
|
|
| 288 |
+ now := time.Now().Unix() |
|
| 289 |
+ cmd(t, "run", "--rm", "busybox", "true") |
|
| 290 |
+ cmd(t, "run", "--rm", "busybox", "true") |
|
| 291 |
+ eventsCmd := exec.Command(dockerBinary, "events", fmt.Sprintf("--since=%d", now), fmt.Sprintf("--until=%d", time.Now().Unix()), "--filter", "event=die")
|
|
| 292 |
+ out, exitCode, err := runCommandWithOutput(eventsCmd) |
|
| 293 |
+ if exitCode != 0 || err != nil {
|
|
| 294 |
+ t.Fatalf("Failed to get events with exit code %d: %s", exitCode, err)
|
|
| 295 |
+ } |
|
| 296 |
+ events := strings.Split(out, "\n") |
|
| 297 |
+ events = events[:len(events)-1] |
|
| 298 |
+ if len(events) != 2 {
|
|
| 299 |
+ fmt.Printf("%v\n", events)
|
|
| 300 |
+ t.Fatalf("Unexpected event")
|
|
| 301 |
+ } |
|
| 302 |
+ dieEvent := strings.Fields(events[len(events)-1]) |
|
| 303 |
+ if dieEvent[len(dieEvent)-1] != "die" {
|
|
| 304 |
+ t.Fatalf("event should be die, not %#v", dieEvent)
|
|
| 305 |
+ } |
|
| 306 |
+ |
|
| 307 |
+ dieEvent = strings.Fields(events[len(events)-2]) |
|
| 308 |
+ if dieEvent[len(dieEvent)-1] != "die" {
|
|
| 309 |
+ t.Fatalf("event should be die, not %#v", dieEvent)
|
|
| 310 |
+ } |
|
| 311 |
+ |
|
| 312 |
+ eventsCmd = exec.Command(dockerBinary, "events", "--since=0", fmt.Sprintf("--until=%d", time.Now().Unix()), "--filter", "event=die", "--filter", "event=start")
|
|
| 313 |
+ out, exitCode, err = runCommandWithOutput(eventsCmd) |
|
| 314 |
+ if exitCode != 0 || err != nil {
|
|
| 315 |
+ t.Fatalf("Failed to get events with exit code %d: %s", exitCode, err)
|
|
| 316 |
+ } |
|
| 317 |
+ events = strings.Split(out, "\n") |
|
| 318 |
+ events = events[:len(events)-1] |
|
| 319 |
+ if len(events) != 4 {
|
|
| 320 |
+ t.Fatalf("Unexpected event")
|
|
| 321 |
+ } |
|
| 322 |
+ startEvent := strings.Fields(events[len(events)-4]) |
|
| 323 |
+ if startEvent[len(startEvent)-1] != "start" {
|
|
| 324 |
+ t.Fatalf("event should be start, not %#v", startEvent)
|
|
| 325 |
+ } |
|
| 326 |
+ dieEvent = strings.Fields(events[len(events)-3]) |
|
| 327 |
+ if dieEvent[len(dieEvent)-1] != "die" {
|
|
| 328 |
+ t.Fatalf("event should be die, not %#v", dieEvent)
|
|
| 329 |
+ } |
|
| 330 |
+ startEvent = strings.Fields(events[len(events)-2]) |
|
| 331 |
+ if startEvent[len(startEvent)-1] != "start" {
|
|
| 332 |
+ t.Fatalf("event should be start, not %#v", startEvent)
|
|
| 333 |
+ } |
|
| 334 |
+ dieEvent = strings.Fields(events[len(events)-1]) |
|
| 335 |
+ if dieEvent[len(dieEvent)-1] != "die" {
|
|
| 336 |
+ t.Fatalf("event should be die, not %#v", dieEvent)
|
|
| 337 |
+ } |
|
| 338 |
+ |
|
| 339 |
+ logDone("events - filters")
|
|
| 340 |
+} |