Added initial implementation of Topology-related event and
event metrics collector. It can be loaded by one of the following two (new)
features: onos-app-metrics, onos-app-metrics-topology

After loading the module, it subscribes for topology-related events
and keeps the following state:
 (a) The last 10 events
 (b) The timestamp of the last event (ms after epoch) as observed by this
     module
 (c) The rate of the topology events: count, median rate, average rate
     over the last 1, 5 or 15 minutes

The following CLI commands are added:
 * onos:topology-events
   Shows the last 10 topology events

 * onos:topology-events-metrics
   Shows the timestamp of the last event, and the rate of the topology
   events: see (b) and (c) above
diff --git a/apps/metrics/pom.xml b/apps/metrics/pom.xml
new file mode 100644
index 0000000..0ce3913
--- /dev/null
+++ b/apps/metrics/pom.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<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/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.onlab.onos</groupId>
+    <artifactId>onos-apps</artifactId>
+    <version>1.0.0-SNAPSHOT</version>
+    <relativePath>../pom.xml</relativePath>
+  </parent>
+
+  <artifactId>onos-app-metrics</artifactId>
+  <packaging>pom</packaging>
+
+  <description>ONOS metrics applications</description>
+
+  <modules>
+    <module>topology</module>
+  </modules>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.onlab.onos</groupId>
+      <artifactId>onlab-misc</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-databind</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-annotations</artifactId>
+    </dependency>
+
+  </dependencies>
+
+</project>
diff --git a/apps/metrics/topology/pom.xml b/apps/metrics/topology/pom.xml
new file mode 100644
index 0000000..dc1c000
--- /dev/null
+++ b/apps/metrics/topology/pom.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<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/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.onlab.onos</groupId>
+    <artifactId>onos-app-metrics</artifactId>
+    <version>1.0.0-SNAPSHOT</version>
+    <relativePath>../pom.xml</relativePath>
+  </parent>
+
+  <artifactId>onos-app-metrics-topology</artifactId>
+  <packaging>bundle</packaging>
+
+  <description>ONOS topology metrics application</description>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.onlab.onos</groupId>
+      <artifactId>onos-cli</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.karaf.shell</groupId>
+      <artifactId>org.apache.karaf.shell.console</artifactId>
+    </dependency>
+  </dependencies>
+
+</project>
diff --git a/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/TopologyMetrics.java b/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/TopologyMetrics.java
new file mode 100644
index 0000000..e2a4532
--- /dev/null
+++ b/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/TopologyMetrics.java
@@ -0,0 +1,166 @@
+package org.onlab.onos.metrics.topology;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.Meter;
+import com.google.common.collect.ImmutableList;
+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.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.Service;
+import org.onlab.metrics.MetricsComponent;
+import org.onlab.metrics.MetricsFeature;
+import org.onlab.metrics.MetricsService;
+import org.onlab.onos.event.Event;
+import org.onlab.onos.net.topology.TopologyEvent;
+import org.onlab.onos.net.topology.TopologyListener;
+import org.onlab.onos.net.topology.TopologyService;
+import org.slf4j.Logger;
+
+/**
+ * ONOS Topology Metrics Application that collects topology-related metrics.
+ */
+@Component(immediate = true)
+@Service
+public class TopologyMetrics implements TopologyMetricsService,
+                                        TopologyListener {
+    private static final Logger log = getLogger(TopologyMetrics.class);
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected TopologyService topologyService;
+    private LinkedList<TopologyEvent> lastEvents = new LinkedList<>();
+    private static final int LAST_EVENTS_MAX_N = 10;
+
+    //
+    // Metrics
+    //
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected MetricsService metricsService;
+    //
+    private static final String COMPONENT_NAME = "Topology";
+    private static final String FEATURE_NAME = "EventNotification";
+    private static final String GAUGE_NAME = "LastEventTimestamp.EpochMs";
+    private static final String METER_NAME = "EventRate";
+    //
+    private MetricsComponent metricsComponent;
+    private MetricsFeature metricsFeatureEventNotification;
+    //
+    // Timestamp of the last Topology event (ms from the Epoch)
+    private volatile long lastEventTimestampEpochMs = 0;
+    private Gauge<Long> lastEventTimestampEpochMsGauge;
+    // Rate of the Topology events published to the Topology listeners
+    private Meter eventRateMeter;
+
+    @Activate
+    protected void activate() {
+        clear();
+        registerMetrics();
+        topologyService.addListener(this);
+        log.info("ONOS Topology Metrics started.");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        topologyService.removeListener(this);
+        removeMetrics();
+        clear();
+        log.info("ONOS Topology Metrics stopped.");
+    }
+
+    @Override
+    public List<TopologyEvent> getEvents() {
+        synchronized (lastEvents) {
+            return ImmutableList.<TopologyEvent>copyOf(lastEvents);
+        }
+    }
+
+    @Override
+    public Gauge<Long> lastEventTimestampEpochMsGauge() {
+        return lastEventTimestampEpochMsGauge;
+    }
+
+    @Override
+    public Meter eventRateMeter() {
+        return eventRateMeter;
+    }
+
+    @Override
+    public void event(TopologyEvent event) {
+        lastEventTimestampEpochMs = System.currentTimeMillis();
+        //
+        // NOTE: If we want to count each "reason" as a separate event,
+        // then we should use 'event.reason().size()' instead of '1' to
+        // mark the meter below.
+        //
+        eventRateMeter.mark(1);
+
+        log.debug("Topology Event: time = {} type = {} subject = {}",
+                  event.time(), event.type(), event.subject());
+        for (Event reason : event.reasons()) {
+            log.debug("Topology Event Reason: time = {} type = {} subject = {}",
+                      reason.time(), reason.type(), reason.subject());
+        }
+
+        //
+        // Keep only the last N events, where N = LAST_EVENTS_MAX_N
+        //
+        synchronized (lastEvents) {
+            while (lastEvents.size() >= LAST_EVENTS_MAX_N) {
+                lastEvents.remove();
+            }
+            lastEvents.add(event);
+        }
+    }
+
+    /**
+     * Clears the internal state.
+     */
+    private void clear() {
+        lastEventTimestampEpochMs = 0;
+        synchronized (lastEvents) {
+            lastEvents.clear();
+        }
+    }
+
+    /**
+     * Registers the metrics.
+     */
+    private void registerMetrics() {
+        metricsComponent = metricsService.registerComponent(COMPONENT_NAME);
+        metricsFeatureEventNotification =
+            metricsComponent.registerFeature(FEATURE_NAME);
+        lastEventTimestampEpochMsGauge =
+            metricsService.registerMetric(metricsComponent,
+                                          metricsFeatureEventNotification,
+                                          GAUGE_NAME,
+                                          new Gauge<Long>() {
+                                              @Override
+                                              public Long getValue() {
+                                                  return lastEventTimestampEpochMs;
+                                              }
+                                          });
+        eventRateMeter =
+            metricsService.createMeter(metricsComponent,
+                                       metricsFeatureEventNotification,
+                                       METER_NAME);
+
+    }
+
+    /**
+     * Removes the metrics.
+     */
+    private void removeMetrics() {
+        metricsService.removeMetric(metricsComponent,
+                                    metricsFeatureEventNotification,
+                                    GAUGE_NAME);
+        metricsService.removeMetric(metricsComponent,
+                                    metricsFeatureEventNotification,
+                                    METER_NAME);
+    }
+}
diff --git a/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/TopologyMetricsService.java b/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/TopologyMetricsService.java
new file mode 100644
index 0000000..cc370fa
--- /dev/null
+++ b/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/TopologyMetricsService.java
@@ -0,0 +1,35 @@
+package org.onlab.onos.metrics.topology;
+
+import java.util.List;
+
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.Meter;
+import org.onlab.onos.net.topology.TopologyEvent;
+
+/**
+ * Service interface exported by TopologyMetrics.
+ */
+public interface TopologyMetricsService {
+    /**
+     * Gets the last saved topology events.
+     *
+     * @return the last saved topology events.
+     */
+    public List<TopologyEvent> getEvents();
+
+    /**
+     * Gets the Metrics' Gauge for the last topology event timestamp
+     * (ms from the epoch).
+     *
+     * @return the Metrics' Gauge for the last topology event timestamp
+     * (ms from the epoch)
+     */
+    public Gauge<Long> lastEventTimestampEpochMsGauge();
+
+    /**
+     * Gets the Metrics' Meter for the topology events rate.
+     *
+     * @return the Metrics' Meter for the topology events rate
+     */
+    public Meter eventRateMeter();
+}
diff --git a/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/cli/TopologyEventsListCommand.java b/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/cli/TopologyEventsListCommand.java
new file mode 100644
index 0000000..8bab4d0
--- /dev/null
+++ b/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/cli/TopologyEventsListCommand.java
@@ -0,0 +1,97 @@
+package org.onlab.onos.metrics.topology.cli;
+
+import java.util.List;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.karaf.shell.commands.Command;
+import org.onlab.onos.cli.AbstractShellCommand;
+import org.onlab.onos.event.Event;
+import org.onlab.onos.metrics.topology.TopologyMetricsService;
+import org.onlab.onos.net.topology.TopologyEvent;
+
+/**
+ * Command to show the list of last topology events.
+ */
+@Command(scope = "onos", name = "topology-events",
+         description = "Lists the last topology events")
+public class TopologyEventsListCommand extends AbstractShellCommand {
+
+    private static final String FORMAT_EVENT =
+        "Topology Event time=%d type=%s subject=%s";
+    private static final String FORMAT_REASON =
+        "    Reason time=%d type=%s subject=%s";
+
+    @Override
+    protected void execute() {
+        TopologyMetricsService service = get(TopologyMetricsService.class);
+
+        if (outputJson()) {
+            print("%s", json(service.getEvents()));
+        } else {
+            for (TopologyEvent event : service.getEvents()) {
+                print(FORMAT_EVENT, event.time(), event.type(),
+                      event.subject());
+                for (Event reason : event.reasons()) {
+                    print(FORMAT_REASON, reason.time(), reason.type(),
+                          reason.subject());
+                }
+                print("");          // Extra empty line for clarity
+            }
+        }
+    }
+
+    /**
+     * Produces a JSON array of topology events.
+     *
+     * @param topologyEvents the topology events with the data
+     * @return JSON array with the topology events
+     */
+    private JsonNode json(List<TopologyEvent> topologyEvents) {
+        ObjectMapper mapper = new ObjectMapper();
+        ArrayNode result = mapper.createArrayNode();
+
+        for (TopologyEvent event : topologyEvents) {
+            result.add(json(mapper, event));
+        }
+        return result;
+    }
+
+    /**
+     * Produces JSON object for a topology event.
+     *
+     * @param mapper the JSON object mapper to use
+     * @param topologyEvent the topology event with the data
+     * @return JSON object for the topology event
+     */
+    private ObjectNode json(ObjectMapper mapper, TopologyEvent topologyEvent) {
+        ObjectNode result = mapper.createObjectNode();
+        ArrayNode reasons = mapper.createArrayNode();
+
+        for (Event reason : topologyEvent.reasons()) {
+            reasons.add(json(mapper, reason));
+        }
+        result.put("time", topologyEvent.time())
+            .put("type", topologyEvent.type().toString())
+            .put("subject", topologyEvent.subject().toString())
+            .put("reasons", reasons);
+        return result;
+    }
+
+    /**
+     * Produces JSON object for a generic event.
+     *
+     * @param event the generic event with the data
+     * @return JSON object for the generic event
+     */
+    private ObjectNode json(ObjectMapper mapper, Event event) {
+        ObjectNode result = mapper.createObjectNode();
+
+        result.put("time", event.time())
+            .put("type", event.type().toString())
+            .put("subject", event.subject().toString());
+        return result;
+    }
+}
diff --git a/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/cli/TopologyEventsMetricsCommand.java b/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/cli/TopologyEventsMetricsCommand.java
new file mode 100644
index 0000000..1bc0b57
--- /dev/null
+++ b/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/cli/TopologyEventsMetricsCommand.java
@@ -0,0 +1,69 @@
+package org.onlab.onos.metrics.topology.cli;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.json.MetricsModule;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.karaf.shell.commands.Command;
+import org.onlab.onos.cli.AbstractShellCommand;
+import org.onlab.onos.metrics.topology.TopologyMetricsService;
+
+/**
+ * Command to show the topology events metrics.
+ */
+@Command(scope = "onos", name = "topology-events-metrics",
+         description = "Lists topology events metrics")
+public class TopologyEventsMetricsCommand extends AbstractShellCommand {
+
+    private static final String FORMAT_GAUGE =
+        "Last Topology Event Timestamp (ms from epoch)=%d";
+    private static final String FORMAT_METER =
+        "Topology Events count=%d rate(events/sec) mean=%f m1=%f m5=%f m15=%f";
+
+    @Override
+    protected void execute() {
+        TopologyMetricsService service = get(TopologyMetricsService.class);
+        Gauge<Long> gauge = service.lastEventTimestampEpochMsGauge();
+        Meter meter = service.eventRateMeter();
+
+        if (outputJson()) {
+            ObjectMapper mapper = new ObjectMapper()
+                .registerModule(new MetricsModule(TimeUnit.SECONDS,
+                                                  TimeUnit.MILLISECONDS,
+                                                  false));
+            ObjectNode result = mapper.createObjectNode();
+            try {
+                //
+                // NOTE: The API for custom serializers is incomplete,
+                // hence we have to parse the JSON string to create JsonNode.
+                //
+                final String gaugeJson = mapper.writeValueAsString(gauge);
+                final String meterJson = mapper.writeValueAsString(meter);
+                JsonNode gaugeNode = mapper.readTree(gaugeJson);
+                JsonNode meterNode = mapper.readTree(meterJson);
+                result.put("lastTopologyEventTimestamp", gaugeNode);
+                result.put("listenerEventRate", meterNode);
+            } catch (JsonProcessingException e) {
+                log.error("Error writing value as JSON string", e);
+            } catch (IOException e) {
+                log.error("Error writing value as JSON string", e);
+            }
+            print("%s", result);
+        } else {
+            TimeUnit rateUnit = TimeUnit.SECONDS;
+            double rateFactor = rateUnit.toSeconds(1);
+            print(FORMAT_GAUGE, gauge.getValue());
+            print(FORMAT_METER, meter.getCount(),
+                  meter.getMeanRate() * rateFactor,
+                  meter.getOneMinuteRate() * rateFactor,
+                  meter.getFiveMinuteRate() * rateFactor,
+                  meter.getFifteenMinuteRate() * rateFactor);
+        }
+    }
+}
diff --git a/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/package-info.java b/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/package-info.java
new file mode 100644
index 0000000..42826b7
--- /dev/null
+++ b/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * ONOS Topology Metrics Application that collects topology-related metrics.
+ */
+package org.onlab.onos.metrics.topology;
diff --git a/apps/metrics/topology/src/main/resources/OSGI-INF/blueprint/shell-config.xml b/apps/metrics/topology/src/main/resources/OSGI-INF/blueprint/shell-config.xml
new file mode 100644
index 0000000..c1471da
--- /dev/null
+++ b/apps/metrics/topology/src/main/resources/OSGI-INF/blueprint/shell-config.xml
@@ -0,0 +1,12 @@
+<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.onlab.onos.metrics.topology.cli.TopologyEventsListCommand"/>
+    </command>
+    <command>
+      <action class="org.onlab.onos.metrics.topology.cli.TopologyEventsMetricsCommand"/>
+    </command>
+  </command-bundle>
+
+</blueprint>
diff --git a/apps/pom.xml b/apps/pom.xml
index e812c47..b2a3494 100644
--- a/apps/pom.xml
+++ b/apps/pom.xml
@@ -27,6 +27,7 @@
         <module>sdnip</module>
         <module>calendar</module>
         <module>optical</module>
+        <module>metrics</module>
     </modules>
 
     <properties>
diff --git a/features/features.xml b/features/features.xml
index cc7aa99..e6c4beb 100644
--- a/features/features.xml
+++ b/features/features.xml
@@ -37,6 +37,7 @@
 
         <bundle>mvn:com.hazelcast/hazelcast/3.3</bundle>
         <bundle>mvn:io.dropwizard.metrics/metrics-core/3.1.0</bundle>
+        <bundle>mvn:io.dropwizard.metrics/metrics-json/3.1.0</bundle>
         <bundle>mvn:com.eclipsesource.minimal-json/minimal-json/0.9.1</bundle>
 
         <bundle>mvn:com.esotericsoftware/kryo/3.0.0</bundle>
@@ -183,7 +184,6 @@
         <bundle>mvn:org.onlab.onos/onos-app-optical/1.0.0-SNAPSHOT</bundle>
      </feature>
 
-
     <feature name="onos-app-sdnip" version="1.0.0"
              description="SDN-IP peering application">
         <feature>onos-api</feature>
@@ -197,4 +197,15 @@
         <bundle>mvn:org.onlab.onos/onos-app-calendar/1.0.0-SNAPSHOT</bundle>
     </feature>
 
+    <feature name="onos-app-metrics" version="1.0.0"
+             description="ONOS metrics applications">
+        <feature>onos-app-metrics-topology</feature>
+    </feature>
+
+    <feature name="onos-app-metrics-topology" version="1.0.0"
+             description="ONOS topology metrics application">
+        <feature>onos-api</feature>
+        <bundle>mvn:org.onlab.onos/onos-app-metrics-topology/1.0.0-SNAPSHOT</bundle>
+    </feature>
+
 </features>
diff --git a/utils/misc/pom.xml b/utils/misc/pom.xml
index 5040fb0..d1d3a50 100644
--- a/utils/misc/pom.xml
+++ b/utils/misc/pom.xml
@@ -53,6 +53,11 @@
             <version>3.1.0</version>
         </dependency>
         <dependency>
+            <groupId>io.dropwizard.metrics</groupId>
+            <artifactId>metrics-json</artifactId>
+            <version>3.1.0</version>
+        </dependency>
+        <dependency>
             <groupId>org.apache.felix</groupId>
             <artifactId>org.apache.felix.scr.annotations</artifactId>
         </dependency>