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 <romb...@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> +