You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@felix.apache.org by ro...@apache.org on 2020/09/03 14:43:35 UTC

[felix-dev] branch feature/contribute-osgi-metrics created (now 24cb81f)

This is an automated email from the ASF dual-hosted git repository.

rombert pushed a change to branch feature/contribute-osgi-metrics
in repository https://gitbox.apache.org/repos/asf/felix-dev.git.


      at 24cb81f  List the osgi metrics modules in the top-level README

This branch includes the following new commits:

     new 8863470  metrics/osgi: initial contribution
     new 24cb81f  List the osgi metrics modules in the top-level README

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[felix-dev] 02/02: List the osgi metrics modules in the top-level README

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rombert pushed a commit to branch feature/contribute-osgi-metrics
in repository https://gitbox.apache.org/repos/asf/felix-dev.git

commit 24cb81ffaebfdb2b071da5e9e4b438a67bbe3b80
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Thu Sep 3 16:43:00 2020 +0200

    List the osgi metrics modules in the top-level README
---
 README.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/README.md b/README.md
index 25c2cc6..b668aa1 100644
--- a/README.md
+++ b/README.md
@@ -49,6 +49,7 @@ Several projects provide extra features to an OSGi runtime.
 - **ipojo** `/ipojo` - A *service component runtime* aiming to simplify OSGi application development.
 - **jaas support** `/jaas` - Bundle to simplify JAAS usage within OSGi environment.
 - **logback** `/logback` - A simple integration of the OSGi R7 Log (1.4) service to Logback backend.
+- **OSGi metrics** `/metrics/osgi` - Collecting and publishing metrics related to OSGi applications
 - **rootcause** `/rootcause` - Finding the root cause of problems with OSGi declarative services components.
 - **utils** `/utils` - Utility classes for OSGi (intended for embedding within other bundles.)
 - **webconsole** `/webconsole*` - Web Based Management Console for OSGi Frameworks.


[felix-dev] 01/02: metrics/osgi: initial contribution

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rombert pushed a commit to branch feature/contribute-osgi-metrics
in repository https://gitbox.apache.org/repos/asf/felix-dev.git

commit 8863470e1b194d6c5593b6fa955e7600e2b4c018
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Thu Sep 3 16:41:15 2020 +0200

    metrics/osgi: initial contribution
    
    The modules still refer to the Sling parent pom, but that should not
    be a blocker for the contribution (maybe for a first release).
---
 metrics/osgi/README.md                             |  70 ++++++
 metrics/osgi/collector/bnd.bnd                     |   2 +
 metrics/osgi/collector/pom.xml                     | 157 ++++++++++++++
 .../felix/metrics/osgi/BundleStartDuration.java    |  45 ++++
 .../felix/metrics/osgi/ServiceRestartCounter.java  |  39 ++++
 .../apache/felix/metrics/osgi/StartupMetrics.java  | 101 +++++++++
 .../felix/metrics/osgi/StartupMetricsListener.java |  38 ++++
 .../apache/felix/metrics/osgi/impl/Activator.java  |  51 +++++
 .../osgi/impl/BundleStartTimeCalculator.java       | 112 ++++++++++
 .../org/apache/felix/metrics/osgi/impl/Log.java    |  39 ++++
 .../osgi/impl/ServiceRestartCountCalculator.java   | 239 +++++++++++++++++++++
 .../osgi/impl/ServiceTrackerCustomizerAdapter.java |  33 +++
 .../metrics/osgi/impl/StartupTimeCalculator.java   | 136 ++++++++++++
 .../apache/felix/metrics/osgi/package-info.java    |  18 ++
 .../apache/felix/metrics/osgi/impl/AbstractIT.java | 126 +++++++++++
 .../osgi/impl/BundleStartTimeCalculatorTest.java   |  62 ++++++
 .../metrics/osgi/impl/HealthCheckSmokeIT.java      |  61 ++++++
 .../osgi/impl/ServiceRegistrationsTrackerTest.java |  76 +++++++
 .../impl/ServiceRestartCountCalculatorTest.java    | 192 +++++++++++++++++
 .../metrics/osgi/impl/SystemReadySmokeIT.java      |  47 ++++
 .../impl/WaitForResultsStartupMetricsListener.java |  45 ++++
 metrics/osgi/consumers/bnd.bnd                     |   1 +
 metrics/osgi/consumers/pom.xml                     | 104 +++++++++
 .../impl/dropwizard/DropwizardMetricsListener.java |  85 ++++++++
 .../impl/json/JsonWritingMetricsListener.java      |  95 ++++++++
 .../consumers/impl/log/LoggingMetricsListener.java |  92 ++++++++
 .../impl/json/JsonWritingMetricsListenerTest.java  |  78 +++++++
 metrics/osgi/pom.xml                               |  42 ++++
 28 files changed, 2186 insertions(+)

diff --git a/metrics/osgi/README.md b/metrics/osgi/README.md
new file mode 100644
index 0000000..c0a2e24
--- /dev/null
+++ b/metrics/osgi/README.md
@@ -0,0 +1,70 @@
+# Apache Felix OSGi Metrics
+
+The OSGi metrics module defines a simple mechanism to gather OSGi-related metrics for application startup.
+
+The module is split into two bundles:
+
+- _collector_ - a zero-dependencies bundle that uses the OSGi APIs to gather various metrics
+- _consumers_ - a single bundle that contains various consumers
+
+## Metric collection
+
+The metrics are collected by the `org.apache.felix.metrics.osgi.collector` bundle. This bundle requires no configuration and imports a minimal set of packages, to allow starting as early as possible.
+
+As soon as startup is completed the metrics are made available to consumers that implement the `StartupMetricsListener` interface. The metrics are published after an optional delay, to prevent on-off bounces in startup completion.
+
+Startup completion is delegated to either the `org.apache.felix.systemready` or the `org.apache.felix.healtchecks.api` bundles, which publish marker services once the system is considered ready.
+
+## Metric publication
+
+The `org.apache.felix.metrics.osgi.consumers` bundle contains three out-of-the-box implementation for publishing the metrics
+
+- DropWizard metrics using a `MetricRegistry`
+- JSON file written in the bundle data directory
+- Log entries using the SLF4j API
+
+### JSON metrics file sample
+
+The following (truncated) JSON file exemplifies how the metrics are written
+
+```json
+{
+  "application": {
+    "startTimeMillis": 1587469534671,
+    "startDurationMillis": 14635
+  },
+  "bundles": [
+    {
+      "symbolicName": "org.osgi.util.pushstream",
+      "startTimeMillis": 1587469535933,
+      "startDurationMillis": 0
+    },
+    {
+      "symbolicName": "org.apache.aries.util",
+      "startTimeMillis": 1587469535935,
+      "startDurationMillis": 0
+    },
+    {
+      "symbolicName": "org.apache.felix.configadmin",
+      "startTimeMillis": 1587469536313,
+      "startDurationMillis": 58
+    }
+  ],
+  "services": [
+    {
+      "identifier": "jmx.objectname=org.apache.sling.classloader:name=FSClassLoader,type=ClassLoader",
+      "restarts": 2
+    }
+  ]
+}
+```
+
+Similar metrics are reported through the other collectors.
+
+## Usage
+
+1. Add the `org.apache.felix/org.apache.felix.metrics.osgi.collector` bundle and ensure that
+   it starts as early as possible
+1. Add the `org.apache.felix/org.apache.felix.metrics.osgi.consumers` bundle.
+1. Add the required bundles, either Apache Felix SystemReady or Apache Felix Health Checks
+1. Start up the application
diff --git a/metrics/osgi/collector/bnd.bnd b/metrics/osgi/collector/bnd.bnd
new file mode 100644
index 0000000..6dee250
--- /dev/null
+++ b/metrics/osgi/collector/bnd.bnd
@@ -0,0 +1,2 @@
+Import-Package: org.slf4j;resolution:=optional, *
+DynamicImport-Package: org.slf4j
\ No newline at end of file
diff --git a/metrics/osgi/collector/pom.xml b/metrics/osgi/collector/pom.xml
new file mode 100644
index 0000000..192f732
--- /dev/null
+++ b/metrics/osgi/collector/pom.xml
@@ -0,0 +1,157 @@
+<?xml version="1.0"?>
+<!-- Licensed to the Apache Software Foundation (ASF) under one or more contributor 
+    license agreements. See the NOTICE file distributed with this work for additional 
+    information regarding copyright ownership. The ASF licenses this file to 
+    you 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/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.sling</groupId>
+        <artifactId>sling-bundle-parent</artifactId>
+        <version>38</version>
+        <relativePath />
+    </parent>
+
+    <groupId>org.apache.felix</groupId>
+    <artifactId>org.apache.felix.metrics.osgi.collector</artifactId>
+    <version>0.1.0-SNAPSHOT</version>
+
+    <name>Apache Felix OSGi Metrics Collector</name>
+    <description> 
+        Collects metrics related to the OSGi framework and makes them available to consumers.
+    </description>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>biz.aQute.bnd</groupId>
+                <artifactId>bnd-baseline-maven-plugin</artifactId>
+                <configuration>
+                    <failOnMissing>false</failOnMissing>
+                </configuration>
+            </plugin>
+            <plugin>
+                <artifactId>maven-failsafe-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>integration-test</goal>
+                            <goal>verify</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.annotation.versioning</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.annotation.bundle</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>osgi.core</artifactId>
+        </dependency>
+
+        <!-- note this is set to optional in bnd.bnd, to help the bundle start as soon as possible -->
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+
+        <!--  testing dependencies -->
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <version>3.3.3</version>
+            <scope>test</scope>
+        </dependency>
+
+        <!-- IT dependencies -->
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam-container-native</artifactId>
+            <version>${pax-exam.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam-junit4</artifactId>
+            <version>${pax-exam.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam-link-mvn</artifactId>
+            <version>${pax-exam.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.ops4j.pax.url</groupId>
+            <artifactId>pax-url-aether</artifactId>
+            <version>${pax-url.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.framework</artifactId>
+            <version>6.0.3</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.inject</groupId>
+            <artifactId>javax.inject</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.systemready</artifactId>
+            <version>0.4.2</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.healthcheck.api</artifactId>
+            <version>2.0.4</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.healthcheck.core</artifactId>
+            <version>2.0.8</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.healthcheck.generalchecks</artifactId>
+            <version>2.0.4</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+        </dependency>
+    </dependencies>
+
+    <properties>
+        <pax-exam.version>4.13.2</pax-exam.version>
+        <pax-url.version>2.6.2</pax-url.version>
+    </properties>
+</project>
diff --git a/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/BundleStartDuration.java b/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/BundleStartDuration.java
new file mode 100644
index 0000000..214b47b
--- /dev/null
+++ b/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/BundleStartDuration.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.felix.metrics.osgi;
+
+import java.time.Duration;
+import java.time.Instant;
+
+public final class BundleStartDuration {
+
+    private final String symbolicName;
+    private final Instant startingAt;
+    private final Duration startedAfter;
+
+    public BundleStartDuration(String symbolicName, Instant startingAt, Duration startedAfter) {
+        this.symbolicName = symbolicName;
+        this.startingAt = startingAt;
+        this.startedAfter = startedAfter;
+    }
+    
+    public String getSymbolicName() {
+        return symbolicName;
+    }
+
+    public Instant getStartingAt() {
+        return startingAt;
+    }
+    
+    public Duration getStartedAfter() {
+        return startedAfter;
+    }
+}
diff --git a/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/ServiceRestartCounter.java b/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/ServiceRestartCounter.java
new file mode 100644
index 0000000..8be29ef
--- /dev/null
+++ b/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/ServiceRestartCounter.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.felix.metrics.osgi;
+
+public final class ServiceRestartCounter {
+    
+    private final String serviceIdentifier;
+    private final int serviceRestarts;
+
+    public ServiceRestartCounter(String serviceIdentifier, int serviceRestarts) {
+        this.serviceIdentifier = serviceIdentifier;
+        this.serviceRestarts = serviceRestarts;
+    }
+
+    /**
+     * @return a opaque service identifier, used for describing the service that has restarted
+     */
+    public String getServiceIdentifier() {
+        return serviceIdentifier;
+    }
+    
+    public int getServiceRestarts() {
+        return serviceRestarts;
+    }
+}
diff --git a/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/StartupMetrics.java b/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/StartupMetrics.java
new file mode 100644
index 0000000..d878766
--- /dev/null
+++ b/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/StartupMetrics.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.felix.metrics.osgi;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Provides metrics about the OSGi framework startup and associated services
+ *
+ * <p>The calculation of the application being "ready" is based on the Apache Felix SystemReady bundle and
+ * requires a proper configuration of all checks.</p>
+ */
+public final class StartupMetrics {
+    
+    public static final class Builder {
+        
+        private StartupMetrics startupMetrics = new StartupMetrics();
+        
+        public static Builder withJvmStartup(Instant jvmStartup) {
+            Builder builder = new Builder();
+            builder.startupMetrics.jvmStartup = jvmStartup;
+            return builder;
+        }
+        
+        public Builder withStartupTime(Duration startupTime) {
+            startupMetrics.startupTime = startupTime;
+            return this;
+        }
+        
+        public Builder withBundleStartDurations(List<BundleStartDuration> bundleStartDurations) {
+            startupMetrics.bundleStartDurations = Collections.unmodifiableList(bundleStartDurations);
+            return this;
+        }
+        
+        public Builder withServiceRestarts(List<ServiceRestartCounter> serviceRestarts) {
+            startupMetrics.serviceRestarts = Collections.unmodifiableList(serviceRestarts);
+            return this;
+        }
+        
+        public StartupMetrics build() {
+            return startupMetrics;
+        }
+    }
+
+    private Instant jvmStartup;
+    private Duration startupTime;
+    private List<BundleStartDuration> bundleStartDurations;
+    private List<ServiceRestartCounter> serviceRestarts;
+ 
+    private StartupMetrics() { }
+
+    /**
+     * Returns the instant when the JVM has started
+     * 
+     * <p>Note that this is different from the OSGi startup process, and may lead to unexpected results if the
+     * OSGi framework starts considerably later compared to the JVM.</p>
+     * 
+     * @return the instant when the JVM has started
+     */
+    public Instant getJvmStartup() {
+        return jvmStartup;
+    }
+    
+    /**
+     * @return the time between the {@link #getJvmStartup()} and the application being ready
+     */
+    public Duration getStartupTime() {
+        return startupTime;
+    }
+    
+    /**
+     * @return all bundle start durations
+     */
+    public List<BundleStartDuration> getBundleStartDurations() {
+        return bundleStartDurations;
+    }
+    
+    /**
+     * @return tracked services with at least one restart
+     */
+    public List<ServiceRestartCounter> getServiceRestarts() {
+        return serviceRestarts;
+    }
+}
diff --git a/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/StartupMetricsListener.java b/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/StartupMetricsListener.java
new file mode 100644
index 0000000..07022d2
--- /dev/null
+++ b/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/StartupMetricsListener.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.felix.metrics.osgi;
+
+import org.osgi.annotation.versioning.ConsumerType;
+
+/**
+ * A listener that is notified of the startup metrics
+ * 
+ * <p>The time of the notification can be delayed after the actual application start, as
+ * the implementation may choose to delay it to ensure that the startup is not affected
+ * by e.g. bouncing services.</p>
+ * 
+ * <p>Listeners that register after the application startup will receive a notification anyway.</p>
+ *
+ */
+@ConsumerType
+public interface StartupMetricsListener {
+    
+    /**
+     * @param metrics the startup metrics
+     */
+    void onStartupComplete(StartupMetrics metrics);
+}
diff --git a/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/impl/Activator.java b/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/impl/Activator.java
new file mode 100644
index 0000000..abce910
--- /dev/null
+++ b/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/impl/Activator.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.felix.metrics.osgi.impl;
+
+import org.osgi.annotation.bundle.Header;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+
+@Header(name = Constants.BUNDLE_ACTIVATOR, value = "${@class}")
+// avoid dependency to SCR so we can start early on
+public class Activator implements BundleActivator {
+
+    private BundleStartTimeCalculator bstc;
+    private StartupTimeCalculator stc;
+    private ServiceRestartCountCalculator srcc;
+
+    @Override
+    public void start(BundleContext context) throws Exception {
+        
+        bstc = new BundleStartTimeCalculator(context.getBundle().getBundleId());
+        context.addBundleListener(bstc);
+
+        srcc = new ServiceRestartCountCalculator();
+        context.addServiceListener(srcc);
+
+        stc = new StartupTimeCalculator(context, bstc, srcc);
+    }
+
+    @Override
+    public void stop(BundleContext context) throws Exception {
+        stc.close();
+        context.removeServiceListener(srcc);
+        context.removeBundleListener(bstc);
+    }
+
+}
diff --git a/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/impl/BundleStartTimeCalculator.java b/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/impl/BundleStartTimeCalculator.java
new file mode 100644
index 0000000..449497f
--- /dev/null
+++ b/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/impl/BundleStartTimeCalculator.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.felix.metrics.osgi.impl;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.apache.felix.metrics.osgi.BundleStartDuration;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleEvent;
+import org.osgi.framework.Constants;
+import org.osgi.framework.SynchronousBundleListener;
+
+public class BundleStartTimeCalculator implements SynchronousBundleListener {
+
+    private Map<Long, StartTime> bundleToStartTime = new HashMap<>();
+    private Clock clock = Clock.systemUTC();
+    private final long ourBundleId;
+
+    public BundleStartTimeCalculator(long ourBundleId) {
+        this.ourBundleId = ourBundleId;
+    }
+
+    @Override
+    public void bundleChanged(BundleEvent event) {
+        Bundle bundle = event.getBundle();
+        
+        // this bundle is already starting by the time this is invoked. We also can't get proper timing
+        // from the framework bundle
+        
+        if ( bundle.getBundleId() == Constants.SYSTEM_BUNDLE_ID 
+                || bundle.getBundleId() == ourBundleId ) {
+            return;
+        }
+        
+        synchronized (bundleToStartTime) {
+
+            switch (event.getType()) {
+                case BundleEvent.STARTING:
+                    bundleToStartTime.put(bundle.getBundleId(), new StartTime(bundle.getSymbolicName(), clock.millis()));
+                    break;
+    
+                case BundleEvent.STARTED:
+                    StartTime startTime = bundleToStartTime.get(bundle.getBundleId());
+                    if ( startTime == null ) {
+                        Log.debug(getClass(), "No previous data for started bundle {}/{}", new Object[] { bundle.getBundleId(), bundle.getSymbolicName() });
+                        return;
+                    }
+                    startTime.started(clock.millis());
+                    break;
+                
+                default: // nothing to do here
+                    break;
+            }
+        }
+    }
+    
+    public List<BundleStartDuration> getBundleStartDurations() {
+        
+        synchronized (bundleToStartTime) {
+            return bundleToStartTime.values().stream()
+                .map( StartTime::toBundleStartDuration )
+                .collect( Collectors.toList() );                    
+        }
+    }
+
+    class StartTime {
+        private final String bundleSymbolicName;
+        private long startingTimestamp;
+        private long startedTimestamp;
+        
+        public StartTime(String bundleSymbolicName, long startingTimestamp) {
+            this.bundleSymbolicName = bundleSymbolicName;
+            this.startingTimestamp = startingTimestamp;
+        }
+
+        public long getDuration() {
+            return startedTimestamp - startingTimestamp;
+        }
+        
+        public String getBundleSymbolicName() {
+            return bundleSymbolicName;
+        }
+
+        public void started(long startedTimestamp) {
+            this.startedTimestamp = startedTimestamp;
+        }
+        
+        public BundleStartDuration toBundleStartDuration() {
+            return new BundleStartDuration(bundleSymbolicName, Instant.ofEpochMilli(startingTimestamp), Duration.ofMillis(startedTimestamp - startingTimestamp));
+        }
+    }
+}
diff --git a/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/impl/Log.java b/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/impl/Log.java
new file mode 100644
index 0000000..2c3e1ad
--- /dev/null
+++ b/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/impl/Log.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.felix.metrics.osgi.impl;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Delegates to slf4j if available, otherwise silently fails 
+ *
+ */
+public abstract class Log {
+    public static void debug(Class<?> caller, String message, Object... args) {
+        try {
+            Logger logger = LoggerFactory.getLogger(caller);
+            logger.debug(message, args);
+        } catch ( NoClassDefFoundError e ) {
+            // not available, just carry on
+        }
+    }
+
+    private Log() {
+        
+    }
+}
diff --git a/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/impl/ServiceRestartCountCalculator.java b/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/impl/ServiceRestartCountCalculator.java
new file mode 100644
index 0000000..bf6abbd
--- /dev/null
+++ b/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/impl/ServiceRestartCountCalculator.java
@@ -0,0 +1,239 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.felix.metrics.osgi.impl;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import org.apache.felix.metrics.osgi.ServiceRestartCounter;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceListener;
+
+public class ServiceRestartCountCalculator implements ServiceListener {
+
+    private static final String[] GENERAL_IDENTIFIER_PROPERTIES = new String[] { Constants.SERVICE_PID, "component.name", "jmx.objectname" };
+    private static final Map<String, Collection<String>> SPECIFIC_IDENTIFIER_PROPERTIES = new HashMap<>();
+    static {
+        SPECIFIC_IDENTIFIER_PROPERTIES.put("org.apache.sling.commons.metrics.Gauge", Arrays.asList("name"));
+        SPECIFIC_IDENTIFIER_PROPERTIES.put("org.apache.sling.spi.resource.provider.ResourceProvider", Arrays.asList("provider.root"));
+        SPECIFIC_IDENTIFIER_PROPERTIES.put("org.apache.sling.servlets.post.PostOperation", Arrays.asList("sling.post.operation"));
+        SPECIFIC_IDENTIFIER_PROPERTIES.put("javax.servlet.Servlet", Arrays.asList("felix.webconsole.label"));
+        SPECIFIC_IDENTIFIER_PROPERTIES.put("org.apache.felix.inventory.InventoryPrinter", Arrays.asList("felix.inventory.printer.name"));
+    }
+    
+    private final Map<ServiceIdentifier, ServiceRegistrationsTracker> registrations = new HashMap<>();
+    private final Map<String, Integer> unidentifiedRegistrationsByClassName = new HashMap<>();
+    
+    @Override
+    public void serviceChanged(ServiceEvent event) {
+        
+        if ( shouldIgnore(event) ) 
+            return;
+
+        ServiceIdentifier id = tryFindIdFromGeneralProperties(event);
+        if ( id == null )
+            id = tryFindIdFromSpecificProperties(event);
+        
+        if ( id == null ) {
+            logUnknownService(event);
+            if ( event.getType() == ServiceEvent.UNREGISTERING )
+                recordUnknownServiceUnregistration(event);
+            return;
+        }
+
+        ServiceRegistrationsTracker tracker;
+        synchronized (registrations) {
+            
+            if ( event.getType() == ServiceEvent.REGISTERED ) {
+                tracker = registrations.computeIfAbsent(id, ServiceRegistrationsTracker::new);
+                tracker.registered();
+            } else if ( event.getType() == ServiceEvent.UNREGISTERING ) {
+                
+                tracker = registrations.get(id);
+                if (tracker == null) {
+                    Log.debug(getClass(), "Service with identifier {} was unregistered, but no previous registration data was found", id);
+                    return;
+                }
+                tracker.unregistered();
+            }
+        }
+    }
+
+    private boolean shouldIgnore(ServiceEvent event) {
+        
+        return event.getType() != ServiceEvent.REGISTERED && event.getType() != ServiceEvent.UNREGISTERING;
+    }
+
+    private ServiceIdentifier tryFindIdFromGeneralProperties(ServiceEvent event) {
+        for ( String identifierProp : GENERAL_IDENTIFIER_PROPERTIES ) {
+            Object identifierVal = event.getServiceReference().getProperty(identifierProp);
+            if ( identifierVal != null )
+                return new ServiceIdentifier(identifierProp, identifierVal.toString() );
+        }
+        
+        return null;
+    }
+
+    private ServiceIdentifier tryFindIdFromSpecificProperties(ServiceEvent event) {
+        for ( Map.Entry<String, Collection<String>> entry : SPECIFIC_IDENTIFIER_PROPERTIES.entrySet() ) {
+            String[] classNames = (String[]) event.getServiceReference().getProperty(Constants.OBJECTCLASS);
+            for ( String className : classNames ) {
+                if ( entry.getKey().equals(className) ) {
+                    StringBuilder propKey = new StringBuilder();
+                    StringBuilder propValue = new StringBuilder();
+                    
+                    for ( String idPropName : entry.getValue() ) {
+                        Object idPropVal = event.getServiceReference().getProperty(idPropName);
+                        if ( idPropVal != null ) {
+                            propKey.append(idPropName).append('~');
+                            propValue.append(idPropVal).append('~');
+                        }
+                    }
+                    
+                    if ( propKey.length() != 0 ) {
+                        propKey.deleteCharAt(propKey.length() - 1);
+                        propValue.deleteCharAt(propValue.length() - 1);
+                        ServiceIdentifier id = new ServiceIdentifier(propKey.toString(), propValue.toString());
+                        id.setAdditionalInfo(Constants.OBJECTCLASS + "=" + Arrays.toString(classNames));
+                        return id;
+                    }
+                }
+            }
+        }
+        
+        return null;
+    }
+
+    private void logUnknownService(ServiceEvent event) {
+        if ( event.getType() == ServiceEvent.UNREGISTERING ) {
+            Map<String, Object> props = new HashMap<>();
+            for ( String propertyName : event.getServiceReference().getPropertyKeys() ) {
+                Object propVal = event.getServiceReference().getProperty(propertyName);
+                if ( propVal.getClass() == String[].class )
+                    propVal = Arrays.toString((String[]) propVal);
+                props.put(propertyName, propVal);
+            }
+
+            Log.debug(getClass(), "Ignoring unregistration of service with props {}, as it has none of identifier properties {}", props, Arrays.toString(GENERAL_IDENTIFIER_PROPERTIES));
+        }
+    }
+
+    private void recordUnknownServiceUnregistration(ServiceEvent event) {
+        String[] classNames = (String[]) event.getServiceReference().getProperty(Constants.OBJECTCLASS);
+        synchronized (unidentifiedRegistrationsByClassName) {
+            for ( String className : classNames )
+                unidentifiedRegistrationsByClassName.compute(className, (k,v) -> v == null ? 1 : ++v);
+        }
+    }
+    
+    // visible for testing
+    Map<ServiceIdentifier, ServiceRegistrationsTracker> getRegistrations() {
+        synchronized (registrations) {
+            return new HashMap<>(registrations);
+        }
+    }
+    
+    // visible for testing
+    Map<String, Integer> getUnidentifiedRegistrationsByClassName() {
+        synchronized (unidentifiedRegistrationsByClassName) {
+            return unidentifiedRegistrationsByClassName;
+        }
+    }
+    
+    public List<ServiceRestartCounter> getServiceRestartCounters() {
+        synchronized (registrations) {
+            return registrations.values().stream()
+                .filter( r -> r.restartCount() > 0)
+                .map( ServiceRegistrationsTracker::toServiceRestartCounter )
+                .collect(Collectors.toList());
+        }
+    }
+    
+    static class ServiceIdentifier {
+        private String key;
+        private String value;
+        private String additionalInfo;
+
+        public ServiceIdentifier(String key, String value) {
+            this.key = key;
+            this.value = value;
+        }
+        
+        public void setAdditionalInfo(String additionalInfo) {
+            this.additionalInfo = additionalInfo;
+        }
+
+        @Override
+        public int hashCode() {
+            
+            return Objects.hash(key, value);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj)
+                return true;
+            if (obj == null)
+                return false;
+            if (getClass() != obj.getClass())
+                return false;
+            ServiceIdentifier other = (ServiceIdentifier) obj;
+            
+            return Objects.equals(key, other.key) && Objects.equals(value, other.value);
+        }
+        
+        @Override
+        public String toString() {
+            return this.key + "=" + this.value + ( additionalInfo != null ? "(" + additionalInfo + ")" : "") ;
+        }
+    }
+    
+    static class ServiceRegistrationsTracker {
+        private final ServiceIdentifier id;
+        private int registrationCount;
+        private int unregistrationCount;
+        
+        public ServiceRegistrationsTracker(ServiceIdentifier id) {
+            this.id = id;
+        }
+        
+        public void registered() {
+            this.registrationCount++;
+        }
+        
+        public void unregistered() {
+            this.unregistrationCount++;
+        }
+        
+        public int restartCount() {
+            if ( unregistrationCount == 0 )
+                return 0;
+            
+            return registrationCount - 1;
+        }
+        
+        public ServiceRestartCounter toServiceRestartCounter() {
+            return new ServiceRestartCounter(id.toString(), restartCount());
+        }
+    }
+}
diff --git a/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/impl/ServiceTrackerCustomizerAdapter.java b/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/impl/ServiceTrackerCustomizerAdapter.java
new file mode 100644
index 0000000..bfa6054
--- /dev/null
+++ b/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/impl/ServiceTrackerCustomizerAdapter.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.felix.metrics.osgi.impl;
+
+import org.osgi.framework.ServiceReference;
+import org.osgi.util.tracker.ServiceTrackerCustomizer;
+
+public abstract class ServiceTrackerCustomizerAdapter<S, T> implements ServiceTrackerCustomizer<S, T> {
+
+    @Override
+    public void modifiedService(ServiceReference<S> reference, T service) {
+        // nothing by default
+    }
+
+    @Override
+    public void removedService(ServiceReference<S> reference, T service) {
+        // nothing by default
+    }
+}
diff --git a/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/impl/StartupTimeCalculator.java b/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/impl/StartupTimeCalculator.java
new file mode 100644
index 0000000..3fbeb42
--- /dev/null
+++ b/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/impl/StartupTimeCalculator.java
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.felix.metrics.osgi.impl;
+
+import java.lang.management.ManagementFactory;
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+
+import org.apache.felix.metrics.osgi.BundleStartDuration;
+import org.apache.felix.metrics.osgi.ServiceRestartCounter;
+import org.apache.felix.metrics.osgi.StartupMetrics;
+import org.apache.felix.metrics.osgi.StartupMetricsListener;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.util.tracker.ServiceTracker;
+
+public class StartupTimeCalculator {
+
+    // delay activation until the system is marked as ready
+    // don't explicitly import the systemready class as this bundle must be started as early as possible in order
+    // to record bundle starup times
+    
+    static final String PROPERTY_READINESS_DELAY = "org.apache.felix.metrics.osgi.additionalReadinessDelayMillis";
+    private ServiceTracker<Object, Object> readyTracker;
+    private ServiceTracker<StartupMetricsListener, StartupMetricsListener> listenersTracker;
+    private BundleStartTimeCalculator bundleCalculator;
+    private ServiceRestartCountCalculator serviceCalculator;
+    private ScheduledExecutorService executor;
+    private Future<Void> future;
+    private Supplier<StartupMetrics> metricsSupplier;
+    private long additionalReadinessDelayMillis = TimeUnit.SECONDS.toMillis(5);
+
+    public StartupTimeCalculator(BundleContext ctx, BundleStartTimeCalculator bundleCalculator, ServiceRestartCountCalculator serviceCalculator) throws InvalidSyntaxException {
+        executor = Executors.newScheduledThreadPool(1);
+        try {
+            String readinessDelay = ctx.getProperty(PROPERTY_READINESS_DELAY);
+            additionalReadinessDelayMillis = Long.parseLong(readinessDelay);
+        } catch ( NumberFormatException e) {
+            Log.debug(getClass(), "Failed parsing readiness delay", e);
+        }
+        this.bundleCalculator = bundleCalculator;
+        this.serviceCalculator = serviceCalculator;
+        this.readyTracker = new ServiceTracker<>(ctx, 
+                ctx.createFilter("(|(" + Constants.OBJECTCLASS+"=org.apache.felix.systemready.SystemReady)(&(" + Constants.OBJECTCLASS+ "=org.apache.felix.hc.api.condition.Healthy)(tag=systemalive)))"),
+                new ServiceTrackerCustomizerAdapter<Object, Object>() {
+
+                    @Override
+                    public Object addingService(ServiceReference<Object> reference) {
+                        if ( future == null ) 
+                            future = calculate();
+                        return ctx.getService(reference);
+                    }
+                    
+                    @Override
+                    public void removedService(ServiceReference<Object> reference, Object service) {
+                        if ( future != null && !future.isDone() ) {
+                            boolean cancelled = future.cancel(false);
+                            if ( cancelled ) {
+                                metricsSupplier = null;
+                                future = null;
+                            }
+                        }
+                    }
+                });
+        this.readyTracker.open();
+        
+        this.listenersTracker = new ServiceTracker<>(ctx, StartupMetricsListener.class, new ServiceTrackerCustomizerAdapter<StartupMetricsListener, StartupMetricsListener>() {
+            @Override
+            public StartupMetricsListener addingService(ServiceReference<StartupMetricsListener> reference) {
+                StartupMetricsListener service = ctx.getService(reference);
+                // TODO - there is still a minor race condition, between the supplier being set and the registration of services
+                // which can cause the listener to receive the event twice
+                if ( metricsSupplier != null )
+                    service.onStartupComplete(metricsSupplier.get());
+                return service;
+            }
+        });
+        this.listenersTracker.open();
+    }
+
+    public void close() {
+        this.readyTracker.close();
+    }
+
+    private Future<Void> calculate() {
+
+        long currentMillis = Clock.systemUTC().millis();
+        
+        return executor.schedule(() -> {
+            long startupMillis = ManagementFactory.getRuntimeMXBean().getStartTime();
+            
+            Duration startupDuration = Duration.ofMillis(currentMillis - startupMillis);
+            Instant startupInstant = Instant.ofEpochMilli(startupMillis);
+            List<BundleStartDuration> bundleDurations = bundleCalculator.getBundleStartDurations();
+            List<ServiceRestartCounter> serviceRestarts = serviceCalculator.getServiceRestartCounters();
+            
+            metricsSupplier = () -> {
+                return StartupMetrics.Builder.withJvmStartup(startupInstant)
+                    .withStartupTime(startupDuration)
+                    .withBundleStartDurations(bundleDurations)
+                    .withServiceRestarts(serviceRestarts)
+                    .build();
+            };
+            
+            for ( StartupMetricsListener listener : listenersTracker.getServices(new StartupMetricsListener[0]) )
+                listener.onStartupComplete(metricsSupplier.get());
+            
+            return null;
+        }, additionalReadinessDelayMillis, TimeUnit.MILLISECONDS);
+        
+
+    }
+}
diff --git a/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/package-info.java b/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/package-info.java
new file mode 100644
index 0000000..12d77b2
--- /dev/null
+++ b/metrics/osgi/collector/src/main/java/org/apache/felix/metrics/osgi/package-info.java
@@ -0,0 +1,18 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+@org.osgi.annotation.versioning.Version("1.0.0")
+package org.apache.felix.metrics.osgi;
diff --git a/metrics/osgi/collector/src/test/java/org/apache/felix/metrics/osgi/impl/AbstractIT.java b/metrics/osgi/collector/src/test/java/org/apache/felix/metrics/osgi/impl/AbstractIT.java
new file mode 100644
index 0000000..f7fc59a
--- /dev/null
+++ b/metrics/osgi/collector/src/test/java/org/apache/felix/metrics/osgi/impl/AbstractIT.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.felix.metrics.osgi.impl;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.ops4j.pax.exam.CoreOptions.bundle;
+import static org.ops4j.pax.exam.CoreOptions.composite;
+import static org.ops4j.pax.exam.CoreOptions.frameworkProperty;
+import static org.ops4j.pax.exam.CoreOptions.junitBundles;
+import static org.ops4j.pax.exam.CoreOptions.mavenBundle;
+import static org.ops4j.pax.exam.CoreOptions.options;
+
+import java.util.Arrays;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.inject.Inject;
+
+import org.apache.felix.metrics.osgi.StartupMetrics;
+import org.apache.felix.metrics.osgi.StartupMetricsListener;
+import org.apache.felix.metrics.osgi.impl.StartupTimeCalculator;
+import org.junit.Test;
+import org.ops4j.pax.exam.Configuration;
+import org.ops4j.pax.exam.Option;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceRegistration;
+
+public abstract class AbstractIT {
+
+    private static final String TESTED_BUNDLE_LOCATION = "reference:file:target/classes";
+
+    @Inject
+    protected BundleContext bc;
+
+    @Configuration
+    public Option[] config() {
+        return options(
+            // lower timeout, we don't have bounces
+            frameworkProperty(StartupTimeCalculator.PROPERTY_READINESS_DELAY).value("100"),
+            bundle(TESTED_BUNDLE_LOCATION),
+            junitBundles(),
+            mavenBundle("org.apache.felix", "org.apache.felix.scr", "2.1.16"),
+            mavenBundle("org.osgi", "org.osgi.util.promise", "1.1.1"),
+            mavenBundle("org.osgi", "org.osgi.util.function", "1.1.0"),
+            composite(specificOptions())
+        );
+    }
+    
+    protected abstract Option[] specificOptions();
+
+    @Test
+    public void registerListenerAfterSystemIsReady() throws InterruptedException {
+        runBasicTest(false);
+    }
+
+    @Test
+    public void registerListenerBeforeSystemIsReady() throws InterruptedException {
+        runBasicTest(true);
+    }
+
+    private void runBasicTest(boolean registerListenerFirst) throws InterruptedException {
+        
+        Set<String> expectedBundleNames = Arrays.stream(bc.getBundles())
+            .filter( b -> b.getBundleId() != Constants.SYSTEM_BUNDLE_ID ) // no framework bundle
+            .filter( b -> !b.getLocation().equals(TESTED_BUNDLE_LOCATION) ) // not the bundle under test
+            .map ( b -> b.getSymbolicName() )
+            .filter( bsn ->  ! bsn.startsWith("org.ops4j")  ) // no ops4j bundles
+            .filter( bsn ->  ! bsn.startsWith("PAXEXAM")  ) // no ops4j bundles
+            .filter( bsn -> ! bsn.contains("geronimo-atinject")) // injected early on by Pax-Exam
+            .collect(Collectors.toSet());
+        
+        WaitForResultsStartupMetricsListener listener = new WaitForResultsStartupMetricsListener();
+
+        // service that will be tracked as restarting
+        Runnable foo = () -> {};
+        Dictionary<String, Object> props = new Hashtable<>();
+        props.put(Constants.SERVICE_PID, "some.service.pid");
+        ServiceRegistration<Runnable> reg = bc.registerService(Runnable.class, foo, props);
+        reg.unregister();
+        reg = bc.registerService(Runnable.class, foo, props);
+        reg.unregister();
+
+        if ( registerListenerFirst ) {
+            markSystemReady();
+            bc.registerService(StartupMetricsListener.class, listener, null);
+        } else {
+            markSystemReady();
+            bc.registerService(StartupMetricsListener.class, listener, null);
+        }
+        
+        StartupMetrics metrics = listener.getMetrics();
+        
+        assertThat(metrics, notNullValue());
+        Set<String> trackedBundleNames = metrics.getBundleStartDurations().stream()
+                .map( bsd -> bsd.getSymbolicName())
+                .collect(Collectors.toSet());
+        
+        assertTrue("Tracked bundle names " + trackedBundleNames + " did not contain " + expectedBundleNames, 
+            trackedBundleNames.containsAll(expectedBundleNames));
+        
+        assertThat("Service restarts", metrics.getServiceRestarts().size(), equalTo(1));
+        assertThat("Restarted component service identifier", metrics.getServiceRestarts().get(0).getServiceIdentifier(), equalTo(Constants.SERVICE_PID+"="+props.get(Constants.SERVICE_PID)));
+    }
+
+    protected abstract void markSystemReady();
+}
diff --git a/metrics/osgi/collector/src/test/java/org/apache/felix/metrics/osgi/impl/BundleStartTimeCalculatorTest.java b/metrics/osgi/collector/src/test/java/org/apache/felix/metrics/osgi/impl/BundleStartTimeCalculatorTest.java
new file mode 100644
index 0000000..70d18e7
--- /dev/null
+++ b/metrics/osgi/collector/src/test/java/org/apache/felix/metrics/osgi/impl/BundleStartTimeCalculatorTest.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.felix.metrics.osgi.impl;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import java.time.Instant;
+
+import org.apache.felix.metrics.osgi.BundleStartDuration;
+import org.apache.felix.metrics.osgi.impl.BundleStartTimeCalculator;
+import org.hamcrest.CoreMatchers;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleEvent;
+
+public class BundleStartTimeCalculatorTest {
+
+    @Test
+    public void bundleStarted() {
+
+        Instant now = Instant.now();
+        
+        BundleStartTimeCalculator c = new BundleStartTimeCalculator(1l);
+        Bundle mockBundle = newMockBundle(5l, "foo");
+        c.bundleChanged(new BundleEvent(BundleEvent.STARTING, mockBundle));
+        c.bundleChanged(new BundleEvent(BundleEvent.STARTED, mockBundle));
+        
+        assertThat("Expected one entry for bundle durations",c.getBundleStartDurations().size(), CoreMatchers.equalTo(1));
+        BundleStartDuration duration = c.getBundleStartDurations().get(0);
+        assertThat("Bundle duration refers to wrong bundle symbolic name", duration.getSymbolicName(), CoreMatchers.equalTo("foo"));
+        
+        assertTrue("Bundle STARTING time (" + duration.getStartingAt() + " must be after test start time(" + now + ")", 
+                duration.getStartingAt().isAfter(now));
+        assertFalse("Bundle start duration (" + duration.getStartedAfter() + ") must not be negative",
+                duration.getStartedAfter().isNegative());
+    }
+    
+    private Bundle newMockBundle(long id, String symbolicName) {
+        
+        Bundle mockBundle = Mockito.mock(Bundle.class);
+        Mockito.when(mockBundle.getBundleId()).thenReturn(id);
+        Mockito.when(mockBundle.getSymbolicName()).thenReturn(symbolicName);
+        return mockBundle;
+    }
+}
diff --git a/metrics/osgi/collector/src/test/java/org/apache/felix/metrics/osgi/impl/HealthCheckSmokeIT.java b/metrics/osgi/collector/src/test/java/org/apache/felix/metrics/osgi/impl/HealthCheckSmokeIT.java
new file mode 100644
index 0000000..f30fcdb
--- /dev/null
+++ b/metrics/osgi/collector/src/test/java/org/apache/felix/metrics/osgi/impl/HealthCheckSmokeIT.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.felix.metrics.osgi.impl;
+
+import static org.ops4j.pax.exam.CoreOptions.frameworkProperty;
+import static org.ops4j.pax.exam.CoreOptions.mavenBundle;
+import static org.ops4j.pax.exam.CoreOptions.options;
+
+import java.util.Dictionary;
+import java.util.Hashtable;
+
+import javax.inject.Inject;
+
+import org.apache.felix.hc.api.condition.Healthy;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.osgi.framework.BundleContext;
+
+@RunWith(PaxExam.class)
+public class HealthCheckSmokeIT extends AbstractIT {
+
+    @Inject
+    private BundleContext bc;
+    
+    @Override
+    protected Option[] specificOptions() {
+        return options(
+            frameworkProperty("org.apache.felix.http.enable").value("false"),
+            mavenBundle("org.apache.felix", "org.apache.felix.healthcheck.api", "2.0.4"),
+            mavenBundle("org.apache.felix", "org.apache.felix.healthcheck.core", "2.0.8"),
+            mavenBundle("org.apache.felix", "org.apache.felix.healthcheck.generalchecks", "2.0.4"),
+            mavenBundle("org.apache.felix", "org.apache.felix.http.servlet-api", "1.1.2"),
+            mavenBundle("org.apache.felix", "org.apache.felix.http.jetty", "4.0.18"),
+            mavenBundle("org.apache.commons", "commons-lang3", "3.9"),
+            mavenBundle("org.apache.felix", "org.apache.felix.eventadmin", "1.5.0"),
+            mavenBundle("org.apache.felix", "org.apache.felix.rootcause", "0.1.0")
+        );
+    }
+    
+    @Override
+    protected void markSystemReady() {
+        Dictionary<String, Object> regProps = new Hashtable<>();
+        regProps.put("tag", "systemalive");
+        bc.registerService(Healthy.class, new Healthy() {}, regProps);
+    }
+}
diff --git a/metrics/osgi/collector/src/test/java/org/apache/felix/metrics/osgi/impl/ServiceRegistrationsTrackerTest.java b/metrics/osgi/collector/src/test/java/org/apache/felix/metrics/osgi/impl/ServiceRegistrationsTrackerTest.java
new file mode 100644
index 0000000..e724829
--- /dev/null
+++ b/metrics/osgi/collector/src/test/java/org/apache/felix/metrics/osgi/impl/ServiceRegistrationsTrackerTest.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.felix.metrics.osgi.impl;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.junit.Assert.assertThat;
+
+import org.apache.felix.metrics.osgi.impl.ServiceRestartCountCalculator.ServiceIdentifier;
+import org.apache.felix.metrics.osgi.impl.ServiceRestartCountCalculator.ServiceRegistrationsTracker;
+import org.junit.Before;
+import org.junit.Test;
+import org.osgi.framework.Constants;
+
+public class ServiceRegistrationsTrackerTest {
+
+    private ServiceRegistrationsTracker tracker;
+    
+    @Before
+    public void prepare() {
+        tracker = new ServiceRegistrationsTracker(new ServiceIdentifier(Constants.SERVICE_PID, "foo"));
+    }
+
+    @Test
+    public void singleRegister() {
+        tracker.registered();
+        assertThat(tracker.restartCount(), equalTo(0));
+    }
+
+    @Test
+    public void registerUnregister() {
+        tracker.registered();
+        tracker.unregistered();
+        assertThat(tracker.restartCount(), equalTo(0));
+    }
+
+    @Test
+    public void singleRestart() {
+        tracker.registered();
+        tracker.unregistered();
+        tracker.registered();
+        assertThat(tracker.restartCount(), equalTo(1));
+    }
+
+    @Test
+    public void singleRestartAndUnregister() {
+        tracker.registered();
+        tracker.unregistered();
+        tracker.registered();
+        tracker.unregistered();
+        assertThat(tracker.restartCount(), equalTo(1));
+    }
+    
+    @Test
+    public void twoRestarts() {
+        tracker.registered();
+        tracker.unregistered();
+        tracker.registered();
+        tracker.unregistered();
+        tracker.registered();
+        assertThat(tracker.restartCount(), equalTo(2));
+    }
+}
diff --git a/metrics/osgi/collector/src/test/java/org/apache/felix/metrics/osgi/impl/ServiceRestartCountCalculatorTest.java b/metrics/osgi/collector/src/test/java/org/apache/felix/metrics/osgi/impl/ServiceRestartCountCalculatorTest.java
new file mode 100644
index 0000000..5b89ee8
--- /dev/null
+++ b/metrics/osgi/collector/src/test/java/org/apache/felix/metrics/osgi/impl/ServiceRestartCountCalculatorTest.java
@@ -0,0 +1,192 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.felix.metrics.osgi.impl;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.junit.Assert.assertThat;
+
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.felix.metrics.osgi.impl.ServiceRestartCountCalculator;
+import org.hamcrest.CoreMatchers;
+import org.junit.Test;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+
+public class ServiceRestartCountCalculatorTest {
+
+    @Test
+    public void ignoredEventTypes() {
+        
+        ServiceRestartCountCalculator srcc = new ServiceRestartCountCalculator();
+        srcc.serviceChanged(new ServiceEvent(ServiceEvent.MODIFIED, new DummyServiceReference<>(new HashMap<>())));
+        srcc.serviceChanged(new ServiceEvent(ServiceEvent.MODIFIED_ENDMATCH, new DummyServiceReference<>(new HashMap<>())));
+        
+        assertThat(srcc.getRegistrations().size(), equalTo(0));
+    }
+
+    @Test
+    public void serviceWithServicePidProperty() {
+        
+        assertServiceWithPropertyIsTracked(Constants.SERVICE_PID);
+    }
+
+    @Test
+    public void serviceWithComponentNameProperty() {
+        
+        assertServiceWithPropertyIsTracked("component.name");
+    }
+
+    @Test
+    public void serviceWithJmxObjectNameProperty() {
+        
+        assertServiceWithPropertyIsTracked("jmx.objectname");
+    }
+
+    @Test
+    public void metricsGaugesAreTracked() {
+        HashMap<String, Object> props = new HashMap<>();
+        props.put(Constants.OBJECTCLASS, new String[] { "org.apache.sling.commons.metrics.Gauge" });
+        props.put("name", "commons.threads.tp.script-cache-thread-pool.Name");
+        DummyServiceReference<Object> dsr = new DummyServiceReference<>(props);
+        
+        ServiceRestartCountCalculator srcc = new ServiceRestartCountCalculator();
+        srcc.serviceChanged(new ServiceEvent(ServiceEvent.REGISTERED, dsr));
+        
+        assertThat(srcc.getRegistrations().size(), equalTo(1));
+    }
+
+    @Test
+    public void unknownServiceIsNotTracked() {
+        HashMap<String, Object> props = new HashMap<>();
+        props.put(Constants.OBJECTCLASS, new String[] { "foo" });
+        DummyServiceReference<Object> dsr = new DummyServiceReference<>(props);
+        
+        ServiceRestartCountCalculator srcc = new ServiceRestartCountCalculator();
+        srcc.serviceChanged(new ServiceEvent(ServiceEvent.REGISTERED, dsr));
+        
+        assertThat(srcc.getRegistrations().size(), equalTo(0));
+        assertThat(srcc.getUnidentifiedRegistrationsByClassName().size(), equalTo(0));
+    }
+    
+    @Test
+    public void unknownServiceUnregistrationsAreTracked() {
+        HashMap<String, Object> props = new HashMap<>();
+        props.put(Constants.OBJECTCLASS, new String[] { "foo", "bar" });
+        DummyServiceReference<Object> sr1 = new DummyServiceReference<>(props);
+        
+        HashMap<String, Object> props2 = new HashMap<>();
+        props2.put(Constants.OBJECTCLASS, new String[] { "foo"} );
+        DummyServiceReference<Object> sr2 = new DummyServiceReference<>(props2);
+        
+        ServiceRestartCountCalculator srcc = new ServiceRestartCountCalculator();
+        srcc.serviceChanged(new ServiceEvent(ServiceEvent.REGISTERED, sr1));
+        srcc.serviceChanged(new ServiceEvent(ServiceEvent.UNREGISTERING, sr1));
+        
+        srcc.serviceChanged(new ServiceEvent(ServiceEvent.REGISTERED, sr2));
+        srcc.serviceChanged(new ServiceEvent(ServiceEvent.UNREGISTERING, sr2));
+        
+        assertThat(srcc.getRegistrations().size(), equalTo(0));
+        Map<String, Integer> unidentifiedRegistrations = srcc.getUnidentifiedRegistrationsByClassName();
+        assertThat(unidentifiedRegistrations.size(), equalTo(2));
+        assertThat(unidentifiedRegistrations.get("foo"), equalTo(2));
+        assertThat(unidentifiedRegistrations.get("bar"), equalTo(1));
+    }
+    
+    private void assertServiceWithPropertyIsTracked(String propertyName) {
+        
+        HashMap<String, Object> props = new HashMap<>();
+        props.put(propertyName, new String[] { "foo.bar" });
+        DummyServiceReference<Object> dsr = new DummyServiceReference<>(props);
+        
+        ServiceRestartCountCalculator srcc = new ServiceRestartCountCalculator();
+        srcc.serviceChanged(new ServiceEvent(ServiceEvent.REGISTERED, dsr));
+        
+        assertThat(srcc.getRegistrations().size(), CoreMatchers.equalTo(1));
+    }
+    
+    static class DummyServiceRegistration<S> implements ServiceRegistration<S> {
+        
+        private final DummyServiceReference<S> sr;
+
+        public DummyServiceRegistration(Map<String, Object> props) {
+            this.sr = new DummyServiceReference<>(props);
+        }
+
+        @Override
+        public ServiceReference<S> getReference() {
+            return sr;
+        }
+
+        @Override
+        public void setProperties(Dictionary<String, ?> properties) {
+            throw new UnsupportedOperationException();
+            
+        }
+
+        @Override
+        public void unregister() {
+            throw new UnsupportedOperationException();            
+        }
+    }
+    
+    static class DummyServiceReference<S> implements ServiceReference<S> {
+        
+        private final Map<String, Object> props;
+
+        public DummyServiceReference(Map<String, Object> props) {
+            this.props = props;
+        }
+
+        @Override
+        public Object getProperty(String key) {
+            return props.get(key);
+        }
+
+        @Override
+        public String[] getPropertyKeys() {
+            return props.keySet().toArray(new String[0]);
+        }
+
+        @Override
+        public Bundle getBundle() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public Bundle[] getUsingBundles() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean isAssignableTo(Bundle bundle, String className) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public int compareTo(Object reference) {
+            throw new UnsupportedOperationException();
+        }
+        
+    }
+
+}
diff --git a/metrics/osgi/collector/src/test/java/org/apache/felix/metrics/osgi/impl/SystemReadySmokeIT.java b/metrics/osgi/collector/src/test/java/org/apache/felix/metrics/osgi/impl/SystemReadySmokeIT.java
new file mode 100644
index 0000000..2d9cfc2
--- /dev/null
+++ b/metrics/osgi/collector/src/test/java/org/apache/felix/metrics/osgi/impl/SystemReadySmokeIT.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.felix.metrics.osgi.impl;
+
+import static org.ops4j.pax.exam.CoreOptions.mavenBundle;
+import static org.ops4j.pax.exam.CoreOptions.options;
+
+import javax.inject.Inject;
+
+import org.apache.felix.systemready.SystemReady;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.osgi.framework.BundleContext;
+
+@RunWith(PaxExam.class)
+public class SystemReadySmokeIT extends AbstractIT {
+
+    @Inject
+    protected BundleContext bc;
+
+    @Override
+    protected Option[] specificOptions() {
+        return options(
+            mavenBundle("org.apache.felix", "org.apache.felix.systemready", "0.4.2"),
+            mavenBundle("org.apache.felix", "org.apache.felix.rootcause", "0.1.0")
+        );
+    }
+    protected void markSystemReady() {
+        bc.registerService(SystemReady.class, new SystemReady() {}, null);
+    }
+
+}
diff --git a/metrics/osgi/collector/src/test/java/org/apache/felix/metrics/osgi/impl/WaitForResultsStartupMetricsListener.java b/metrics/osgi/collector/src/test/java/org/apache/felix/metrics/osgi/impl/WaitForResultsStartupMetricsListener.java
new file mode 100644
index 0000000..42bbba1
--- /dev/null
+++ b/metrics/osgi/collector/src/test/java/org/apache/felix/metrics/osgi/impl/WaitForResultsStartupMetricsListener.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.felix.metrics.osgi.impl;
+
+import java.util.concurrent.CountDownLatch;
+
+import org.apache.felix.metrics.osgi.StartupMetrics;
+import org.apache.felix.metrics.osgi.StartupMetricsListener;
+
+class WaitForResultsStartupMetricsListener implements StartupMetricsListener {
+    
+    private final CountDownLatch latch = new CountDownLatch(1);
+    private StartupMetrics metrics;
+
+    @Override
+    public void onStartupComplete(StartupMetrics metrics) {
+        this.metrics = metrics;
+        latch.countDown();
+    }
+
+    public StartupMetrics getMetrics() {
+        try {
+            latch.await();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new RuntimeException(e);
+        }
+        return metrics;
+    }
+    
+}
\ No newline at end of file
diff --git a/metrics/osgi/consumers/bnd.bnd b/metrics/osgi/consumers/bnd.bnd
new file mode 100644
index 0000000..0be6687
--- /dev/null
+++ b/metrics/osgi/consumers/bnd.bnd
@@ -0,0 +1 @@
+Conditional-Package: org.apache.felix.utils.json
\ No newline at end of file
diff --git a/metrics/osgi/consumers/pom.xml b/metrics/osgi/consumers/pom.xml
new file mode 100644
index 0000000..5b02583
--- /dev/null
+++ b/metrics/osgi/consumers/pom.xml
@@ -0,0 +1,104 @@
+<?xml version="1.0"?>
+<!-- Licensed to the Apache Software Foundation (ASF) under one or more contributor 
+    license agreements. See the NOTICE file distributed with this work for additional 
+    information regarding copyright ownership. The ASF licenses this file to 
+    you 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/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.sling</groupId>
+        <artifactId>sling-bundle-parent</artifactId>
+        <version>38</version>
+        <relativePath />
+    </parent>
+
+    <groupId>org.apache.felix</groupId>
+    <artifactId>org.apache.felix.metrics.osgi.consumers</artifactId>
+    <version>0.1.0-SNAPSHOT</version>
+
+    <name>Apache Felix OSGi Metrics Consumers</name>
+    <description> 
+        Provides various out-of-the-box consumers for OSGi framework metrics.
+    </description>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>biz.aQute.bnd</groupId>
+                <artifactId>bnd-baseline-maven-plugin</artifactId>
+                <configuration>
+                    <failOnMissing>false</failOnMissing>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.annotation.versioning</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.annotation.bundle</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.service.component.annotations</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.service.metatype.annotations</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>osgi.core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard.metrics</groupId>
+            <artifactId>metrics-core</artifactId>
+            <version>3.2.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.metrics.osgi.collector</artifactId>
+            <version>0.1.0-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+             <artifactId>org.apache.felix.utils</artifactId>
+             <version>1.11.4</version>
+             <scope>provided</scope>
+        </dependency>
+        
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <version>3.3.3</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/metrics/osgi/consumers/src/main/java/org/apache/felix/metrics/osgi/consumers/impl/dropwizard/DropwizardMetricsListener.java b/metrics/osgi/consumers/src/main/java/org/apache/felix/metrics/osgi/consumers/impl/dropwizard/DropwizardMetricsListener.java
new file mode 100644
index 0000000..7333576
--- /dev/null
+++ b/metrics/osgi/consumers/src/main/java/org/apache/felix/metrics/osgi/consumers/impl/dropwizard/DropwizardMetricsListener.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.felix.metrics.osgi.consumers.impl.dropwizard;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.felix.metrics.osgi.StartupMetrics;
+import org.apache.felix.metrics.osgi.StartupMetricsListener;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.Metric;
+import com.codahale.metrics.MetricRegistry;
+
+@Component
+@Designate(ocd = DropwizardMetricsListener.Config.class)
+public class DropwizardMetricsListener implements StartupMetricsListener {
+
+    @ObjectClassDefinition(name = "Apache Felix Dropwizard Startup Metrics Listener")
+    public @interface Config {
+        @AttributeDefinition(name = "Service Restart Threshold", description="Minimum number of service restarts during startup needed to create a metric for the service")
+        int serviceRestartThreshold() default 3;
+        @AttributeDefinition(name = "Slow Bundle Startup Threshold", description="Minimum bundle startup duration in milliseconds needed to create a metric for the bundle")
+        long slowBundleThresholdMillis() default 200;
+    }
+
+    private static final String APPLICATION_STARTUP_GAUGE_NAME = "osgi.application_startup_time_millis";
+    private static final String BUNDLE_STARTUP_GAUGE_NAME_PREFIX = "osgi.slow_bundle_startup_time_millis.";
+    private static final String SERVICE_RESTART_GAUGE_NAME_PREFIX = "osgi.excessive_service_restarts_count.";
+    
+    @Reference
+    private MetricRegistry registry;
+    
+    private int serviceRestartThreshold;
+    private long slowBundleThresholdMillis;
+    private List<String> registeredMetricNames = new ArrayList<>();
+    
+    @Activate
+    protected void activate(Config cfg) {
+        this.serviceRestartThreshold = cfg.serviceRestartThreshold();
+        this.slowBundleThresholdMillis = cfg.slowBundleThresholdMillis();
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        registeredMetricNames.forEach( m -> registry.remove(m) );
+    }
+    
+    @Override
+    public void onStartupComplete(StartupMetrics event) {
+        register(APPLICATION_STARTUP_GAUGE_NAME, (Gauge<Long>) () -> event.getStartupTime().toMillis() );
+        event.getBundleStartDurations().stream()
+            .filter( bsd -> bsd.getStartedAfter().toMillis() >= slowBundleThresholdMillis )
+            .forEach( bsd -> register(BUNDLE_STARTUP_GAUGE_NAME_PREFIX + bsd.getSymbolicName(), (Gauge<Long>) () -> bsd.getStartedAfter().toMillis()));
+        event.getServiceRestarts().stream()
+            .filter( src -> src.getServiceRestarts() >= serviceRestartThreshold )
+            .forEach( src -> register(SERVICE_RESTART_GAUGE_NAME_PREFIX + src.getServiceIdentifier(), (Gauge<Integer>) src::getServiceRestarts) );
+    }
+    
+    private void register(String name, Metric metric) {
+        registry.register(name, metric);
+        registeredMetricNames.add(name);
+    }
+}
diff --git a/metrics/osgi/consumers/src/main/java/org/apache/felix/metrics/osgi/consumers/impl/json/JsonWritingMetricsListener.java b/metrics/osgi/consumers/src/main/java/org/apache/felix/metrics/osgi/consumers/impl/json/JsonWritingMetricsListener.java
new file mode 100644
index 0000000..4174e39
--- /dev/null
+++ b/metrics/osgi/consumers/src/main/java/org/apache/felix/metrics/osgi/consumers/impl/json/JsonWritingMetricsListener.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.felix.metrics.osgi.consumers.impl.json;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+
+import org.apache.felix.metrics.osgi.BundleStartDuration;
+import org.apache.felix.metrics.osgi.ServiceRestartCounter;
+import org.apache.felix.metrics.osgi.StartupMetrics;
+import org.apache.felix.metrics.osgi.StartupMetricsListener;
+import org.apache.felix.utils.json.JSONWriter;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Component
+public class JsonWritingMetricsListener implements StartupMetricsListener {
+
+    private final Logger logger = LoggerFactory.getLogger(getClass());
+    
+    private BundleContext ctx;
+
+    @Activate
+    protected void activate(BundleContext ctx) {
+        this.ctx = ctx;
+    }
+
+    @Override
+    public void onStartupComplete(StartupMetrics metrics) {
+
+        File metricsFile = ctx.getDataFile("startup-metrics-" + System.currentTimeMillis() + ".json");
+        if ( metricsFile == null ) {
+            logger.warn("Unable to get data file in the bundle area, startup metrics will not be written");
+            return;
+        }
+        
+        try {
+            try ( FileWriter fw = new FileWriter(metricsFile)) {
+                JSONWriter w = new JSONWriter(fw);
+                w.object();
+                // application metrics
+                w.key("application");
+                w.object();
+                w.key("startTimeMillis").value(metrics.getJvmStartup().toEpochMilli());
+                w.key("startDurationMillis").value(metrics.getStartupTime().toMillis());
+                w.endObject();
+                
+                // bundle metrics
+                w.key("bundles");
+                w.array();
+                for ( BundleStartDuration bsd : metrics.getBundleStartDurations() ) {
+                    w.object();
+                    w.key("symbolicName").value(bsd.getSymbolicName());
+                    w.key("startTimeMillis").value(bsd.getStartingAt().toEpochMilli());
+                    w.key("startDurationMillis").value(bsd.getStartedAfter().toMillis());
+                    w.endObject();
+                }
+                w.endArray();
+                
+                // service metrics
+                w.key("services");
+                w.array();
+                for ( ServiceRestartCounter src : metrics.getServiceRestarts() ) {
+                    w.object();
+                    w.key("identifier").value(src.getServiceIdentifier());
+                    w.key("restarts").value(src.getServiceRestarts());
+                    w.endObject();
+                }
+                w.endArray();
+                
+                w.endObject();
+            }
+        } catch (IOException e) {
+            logger.warn("Failed wrting startup metrics", e);
+        }
+    }
+}
diff --git a/metrics/osgi/consumers/src/main/java/org/apache/felix/metrics/osgi/consumers/impl/log/LoggingMetricsListener.java b/metrics/osgi/consumers/src/main/java/org/apache/felix/metrics/osgi/consumers/impl/log/LoggingMetricsListener.java
new file mode 100644
index 0000000..e96619e
--- /dev/null
+++ b/metrics/osgi/consumers/src/main/java/org/apache/felix/metrics/osgi/consumers/impl/log/LoggingMetricsListener.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.felix.metrics.osgi.consumers.impl.log;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.felix.metrics.osgi.BundleStartDuration;
+import org.apache.felix.metrics.osgi.ServiceRestartCounter;
+import org.apache.felix.metrics.osgi.StartupMetrics;
+import org.apache.felix.metrics.osgi.StartupMetricsListener;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Component
+@Designate(ocd = LoggingMetricsListener.Config.class)
+public class LoggingMetricsListener implements StartupMetricsListener {
+    
+    @ObjectClassDefinition(name = "Apache Felix Logging Startup Metrics Listener")
+    public @interface Config {
+        
+        @AttributeDefinition(name = "Service Restart Threshold", description="Minimum number of service restarts during startup needed log the number of service restarts")
+        int serviceRestartThreshold() default 3;
+        @AttributeDefinition(name = "Slow Bundle Startup Threshold", description="Minimum bundle startup duration in milliseconds needed to log the bundle startup time")
+        long slowBundleThresholdMillis() default 200;
+    }
+
+    private int serviceRestartThreshold;
+    private long slowBundleThresholdMillis;
+    
+    @Activate
+    protected void activate(Config cfg) {
+        this.serviceRestartThreshold = cfg.serviceRestartThreshold();
+        this.slowBundleThresholdMillis = cfg.slowBundleThresholdMillis();
+    }
+    
+    @Override
+    public void onStartupComplete(StartupMetrics event) {
+        Logger log = LoggerFactory.getLogger(getClass());
+        log.info("Application startup completed in {}", event.getStartupTime());
+
+        List<BundleStartDuration> slowStartBundles = event.getBundleStartDurations().stream()
+                .filter( bsd -> bsd.getStartedAfter().toMillis() >= slowBundleThresholdMillis )
+                .collect(Collectors.toList());
+        
+        if ( !slowStartBundles.isEmpty() && log.isInfoEnabled() ) {
+            StringBuilder logEntry = new StringBuilder();
+            logEntry.append("The following bundles started in more than ")
+                .append(slowBundleThresholdMillis)
+                .append(" milliseconds: \n");
+            slowStartBundles
+                .forEach( ssb -> logEntry.append("- ").append(ssb.getSymbolicName()).append(" : ").append(ssb.getStartedAfter()).append('\n'));
+            
+            log.info(logEntry.toString());
+        }
+        
+        List<ServiceRestartCounter> oftenRestartedServices = event.getServiceRestarts().stream()
+                .filter( src -> src.getServiceRestarts() >= serviceRestartThreshold )
+                .collect(Collectors.toList());
+        
+        if ( !oftenRestartedServices.isEmpty() && log.isInfoEnabled() ) {
+            StringBuilder logEntry = new StringBuilder();
+            logEntry.append("The following services have restarted more than ")
+                .append(serviceRestartThreshold)
+                .append(" times during startup :\n");
+            oftenRestartedServices
+                .forEach(ors -> logEntry.append("- ").append(ors.getServiceIdentifier()).append(" : ").append(ors.getServiceRestarts()).append(" restarts\n"));
+            
+            log.info(logEntry.toString());
+        }
+    }
+
+}
diff --git a/metrics/osgi/consumers/src/test/java/org/apache/felix/metrics/osgi/consumers/impl/json/JsonWritingMetricsListenerTest.java b/metrics/osgi/consumers/src/test/java/org/apache/felix/metrics/osgi/consumers/impl/json/JsonWritingMetricsListenerTest.java
new file mode 100644
index 0000000..bd877f4
--- /dev/null
+++ b/metrics/osgi/consumers/src/test/java/org/apache/felix/metrics/osgi/consumers/impl/json/JsonWritingMetricsListenerTest.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.felix.metrics.osgi.consumers.impl.json;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.hasItem;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Arrays;
+
+import org.apache.felix.metrics.osgi.BundleStartDuration;
+import org.apache.felix.metrics.osgi.ServiceRestartCounter;
+import org.apache.felix.metrics.osgi.StartupMetrics;
+import org.apache.felix.metrics.osgi.consumers.impl.json.JsonWritingMetricsListener;
+import org.apache.felix.utils.json.JSONParser;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.Mockito;
+import org.osgi.framework.BundleContext;
+
+public class JsonWritingMetricsListenerTest {
+
+    @Rule
+    public TemporaryFolder tmp = new TemporaryFolder();
+    
+    @Test
+    public void metricsArePersisted() throws IOException {
+        // bridge the mock bundle context with the temporary folder
+        BundleContext mockBundleContext = mock(BundleContext.class);
+        when(mockBundleContext.getDataFile(Mockito.anyString())).thenAnswer( i -> tmp.newFile(i.getArgument(0, String.class)));
+        
+        JsonWritingMetricsListener listener = new JsonWritingMetricsListener();
+        listener.activate(mockBundleContext);
+        
+        StartupMetrics metrics = StartupMetrics.Builder
+                .withJvmStartup(Instant.now())
+                .withStartupTime(Duration.ofMillis(50))
+                .withBundleStartDurations(Arrays.asList(new BundleStartDuration("foo", Instant.now(), Duration.ofMillis(5))))
+                .withServiceRestarts(Arrays.asList(new ServiceRestartCounter("some.service", 1)))
+                .build();
+        
+        listener.onStartupComplete(metrics);
+        
+        File[] files = tmp.getRoot().listFiles();
+        
+        assertThat("Bundle data area should hold one file", files.length, equalTo(1));
+        
+        File metricsFile = files[0];
+        try ( FileInputStream fis = new FileInputStream(metricsFile)) {
+            JSONParser p = new JSONParser(fis);
+            assertThat(p.getParsed().keySet(), hasItem("application"));
+            assertThat(p.getParsed().keySet(), hasItem("bundles"));
+            assertThat(p.getParsed().keySet(), hasItem("services"));
+        }
+    }
+}
diff --git a/metrics/osgi/pom.xml b/metrics/osgi/pom.xml
new file mode 100644
index 0000000..0e6ac13
--- /dev/null
+++ b/metrics/osgi/pom.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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/maven-v4_0_0.xsd">
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache</groupId>
+        <artifactId>apache</artifactId>
+        <version>18</version>
+        <relativePath/>
+    </parent>
+
+    <groupId>org.apache.felix</groupId>
+    <artifactId>felix-metrics-osgi-builder</artifactId>
+    <packaging>pom</packaging>
+    <version>1</version>
+
+    <name>Apache Felix OSGi Metrics (Builder)</name>
+
+    <modules>
+          <module>collector</module>
+          <module>consumers</module>
+    </modules>
+</project>
+