You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@felix.apache.org by gh...@apache.org on 2021/12/14 20:36:18 UTC

[felix-dev] branch master updated (31f7a4b -> 9bdd67d)

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

ghenzler pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/felix-dev.git.


    from 31f7a4b  [maven-release-plugin] prepare for next development iteration
     new 8ecf206  FELIX-6480 Make logging of HC monitor more flexible
     new 9bdd67d  FELIX-6480 FELIX-6447 Reduce logging of health check result cache to debug

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


Summary of changes:
 healthcheck/README.md                              |   4 +-
 .../core/impl/executor/HealthCheckResultCache.java |   6 +-
 .../hc/core/impl/monitor/HealthCheckMonitor.java   |  92 ++++-----
 .../felix/hc/core/impl/monitor/HealthState.java    |  20 +-
 .../impl/servlet/ResultTxtVerboseSerializer.java   |  28 ++-
 .../core/impl/monitor/HealthCheckMonitorTest.java  | 207 ++++++++++++++++++++-
 6 files changed, 294 insertions(+), 63 deletions(-)

[felix-dev] 02/02: FELIX-6480 FELIX-6447 Reduce logging of health check result cache to debug

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

ghenzler pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/felix-dev.git

commit 9bdd67dbf9a3de6be2484ad381acd91e9f647e2b
Author: georg.henzler <ge...@netcentric.biz>
AuthorDate: Tue Dec 14 21:33:05 2021 +0100

    FELIX-6480 FELIX-6447 Reduce logging of health check result cache to
    debug
---
 .../apache/felix/hc/core/impl/executor/HealthCheckResultCache.java  | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/HealthCheckResultCache.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/HealthCheckResultCache.java
index 8618ce1..c021b82 100644
--- a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/HealthCheckResultCache.java
+++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/HealthCheckResultCache.java
@@ -53,10 +53,8 @@ public class HealthCheckResultCache {
     public void updateWith(HealthCheckExecutionResult result) {
         final ExecutionResult executionResult = (ExecutionResult) result;
         final HealthCheckExecutionResult previous = cache.put(executionResult.getServiceId(), result);
-        if ( previous == null
-                 || previous.getHealthCheckResult().getStatus() != result.getHealthCheckResult().getStatus()
-                 || !previous.getHealthCheckResult().toString().equals(result.getHealthCheckResult().toString())) {
-            logger.info("Updating HC result for {} : {}", result.getHealthCheckMetadata().getName(), result.getHealthCheckResult());
+        if ( previous == null || previous.getHealthCheckResult().getStatus() != result.getHealthCheckResult().getStatus()) {
+            logger.debug("Updated HC result for {} : {}", result.getHealthCheckMetadata().getName(), result.getHealthCheckResult());
         }
 
         // update cache for sticky handling

[felix-dev] 01/02: FELIX-6480 Make logging of HC monitor more flexible

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

ghenzler pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/felix-dev.git

commit 8ecf2064bf9f51cfea6bbabadbc0b769b1432978
Author: georg.henzler <ge...@netcentric.biz>
AuthorDate: Tue Dec 14 21:28:08 2021 +0100

    FELIX-6480 Make logging of HC monitor more flexible
---
 healthcheck/README.md                              |   4 +-
 .../hc/core/impl/monitor/HealthCheckMonitor.java   |  92 ++++-----
 .../felix/hc/core/impl/monitor/HealthState.java    |  20 +-
 .../impl/servlet/ResultTxtVerboseSerializer.java   |  28 ++-
 .../core/impl/monitor/HealthCheckMonitorTest.java  | 207 ++++++++++++++++++++-
 5 files changed, 292 insertions(+), 59 deletions(-)

diff --git a/healthcheck/README.md b/healthcheck/README.md
index 2007d5f..b40505b 100644
--- a/healthcheck/README.md
+++ b/healthcheck/README.md
@@ -274,8 +274,8 @@ Property    | Type     | Default | Description
 `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` or `ALL` | `STATUS_CHANGES` | Whether to send events for health check status changes. See [below](#osgi-events-for-health-check-status-changes) for details.
-`logResults` | enum `NONE`, `STATUS_CHANGES` or `ALL` | `NONE ` | Whether to log the result of the monitor to the regular log file
+`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.
+`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
 `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
diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/monitor/HealthCheckMonitor.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/monitor/HealthCheckMonitor.java
index 6da64ed..17bd1d8 100644
--- a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/monitor/HealthCheckMonitor.java
+++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/monitor/HealthCheckMonitor.java
@@ -26,7 +26,6 @@ import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
 import org.apache.felix.hc.api.HealthCheck;
-import org.apache.felix.hc.api.Result.Status;
 import org.apache.felix.hc.api.condition.Healthy;
 import org.apache.felix.hc.api.condition.SystemReady;
 import org.apache.felix.hc.api.condition.Unhealthy;
@@ -78,22 +77,22 @@ public class HealthCheckMonitor implements Runnable {
     private static final Logger LOG = LoggerFactory.getLogger(HealthCheckMonitor.class);
 
     public enum ChangeType {
-        NONE, STATUS_CHANGES, ALL
+        NONE, STATUS_CHANGES, STATUS_CHANGES_OR_NOT_OK, ALL
     }
     
     @ObjectClassDefinition(name = "Health Check Monitor", description = "Regularly executes health checks according to given interval/cron expression")
     public @interface Config {
 
-        @AttributeDefinition(name = "Tags", description = "List of tags to query regularly")
+        @AttributeDefinition(name = "Tags", description = "List of tags to monitor")
         String[] tags() default {};
 
-        @AttributeDefinition(name = "Names", description = "List of health check names to query regularly")
+        @AttributeDefinition(name = "Names", description = "List of health check names to monitor")
         String[] names() default {};
 
-        @AttributeDefinition(name = "Interval (Sec)", description = "Will execute the checks for give tags every n seconds (either use intervalInSec or cronExpression )")
+        @AttributeDefinition(name = "Interval (Sec)", description = "Will execute the checks for given tags/names every n seconds (either use intervalInSec or cronExpression )")
         long intervalInSec() default 0;
 
-        @AttributeDefinition(name = "Interval (Cron Expresson)", description = "Will execute the checks for give tags according to cron expression")
+        @AttributeDefinition(name = "Interval (Cron Expresson)", description = "Will execute the checks for given tags/names according to cron expression")
         String cronExpression() default "";
 
         @AttributeDefinition(name = "Register Healthy Marker Service", description = "For the case a given tag/name is healthy, will register a service Healthy with property tag=<tagname> (or name=<hc.name>) that other services can depend on")
@@ -102,16 +101,16 @@ public class HealthCheckMonitor implements Runnable {
         @AttributeDefinition(name = "Register Unhealthy Marker Service", description = "For the case a given tag/name is unhealthy, will register a service Unhealthy with property tag=<tagname> (or name=<hc.name>) that other services can depend on")
         boolean registerUnhealthyMarkerService() default false;
 
-        @AttributeDefinition(name = "Treat WARN as Healthy", description = "Whether to treat status WARN as healthy (it normally should because WARN indicates a working system that only possibly might become unavailable if no action is taken")
+        @AttributeDefinition(name = "Treat WARN as Healthy", description = "Whether to treat status WARN as healthy (defaults to true because WARN indicates a working system that only possibly might become unavailable if no action is taken)")
         boolean treatWarnAsHealthy() default true;
 
-        @AttributeDefinition(name = "Send Events", description = "Send OSGi events for the case a status has changed or for all executions or for none.")
+        @AttributeDefinition(name = "Send Events", description = "What updates should be sent as OSGi events (none, status changes, status changes and not ok results, all updates)")
         ChangeType sendEvents() default ChangeType.STATUS_CHANGES;
 
-        @AttributeDefinition(name = "Log results", description = "Log the result to the regular log file.")
+        @AttributeDefinition(name = "Log results", description = "What updates should be logged to regular log file (none, status changes, status changes and not ok results, all updates)")
         ChangeType logResults() default ChangeType.NONE;
         
-        @AttributeDefinition(name = "Dynamic Mode", description = "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.")
+        @AttributeDefinition(name = "Resolve Tags (dynamic)", description = "In dynamic mode tags are resolved to a list of health checks that are monitored individually (this means events are sent/services are 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.")
         boolean isDynamic() default false;
         
         @AttributeDefinition
@@ -166,7 +165,7 @@ public class HealthCheckMonitor implements Runnable {
         this.names = Arrays.stream(config.names()).filter(StringUtils::isNotBlank).collect(toList());
         this.isDynamic = config.isDynamic();
         initHealthStates();
-        
+
         this.registerHealthyMarkerService = config.registerHealthyMarkerService();
         this.registerUnhealthyMarkerService = config.registerUnhealthyMarkerService();
 
@@ -263,12 +262,10 @@ public class HealthCheckMonitor implements Runnable {
                 healthStates.values().parallelStream().forEach(healthState -> 
                     runWithThreadNameContext(healthState::update)
                 );
-                
 
                 if(logResults != ChangeType.NONE) {
                     logResults();
                 }
-                
 
                 LOG.debug("Updated {} health states for tags {} and names {}", healthStates.size(), this.tags, this.names);
             } catch (Exception e) {
@@ -278,42 +275,51 @@ public class HealthCheckMonitor implements Runnable {
     }
 
     private void logResults() {
-
-        List<HealthCheckExecutionResult> executionResults = healthStates.values().stream()
-            .filter(healthState -> { return healthState.hasChanged() || logResults == ChangeType.ALL; })
-            .flatMap( healthState -> {
-                HealthCheckExecutionResult executionResult = healthState.getExecutionResult();
-                List<HealthCheckExecutionResult> execResults;
-                if (executionResult instanceof CombinedExecutionResult) {
-                    execResults = ((CombinedExecutionResult) executionResult).getExecutionResults();
-                } else {
-                    execResults = Arrays.asList(executionResult);
-                }
-                return execResults.stream();
-            })
-            .sorted()
-            .collect(toList());
-        
-        if(executionResults.isEmpty()) {
-            return;
-        }
         
-        CombinedExecutionResult combinedResultForLogging = new CombinedExecutionResult(executionResults);
-        Status hcStatus = combinedResultForLogging.getHealthCheckResult().getStatus();
-        if(!LOG.isInfoEnabled() && hcStatus == Status.OK) {
-            return;
+        for(HealthState healthState: healthStates.values()) {
+
+            HealthCheckExecutionResult executionResult = healthState.getExecutionResult();
+
+            boolean isOk = executionResult.getHealthCheckResult().isOk();
+            if(!LOG.isInfoEnabled() && isOk) {
+                return; // with INFO disabled even ChangeType.ALL would not log it
+            }
+            boolean changeToBeLogged = healthState.hasChanged() && (logResults == ChangeType.STATUS_CHANGES || logResults == ChangeType.STATUS_CHANGES_OR_NOT_OK);
+            boolean notOkToBeLogged = !isOk && logResults == ChangeType.STATUS_CHANGES_OR_NOT_OK;
+            if(!changeToBeLogged && !notOkToBeLogged && logResults != ChangeType.ALL) {
+                continue;
+            }
+
+            List<HealthCheckExecutionResult> execResults;
+            boolean isCombinedResult = executionResult instanceof CombinedExecutionResult;
+            if (isCombinedResult) {
+                execResults = ((CombinedExecutionResult) executionResult).getExecutionResults();
+            } else {
+                execResults = Arrays.asList(executionResult);
+            }
+
+            String label = 
+                    isCombinedResult ?  String.format("Health State for %s '%s': healthy:%b isOk:%b hasChanged:%b count HCs:%d", (healthState.isTag() ? "tag" : "name"), healthState.getTagOrName(), healthState.isHealthy(), isOk, healthState.hasChanged(), execResults.size())
+                            : String.format("Health State for '%s': healthy:%b hasChanged:%b", executionResult.getHealthCheckMetadata().getTitle(), healthState.isHealthy(), healthState.hasChanged());
+            if(!healthState.hasChanged() && notOkToBeLogged) {
+                // filter the ok items to not clutter the log file
+                execResults = execResults.stream().filter(r -> !r.getHealthCheckResult().isOk()).collect(toList());
+            }
+
+            String logMsg = resultTxtVerboseSerializer.serialize(label, execResults, false);
+            logResultItem(isOk, logMsg);
         }
 
-        String logMsg = resultTxtVerboseSerializer.serialize(combinedResultForLogging.getHealthCheckResult(), combinedResultForLogging.getExecutionResults(), false);
-        String firstLineMsg = (logResults == ChangeType.STATUS_CHANGES) ? "Status Changes:" : "";
-        if(hcStatus == Status.OK) {
-            LOG.info(firstLineMsg+"\n"+logMsg);
+    }
+
+    void logResultItem(boolean isOk, String msg) {
+        if(isOk) {
+            LOG.info(msg);
         } else {
-            LOG.warn(firstLineMsg+"\n"+logMsg);
+            LOG.warn(msg);
         }
-
     }
-    
+
     private void runWithThreadNameContext(Runnable r) {
         String threadNameToRestore = Thread.currentThread().getName();
         try {
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 ed5d33f..3f2c345 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
@@ -25,11 +25,9 @@ import java.util.HashMap;
 import java.util.Hashtable;
 import java.util.List;
 import java.util.Map;
-import java.util.function.Function;
 
 import org.apache.felix.hc.api.HealthCheck;
 import org.apache.felix.hc.api.Result;
-import org.apache.felix.hc.api.Result.Status;
 import org.apache.felix.hc.api.condition.Healthy;
 import org.apache.felix.hc.api.condition.SystemReady;
 import org.apache.felix.hc.api.condition.Unhealthy;
@@ -109,6 +107,18 @@ class HealthState {
         return statusChanged;
     }
 
+    public boolean isHealthy() {
+        return isHealthy;
+    }
+
+    String getTagOrName() {
+        return tagOrName;
+    }
+
+    boolean isTag() {
+        return isTag;
+    }
+
     HealthCheckExecutionResult getExecutionResult() {
         return executionResult;
     }
@@ -214,8 +224,10 @@ class HealthState {
 
     private void sendEvents(HealthCheckExecutionResult executionResult, Result.Status previousStatus) {
         ChangeType sendEventsConfig = monitor.getSendEvents();
-        if ((sendEventsConfig == ChangeType.STATUS_CHANGES && statusChanged) || 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)) {
+
             String eventSuffix = statusChanged ? EVENT_TOPIC_SUFFIX_STATUS_CHANGED : EVENT_TOPIC_SUFFIX_STATUS_UPDATED;
             String logMsg = "Posted event for topic '{}': " + (statusChanged ? "Status change from {} to {}" : "Result updated (status {})");
 
diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultTxtVerboseSerializer.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultTxtVerboseSerializer.java
index 1148344..f5bbc8f 100644
--- a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultTxtVerboseSerializer.java
+++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultTxtVerboseSerializer.java
@@ -62,27 +62,47 @@ public class ResultTxtVerboseSerializer {
         colWidthLog = totalWidth - colWidthWithoutLog;
     }
 
-    public String serialize(final Result overallResult, final List<HealthCheckExecutionResult> executionResults, boolean includeDebug) {
+    // without overall result, only the execution results are listed without header
+    public String serialize(String label, final List<HealthCheckExecutionResult> executionResults, boolean includeDebug) {
+        StringBuilder resultStr = new StringBuilder();
+
+        resultStr.append(label + "\n");
+        resultStr.append(serializeResults(executionResults, includeDebug));
 
-        LOG.debug("Sending verbose txt response... ");
+        return resultStr.toString();
+    }
+
+    public String serialize(final Result overallResult, final List<HealthCheckExecutionResult> executionResults, boolean includeDebug) {
 
         StringBuilder resultStr = new StringBuilder();
 
         resultStr.append(StringUtils.repeat("-", totalWidth) + NEWLINE);
-        resultStr.append(center("Overall Health Result: " + overallResult.getStatus().toString(), totalWidth) + NEWLINE);
+        resultStr.append(
+                center("Overall Health Result: " + overallResult.getStatus().toString(), totalWidth) + NEWLINE);
         resultStr.append(StringUtils.repeat("-", totalWidth) + NEWLINE);
+
         resultStr.append(rightPad("Name", colWidthName));
         resultStr.append(rightPad("Result", colWidthResult));
         resultStr.append(rightPad("Timing", colWidthTiming));
         resultStr.append("Logs" + NEWLINE);
         resultStr.append(StringUtils.repeat("-", totalWidth) + NEWLINE);
 
+        resultStr.append(serializeResults(executionResults, includeDebug));
+        resultStr.append(StringUtils.repeat("-", totalWidth) + NEWLINE);
+
+        return resultStr.toString();
+
+    }
+    
+    private String serializeResults(final List<HealthCheckExecutionResult> executionResults, boolean includeDebug) {
+
+        StringBuilder resultStr = new StringBuilder();
+
         final DateFormat dfShort = new SimpleDateFormat("HH:mm:ss.SSS");
 
         for (HealthCheckExecutionResult healthCheckResult : executionResults) {
             appendVerboseTxtForResult(resultStr, healthCheckResult, includeDebug, dfShort);
         }
-        resultStr.append(StringUtils.repeat("-", totalWidth) + NEWLINE);
 
         return resultStr.toString();
 
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 2d9502d..47f69b5 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
@@ -20,7 +20,12 @@ package org.apache.felix.hc.core.impl.monitor;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.matches;
+import static org.mockito.Mockito.lenient;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
@@ -44,13 +49,18 @@ import org.apache.felix.hc.core.impl.executor.ExecutionResult;
 import org.apache.felix.hc.core.impl.executor.ExtendedHealthCheckExecutor;
 import org.apache.felix.hc.core.impl.executor.HealthCheckExecutorThreadPool;
 import org.apache.felix.hc.core.impl.scheduling.AsyncIntervalJob;
+import org.apache.felix.hc.core.impl.servlet.ResultTxtVerboseSerializer;
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.InvalidSyntaxException;
 import org.osgi.framework.ServiceReference;
@@ -60,12 +70,15 @@ import org.osgi.service.component.ComponentContext;
 import org.osgi.service.event.Event;
 import org.osgi.service.event.EventAdmin;
 
+@RunWith(MockitoJUnitRunner.class)
 public class HealthCheckMonitorTest {
 
     private static final String TEST_TAG = "test-tag";
+    private static final String HC_RESULT_SERIALIZED = "HC result serialized";
 
+    @Spy
     @InjectMocks
-    private HealthCheckMonitor healthCheckMonitor = new HealthCheckMonitor();
+    private HealthCheckMonitor healthCheckMonitor;
 
     @Mock
     private BundleContext bundleContext;
@@ -103,9 +116,11 @@ public class HealthCheckMonitorTest {
     @Mock
     private ServiceRegistration<Unhealthy> unhealthyRegistration;
     
+    @Mock
+    private ResultTxtVerboseSerializer resultTxtVerboseSerializer;
+    
     @Before
     public void before() throws ReflectiveOperationException {
-        MockitoAnnotations.initMocks(this);
 
         for (Method m : HealthCheckMonitor.Config.class.getDeclaredMethods()) {
             when(m.invoke(config)).thenReturn(m.getDefaultValue());
@@ -115,10 +130,12 @@ public class HealthCheckMonitorTest {
         when(config.tags()).thenReturn(new String[] { TEST_TAG });
         
         when(healthCheckMetadata.getServiceReference()).thenReturn(healthCheckServiceRef);
-        
+        when(healthCheckMetadata.getTitle()).thenReturn("Test Check");
+
         Dictionary<String,Object> componentProps = new Hashtable<>();
         componentProps.put(ComponentConstants.COMPONENT_ID, 7L);
         when(componentContext.getProperties()).thenReturn(componentProps);
+
     }
 
     @Test
@@ -188,9 +205,9 @@ public class HealthCheckMonitorTest {
     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(Unhealthy.class), eq(HealthState.MARKER_SERVICE_UNHEALTHY), any())).thenReturn(unhealthyRegistration);
+        lenient().when(bundleContext.registerService(eq(Unhealthy.class), eq(HealthState.MARKER_SERVICE_UNHEALTHY), any())).thenReturn(unhealthyRegistration);
     }
-    
+
     @Test
     public void testRunSendEventsStatusChanges() throws InvalidSyntaxException {
 
@@ -271,7 +288,185 @@ public class HealthCheckMonitorTest {
         assertEquals(Result.Status.OK, postedEvents.get(0).getProperty(HealthState.EVENT_PROP_PREVIOUS_STATUS));
         assertEquals("org/apache/felix/health/component/org/apache/felix/TestHealthCheck/UPDATED", postedEvents.get(1).getTopic());
     }
+
+
+
+    @Test
+    public void testRunLogAll() throws InvalidSyntaxException {
+
+        prepareLoggingTest(HealthCheckMonitor.ChangeType.ALL);
+
+        setHcResult(Result.Status.OK);
+        healthCheckMonitor.run();
+        verify(healthCheckMonitor).logResultItem(eq(true), matches(".*healthy:true hasChanged:true .*" + HC_RESULT_SERIALIZED));
+
+        reset(healthCheckMonitor);
+        setHcResult(Result.Status.OK);
+        healthCheckMonitor.run();
+        verify(healthCheckMonitor).logResultItem(eq(true), matches(".*healthy:true hasChanged:false .*" + HC_RESULT_SERIALIZED));
+
+        reset(healthCheckMonitor);
+        setHcResult(Result.Status.WARN);
+        healthCheckMonitor.run();
+        verify(healthCheckMonitor).logResultItem(eq(false), matches(".*healthy:true hasChanged:true .*" + HC_RESULT_SERIALIZED));
+
+        reset(healthCheckMonitor);
+        setHcResult(Result.Status.WARN);
+        healthCheckMonitor.run();
+        verify(healthCheckMonitor).logResultItem(eq(false), matches(".*healthy:true hasChanged:false .*" + HC_RESULT_SERIALIZED));
+
+        reset(healthCheckMonitor);
+        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();
+        verify(healthCheckMonitor).logResultItem(eq(false), matches(".*healthy:false hasChanged:false .*" + HC_RESULT_SERIALIZED));
+
+        reset(healthCheckMonitor);
+        setHcResult(Result.Status.OK);
+        healthCheckMonitor.run();
+        verify(healthCheckMonitor).logResultItem(eq(true), matches(".*healthy:true hasChanged:true .*" + HC_RESULT_SERIALIZED));
+
+        reset(healthCheckMonitor);
+        setHcResult(Result.Status.OK);
+        healthCheckMonitor.run();
+        verify(healthCheckMonitor).logResultItem(eq(true), matches(".*healthy:true hasChanged:false .*" + HC_RESULT_SERIALIZED));
+
+    }
+
+
+
+    @Test
+    public void testRunLogStatusChanges() throws InvalidSyntaxException {
+
+        prepareLoggingTest(HealthCheckMonitor.ChangeType.STATUS_CHANGES);
+
+        setHcResult(Result.Status.OK);
+        healthCheckMonitor.run();
+        verify(healthCheckMonitor).logResultItem(eq(true), matches(".*healthy:true hasChanged:true .*" + HC_RESULT_SERIALIZED));
+
+        reset(healthCheckMonitor);
+        setHcResult(Result.Status.OK);
+        healthCheckMonitor.run();
+        verify(healthCheckMonitor, never()).logResultItem(anyBoolean(), anyString());
+
+        reset(healthCheckMonitor);
+        setHcResult(Result.Status.WARN);
+        healthCheckMonitor.run();
+        verify(healthCheckMonitor).logResultItem(eq(false), matches(".*healthy:true hasChanged:true .*" + HC_RESULT_SERIALIZED));
+
+        reset(healthCheckMonitor);
+        setHcResult(Result.Status.WARN);
+        healthCheckMonitor.run();
+        verify(healthCheckMonitor, never()).logResultItem(anyBoolean(), anyString());
+
+        reset(healthCheckMonitor);
+        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();
+        verify(healthCheckMonitor, never()).logResultItem(anyBoolean(), anyString());
+
+        reset(healthCheckMonitor);
+        setHcResult(Result.Status.OK);
+        healthCheckMonitor.run();
+        verify(healthCheckMonitor).logResultItem(eq(true), matches(".*healthy:true hasChanged:true .*" + HC_RESULT_SERIALIZED));
+
+        reset(healthCheckMonitor);
+        setHcResult(Result.Status.OK);
+        healthCheckMonitor.run();
+        verify(healthCheckMonitor, never()).logResultItem(anyBoolean(), anyString());
+
+    }
+
+
+    @Test
+    public void testRunLogStatusChangesOrNotOk() throws InvalidSyntaxException {
+
+        prepareLoggingTest(HealthCheckMonitor.ChangeType.STATUS_CHANGES_OR_NOT_OK);
+
+        setHcResult(Result.Status.OK);
+        healthCheckMonitor.run();
+        verify(healthCheckMonitor).logResultItem(eq(true), matches(".*healthy:true hasChanged:true .*" + HC_RESULT_SERIALIZED));
+
+        reset(healthCheckMonitor);
+        setHcResult(Result.Status.OK);
+        healthCheckMonitor.run();
+        verify(healthCheckMonitor, never()).logResultItem(anyBoolean(), anyString());
+
+        reset(healthCheckMonitor);
+        setHcResult(Result.Status.WARN);
+        healthCheckMonitor.run();
+        verify(healthCheckMonitor).logResultItem(eq(false), matches(".*healthy:true hasChanged:true .*" + HC_RESULT_SERIALIZED));
+
+        reset(healthCheckMonitor);
+        setHcResult(Result.Status.WARN);
+        healthCheckMonitor.run();
+        verify(healthCheckMonitor).logResultItem(eq(false), matches(".*healthy:true hasChanged:false .*" + HC_RESULT_SERIALIZED));
+
+        reset(healthCheckMonitor);
+        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();
+        verify(healthCheckMonitor).logResultItem(eq(false), matches(".*healthy:false hasChanged:false .*" + HC_RESULT_SERIALIZED));
+
+        reset(healthCheckMonitor);
+        setHcResult(Result.Status.OK);
+        healthCheckMonitor.run();
+        verify(healthCheckMonitor).logResultItem(eq(true), matches(".*healthy:true hasChanged:true .*" + HC_RESULT_SERIALIZED));
+
+        reset(healthCheckMonitor);
+        setHcResult(Result.Status.OK);
+        healthCheckMonitor.run();
+        verify(healthCheckMonitor, never()).logResultItem(anyBoolean(), anyString());
+
+    }
+
+
+    @Test
+    public void testRunLogStatusNone() throws InvalidSyntaxException {
+
+        prepareLoggingTest(HealthCheckMonitor.ChangeType.NONE);
+
+        for (Result.Status status : Arrays.asList(
+                Result.Status.OK, Result.Status.OK,
+                Result.Status.WARN, Result.Status.WARN,
+                Result.Status.CRITICAL, Result.Status.CRITICAL,
+                Result.Status.OK, Result.Status.OK)) {
+
+            setHcResult(status);
+            healthCheckMonitor.run();
+        }
+
+        // ensure logging is never called for whatever state remains the same or changes
+        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);
+        healthCheckMonitor.activate(bundleContext, config, componentContext);
+        healthCheckMonitor.healthStates.put(TEST_TAG, new HealthState(healthCheckMonitor, TEST_TAG, true));
+
+        when(resultTxtVerboseSerializer.serialize(any(String.class), anyList(), eq(false))).thenAnswer(new Answer<String>() {
+            @Override
+            public String answer(InvocationOnMock invocation) throws Throwable {
+              Object[] args = invocation.getArguments();
+              return (String) args[0] + " " + HC_RESULT_SERIALIZED;
+            }
+          });
+    }
     
     private void setHcResult(Result.Status status) {
         when(healthCheckExecutor.execute(HealthCheckSelector.tags(TEST_TAG)))