This is an automated email from the ASF dual-hosted git repository. cziegeler pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/felix-dev.git
The following commit(s) were added to refs/heads/master by this push: new 1692706ec7 FELIX-6768 : Support OSGi Conditions (#406) 1692706ec7 is described below commit 1692706ec713ffb14fec63711ababacd4e58166d Author: Carsten Ziegeler <cziege...@apache.org> AuthorDate: Mon Apr 14 18:21:58 2025 +0200 FELIX-6768 : Support OSGi Conditions (#406) * FELIX-6768 : Support OSGi Conditions * Update readme * Add additional IT test and make it buildable with Java 17 --- healthcheck/README.md | 135 ++++++----- healthcheck/core/pom.xml | 23 +- .../felix/hc/core/impl/monitor/HealthState.java | 39 ++-- .../hc/core/impl/CompositeHealthCheckTest.java | 19 +- .../core/impl/monitor/HealthCheckMonitorTest.java | 88 +++++--- .../felix/hc/core/it/HealthCheckMonitorIT.java | 247 +++++++++++++++++++++ .../test/java/org/apache/felix/hc/core/it/U.java | 12 +- 7 files changed, 437 insertions(+), 126 deletions(-) diff --git a/healthcheck/README.md b/healthcheck/README.md index af8418e2c1..8993ed67dc 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 @@ -34,11 +33,11 @@ The strength of Health Checks are to surface internal state for external use: * Check that all OSGi bundles are up and running * Verify that performance counters are in range * Ping external systems and raise alarms if they are down -* Run smoke tests at system startup +* Run smoke tests at system startup * Check that demo content has been removed from a production system * Check that demo accounts are disabled -The health check subsystem uses tags to select which health checks to execute so you can for example execute just the _performance_ or _security_ health +The health check subsystem uses tags to select which health checks to execute so you can for example execute just the _performance_ or _security_ health checks once they are configured with the corresponding tags. The out of the box health check services also allow for using them as JMX aggregators and processors, which take JMX @@ -54,15 +53,15 @@ A `HealthCheck` is just an OSGi service that returns a `Result`. ``` public interface HealthCheck { - - /** Execute this health check and return a {@link Result} + + /** Execute this health check and return a {@link Result} * This is meant to execute quickly, access to external * systems, for example, should be managed asynchronously. */ public Result execute(); } ``` - + A simple health check implementation might look like follows: ``` @@ -93,9 +92,9 @@ Instead of using Log4j side by side with ResultLog/FormattingResultLog it is rec ### Semantic meaning of health check results In order to make health check results aggregatable in a reasonable way, it is important that result status values are used in a consistent way across different checks. When implementing custom health checks, comply to the following table: -Status | System is functional | Meaning | Actions possible for machine clients | Actions possible for human clients ---- | --- | --- | --- | --- -OK | yes | Everything is ok. | <ul><li>If system is not actively used yet, a load balancer might decide to take the system to production after receiving this status for the first time.</li><li>Otherwise no action needed</li></ul> | Response logs might still provide information to a human on why the system currently is healthy. E.g. it might show 30% disk used which indicates that no action will be required for a long time +Status | System is functional | Meaning | Actions possible for machine clients | Actions possible for human clients +--- | --- | --- | --- | --- +OK | yes | Everything is ok. | <ul><li>If system is not actively used yet, a load balancer might decide to take the system to production after receiving this status for the first time.</li><li>Otherwise no action needed</li></ul> | Response logs might still provide information to a human on why the system currently is healthy. E.g. it might show 30% disk used which indicates that no action will be required for a long time WARN | yes | **Tendency to CRITICAL** <br>System is fully functional but actions are needed to avoid a CRITICAL status in the future | <ul><li>Certain actions can be configured for known, actionable warnings, e.g. if disk space is low, it could be dynamically extended using infrastructure APIs if on virtual infrastructure)</li><li>Pass on information to monitoring system to be available to humans (in other aggregator UIs)</li></ul> | Any manual steps that a human can perform based on the [...] TEMPORARILY_UNAVAILABLE *) | no | **Tendency to OK** <br>System is not functional at the moment but is expected to become OK (or at least WARN) without action. An health check using this status is expected to turn CRITICAL after a certain period returning TEMPORARILY_UNAVAILABLE | <ul><li>Take out system from load balancing</li><li>Wait until TEMPORARILY_UNAVAILABLE status turns into either OK or CRITICAL</li></ul> | Wait and monitor result logs of health check returning TEMPORARILY_UNAVAILABLE CRITICAL | no | System is not functional and must not be used | <ul><li>Take out system from load balancing</li><li>Decommission system entirely and re-provision from scratch</li></ul> | Any manual steps that a human can perform based on their knowledge to bring the system back to state OK @@ -109,12 +108,12 @@ HEALTH\_CHECK\_ERROR | no | **Actual status unknown** <br>There was an error in The following generic Health Check properties may be used for all checks (**all service properties are optional**): -Property | Type | Description +Property | Type | Description ----------- | -------- | ------------ hc.name | String | The name of the health check as shown in UI hc.tags | String[] | List of tags: Both Felix Console Plugin and Health Check servlet support selecting relevant checks by providing a list of tags hc.mbean.name | String | Makes the HC result available via given MBean name. If not provided no MBean is created for that `HealthCheck` -hc.async.cronExpression | String | Executes the health check asynchronously using the cron expression provided. Use this for **long running health checks** to avoid execution every time the tag/name is queried. Prefer configuring a HealthCheckMonitor if you only want to regularly execute a HC. +hc.async.cronExpression | String | Executes the health check asynchronously using the cron expression provided. Use this for **long running health checks** to avoid execution every time the tag/name is queried. Prefer configuring a HealthCheckMonitor if you only want to regularly execute a HC. hc.async.intervalInSec | Long | Async execution like `hc.async.cronExpression` but using an interval hc.resultCacheTtlInMs | Long | Overrides the global default TTL as configured in health check executor for health check responses hc.keepNonOkResultsStickyForSec | Long | If given, non-ok results from past executions will be taken into account as well for the given seconds (use Long.MAX_VALUE for indefinitely). Useful for unhealthy system states that disappear but might leave the system at an inconsistent state (e.g. an event queue overflow where somebody needs to intervene manually) or for checks that should only go back to OK with a delay (can be useful for load balancers). @@ -124,18 +123,18 @@ hc.keepNonOkResultsStickyForSec | Long | If given, non-ok results from past exec To configure the defaults for the service properties [above](#configuring-health-checks), the following annotations can be used: // standard OSGi - @Component - @Designate(ocd = MyCustomCheckConfig.class, factory = true) - + @Component + @Designate(ocd = MyCustomCheckConfig.class, factory = true) + // to set `hc.name` and `hc.tags` @HealthCheckService(name = "Custom Check Name", tags= {"tag1", "tag2"}) - + // to set `hc.async.cronExpression` or `hc.async.intervalInSec` @Async(cronExpression="0 0 12 1 * ?" /*, intervalInSec = 60 */) - + // to set `hc.resultCacheTtlInMs`: @ResultTTL(resultCacheTtlInMs = 10000) - + // to set `hc.mbean.name`: @HealthCheckMBean(name = "MyCustomCheck") @@ -145,20 +144,20 @@ To configure the defaults for the service properties [above](#configuring-health ... -## General purpose health checks available out-of-the-box +## General purpose health checks available out-of-the-box The following checks are contained in bundle `org.apache.felix.healthcheck.generalchecks` and can be activated by simple configuration: -Check | PID | Factory | Description +Check | PID | Factory | Description --- | --- | --- | --- -Framework Startlevel | org.apache.felix.hc.generalchecks.FrameworkStartCheck | no | Checks the OSGi framework startlevel - `targetStartLevel` allows to configure a target start level, `targetStartLevel.propName` can be used to read it from the framework/system properties. -Services Ready | org.apache.felix.hc.generalchecks.ServicesCheck | yes | Checks for the existance of the given services. `services.list` can contain simple service names or filter expressions -Components Ready | org.apache.felix.hc.generalchecks.DsComponentsCheck | yes | Checks for the existance of the given components. Use `components.list` to list required active components (use component names) -Bundles Started | org.apache.felix.hc.generalchecks.BundlesStartedCheck | yes | Checks for started bundles - `includesRegex` and `excludesRegex` control what bundles are checked. +Framework Startlevel | org.apache.felix.hc.generalchecks.FrameworkStartCheck | no | Checks the OSGi framework startlevel - `targetStartLevel` allows to configure a target start level, `targetStartLevel.propName` can be used to read it from the framework/system properties. +Services Ready | org.apache.felix.hc.generalchecks.ServicesCheck | yes | Checks for the existance of the given services. `services.list` can contain simple service names or filter expressions +Components Ready | org.apache.felix.hc.generalchecks.DsComponentsCheck | yes | Checks for the existance of the given components. Use `components.list` to list required active components (use component names) +Bundles Started | org.apache.felix.hc.generalchecks.BundlesStartedCheck | yes | Checks for started bundles - `includesRegex` and `excludesRegex` control what bundles are checked. Disk Space | org.apache.felix.hc.generalchecks.DiskSpaceCheck | yes | Checks for disk space usage at the given paths `diskPaths` and checks them against thresholds `diskUsedThresholdWarn` (default 90%) and diskUsedThresholdCritical (default 97%) Memory | org.apache.felix.hc.generalchecks.MemoryCheck | no | Checks for Memory usage - `heapUsedPercentageThresholdWarn` (default 90%) and `heapUsedPercentageThresholdCritical` (default 99%) can be set to control what memory usage produces status `WARN` and `CRITICAL` CPU | org.apache.felix.hc.generalchecks.CpuCheck | no | Checks for CPU usage - `cpuPercentageThresholdWarn` (default 95%) can be set to control what CPU usage produces status `WARN` (check never results in `CRITICAL`) -Thread Usage | org.apache.felix.hc.generalchecks.ThreadUsageCheck | no | Checks via `ThreadMXBean.findDeadlockedThreads()` for deadlocks and analyses the CPU usage of each thread via a configurable time period (`samplePeriodInMs` defaults to 200ms). Uses `cpuPercentageThresholdWarn` (default 95%) to `WARN` about high thread utilisation. +Thread Usage | org.apache.felix.hc.generalchecks.ThreadUsageCheck | no | Checks via `ThreadMXBean.findDeadlockedThreads()` for deadlocks and analyses the CPU usage of each thread via a configurable time period (`samplePeriodInMs` defaults to 200ms). Uses `cpuPercentageThresholdWarn` (default 95%) to `WARN` about high thread utilisation. JMX Attribute Check | org.apache.felix.hc.generalchecks.JmxAttributeCheck | yes | Allows to check an arbitrary JMX attribute (using the configured mbean `mbean.name`'s attribute `attribute.name`) against a given constraint `attribute.value.constraint` (see [Constraints](#constraints)). Can check multiple attributes by providing additional config properties with numbers: `mbean2.name` (defaults to `mbean.name` if ommitted), `attribute2.name` and `attribute2.value.constraint` and `mbean3.n [...] Http Requests Check | org.apache.felix.hc.generalchecks.HttpRequestsCheck | yes | Allows to check a list of URLs against response code, response headers, timing, response content (plain content via RegEx or JSON via path expression). See [Request Spec Syntax](#request-spec-syntax) Scripted Check | org.apache.felix.hc.generalchecks.ScriptedHealthCheck | yes | Allows to run an arbitrary script. To configure use either `script` to provide a script directly or `scriptUrl` to link to an external script (may be a file URL or a link to a JCR file if a Sling Repository exists, e.g. `jcr:/etc/hc/check1.groovy`). Use the `language` property to refer to a registered script engine (e.g. install bundle `groovy-all` to be able to use language `groovy`). The script has the bindi [...] @@ -168,7 +167,7 @@ Scripted Check | org.apache.felix.hc.generalchecks.ScriptedHealthCheck | yes | A The `JMX Attribute Check` and `Http Requests Check` allow to check values against contraints. See the following examples: * value `string value` (checks for equality) -* value ` = 0` +* value ` = 0` * value ` > 0` * value ` < 100` * value ` BETWEEN 3 AND 7` @@ -183,7 +182,7 @@ Also see class `org.apache.felix.hc.generalchecks.util.SimpleConstraintsChecker` ### Request Spec Syntax -The `Http Requests Check` allows to configure a list of request specs. Requests specs have two parts: Before `=>` can be a simple URL/path with curl-syntax advanced options (e.g. setting a header with `-H "Test: Test val"`), after the `=>` it is a simple response code that can be followed ` && MATCHES <RegEx>` to match the response entity against or other matchers like HEADER, TIME or JSON. +The `Http Requests Check` allows to configure a list of request specs. Requests specs have two parts: Before `=>` can be a simple URL/path with curl-syntax advanced options (e.g. setting a header with `-H "Test: Test val"`), after the `=>` it is a simple response code that can be followed ` && MATCHES <RegEx>` to match the response entity against or other matchers like HEADER, TIME or JSON. Examples: @@ -205,9 +204,9 @@ This is a health check that can be dynamically controlled via JMX bean `org.apac ## Executing Health Checks -Health Checks can be executed via a [webconsole plugin](#webconsole-plugin), the [health check servlet](#health-check-servlet) or via [JMX](#jmx-access-to-health-checks). `HealthCheck` services can be selected for execution based on their `hc.tags` multi-value service property. +Health Checks can be executed via a [webconsole plugin](#webconsole-plugin), the [health check servlet](#health-check-servlet) or via [JMX](#jmx-access-to-health-checks). `HealthCheck` services can be selected for execution based on their `hc.tags` multi-value service property. -The `HealthCheckFilter` utility accepts positive and negative tag parameters, so that `osgi,-security` +The `HealthCheckFilter` utility accepts positive and negative tag parameters, so that `osgi,-security` selects all `HealthCheck` having the `osgi` tag but not the `security` tag, for example. For advanced use cases it is also possible to use the API directly by using the interface `org.apache.felix.hc.api.execution.HealthCheckExecutor`. @@ -216,7 +215,7 @@ For advanced use cases it is also possible to use the API directly by using the The health check executor can **optionally** be configured via service PID `org.apache.felix.hc.core.impl.executor.HealthCheckExecutorImpl`: -Property | Type | Default | Description +Property | Type | Default | Description ----------- | -------- | ------ | ------------ `timeoutInMs` | Long | 2000ms | Timeout in ms until a check is marked as timed out `longRunningFutureThresholdForCriticalMs` | Long | 300000ms (5min) | Threshold in ms until a check is marked as 'exceedingly' timed out and will marked CRITICAL instead of WARN only @@ -248,7 +247,7 @@ By default the HC servlet sends the CORS header `Access-Control-Allow-Origin: *` ### Webconsole plugin -If the `org.apache.felix.hc.webconsole` bundle is installed, a webconsole plugin +If the `org.apache.felix.hc.webconsole` bundle is installed, a webconsole plugin at `/system/console/healthcheck` allows for executing health checks, optionally selected based on their tags (positive and negative selection, see the `HealthCheckFilter` mention above). @@ -261,7 +260,7 @@ The Gogo command `hc:exec` can be used as follows: hc:exec [-v] [-a] tag1,tag2 -v verbose/debug -a combine tags with and logic (instead of or logic) - + The command is available without installing additional bundles (it is included in the core bundle `org.apache.felix.healthcheck.core`) ## Monitoring Health Checks @@ -270,27 +269,60 @@ The command is available without installing additional bundles (it is included i By default, health checks are only executed if explicitly triggered via one of the mechanisms as described in [Executing Health Checks](#executing-health-checks) (servlet, web console plugin, JMX, executor API). With the `HealthCheckMonitor`, Health checks can be regularly monitored by configuring the the **factory PID** `org.apache.felix.hc.core.impl.monitor.HealthCheckMonitor` with the following properties: -Property | Type | Default | Description +Property | Type | Default | Description ----------- | -------- | ------ | ------------ `tags` and/or `names` | String[] | none, at least one of the two is required | **Will regularly call all given tags and/or names**. All given tags/names are executed in parallel. If the set of tags/names include some checks multiple times it does not matter, the `HealthCheckExecutor` will always ensure checks are executed once at a time only. `intervalInSec` or `cronExpression` | Long or String (cron) | none, one of the two is required | The interval in which the given tags/names will be executed `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`. -### Marker Service to depend on a health status in SCR Components +### OSGi Condition to depend on a health status in -It is possible to use OSGi service references to depend on the health status of a certain -tag or name. For that to work, a `HealthCheckMonitor` needs to be configured for the relevant tag or name. To depend on a health status in a component, use a `@Reference` to one of the marker services `Healthy`, `Unhealthy` and `SystemReady` - this will then automatically activate/deactivate the component based on the certain health status. To activate a component only upon healthiness of a certain tag/name use the following code: +It is possible to use [OSGi Conditions](https://docs.osgi.org/specification/osgi.core/8.0.0/service.condition.html) to depend on the health status of a certain tag or name. For that to work, a `HealthCheckMonitor` needs to be configured for the relevant tag or name. An OSGi Condition service is registered using the tag or name prefixed by `felix.hc.`. + +For example, to depend on a health status in a Declarative Service component, use `@SatisfyingConditionTarget`. The below will only activate the component on healthiness of a certain tag/name: + +``` +@Component +@SatisfyingConditionTarget("(osgi.condition.id=felix.hc.dbavail)") +public class MyComponent { + ... +} +``` + +It is also possible to use a Condition in a reference and later on in the code figure out if healthiness is reached: + +``` +@Component +public class MyComponent { + + @Reference(target="(osgi.condition.id=felix.hc.dbavail)", policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.OPTIONAL) + volatile Condition healthy; + + public void mymethod() { + if (healthy == null) { + // not healthy + } else { + // healthy + } + } +} +``` + + +### Marker Service to depend on a health status in Declarative Service Components + +It is possible to use OSGi service references to depend on the health status of a certain tag or name. For that to work, a `HealthCheckMonitor` needs to be configured for the relevant tag or name. To depend on a health status in a component, use a `@Reference` to one of the marker services `Healthy`, `Unhealthy` and `SystemReady` - this will then automatically activate/deactivate the component based on the certain health status. To activate a component only upon healthiness of a certain [...] ``` @Reference(target="(tag=dbavail)") Healthy healthy; - + @Reference(target="(name=My Health Check)") Healthy healthy; ``` @@ -302,12 +334,11 @@ For the special tag `systemready`, there is a convenience marker interface avail ``` It is also possible to depend on a unhealthy state (e.g. for fallback functionality or self-healing): -``` +``` @Reference(target="(tag=dbavail)") Unhealthy unhealthy; ``` -NOTE: This does not support the [RFC 242 Condition Service](https://github.com/osgi/design/blob/master/rfcs/rfc0242/rfc-0242-Condition-Service.pdf) yet - however once final the marker services will also be able to implement the `Condition` interface. ### OSGi events for Health Check status changes and updates @@ -318,13 +349,13 @@ OSGi events with topic `org/apache/felix/health/*` are sent for tags/names that All events sent generally carry the properties `executionResult`, `status` and `previousStatus`. -| Example | Description +| Example | Description ------- | ----- `org/apache/felix/health/tag/mytag/STATUS_CHANGED` | Status for tag `mytag` has changed compared to last execution `org/apache/felix/health/tag/My_HC_Name/UPDATED ` (spaces in names are replaced with underscores to ensure valid topic names) | Status for name `My HC Name` has not changed but HC was executed and execution result is available in event property `executionResult`. `org/apache/felix/health/component/com/myprj/MyHealthCheck/UPDATED` (`.` are replaced with slashes to produce valid topic names) | HC based on SCR component `com.myprj.MyHealthCheck` was executed without having the status changed. The SCR component event is sent in addition to the name event -Event listener example: +Event listener example: ``` @Component(property = { EventConstants.EVENT_TOPIC + "=org/apache/felix/health/*"}) @@ -346,39 +377,35 @@ public class HealthEventHandler implements EventHandler { ### Service Unavailable Filter -For health states of the system that mean that requests can only fail it is possible to configure a Service Unavailable Filter that will cut off all requests if certain tags are in a `CRITICAL` or `TEMPORARILY_UNAVAILABLE` status. Typical usecases are startup/shutdown and deployments. Other scenarios include maintenance processes that require request processing of certain servlets to be stalled (the filter can be configured to be active on arbitrary paths). It is possible to configure a [...] +For health states of the system that mean that requests can only fail it is possible to configure a Service Unavailable Filter that will cut off all requests if certain tags are in a `CRITICAL` or `TEMPORARILY_UNAVAILABLE` status. Typical usecases are startup/shutdown and deployments. Other scenarios include maintenance processes that require request processing of certain servlets to be stalled (the filter can be configured to be active on arbitrary paths). It is possible to configure a [...] -Configure the factory configuration with PID +Configure the factory configuration with PID `org.apache.felix.hc.core.impl.filter.ServiceUnavailableFilter` with specific parameters to activate the Service Unavailable Filter: | 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 +To achieve this configure the factory configuration with PID `org.apache.felix.hc.core.impl.filter.AdhocResultDuringRequestProcessingFilter` with specific parameters: | 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 | | `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 | @@ -390,3 +417,5 @@ To achieve this configure the factory configuration with PID | `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 b1ecca7027..5d016c0b2e 100644 --- a/healthcheck/core/pom.xml +++ b/healthcheck/core/pom.xml @@ -7,9 +7,9 @@ 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 @@ -23,7 +23,7 @@ <parent> <groupId>org.apache.felix</groupId> <artifactId>felix-parent</artifactId> - <version>7</version> + <version>9</version> <relativePath /> </parent> @@ -38,8 +38,8 @@ </description> <properties> - <felix.java.version>8</felix.java.version> - <pax-exam.version>4.13.4</pax-exam.version> + <felix.java.version>11</felix.java.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> @@ -52,8 +52,7 @@ <connection>scm:git:https://github.com/apache/felix-dev.git</connection> <developerConnection>scm:git:https://github.com/apache/felix-dev.git</developerConnection> <url>https://gitbox.apache.org/repos/asf?p=felix-dev.git</url> - <tag>org.apache.felix.healthcheck.core-2.2.0</tag> - </scm> + </scm> <build> <plugins> @@ -61,7 +60,7 @@ <plugin> <groupId>biz.aQute.bnd</groupId> <artifactId>bnd-maven-plugin</artifactId> - <version>5.3.0</version> + <version>6.4.0</version> <executions> <execution> <goals> @@ -82,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> @@ -111,7 +110,7 @@ <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-failsafe-plugin</artifactId> - <version>3.0.0-M5</version> + <version>3.5.3</version> <executions> <execution> <goals> @@ -136,7 +135,7 @@ <dependency> <groupId>org.osgi</groupId> <artifactId>osgi.core</artifactId> - <version>6.0.0</version> + <version>8.0.0</version> <scope>provided</scope> </dependency> <dependency> @@ -225,7 +224,7 @@ <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> - <version>3.11.2</version> + <version>5.17.0</version> <scope>test</scope> </dependency> <dependency> diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/monitor/HealthState.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/monitor/HealthState.java index 3f2c3452db..d399c3c476 100644 --- a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/monitor/HealthState.java +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/monitor/HealthState.java @@ -18,6 +18,7 @@ package org.apache.felix.hc.core.impl.monitor; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.Dictionary; @@ -40,13 +41,14 @@ import org.apache.felix.hc.core.impl.util.lang.StringUtils; import org.osgi.framework.ServiceReference; import org.osgi.framework.ServiceRegistration; import org.osgi.service.component.ComponentConstants; +import org.osgi.service.condition.Condition; import org.osgi.service.event.Event; import org.slf4j.Logger; import org.slf4j.LoggerFactory; class HealthState { private static final Logger LOG = LoggerFactory.getLogger(HealthState.class); - + public static final String TAG_SYSTEMREADY = "systemready"; public static final String EVENT_TOPIC_PREFIX = "org/apache/felix/health"; @@ -57,15 +59,16 @@ class HealthState { public static final String EVENT_PROP_STATUS = "status"; public static final String EVENT_PROP_PREVIOUS_STATUS = "previousStatus"; - static final Healthy MARKER_SERVICE_HEALTHY = new Healthy() { - }; + static final class HealthyCondition implements Condition, Healthy {}; + static final class SystemReadyCondition implements Condition, SystemReady {}; + + static final Healthy MARKER_SERVICE_HEALTHY = new HealthyCondition(); static final Unhealthy MARKER_SERVICE_UNHEALTHY = new Unhealthy() { }; - static final SystemReady MARKER_SERVICE_SYSTEMREADY = new SystemReady() { - }; - + static final SystemReady MARKER_SERVICE_SYSTEMREADY = new SystemReadyCondition(); + private final HealthCheckMonitor monitor; - + private final String tagOrName; private final ServiceReference<HealthCheck> healthCheckRef; private final boolean isTag; @@ -140,7 +143,7 @@ class HealthState { update(result); } - + synchronized void update(HealthCheckExecutionResult executionResult) { if(!isLive) { LOG.trace("Not live anymore, skipping result update for {}", this); @@ -177,20 +180,24 @@ class HealthState { private void registerHealthyService() { if (healthyRegistration == null) { + final boolean isSystemReady = TAG_SYSTEMREADY.equals(tagOrName); LOG.debug("HealthCheckMonitor: registerHealthyService() {} ", tagOrName); Dictionary<String, String> registrationProps = new Hashtable<>(); registrationProps.put(propertyName, tagOrName); registrationProps.put("activated", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())); + registrationProps.put(Condition.CONDITION_ID, "felix.hc.".concat(tagOrName)); - if (TAG_SYSTEMREADY.equals(tagOrName)) { + if (isSystemReady) { LOG.debug("HealthCheckMonitor: SYSTEM READY"); - healthyRegistration = monitor.getBundleContext().registerService( - new String[] { SystemReady.class.getName(), Healthy.class.getName() }, - MARKER_SERVICE_SYSTEMREADY, registrationProps); - } else { - healthyRegistration = monitor.getBundleContext().registerService(Healthy.class, MARKER_SERVICE_HEALTHY, - registrationProps); } + final List<String> services = new ArrayList<>(); + services.add(Healthy.class.getName()); + services.add(Condition.class.getName()); + if (isSystemReady) { + services.add(SystemReady.class.getName()); + } + final Object service = isSystemReady ? MARKER_SERVICE_SYSTEMREADY : MARKER_SERVICE_HEALTHY; + healthyRegistration = monitor.getBundleContext().registerService(services.toArray(new String[0]), service, registrationProps); LOG.debug("HealthCheckMonitor: Healthy service for {} '{}' registered", propertyName, tagOrName); } } @@ -224,7 +231,7 @@ class HealthState { private void sendEvents(HealthCheckExecutionResult executionResult, Result.Status previousStatus) { ChangeType sendEventsConfig = monitor.getSendEvents(); - if (sendEventsConfig == ChangeType.ALL + if (sendEventsConfig == ChangeType.ALL || (statusChanged && (sendEventsConfig == ChangeType.STATUS_CHANGES || sendEventsConfig == ChangeType.STATUS_CHANGES_OR_NOT_OK)) || (!executionResult.getHealthCheckResult().isOk() && sendEventsConfig == ChangeType.STATUS_CHANGES_OR_NOT_OK)) { diff --git a/healthcheck/core/src/test/java/org/apache/felix/hc/core/impl/CompositeHealthCheckTest.java b/healthcheck/core/src/test/java/org/apache/felix/hc/core/impl/CompositeHealthCheckTest.java index 6c11dd89ee..933b81220a 100644 --- a/healthcheck/core/src/test/java/org/apache/felix/hc/core/impl/CompositeHealthCheckTest.java +++ b/healthcheck/core/src/test/java/org/apache/felix/hc/core/impl/CompositeHealthCheckTest.java @@ -27,9 +27,9 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.Arrays; +import java.util.Dictionary; import java.util.LinkedList; import java.util.List; -import java.util.Set; import org.apache.felix.hc.api.HealthCheck; import org.apache.felix.hc.api.Result; @@ -43,7 +43,6 @@ import org.apache.felix.hc.core.impl.util.HealthCheckFilter; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentMatcher; -import org.mockito.Matchers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.Spy; @@ -66,7 +65,7 @@ public class CompositeHealthCheckTest { @Before public void setup() { - MockitoAnnotations.initMocks(this); + MockitoAnnotations.openMocks(this); compositeHealthCheck.setHealthCheckExecutor(healthCheckExecutor); compositeHealthCheck.setFilterTags(new String[] {}); compositeHealthCheck.setComponentContext(componentContext); @@ -75,8 +74,7 @@ public class CompositeHealthCheckTest { @Test public void testExecution() { - doReturn((Result) null).when(compositeHealthCheck).checkForRecursion(Matchers.<ServiceReference> any(), - Matchers.<Set<String>> any()); + doReturn((Result) null).when(compositeHealthCheck).checkForRecursion(any(), any()); String[] testTags = new String[] { "tag1" }; compositeHealthCheck.setFilterTags(testTags); @@ -287,5 +285,16 @@ public class CompositeHealthCheckTest { throw new UnsupportedOperationException(); } + @Override + public Object adapt(Class type) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Dictionary getProperties() { + // TODO Auto-generated method stub + return null; + } } } diff --git a/healthcheck/core/src/test/java/org/apache/felix/hc/core/impl/monitor/HealthCheckMonitorTest.java b/healthcheck/core/src/test/java/org/apache/felix/hc/core/impl/monitor/HealthCheckMonitorTest.java index 47f69b5e7b..69d7df0a4b 100644 --- a/healthcheck/core/src/test/java/org/apache/felix/hc/core/impl/monitor/HealthCheckMonitorTest.java +++ b/healthcheck/core/src/test/java/org/apache/felix/hc/core/impl/monitor/HealthCheckMonitorTest.java @@ -18,6 +18,7 @@ package org.apache.felix.hc.core.impl.monitor; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -67,6 +68,7 @@ import org.osgi.framework.ServiceReference; import org.osgi.framework.ServiceRegistration; import org.osgi.service.component.ComponentConstants; import org.osgi.service.component.ComponentContext; +import org.osgi.service.condition.Condition; import org.osgi.service.event.Event; import org.osgi.service.event.EventAdmin; @@ -85,7 +87,7 @@ public class HealthCheckMonitorTest { @Mock private ComponentContext componentContext; - + @Mock private EventAdmin eventAdmin; @@ -97,38 +99,42 @@ public class HealthCheckMonitorTest { @Mock private ExtendedHealthCheckExecutor healthCheckExecutor; - + @Mock private HealthCheckMetadata healthCheckMetadata; @Mock private ServiceReference<HealthCheck> healthCheckServiceRef; - + @Captor private ArgumentCaptor<Event> postedEventsCaptor1; @Captor private ArgumentCaptor<Event> postedEventsCaptor2; - + + @Captor + private ArgumentCaptor<Dictionary<String, Object>> propsCaptor; + + @SuppressWarnings("rawtypes") @Mock - private ServiceRegistration<? extends Healthy> healthyRegistration; - + private ServiceRegistration healthyRegistration; + @Mock private ServiceRegistration<Unhealthy> unhealthyRegistration; - + @Mock private ResultTxtVerboseSerializer resultTxtVerboseSerializer; - + @Before public void before() throws ReflectiveOperationException { for (Method m : HealthCheckMonitor.Config.class.getDeclaredMethods()) { when(m.invoke(config)).thenReturn(m.getDefaultValue()); } - + when(config.intervalInSec()).thenReturn(1000L); when(config.tags()).thenReturn(new String[] { TEST_TAG }); - + when(healthCheckMetadata.getServiceReference()).thenReturn(healthCheckServiceRef); when(healthCheckMetadata.getTitle()).thenReturn("Test Check"); @@ -152,15 +158,14 @@ public class HealthCheckMonitorTest { assertEquals(1, healthCheckMonitor.healthStates.size()); assertEquals("[HealthState tagOrName=test-tag, isTag=true, status=null, isHealthy=false, statusChanged=false]", healthCheckMonitor.healthStates.get(TEST_TAG).toString()); - + healthCheckMonitor.deactivate(); assertEquals(0, healthCheckMonitor.healthStates.size()); - + } @Test public void testRunRegisterMarkerServices() throws InvalidSyntaxException { - when(config.registerHealthyMarkerService()).thenReturn(true); when(config.registerUnhealthyMarkerService()).thenReturn(true); healthCheckMonitor.activate(bundleContext, config, componentContext); @@ -170,10 +175,16 @@ public class HealthCheckMonitorTest { setHcResult(Result.Status.OK); healthCheckMonitor.run(); - + verify(healthCheckExecutor).execute(HealthCheckSelector.tags(TEST_TAG)); - - verify(bundleContext).registerService(eq(Healthy.class), eq(HealthState.MARKER_SERVICE_HEALTHY), any()); + + verify(bundleContext).registerService(eq(new String[] {Healthy.class.getName(), Condition.class.getName()}), + eq(HealthState.MARKER_SERVICE_HEALTHY), propsCaptor.capture()); + final Dictionary<String, Object> capturedProps1 = propsCaptor.getValue(); + assertEquals("test-tag", capturedProps1.get("tag")); + assertEquals("felix.hc.test-tag", capturedProps1.get("osgi.condition.id")); + assertNotNull(capturedProps1.get("activated")); + verify(bundleContext, never()).registerService(eq(Unhealthy.class), eq(HealthState.MARKER_SERVICE_UNHEALTHY), any()); verifyNoInteractions(healthyRegistration, unhealthyRegistration); @@ -181,30 +192,39 @@ public class HealthCheckMonitorTest { healthCheckMonitor.run(); // no status change, no interaction verifyNoInteractions(bundleContext, healthyRegistration, unhealthyRegistration); - + // change, unhealthy should be registered resetMarkerServicesContext(); setHcResult(Result.Status.TEMPORARILY_UNAVAILABLE); healthCheckMonitor.run(); - - verify(bundleContext, never()).registerService(eq(Healthy.class), eq(HealthState.MARKER_SERVICE_HEALTHY), any()); + + verify(bundleContext, never()).registerService(eq(new String[] {Healthy.class.getName(), Condition.class.getName()}), + eq(HealthState.MARKER_SERVICE_HEALTHY), any()); verify(bundleContext).registerService(eq(Unhealthy.class), eq(HealthState.MARKER_SERVICE_UNHEALTHY), any()); verify(healthyRegistration).unregister(); verifyNoInteractions(unhealthyRegistration); - + // change, health should be registered resetMarkerServicesContext(); setHcResult(Result.Status.WARN); // WARN is healthy by default config healthCheckMonitor.run(); - verify(bundleContext).registerService(eq(Healthy.class), eq(HealthState.MARKER_SERVICE_HEALTHY), any()); + verify(bundleContext).registerService(eq(new String[] {Healthy.class.getName(), Condition.class.getName()}), + eq(HealthState.MARKER_SERVICE_HEALTHY), propsCaptor.capture()); + final Dictionary<String, Object> capturedProps2 = propsCaptor.getValue(); + assertEquals("test-tag", capturedProps2.get("tag")); + assertEquals("felix.hc.test-tag", capturedProps2.get("osgi.condition.id")); + assertNotNull(capturedProps2.get("activated")); + verify(bundleContext, never()).registerService(eq(Unhealthy.class), eq(HealthState.MARKER_SERVICE_UNHEALTHY), any()); verify(unhealthyRegistration).unregister(); verifyNoInteractions(healthyRegistration); } + @SuppressWarnings("unchecked") private void resetMarkerServicesContext() { reset(bundleContext, healthyRegistration, unhealthyRegistration); - when(bundleContext.registerService(eq(Healthy.class), eq(HealthState.MARKER_SERVICE_HEALTHY), any())).thenReturn((ServiceRegistration<Healthy>) healthyRegistration); + when(bundleContext.registerService(eq(new String[] {Healthy.class.getName(), Condition.class.getName()}), + eq(HealthState.MARKER_SERVICE_HEALTHY), any())).thenReturn(healthyRegistration); lenient().when(bundleContext.registerService(eq(Unhealthy.class), eq(HealthState.MARKER_SERVICE_UNHEALTHY), any())).thenReturn(unhealthyRegistration); } @@ -219,9 +239,9 @@ public class HealthCheckMonitorTest { setHcResult(Result.Status.OK); healthCheckMonitor.run(); - + verify(healthCheckExecutor).execute(HealthCheckSelector.tags(TEST_TAG)); - + verify(eventAdmin, times(2)).postEvent(postedEventsCaptor1.capture()); List<Event> postedEvents = postedEventsCaptor1.getAllValues(); assertEquals(2, postedEvents.size()); @@ -234,7 +254,7 @@ public class HealthCheckMonitorTest { healthCheckMonitor.run(); // no event verifyNoInteractions(eventAdmin); - + setHcResult(Result.Status.CRITICAL); reset(eventAdmin); // with status change @@ -246,7 +266,7 @@ public class HealthCheckMonitorTest { assertEquals(Result.Status.CRITICAL, postedEvents.get(0).getProperty(HealthState.EVENT_PROP_STATUS)); assertEquals(Result.Status.OK, postedEvents.get(0).getProperty(HealthState.EVENT_PROP_PREVIOUS_STATUS)); assertEquals("org/apache/felix/health/component/org/apache/felix/TestHealthCheck/STATUS_CHANGED", postedEvents.get(1).getTopic()); - + reset(eventAdmin); // without status change healthCheckMonitor.run(); @@ -262,13 +282,13 @@ public class HealthCheckMonitorTest { when(healthCheckServiceRef.getProperty(ComponentConstants.COMPONENT_NAME)).thenReturn("org.apache.felix.TestHealthCheck"); healthCheckMonitor.activate(bundleContext, config, componentContext); - + setHcResult(Result.Status.OK); healthCheckMonitor.run(); - + verify(healthCheckExecutor).execute(HealthCheckSelector.tags(TEST_TAG)); - + verify(eventAdmin, times(2)).postEvent(postedEventsCaptor1.capture()); List<Event> postedEvents = postedEventsCaptor1.getAllValues(); assertEquals(2, postedEvents.size()); @@ -319,7 +339,7 @@ public class HealthCheckMonitorTest { setHcResult(Result.Status.CRITICAL); healthCheckMonitor.run(); verify(healthCheckMonitor).logResultItem(eq(false), matches(".*healthy:false hasChanged:true .*" + HC_RESULT_SERIALIZED)); - + reset(healthCheckMonitor); setHcResult(Result.Status.CRITICAL); healthCheckMonitor.run(); @@ -367,7 +387,7 @@ public class HealthCheckMonitorTest { setHcResult(Result.Status.CRITICAL); healthCheckMonitor.run(); verify(healthCheckMonitor).logResultItem(eq(false), matches(".*healthy:false hasChanged:true .*" + HC_RESULT_SERIALIZED)); - + reset(healthCheckMonitor); setHcResult(Result.Status.CRITICAL); healthCheckMonitor.run(); @@ -414,7 +434,7 @@ public class HealthCheckMonitorTest { setHcResult(Result.Status.CRITICAL); healthCheckMonitor.run(); verify(healthCheckMonitor).logResultItem(eq(false), matches(".*healthy:false hasChanged:true .*" + HC_RESULT_SERIALIZED)); - + reset(healthCheckMonitor); setHcResult(Result.Status.CRITICAL); healthCheckMonitor.run(); @@ -452,7 +472,7 @@ public class HealthCheckMonitorTest { verify(healthCheckMonitor, never()).logResultItem(anyBoolean(), anyString()); } - + private void prepareLoggingTest(HealthCheckMonitor.ChangeType loggingChangeType) throws InvalidSyntaxException { when(config.sendEvents()).thenReturn(HealthCheckMonitor.ChangeType.NONE); when(config.logResults()).thenReturn(loggingChangeType); @@ -467,7 +487,7 @@ public class HealthCheckMonitorTest { } }); } - + private void setHcResult(Result.Status status) { when(healthCheckExecutor.execute(HealthCheckSelector.tags(TEST_TAG))) .thenReturn(Arrays.asList(new ExecutionResult(healthCheckMetadata, new Result(status, status.toString()), 1))); 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 0000000000..fefd65c3ab --- /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 64f057ff52..352535fb0f 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 @@ public class U { 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 @@ public class U { 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);