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:36 UTC
[felix-dev] 01/02: metrics/osgi: initial contribution
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>
+