Event history service and CLI

- Tool created while debugging ONOS-3509

  Usage Example: (See recent Mastership and Device events)
   onos> events -m -d

Change-Id: I87aceaf8fe61732a61c2d1e39399d0f10a729b54
diff --git a/apps/events/pom.xml b/apps/events/pom.xml
new file mode 100644
index 0000000..8415e92
--- /dev/null
+++ b/apps/events/pom.xml
@@ -0,0 +1,156 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2016 Open Networking Laboratory
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <artifactId>onos-apps</artifactId>
+        <groupId>org.onosproject</groupId>
+        <version>1.5.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <groupId>org.onosproject</groupId>
+    <artifactId>onos-events</artifactId>
+    <packaging>bundle</packaging>
+
+    <description>App to display ONOS event history</description>
+    <url>http://onosproject.org</url>
+
+    <properties>
+        <onos.version>1.5.0-SNAPSHOT</onos.version>
+        <onos.app.name>org.onosproject.events</onos.app.name>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-api</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onlab-osgi</artifactId>
+            </dependency>
+
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-api</artifactId>
+            <scope>test</scope>
+            <classifier>tests</classifier>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.scr.annotations</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-cli</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.core</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.karaf.shell</groupId>
+            <artifactId>org.apache.karaf.shell.console</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onlab-junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onlab-misc</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-scr-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>generate-scr-srcdescriptor</id>
+                        <goals>
+                            <goal>scr</goal>
+                        </goals>
+                    </execution>
+                </executions>
+                <configuration>
+                    <supportedProjectTypes>
+                        <supportedProjectType>bundle</supportedProjectType>
+                        <supportedProjectType>war</supportedProjectType>
+                    </supportedProjectTypes>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.onosproject</groupId>
+                <artifactId>onos-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>cfg</id>
+                        <phase>generate-resources</phase>
+                        <goals>
+                            <goal>cfg</goal>
+                        </goals>
+                    </execution>
+                    <execution>
+                        <id>swagger</id>
+                        <phase>generate-sources</phase>
+                        <goals>
+                            <goal>swagger</goal>
+                        </goals>
+                    </execution>
+                    <execution>
+                        <id>app</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>app</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git a/apps/events/src/main/java/org/onosproject/events/EventHistoryManager.java b/apps/events/src/main/java/org/onosproject/events/EventHistoryManager.java
new file mode 100644
index 0000000..8ace0e1
--- /dev/null
+++ b/apps/events/src/main/java/org/onosproject/events/EventHistoryManager.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright 2016 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.events;
+
+import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor;
+import static org.onlab.util.Tools.groupedThreads;
+import static org.onlab.util.Tools.minPriority;
+
+import java.util.Deque;
+import java.util.concurrent.ConcurrentLinkedDeque;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.Service;
+import org.onlab.util.UnmodifiableDeque;
+import org.onosproject.cluster.ClusterEvent;
+import org.onosproject.cluster.ClusterEventListener;
+import org.onosproject.cluster.ClusterService;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.event.Event;
+import org.onosproject.event.ListenerTracker;
+import org.onosproject.mastership.MastershipEvent;
+import org.onosproject.mastership.MastershipListener;
+import org.onosproject.mastership.MastershipService;
+import org.onosproject.net.device.DeviceEvent;
+import org.onosproject.net.device.DeviceListener;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.host.HostEvent;
+import org.onosproject.net.host.HostListener;
+import org.onosproject.net.host.HostService;
+import org.onosproject.net.link.LinkEvent;
+import org.onosproject.net.link.LinkListener;
+import org.onosproject.net.link.LinkService;
+import org.onosproject.net.topology.TopologyEvent;
+import org.onosproject.net.topology.TopologyListener;
+import org.onosproject.net.topology.TopologyService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Application to store history of instance local ONOS Events.
+ */
+@Component(immediate = true)
+@Service
+public class EventHistoryManager
+    implements EventHistoryService {
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected CoreService coreService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected MastershipService mastershipService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected DeviceService deviceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected LinkService linkService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected TopologyService topologyService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected HostService hostService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected ClusterService clusterService;
+
+    @Property(name = "excludeStatsEvent", boolValue = true,
+              label = "Exclude stats related events")
+    private boolean excludeStatsEvent = true;
+
+    @Property(name = "sizeLimit", intValue = 10_000,
+              label = "Number of event history to store")
+    private int sizeLimit = 10_000;
+
+    private ApplicationId appId;
+
+    private ListenerTracker listeners;
+
+    // Using Deque so that it'll be possible to iterate from both ends
+    // (Tail-end is the most recent event)
+    private final Deque<Event<?, ?>> history = new ConcurrentLinkedDeque<>();
+
+    private ScheduledExecutorService pruner;
+
+    // pruneEventHistoryTask() execution interval in seconds
+    private long pruneInterval = 5;
+
+
+    @Activate
+    protected void activate() {
+        appId = coreService.registerApplication("org.onosproject.events");
+        log.debug("Registered as {}", appId);
+
+        pruner = newSingleThreadScheduledExecutor(minPriority(groupedThreads("onos/events", "history-pruner")));
+
+        pruner.scheduleWithFixedDelay(this::pruneEventHistoryTask,
+                                      pruneInterval , pruneInterval, TimeUnit.SECONDS);
+
+        listeners = new ListenerTracker();
+        listeners.addListener(mastershipService, new InternalMastershipListener())
+                 .addListener(deviceService, new InternalDeviceListener())
+                 .addListener(linkService, new InternalLinkListener())
+                 .addListener(topologyService, new InternalTopologyListener())
+                 .addListener(hostService, new InternalHostListener())
+                 .addListener(clusterService, new InternalClusterListener());
+
+        log.info("Started");
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        listeners.removeListeners();
+
+        pruner.shutdownNow();
+        history.clear();
+
+        log.info("Stopped");
+    }
+
+    @Override
+    public Deque<Event<?, ?>> history() {
+        return UnmodifiableDeque.unmodifiableDeque(history);
+    }
+
+    @Override
+    public void clear() {
+        history.clear();
+    }
+
+    // This method assumes only 1 call is in flight at the same time.
+    private void pruneEventHistoryTask() {
+        int size = history.size();
+        int overflows = size - sizeLimit;
+        if (overflows > 0) {
+            for (int i = 0; i < overflows; ++i) {
+                history.poll();
+            }
+        }
+    }
+
+    private void addEvent(Event<?, ?> event) {
+        history.offer(event);
+    }
+
+    class InternalMastershipListener
+            implements MastershipListener {
+
+        @Override
+        public void event(MastershipEvent event) {
+            addEvent(event);
+        }
+    }
+
+    class InternalDeviceListener
+            implements DeviceListener {
+
+        @Override
+        public boolean isRelevant(DeviceEvent event) {
+            if (excludeStatsEvent) {
+                return event.type() != DeviceEvent.Type.PORT_STATS_UPDATED;
+            } else {
+                return true;
+            }
+        }
+
+        @Override
+        public void event(DeviceEvent event) {
+            addEvent(event);
+        }
+    }
+
+    class InternalLinkListener
+            implements LinkListener {
+
+        @Override
+        public void event(LinkEvent event) {
+            addEvent(event);
+        }
+    }
+
+    class InternalTopologyListener
+            implements TopologyListener {
+
+        @Override
+        public void event(TopologyEvent event) {
+            addEvent(event);
+        }
+    }
+
+    class InternalHostListener
+            implements HostListener {
+
+        @Override
+        public void event(HostEvent event) {
+            addEvent(event);
+        }
+    }
+
+    class InternalClusterListener
+            implements ClusterEventListener {
+
+        @Override
+        public void event(ClusterEvent event) {
+            addEvent(event);
+        }
+    }
+
+}
diff --git a/apps/events/src/main/java/org/onosproject/events/EventHistoryService.java b/apps/events/src/main/java/org/onosproject/events/EventHistoryService.java
new file mode 100644
index 0000000..f653c53
--- /dev/null
+++ b/apps/events/src/main/java/org/onosproject/events/EventHistoryService.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2016 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.events;
+
+import java.util.Deque;
+
+import org.onosproject.event.Event;
+
+import com.google.common.annotations.Beta;
+
+/**
+ * Provides history of instance local ONOS Events.
+ */
+@Beta
+public interface EventHistoryService {
+
+    /**
+     * Returns unmodifiable view of ONOS events history.
+     *
+     * @return ONOS events (First element is the oldest event stored)
+     */
+    Deque<Event<?, ?>> history();
+
+    /**
+     * Clears all stored history.
+     */
+    void clear();
+}
diff --git a/apps/events/src/main/java/org/onosproject/events/EventsCommand.java b/apps/events/src/main/java/org/onosproject/events/EventsCommand.java
new file mode 100644
index 0000000..c6f15de
--- /dev/null
+++ b/apps/events/src/main/java/org/onosproject/events/EventsCommand.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright 2016 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.events;
+
+import static java.util.stream.Collectors.toList;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.stream.Collector;
+import java.util.stream.Stream;
+
+import org.apache.karaf.shell.commands.Command;
+import org.apache.karaf.shell.commands.Option;
+import org.joda.time.LocalDateTime;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.cluster.ClusterEvent;
+import org.onosproject.event.Event;
+import org.onosproject.mastership.MastershipEvent;
+import org.onosproject.net.Link;
+import org.onosproject.net.device.DeviceEvent;
+import org.onosproject.net.host.HostEvent;
+import org.onosproject.net.link.LinkEvent;
+import org.onosproject.net.topology.Topology;
+import org.onosproject.net.topology.TopologyEvent;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Command to print history of instance local ONOS Events.
+ */
+@Command(scope = "onos", name = "events",
+         description = "Command to print history of instance local ONOS Events")
+public class EventsCommand
+    extends AbstractShellCommand {
+
+    @Option(name = "--all", aliases = "-a",
+            description = "Include all Events (default behavior)",
+            required = false)
+    private boolean all = false;
+
+    @Option(name = "--mastership", aliases = "-m",
+            description = "Include MastershipEvent",
+            required = false)
+    private boolean mastership = false;
+
+    @Option(name = "--device", aliases = "-d",
+            description = "Include DeviceEvent",
+            required = false)
+    private boolean device = false;
+
+    @Option(name = "--link", aliases = "-l",
+            description = "Include LinkEvent",
+            required = false)
+    private boolean link = false;
+
+    @Option(name = "--topology", aliases = "-t",
+            description = "Include TopologyEvent",
+            required = false)
+    private boolean topology = false;
+
+    @Option(name = "--host", aliases = "-t",
+            description = "Include HostEvent",
+            required = false)
+    private boolean host = false;
+
+    @Option(name = "--cluster", aliases = "-c",
+            description = "Include ClusterEvent",
+            required = false)
+    private boolean cluster = false;
+
+    @Option(name = "--max-events", aliases = "-n",
+            description = "Maximum number of events to print",
+            required = false,
+            valueToShowInHelp = "-1 [no limit]")
+    private long maxSize = -1;
+
+    @Override
+    protected void execute() {
+        EventHistoryService eventHistoryService = get(EventHistoryService.class);
+
+        Stream<Event<?, ?>> events = eventHistoryService.history().stream();
+
+        boolean dumpAll = all || !(mastership || device || link || topology || host);
+
+        if (!dumpAll) {
+            Predicate<Event<?, ?>> filter = (defaultIs) -> false;
+
+            if (mastership) {
+                filter = filter.or(evt -> evt instanceof MastershipEvent);
+            }
+            if (device) {
+                filter = filter.or(evt -> evt instanceof DeviceEvent);
+            }
+            if (link) {
+                filter = filter.or(evt -> evt instanceof LinkEvent);
+            }
+            if (topology) {
+                filter = filter.or(evt -> evt instanceof TopologyEvent);
+            }
+            if (host) {
+                filter = filter.or(evt -> evt instanceof HostEvent);
+            }
+            if (cluster) {
+                filter = filter.or(evt -> evt instanceof ClusterEvent);
+            }
+
+            events = events.filter(filter);
+        }
+
+        if (maxSize > 0) {
+            events = events.limit(maxSize);
+        }
+
+        if (outputJson()) {
+            ArrayNode jsonEvents = events.map(this::json).collect(toArrayNode());
+            printJson(jsonEvents);
+        } else {
+            events.forEach(this::printEvent);
+        }
+
+    }
+
+    private Collector<JsonNode, ArrayNode, ArrayNode> toArrayNode() {
+        return Collector.of(() -> mapper().createArrayNode(),
+                            ArrayNode::add,
+                            ArrayNode::addAll);
+    }
+
+    private ObjectNode json(Event<?, ?> event) {
+        ObjectNode result = mapper().createObjectNode();
+
+        result.put("time", event.time())
+              .put("type", event.type().toString())
+              .put("event", event.toString());
+
+        return result;
+    }
+
+    /**
+     * Print JsonNode using default pretty printer.
+     *
+     * @param json JSON node to print
+     */
+    private void printJson(JsonNode json) {
+        try {
+            print("%s", mapper().writerWithDefaultPrettyPrinter().writeValueAsString(json));
+        } catch (JsonProcessingException e) {
+            StringWriter sw = new StringWriter();
+            e.printStackTrace(new PrintWriter(sw));
+            print("[ERROR] %s\n%s", e.getMessage(), sw.toString());
+        }
+    }
+
+    private void printEvent(Event<?, ?> event) {
+        if (event instanceof DeviceEvent) {
+            DeviceEvent deviceEvent = (DeviceEvent) event;
+            if (event.type().toString().startsWith("PORT")) {
+                // Port event
+                print("%s %s\t%s/%s [%s]",
+                      new LocalDateTime(event.time()),
+                      event.type(),
+                      deviceEvent.subject().id(), deviceEvent.port().number(),
+                      deviceEvent.port()
+                  );
+            } else {
+                // Device event
+                print("%s %s\t%s [%s]",
+                      new LocalDateTime(event.time()),
+                      event.type(),
+                      deviceEvent.subject().id(),
+                      deviceEvent.subject()
+                  );
+            }
+
+        } else if (event instanceof MastershipEvent) {
+            print("%s %s\t%s [%s]",
+                  new LocalDateTime(event.time()),
+                  event.type(),
+                  event.subject(),
+                  ((MastershipEvent) event).roleInfo());
+
+        } else if (event instanceof LinkEvent) {
+            LinkEvent linkEvent = (LinkEvent) event;
+            Link link = linkEvent.subject();
+            print("%s %s\t%s/%s-%s/%s [%s]",
+                  new LocalDateTime(event.time()),
+                  event.type(),
+                  link.src().deviceId(), link.src().port(), link.dst().deviceId(), link.dst().port(),
+                  link);
+
+        } else if (event instanceof HostEvent) {
+            HostEvent hostEvent = (HostEvent) event;
+            print("%s %s\t%s [%s->%s]",
+                  new LocalDateTime(event.time()),
+                  event.type(),
+                  hostEvent.subject().id(),
+                  hostEvent.prevSubject(), hostEvent.subject());
+
+        } else if (event instanceof TopologyEvent) {
+            TopologyEvent topoEvent = (TopologyEvent) event;
+            List<Event> reasons = MoreObjects.firstNonNull(topoEvent.reasons(), ImmutableList.of());
+            Topology topo = topoEvent.subject();
+            String summary = String.format("(d=%d,l=%d,c=%d)",
+                                           topo.deviceCount(),
+                                           topo.linkCount(),
+                                           topo.clusterCount());
+            print("%s %s%s [%s]",
+                  new LocalDateTime(event.time()),
+                  event.type(),
+                  summary,
+                  reasons.stream().map(e -> e.type()).collect(toList()));
+
+        } else if (event instanceof ClusterEvent) {
+            print("%s %s\t%s [%s]",
+                  new LocalDateTime(event.time()),
+                  event.type(),
+                  ((ClusterEvent) event).subject().id(),
+                  event.subject());
+
+        } else {
+            // Unknown Event?
+            print("%s %s\t%s [%s]",
+                  new LocalDateTime(event.time()),
+                  event.type(),
+                  event.subject(),
+                  event);
+        }
+    }
+
+}
diff --git a/apps/events/src/main/java/org/onosproject/events/package-info.java b/apps/events/src/main/java/org/onosproject/events/package-info.java
new file mode 100644
index 0000000..72cf9f7
--- /dev/null
+++ b/apps/events/src/main/java/org/onosproject/events/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2016 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Application to store history of instance local ONOS Events.
+ */
+package org.onosproject.events;
diff --git a/apps/events/src/main/resources/OSGI-INF/blueprint/shell-config.xml b/apps/events/src/main/resources/OSGI-INF/blueprint/shell-config.xml
new file mode 100644
index 0000000..17dccfd
--- /dev/null
+++ b/apps/events/src/main/resources/OSGI-INF/blueprint/shell-config.xml
@@ -0,0 +1,24 @@
+<!--
+  ~ Copyright 2016 Open Networking Laboratory
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0">
+
+    <command-bundle xmlns="http://karaf.apache.org/xmlns/shell/v1.1.0">
+        <command>
+            <action class="org.onosproject.events.EventsCommand"/>
+        </command>
+    </command-bundle>
+
+</blueprint>