Add additional IT test and make it buildable with Java 17
diff --git a/healthcheck/README.md b/healthcheck/README.md
index 5a67422..8993ed6 100644
--- a/healthcheck/README.md
+++ b/healthcheck/README.md
@@ -1,4 +1,3 @@
-
# Felix Health Checks
Based on a simple `HealthCheck` SPI interface, Felix Health Checks are used to check the health/availability of Apache Felix instances at runtime based on inputs like
@@ -277,7 +276,7 @@
`registerHealthyMarkerService` | boolean | true | For the case a given tag/name is healthy, will register a service `org.apache.felix.hc.api.condition.Healthy` with property tag=<tagname> (or name=<hc.name>) that other services can depend on. For the special case of the tag `systemready`, the marker service `org.apache.felix.hc.api.condition.SystemReady` is registered
`registerUnhealthyMarkerService` | boolean | false | For the case a given tag/name is **un**healthy, will register a service `org.apache.felix.hc.api.condition.Unhealthy` with property tag=<tagname> (or name=<hc.name>) that other services can depend on
`treatWarnAsHealthy` | boolean | true | `WARN` usually means [the system is usable](#semantic-meaning-of-health-check-results), hence WARN is treated as healthy by default. When set to false `WARN` is treated as `Unhealthy`
-`sendEvents` | enum `NONE`, `STATUS_CHANGES`, `STATUS_CHANGES_OR_NOT_OK` or `ALL` | `STATUS_CHANGES` | Whether to send events for health check status changes. See [below](#osgi-events-for-health-check-status-changes) for details.
+`sendEvents` | enum `NONE`, `STATUS_CHANGES`, `STATUS_CHANGES_OR_NOT_OK` or `ALL` | `STATUS_CHANGES` | Whether to send events for health check status changes. See [below](#osgi-events-for-health-check-status-changes-and-updates) for details.
`logResults` | enum `NONE`, `STATUS_CHANGES`, `STATUS_CHANGES_OR_NOT_OK` or `ALL` | `NONE ` | Whether to log the result of the monitor to the regular log file
`logAllResultsAsInfo` | boolean | false | If `logResults` is enabled and this is enabled, all results will be logged with INFO log level. Otherwise WARN and INFO are used depending on the health state.
`isDynamic` | boolean | false | In dynamic mode all checks for names/tags are monitored individually (this means events are sent/services registered for name only, never for given tags). This mode allows to use `*` in tags to query for all health checks in system. It is also possible to query for all except certain tags by using `-`, e.g. by configuring the values `*`, `-tag1` and `-tag2` for `tags`.
@@ -385,23 +384,19 @@
| Name | Default/Required | Description |
| --- | --- | --- |
-| `osgi.http.whiteboard.filter.regex` | required | Regex path on where the filter is active, e.g. `(?!/system/).*` or `.*`. See Http Whiteboard documentation [1] and hint [2] |
-| `osgi.http.whiteboard.context.select` | required | OSGi service filter for selecting relevant contexts, e.g. `(osgi.http.whiteboard.context.name=*)` selects all contexts. See Http Whiteboard documentation [1] and hint [2] |
+| `osgi.http.whiteboard.filter.regex` | required | Regex path on where the filter is active, e.g. `(?!/system/).*` or `.*`. See Http Whiteboard documentation[^1] and hint[^2] |
+| `osgi.http.whiteboard.context.select` | required | OSGi service filter for selecting relevant contexts, e.g. `(osgi.http.whiteboard.context.name=*)` selects all contexts. See Http Whiteboard documentation[^1] and hint[^2] |
| `tags` | required | List of tags to query the status in order to decide if it is 503 or not |
| `statusFor503 ` | default `TEMPORARILY_UNAVAILABLE` | First status that causes a 503 response. The default `TEMPORARILY_UNAVAILABLE` will not send 503 for `OK` and `WARN` but for `TEMPORARILY_UNAVAILABLE`, `CRITICAL` and `HEALTH_CHECK_ERROR` |
| `includeExecutionResult ` | `false` | Will include the execution result in the response (as html comment for html case, otherwise as text). |
| `responseTextFor503 ` | required | Response text for 503 responses. Value can be either the content directly (e.g. just the string `Service Unavailable`) or in the format `classpath:<symbolic-bundle-id>:/path/to/file.html` (it uses `Bundle.getEntry()` to retrieve the file). The response content type is auto-detected to either `text/html` or `text/plain`. |
| `autoDisableFilter ` | default `false` | If true, will automatically disable the filter once the filter continued the filter chain without 503 for the first time. The filter will be automatically enabled again if the start level of the framework changes (hence on shutdown it will be active again). Useful for server startup scenarios.|
| `avoid404DuringStartup` | default `false` | If true, will automatically register a dummy servlet to ensure this filter becomes effective (complex applications might have the http whiteboard active but no servlets be active during early phases of startup, a filter only ever becomes active if there is a servlet registered). Useful for server startup scenarios. |
-| `service.ranking` | default `Integer.MAX_VALUE` (first in filter chain) | The `service.ranking` for the filter as respected by http whiteboard [1]. |
-
-[1] [https://felix.apache.org/documentation/subprojects/apache-felix-http-service.html#filter-service-properties](https://felix.apache.org/documentation/subprojects/apache-felix-http-service.html#filter-service-properties)
-
-[2] Choose a combination of `osgi.http.whiteboard.filter.regex`/ `osgi.http.whiteboard.context.select` wisely, e.g. `osgi.http.whiteboard.context.select=(osgi.http.whiteboard.context.name=*)` and `osgi.http.whiteboard.filter.regex=.*` would also cut off all admin paths.
+| `service.ranking` | default `Integer.MAX_VALUE` (first in filter chain) | The `service.ranking` for the filter as respected by http whiteboard[^1]. |
### Adding ad hoc results during request processing
-For certain scenarios it is useful to add a health check dynamically for a specific tag durign request processing, e.g. it can be useful during deployment requests (the tag(s) being added can be queried by e.g. load balancer or Service Unavailable Filter.
+For certain scenarios it is useful to add a health check dynamically for a specific tag during request processing, e.g. it can be useful during deployment requests (the tag(s) being added can be queried by e.g. load balancer or Service Unavailable Filter.
To achieve this configure the factory configuration with PID
`org.apache.felix.hc.core.impl.filter.AdhocResultDuringRequestProcessingFilter` with specific parameters:
@@ -410,7 +405,7 @@
| --- | --- | --- |
| `osgi.http.whiteboard.filter.regex` | required | Regex path on where the filter is active, e.g. `(?!/system/).*` or `.*`. See Http Whiteboard documentation |
| `osgi.http.whiteboard.context.select` | required | OSGi service filter for selecting relevant contexts, e.g. `(osgi.http.whiteboard.context.name=*)` selects all contexts. See Http Whiteboard |
-| `service.ranking` | default `0` | The `service.ranking` for the filter as respected by http whiteboard [1]. |
+| `service.ranking` | default `0` | The `service.ranking` for the filter as respected by http whiteboard[^1]. |
| `method` | default restriction not active | Relevant request method (leave empty to not restrict to a method) |
| `userAgentRegEx` | default restriction not active | Relevant user agent header (leave emtpy to not restrict to a user agent) |
| `hcName` | required | Name of health check during request processing |
@@ -422,3 +417,5 @@
| `waitAfterProcessing.initialWait` | 3 sec | Initial waiting time in sec until 'waitAfterProcessing.forTags' are checked for the first time. |
| `waitAfterProcessing.maxDelay` | 120 sec | Maximum delay in sec that can be caused when 'waitAfterProcessing.forTags' is configured (waiting is aborted after that time) |
+[^1]: [https://felix.apache.org/documentation/subprojects/apache-felix-http-service.html#filter-service-properties](https://felix.apache.org/documentation/subprojects/apache-felix-http-service.html#filter-service-properties)
+[^2]: Choose a combination of `osgi.http.whiteboard.filter.regex`/ `osgi.http.whiteboard.context.select` wisely, e.g. `osgi.http.whiteboard.context.select=(osgi.http.whiteboard.context.name=*)` and `osgi.http.whiteboard.filter.regex=.*` would also cut off all admin paths.
\ No newline at end of file
diff --git a/healthcheck/core/pom.xml b/healthcheck/core/pom.xml
index 35f8252..5d016c0 100644
--- a/healthcheck/core/pom.xml
+++ b/healthcheck/core/pom.xml
@@ -39,7 +39,7 @@
<properties>
<felix.java.version>11</felix.java.version>
- <pax-exam.version>4.13.4</pax-exam.version>
+ <pax-exam.version>4.14.0</pax-exam.version>
<pax-link.version>2.6.7</pax-link.version>
<org.ops4j.pax.logging.DefaultServiceLog.level>INFO</org.ops4j.pax.logging.DefaultServiceLog.level>
<felix.shell>false</felix.shell>
@@ -81,7 +81,7 @@
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
- <version>0.8.2</version>
+ <version>0.8.13</version>
<executions>
<execution>
<id>prepare-agent-integration</id>
diff --git a/healthcheck/core/src/test/java/org/apache/felix/hc/core/it/HealthCheckMonitorIT.java b/healthcheck/core/src/test/java/org/apache/felix/hc/core/it/HealthCheckMonitorIT.java
new file mode 100644
index 0000000..fefd65c
--- /dev/null
+++ b/healthcheck/core/src/test/java/org/apache/felix/hc/core/it/HealthCheckMonitorIT.java
@@ -0,0 +1,247 @@
+/*
+ * 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 SF 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.hc.core.it;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Optional;
+
+import javax.inject.Inject;
+
+import org.apache.felix.hc.api.HealthCheck;
+import org.apache.felix.hc.api.Result;
+import org.apache.felix.hc.api.condition.Healthy;
+import org.apache.felix.hc.api.condition.SystemReady;
+import org.apache.felix.hc.api.condition.Unhealthy;
+import org.apache.felix.hc.api.execution.HealthCheckExecutionOptions;
+import org.apache.felix.hc.api.execution.HealthCheckExecutionResult;
+import org.apache.felix.hc.api.execution.HealthCheckExecutor;
+import org.apache.felix.hc.api.execution.HealthCheckSelector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.Configuration;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.osgi.service.condition.Condition;
+
+@RunWith(PaxExam.class)
+public class HealthCheckMonitorIT {
+
+ private static final String FACTORY_PID = "org.apache.felix.hc.core.impl.monitor.HealthCheckMonitor";
+
+ @Inject
+ private HealthCheckExecutor executor;
+
+ @Inject
+ private BundleContext bundleContext;
+
+ @Configuration
+ public Option[] config() {
+ return U.config();
+ }
+
+ class TestHC implements HealthCheck {
+
+ private final Optional<Boolean> result;
+
+ public TestHC(final Optional<Boolean> result) {
+ this.result = result;
+ }
+
+ @Override
+ public Result execute() {
+ return new Result(result.orElse(true) ? Result.Status.OK : Result.Status.CRITICAL, "TestHC result: " + result);
+ }
+ }
+
+ private ServiceRegistration<HealthCheck> registerHc(final String tag, final Optional<Boolean> status) {
+ final Dictionary<String, Object> props = new Hashtable<String, Object>();
+ props.put(HealthCheck.TAGS, tag);
+
+ final ServiceRegistration<HealthCheck> result = bundleContext.registerService(HealthCheck.class, new TestHC(status), props);
+
+ // Wait for HC to be registered
+ U.expectHealthChecks(1, executor, tag);
+
+ return result;
+ }
+
+ private void registerMonitor(final String tag) {
+ final Dictionary<String, Object> props = new Hashtable<String, Object>();
+ props.put("registerUnhealthyMarkerService", true);
+ props.put("tags", tag);
+ props.put("intervalInSec", 1);
+ final ServiceReference<ConfigurationAdmin> refCA = bundleContext.getServiceReference(ConfigurationAdmin.class);
+ try {
+ final ConfigurationAdmin ca = bundleContext.getService(refCA);
+ final org.osgi.service.cm.Configuration config = ca.getFactoryConfiguration(FACTORY_PID, tag, null);
+ config.update(props);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to register monitor", e);
+ } finally {
+ bundleContext.ungetService(refCA);
+ }
+ // sleep 2 seconds to wait for first monitor run
+ try {
+ Thread.sleep(2000);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ private void unregisterMonitor(final String tag) {
+ final ServiceReference<ConfigurationAdmin> refCA = bundleContext.getServiceReference(ConfigurationAdmin.class);
+ try {
+ final ConfigurationAdmin ca = bundleContext.getService(refCA);
+ final org.osgi.service.cm.Configuration config = ca.getFactoryConfiguration(FACTORY_PID, tag, null);
+ config.delete();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to register monitor", e);
+ } finally {
+ bundleContext.ungetService(refCA);
+ }
+ }
+
+ private void executeHC(final String tag, final Optional<Boolean> status) {
+ final HealthCheckSelector selector = HealthCheckSelector.tags(tag);
+ final List<HealthCheckExecutionResult> executionResult = executor.execute(selector, new HealthCheckExecutionOptions());
+ assertEquals(1, executionResult.size());
+ final Result.Status expectedStatus = status.orElse(true) ? Result.Status.OK : Result.Status.CRITICAL;
+ assertEquals("Expected " + expectedStatus + " result", expectedStatus, executionResult.get(0).getHealthCheckResult().getStatus());
+ }
+
+ private void assertHealthy(final String tag) throws InvalidSyntaxException {
+ // healthy service
+ final Collection<ServiceReference<Healthy>> colH = bundleContext.getServiceReferences(Healthy.class, "(tag=" + tag + ")");
+ assertEquals(1, colH.size());
+ final ServiceReference<Healthy> refH = colH.iterator().next();
+ final Healthy h = bundleContext.getService(refH);
+ assertNotNull(h);
+ bundleContext.ungetService(refH);
+
+ // condition
+ final Collection<ServiceReference<Condition>> colC = bundleContext.getServiceReferences(Condition.class, "(osgi.condition.id=felix.hc." + tag + ")");
+ assertEquals(1, colC.size());
+ final ServiceReference<Condition> refC = colC.iterator().next();
+ final Condition c = bundleContext.getService(refC);
+ assertNotNull(c);
+ bundleContext.ungetService(refC);
+
+ // no unhealthy service
+ final Collection<ServiceReference<Unhealthy>> colU = bundleContext.getServiceReferences(Unhealthy.class, "(tag=" + tag + ")");
+ assertTrue(colU.isEmpty());
+ }
+
+ private void assertNotHealthy(final String tag) throws InvalidSyntaxException {
+ // no healthy service
+ final Collection<ServiceReference<Healthy>> colH = bundleContext.getServiceReferences(Healthy.class, "(tag=" + tag + ")");
+ assertTrue(colH.isEmpty());
+
+ // no condition
+ final Collection<ServiceReference<Condition>> colC = bundleContext.getServiceReferences(Condition.class, "(osgi.condition.id=felix.hc." + tag + ")");
+ assertTrue(colC.isEmpty());
+
+ // unhealthy service
+ final Collection<ServiceReference<Unhealthy>> colU = bundleContext.getServiceReferences(Unhealthy.class, "(tag=" + tag + ")");
+ assertFalse(colU.isEmpty());
+ assertEquals(1, colU.size());
+ final ServiceReference<Unhealthy> refU = colU.iterator().next();
+ final Unhealthy u = bundleContext.getService(refU);
+ assertNotNull(u);
+ bundleContext.ungetService(refU);
+
+ // no system ready service
+ final Collection<ServiceReference<SystemReady>> colS = bundleContext.getServiceReferences(SystemReady.class, null);
+ assertTrue(colS.isEmpty());
+ }
+
+ @Test
+ public void testHealthy() throws InvalidSyntaxException {
+ final String testTag = "testHealthy";
+ this.registerMonitor(testTag);
+
+ final ServiceRegistration<HealthCheck> reg = this.registerHc(testTag, Optional.of(true));
+ try {
+ this.executeHC(testTag, Optional.of(true));
+
+ this.assertHealthy(testTag);
+
+ // no system ready service
+ final Collection<ServiceReference<SystemReady>> colS = bundleContext.getServiceReferences(SystemReady.class, null);
+ assertTrue(colS.isEmpty());
+ } finally {
+ reg.unregister();
+ this.unregisterMonitor(testTag);
+ }
+ }
+
+ @Test
+ public void testUnhealthy() throws InvalidSyntaxException, IOException {
+ final String testTag = "testUnhealthy";
+ this.registerMonitor(testTag);
+
+ final ServiceRegistration<HealthCheck> reg = this.registerHc(testTag, Optional.of(false));
+ try {
+ this.executeHC(testTag, Optional.of(false));
+
+ this.assertNotHealthy(testTag);
+
+ } finally {
+ reg.unregister();
+ this.unregisterMonitor(testTag);
+ }
+ }
+
+ @Test
+ public void testSystemReady() throws InvalidSyntaxException, IOException {
+ final String testTag = "systemready";
+ this.registerMonitor(testTag);
+
+ final ServiceRegistration<HealthCheck> reg = this.registerHc(testTag, Optional.of(true));
+ try {
+ this.executeHC(testTag, Optional.of(true));
+
+ this.assertHealthy(testTag);
+
+ // system ready service
+ final Collection<ServiceReference<SystemReady>> colS = bundleContext.getServiceReferences(SystemReady.class, null);
+ assertFalse(colS.isEmpty());
+ assertEquals(1, colS.size());
+ final ServiceReference<SystemReady> refS = colS.iterator().next();
+ final SystemReady s = bundleContext.getService(refS);
+ assertNotNull(s);
+ bundleContext.ungetService(refS);
+ } finally {
+ reg.unregister();
+ this.unregisterMonitor(testTag);
+ }
+ }
+}
diff --git a/healthcheck/core/src/test/java/org/apache/felix/hc/core/it/U.java b/healthcheck/core/src/test/java/org/apache/felix/hc/core/it/U.java
index 64f057f..352535f 100644
--- a/healthcheck/core/src/test/java/org/apache/felix/hc/core/it/U.java
+++ b/healthcheck/core/src/test/java/org/apache/felix/hc/core/it/U.java
@@ -79,7 +79,7 @@
mavenBundle("org.osgi", "org.osgi.util.promise", "1.2.0"),
mavenBundle("org.osgi", "org.osgi.util.function", "1.2.0"),
mavenBundle("org.osgi", "org.osgi.service.component", "1.5.0"),
- mavenBundle("org.apache.felix", "org.apache.felix.scr", "2.2.6"),
+ mavenBundle("org.apache.felix", "org.apache.felix.scr", "2.2.12"),
mavenBundle("org.apache.felix", "org.apache.felix.configadmin", "1.9.26"),
mavenBundle("org.apache.felix", "org.apache.felix.metatype", "1.2.4"),
mavenBundle("org.apache.felix", "org.apache.felix.eventadmin", "1.6.4"),
@@ -90,15 +90,15 @@
mavenBundle("org.apache.geronimo.specs", "geronimo-annotation_1.3_spec", "1.0"),
mavenBundle("org.apache.felix", "org.apache.felix.http.servlet-api", "2.1.0"),
- mavenBundle("org.apache.felix", "org.apache.felix.http.jetty", "5.0.6"),
-
-
+ mavenBundle("org.apache.felix", "org.apache.felix.http.jetty", "5.1.32"),
+
+
mavenBundle().groupId("org.apache.servicemix.bundles").artifactId("org.apache.servicemix.bundles.quartz")
.versionAsInProject()));
}
-
+
// -- util methods
-
+
/** Wait until the specified number of health checks are seen by supplied executor */
static void expectHealthChecks(int howMany, HealthCheckExecutor executor, String... tags) {
expectHealthChecks(howMany, executor, new HealthCheckExecutionOptions(), tags);