You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@activemq.apache.org by gt...@apache.org on 2022/10/13 17:23:16 UTC

[activemq-artemis] 01/02: ARTEMIS-4025 - trap failure to apply properties as errors and report via the broker status in a configuration/properties/errors status field

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

gtully pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/activemq-artemis.git

commit 03ef286cc8d3124f339515cbb644cd4c0156237d
Author: Gary Tully <ga...@gmail.com>
AuthorDate: Mon Oct 3 13:39:43 2022 +0100

    ARTEMIS-4025 - trap failure to apply properties as errors and report via the broker status in a configuration/properties/errors status field
---
 .../apache/activemq/artemis/api/core/JsonUtil.java |  49 +++++
 .../activemq/artemis/api/core/JsonUtilTest.java    |  85 ++++++++
 .../core/config/impl/ConfigurationImpl.java        | 217 ++++++++++++++++++++-
 .../management/impl/ActiveMQServerControlImpl.java |   2 +-
 .../artemis/core/server/ActiveMQServer.java        |   2 +
 .../core/server/impl/ActiveMQServerImpl.java       |  15 ++
 .../artemis/core/server/impl/ServerStatus.java     |  69 +++++++
 .../core/config/impl/ConfigurationImplTest.java    |  50 +++++
 .../integration/server/ConfigurationTest.java      |   6 +-
 9 files changed, 483 insertions(+), 12 deletions(-)

diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/JsonUtil.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/JsonUtil.java
index c98aa9cf97..bfa7bbce31 100644
--- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/JsonUtil.java
+++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/JsonUtil.java
@@ -360,6 +360,55 @@ public final class JsonUtil {
       return result;
    }
 
+   public static JsonObject mergeAndUpdate(JsonObject source, JsonObject update) {
+      // all immutable so we need to create new merged instance
+      JsonObjectBuilder jsonObjectBuilder = JsonLoader.createObjectBuilder();
+      for (Map.Entry<String, JsonValue> entry : source.entrySet()) {
+         jsonObjectBuilder.add(entry.getKey(), entry.getValue());
+      }
+      // apply any updates
+      for (String updateKey : update.keySet()) {
+         JsonValue updatedValue = update.get(updateKey);
+         if (updatedValue != null) {
+            if (!source.containsKey(updateKey)) {
+               jsonObjectBuilder.add(updateKey, updatedValue);
+            } else {
+               // recursively merge into new value
+               if (updatedValue.getValueType() == JsonValue.ValueType.OBJECT) {
+                  jsonObjectBuilder.add(updateKey, mergeAndUpdate(source.getJsonObject(updateKey), updatedValue.asJsonObject()));
+
+               } else if (updatedValue.getValueType() == JsonValue.ValueType.ARRAY) {
+                  JsonArrayBuilder jsonArrayBuilder = JsonLoader.createArrayBuilder();
+
+                  // update wins
+                  JsonArray updatedArrayValue = update.getJsonArray(updateKey);
+                  JsonArray sourceArrayValue = source.getJsonArray(updateKey);
+
+                  for (int i = 0; i < updatedArrayValue.size(); i++) {
+                     if (i < sourceArrayValue.size()) {
+                        JsonValue element = updatedArrayValue.get(i);
+                        if (element.getValueType() == JsonValue.ValueType.OBJECT) {
+                           jsonArrayBuilder.add(mergeAndUpdate(sourceArrayValue.getJsonObject(i), updatedArrayValue.getJsonObject(i)));
+                        } else {
+                           // take the update
+                           jsonArrayBuilder.add(element);
+                        }
+                     } else {
+                        jsonArrayBuilder.add(updatedArrayValue.get(i));
+                     }
+                  }
+                  jsonObjectBuilder.add(updateKey, jsonArrayBuilder.build());
+               } else {
+                  // update wins!
+                  jsonObjectBuilder.add(updateKey, updatedValue);
+               }
+            }
+         }
+      }
+
+      return jsonObjectBuilder.build();
+   }
+
    private static class NullableJsonString implements JsonValue, JsonString {
 
       private final String value;
diff --git a/artemis-core-client/src/test/java/org/apache/activemq/artemis/api/core/JsonUtilTest.java b/artemis-core-client/src/test/java/org/apache/activemq/artemis/api/core/JsonUtilTest.java
index d6d28470cc..7c11ad4534 100644
--- a/artemis-core-client/src/test/java/org/apache/activemq/artemis/api/core/JsonUtilTest.java
+++ b/artemis-core-client/src/test/java/org/apache/activemq/artemis/api/core/JsonUtilTest.java
@@ -127,4 +127,89 @@ public class JsonUtilTest {
 
       Assert.assertEquals(input, notTruncated);
    }
+
+   @Test
+   public void testMergeEqual() {
+
+      final byte[] bytesA = {0x0a, 0x0b};
+      final byte[] bytesB = {0x1a, 0x1b};
+      final byte[] bytesAB = {0x0a, 0x0b, 0x1a, 0x1b};
+
+      JsonObjectBuilder jsonObjectBuilder = JsonLoader.createObjectBuilder();
+      JsonUtil.addToObject("a", "a", jsonObjectBuilder);
+      JsonUtil.addToObject("byteArray", bytesA, jsonObjectBuilder);
+      JsonUtil.addToObject("null", null, jsonObjectBuilder);
+
+      JsonObject sourceOne = jsonObjectBuilder.build();
+
+      JsonObjectBuilder jsonTargetObjectBuilder = JsonLoader.createObjectBuilder();
+      JsonUtil.addToObject("a", "a", jsonTargetObjectBuilder);
+      JsonUtil.addToObject("byteArray", bytesB, jsonTargetObjectBuilder);
+      JsonUtil.addToObject("null", null, jsonTargetObjectBuilder);
+
+      JsonObject sourceTwo = jsonTargetObjectBuilder.build();
+
+      JsonObjectBuilder jsonMergedObjectBuilder = JsonLoader.createObjectBuilder();
+      JsonUtil.addToObject("a", "a", jsonMergedObjectBuilder);
+      // update wins
+      JsonUtil.addToObject("byteArray", bytesB, jsonMergedObjectBuilder);
+      JsonUtil.addToObject("null", null, jsonMergedObjectBuilder);
+
+      JsonObject mergedExpected = jsonMergedObjectBuilder.build();
+
+      Assert.assertEquals(mergedExpected, JsonUtil.mergeAndUpdate(sourceOne, sourceTwo));
+   }
+
+   @Test
+   public void testMergeArray() {
+
+      final byte[] bytesA = {0x0a, 0x0b};
+      JsonObjectBuilder jsonObjectBuilder = JsonLoader.createObjectBuilder();
+
+      jsonObjectBuilder = JsonLoader.createObjectBuilder();
+      JsonUtil.addToObject("byteArray", bytesA, jsonObjectBuilder);
+
+      JsonObject sourceOne = jsonObjectBuilder.build();
+
+      JsonObjectBuilder jsonTargetObjectBuilder = JsonLoader.createObjectBuilder();
+      JsonUtil.addToObject("byteArray", bytesA, jsonTargetObjectBuilder);
+
+      JsonObject sourceTwo = jsonTargetObjectBuilder.build();
+
+      JsonObjectBuilder jsonMergedObjectBuilder = JsonLoader.createObjectBuilder();
+      JsonUtil.addToObject("byteArray", bytesA, jsonMergedObjectBuilder);
+
+      JsonObject mergedExpected = jsonMergedObjectBuilder.build();
+
+      Assert.assertEquals(mergedExpected, JsonUtil.mergeAndUpdate(sourceOne, sourceTwo));
+
+   }
+
+   @Test
+   public void testMergeDuplicate() {
+
+      // merge duplicate attribute value, two wins
+      JsonObjectBuilder jsonObjectBuilder = JsonLoader.createObjectBuilder();
+      JsonUtil.addToObject("dup", "a", jsonObjectBuilder);
+      JsonObject sourceOne = jsonObjectBuilder.build();
+
+      JsonObjectBuilder jsonTargetObjectBuilder = JsonLoader.createObjectBuilder();
+      JsonUtil.addToObject("dup", "b", jsonTargetObjectBuilder);
+      JsonObject sourceTwo = jsonTargetObjectBuilder.build();
+
+      Assert.assertEquals(sourceTwo, JsonUtil.mergeAndUpdate(sourceOne, sourceTwo));
+   }
+
+   @Test
+   public void testMergeEmpty() {
+
+      // merge into empty
+      JsonObject sourceOne = JsonLoader.createObjectBuilder().build();
+
+      JsonObjectBuilder jsonTargetObjectBuilder = JsonLoader.createObjectBuilder();
+      JsonUtil.addToObject("dup", "b", jsonTargetObjectBuilder);
+      JsonObject sourceTwo = jsonTargetObjectBuilder.build();
+
+      Assert.assertEquals(sourceTwo, JsonUtil.mergeAndUpdate(sourceOne, sourceTwo));
+   }
 }
diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImpl.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImpl.java
index b67f42242b..a7116a1e54 100644
--- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImpl.java
+++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImpl.java
@@ -16,6 +16,7 @@
  */
 package org.apache.activemq.artemis.core.config.impl;
 
+import java.beans.IndexedPropertyDescriptor;
 import java.beans.PropertyDescriptor;
 import java.io.BufferedInputStream;
 import java.io.ByteArrayInputStream;
@@ -52,6 +53,7 @@ import java.util.concurrent.TimeUnit;
 import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration;
 import org.apache.activemq.artemis.api.core.BroadcastGroupConfiguration;
 import org.apache.activemq.artemis.api.core.DiscoveryGroupConfiguration;
+import org.apache.activemq.artemis.api.core.JsonUtil;
 import org.apache.activemq.artemis.api.core.Pair;
 import org.apache.activemq.artemis.api.core.QueueConfiguration;
 import org.apache.activemq.artemis.api.core.SimpleString;
@@ -99,8 +101,12 @@ import org.apache.activemq.artemis.core.server.plugin.ActiveMQServerResourcePlug
 import org.apache.activemq.artemis.core.server.plugin.ActiveMQServerSessionPlugin;
 import org.apache.activemq.artemis.core.settings.impl.AddressSettings;
 import org.apache.activemq.artemis.core.settings.impl.ResourceLimitSettings;
+import org.apache.activemq.artemis.json.JsonArrayBuilder;
+import org.apache.activemq.artemis.json.JsonObject;
+import org.apache.activemq.artemis.json.JsonObjectBuilder;
 import org.apache.activemq.artemis.utils.ByteUtil;
 import org.apache.activemq.artemis.utils.Env;
+import org.apache.activemq.artemis.utils.JsonLoader;
 import org.apache.activemq.artemis.utils.ObjectInputStreamWithClassLoader;
 import org.apache.activemq.artemis.utils.PasswordMaskingUtil;
 import org.apache.activemq.artemis.utils.XMLUtil;
@@ -109,6 +115,7 @@ import org.apache.activemq.artemis.utils.uri.BeanSupport;
 import org.apache.commons.beanutils.BeanUtilsBean;
 import org.apache.commons.beanutils.ConvertUtilsBean;
 import org.apache.commons.beanutils.Converter;
+import org.apache.commons.beanutils.DynaBean;
 import org.apache.commons.beanutils.MappedPropertyDescriptor;
 import org.apache.commons.beanutils.MethodUtils;
 import org.apache.commons.beanutils.PropertyUtilsBean;
@@ -116,6 +123,7 @@ import org.apache.commons.beanutils.expression.DefaultResolver;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import java.lang.invoke.MethodHandles;
+import org.apache.commons.beanutils.expression.Resolver;
 
 public class ConfigurationImpl implements Configuration, Serializable {
 
@@ -403,7 +411,7 @@ public class ConfigurationImpl implements Configuration, Serializable {
     * Parent folder for all data folders.
     */
    private File artemisInstance;
-   private String status;
+   private transient volatile JsonObject status = JsonLoader.createObjectBuilder().build();
 
    @Override
    public String getJournalRetentionDirectory() {
@@ -538,7 +546,146 @@ public class ConfigurationImpl implements Configuration, Serializable {
 
    public void populateWithProperties(Map<String, Object> beanProperties) throws InvocationTargetException, IllegalAccessException {
       CollectionAutoFillPropertiesUtil autoFillCollections = new CollectionAutoFillPropertiesUtil();
-      BeanUtilsBean beanUtils = new BeanUtilsBean(new ConvertUtilsBean(), autoFillCollections);
+      BeanUtilsBean beanUtils = new BeanUtilsBean(new ConvertUtilsBean(), autoFillCollections) {
+         // override to treat missing properties as errors, not skip as the default impl does
+         @Override
+         public void setProperty(final Object bean, String name, final Object value) throws InvocationTargetException, IllegalAccessException {
+            {
+               if (logger.isTraceEnabled()) {
+                  final StringBuilder sb = new StringBuilder("  setProperty(");
+                  sb.append(bean);
+                  sb.append(", ");
+                  sb.append(name);
+                  sb.append(", ");
+                  if (value == null) {
+                     sb.append("<NULL>");
+                  } else if (value instanceof String) {
+                     sb.append((String) value);
+                  } else if (value instanceof String[]) {
+                     final String[] values = (String[]) value;
+                     sb.append('[');
+                     for (int i = 0; i < values.length; i++) {
+                        if (i > 0) {
+                           sb.append(',');
+                        }
+                        sb.append(values[i]);
+                     }
+                     sb.append(']');
+                  } else {
+                     sb.append(value.toString());
+                  }
+                  sb.append(')');
+                  logger.trace(sb.toString());
+               }
+
+               // Resolve any nested expression to get the actual target bean
+               Object target = bean;
+               final Resolver resolver = getPropertyUtils().getResolver();
+               while (resolver.hasNested(name)) {
+                  try {
+                     target = getPropertyUtils().getProperty(target, resolver.next(name));
+                     if (target == null) {
+                        throw new InvocationTargetException(null, "Resolved nested property for:" + name + ", on: " + bean + " was null");
+                     }
+                     name = resolver.remove(name);
+                  } catch (final NoSuchMethodException e) {
+                     throw new InvocationTargetException(e, "No getter for property:" + name + ", on: " + bean);
+                  }
+               }
+               if (logger.isTraceEnabled()) {
+                  logger.trace("    Target bean = " + target);
+                  logger.trace("    Target name = " + name);
+               }
+
+               // Declare local variables we will require
+               final String propName = resolver.getProperty(name); // Simple name of target property
+               Class<?> type = null;                         // Java type of target property
+               final int index = resolver.getIndex(name);         // Indexed subscript value (if any)
+               final String key = resolver.getKey(name);           // Mapped key value (if any)
+
+               // Calculate the property type
+               if (target instanceof DynaBean) {
+                  throw new InvocationTargetException(null, "Cannot determine DynaBean type to access: " + name + " on: " + target);
+               } else if (target instanceof Map) {
+                  type = Object.class;
+               } else if (target != null && target.getClass().isArray() && index >= 0) {
+                  type = Array.get(target, index).getClass();
+               } else {
+                  PropertyDescriptor descriptor = null;
+                  try {
+                     descriptor = getPropertyUtils().getPropertyDescriptor(target, name);
+                     if (descriptor == null) {
+                        throw new InvocationTargetException(null, "No accessor method descriptor for: " + name + " on: " + target.getClass());
+                     }
+                  } catch (final NoSuchMethodException e) {
+                     throw new InvocationTargetException(e, "Failed to get descriptor for: " + name + " on: " + target.getClass());
+                  }
+                  if (descriptor instanceof MappedPropertyDescriptor) {
+                     if (((MappedPropertyDescriptor) descriptor).getMappedWriteMethod() == null) {
+                        throw new InvocationTargetException(null, "No mapped Write method for: " + name + " on: " + target.getClass());
+                     }
+                     type = ((MappedPropertyDescriptor) descriptor).getMappedPropertyType();
+                  } else if (index >= 0 && descriptor instanceof IndexedPropertyDescriptor) {
+                     if (((IndexedPropertyDescriptor) descriptor).getIndexedWriteMethod() == null) {
+                        throw new InvocationTargetException(null, "No indexed Write method for: " + name + " on: " + target.getClass());
+                     }
+                     type = ((IndexedPropertyDescriptor) descriptor).getIndexedPropertyType();
+                  } else if (index >= 0 && List.class.isAssignableFrom(descriptor.getPropertyType())) {
+                     type = Object.class;
+                  } else if (key != null) {
+                     if (descriptor.getReadMethod() == null) {
+                        throw new InvocationTargetException(null, "No Read method for: " + name + " on: " + target.getClass());
+                     }
+                     type = (value == null) ? Object.class : value.getClass();
+                  } else {
+                     if (descriptor.getWriteMethod() == null) {
+                        throw new InvocationTargetException(null, "No Write method for: " + name + " on: " + target.getClass());
+                     }
+                     type = descriptor.getPropertyType();
+                  }
+               }
+
+               // Convert the specified value to the required type
+               Object newValue = null;
+               if (type.isArray() && (index < 0)) { // Scalar value into array
+                  if (value == null) {
+                     final String[] values = new String[1];
+                     values[0] = null;
+                     newValue = getConvertUtils().convert(values, type);
+                  } else if (value instanceof String) {
+                     newValue = getConvertUtils().convert(value, type);
+                  } else if (value instanceof String[]) {
+                     newValue = getConvertUtils().convert((String[]) value, type);
+                  } else {
+                     newValue = convert(value, type);
+                  }
+               } else if (type.isArray()) {         // Indexed value into array
+                  if (value instanceof String || value == null) {
+                     newValue = getConvertUtils().convert((String) value, type.getComponentType());
+                  } else if (value instanceof String[]) {
+                     newValue = getConvertUtils().convert(((String[]) value)[0], type.getComponentType());
+                  } else {
+                     newValue = convert(value, type.getComponentType());
+                  }
+               } else {                             // Value into scalar
+                  if (value instanceof String) {
+                     newValue = getConvertUtils().convert((String) value, type);
+                  } else if (value instanceof String[]) {
+                     newValue = getConvertUtils().convert(((String[]) value)[0], type);
+                  } else {
+                     newValue = convert(value, type);
+                  }
+               }
+
+               // Invoke the setter method
+               try {
+                  getPropertyUtils().setProperty(target, name, newValue);
+               } catch (final NoSuchMethodException e) {
+                  throw new InvocationTargetException(e, "Cannot set: " + propName + " on: " + target.getClass());
+               }
+            }
+         }
+      };
       autoFillCollections.setBeanUtilsBean(beanUtils);
       // nested property keys delimited by . and enclosed by '"' if they key's themselves contain dots
       beanUtils.getPropertyUtils().setResolver(new SurroundResolver(getBrokerPropertiesKeySurround(beanProperties)));
@@ -599,7 +746,46 @@ public class ConfigurationImpl implements Configuration, Serializable {
 
       BeanSupport.customise(beanUtils);
 
-      beanUtils.populate(this, beanProperties);
+      logger.debug("populate:" + this + ", " + beanProperties);
+
+      HashMap<String, String> errors = new HashMap<>();
+      // Loop through the property name/value pairs to be set
+      for (final Map.Entry<String, ? extends Object> entry : beanProperties.entrySet()) {
+         // Identify the property name and value(s) to be assigned
+         final String name = entry.getKey();
+         try {
+            // Perform the assignment for this property
+            beanUtils.setProperty(this, name, entry.getValue());
+         } catch (InvocationTargetException invocationTargetException) {
+            logger.trace("failed to populate property key: " + name, invocationTargetException);
+            Throwable toLog = invocationTargetException;
+            if (invocationTargetException.getCause() != null) {
+               toLog = invocationTargetException.getCause();
+            }
+            trackError(errors, entry, toLog);
+
+         } catch (Exception oops) {
+            trackError(errors, entry, oops);
+         }
+      }
+      updateStatus(errors);
+   }
+
+   private void trackError(HashMap<String, String> errors, Map.Entry<String,?> entry, Throwable oops) {
+      logger.debug("failed to populate property entry(" + entry.toString() + "), reason: " + oops.getLocalizedMessage(), oops);
+      errors.put(entry.toString(), oops.getLocalizedMessage());
+   }
+
+   private void updateStatus(HashMap<String, String> errors) {
+
+      JsonArrayBuilder errorsObjectArray = JsonLoader.createArrayBuilder();
+      for (Map.Entry<String, String> entry : errors.entrySet()) {
+         errorsObjectArray.add(JsonLoader.createObjectBuilder().add("value", entry.getKey()).add("reason", entry.getValue()));
+      }
+      JsonObjectBuilder jsonObjectBuilder = JsonLoader.createObjectBuilder();
+      jsonObjectBuilder.add("properties",  JsonLoader.createObjectBuilder().add("errors", errorsObjectArray));
+
+      status = JsonUtil.mergeAndUpdate(status, jsonObjectBuilder.build());
    }
 
    private String getBrokerPropertiesKeySurround(Map<String, Object> propertiesToApply) {
@@ -2852,13 +3038,14 @@ public class ConfigurationImpl implements Configuration, Serializable {
    }
 
    @Override
-   public String getStatus() {
-      return status;
+   public synchronized String getStatus() {
+      return status.toString();
    }
 
    @Override
-   public void setStatus(String status) {
-      this.status = status;
+   public synchronized void setStatus(String status) {
+      JsonObject update = JsonUtil.readJsonObject(status);
+      this.status = JsonUtil.mergeAndUpdate(this.status, update);
    }
 
    // extend property utils with ability to auto-fill and locate from collections
@@ -2904,7 +3091,14 @@ public class ConfigurationImpl implements Configuration, Serializable {
             }
          }
 
-         Object resolved = getNestedProperty(bean, name);
+         Object resolved = null;
+
+         try {
+            resolved = getNestedProperty(bean, name);
+         } catch (final NoSuchMethodException e) {
+            // to avoid it being swallowed by caller wrap
+            throw new InvocationTargetException(e, "Cannot access property with key: " + name);
+         }
 
          return trackCollectionOrMap(name, resolved, bean);
       }
@@ -3015,7 +3209,12 @@ public class ConfigurationImpl implements Configuration, Serializable {
          // create one and initialise with name
          try {
             Object instance = candidate.getParameterTypes()[candidate.getParameterCount() - 1].getDeclaredConstructor().newInstance();
-            beanUtilsBean.setProperty(instance, "name", name);
+
+            try {
+               beanUtilsBean.setProperty(instance, "name", name);
+            } catch (Throwable ignored) {
+               // for maps a name attribute is not mandatory
+            }
 
             // this is always going to be a little hacky b/c our config is not natively property friendly
             if (instance instanceof TransportConfiguration) {
diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/ActiveMQServerControlImpl.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/ActiveMQServerControlImpl.java
index de0066dcd1..d7edd431c0 100644
--- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/ActiveMQServerControlImpl.java
+++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/ActiveMQServerControlImpl.java
@@ -4499,7 +4499,7 @@ public class ActiveMQServerControlImpl extends AbstractControl implements Active
       }
       checkStarted();
 
-      return server.getConfiguration().getStatus();
+      return server.getStatus();
    }
 
    @Override
diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServer.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServer.java
index bbc32eb727..bfc0feea5a 100644
--- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServer.java
+++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServer.java
@@ -167,6 +167,8 @@ public interface ActiveMQServer extends ServiceComponent {
 
    CriticalAnalyzer getCriticalAnalyzer();
 
+   void updateStatus(String component, String statusJson);
+
    /**
     * it will release hold a lock for the activation.
     */
diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ActiveMQServerImpl.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ActiveMQServerImpl.java
index ccbb53013e..27b6ba1cc9 100644
--- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ActiveMQServerImpl.java
+++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ActiveMQServerImpl.java
@@ -403,6 +403,8 @@ public class ActiveMQServerImpl implements ActiveMQServer {
       }
    };
 
+   private final ServerStatus serverStatus;
+
    public ActiveMQServerImpl() {
       this(null, null, null);
    }
@@ -481,6 +483,7 @@ public class ActiveMQServerImpl implements ActiveMQServer {
 
       this.serviceRegistry = serviceRegistry == null ? new ServiceRegistryImpl() : serviceRegistry;
 
+      this.serverStatus = new ServerStatus(this);
    }
 
    @Override
@@ -608,6 +611,16 @@ public class ActiveMQServerImpl implements ActiveMQServer {
       propertiesFileUrl = fileUrltoBrokerProperties;
    }
 
+   @Override
+   public String getStatus() {
+      return serverStatus.asJson();
+   }
+
+   @Override
+   public void updateStatus(String component, String statusJson) {
+      serverStatus.update(component, statusJson);
+   }
+
    private void internalStart() throws Exception {
       if (state != SERVER_STATE.STOPPED) {
          logger.debug("Server already started!");
@@ -615,6 +628,7 @@ public class ActiveMQServerImpl implements ActiveMQServer {
       }
 
       configuration.parseProperties(propertiesFileUrl);
+      updateStatus("configuration", configuration.getStatus());
 
       initializeExecutorServices();
 
@@ -4388,6 +4402,7 @@ public class ActiveMQServerImpl implements ActiveMQServer {
       configurationReloadDeployed.set(false);
       if (isActive()) {
          configuration.parseProperties(propertiesFileUrl);
+         updateStatus("configuration", configuration.getStatus());
          deployReloadableConfigFromConfiguration();
       }
    }
diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ServerStatus.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ServerStatus.java
new file mode 100644
index 0000000000..50fbe77533
--- /dev/null
+++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ServerStatus.java
@@ -0,0 +1,69 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.activemq.artemis.core.server.impl;
+
+
+import java.util.HashMap;
+
+import org.apache.activemq.artemis.api.core.JsonUtil;
+import org.apache.activemq.artemis.api.core.SimpleString;
+import org.apache.activemq.artemis.json.JsonObject;
+import org.apache.activemq.artemis.json.JsonObjectBuilder;
+import org.apache.activemq.artemis.utils.JsonLoader;
+
+public class ServerStatus {
+
+   private final ActiveMQServerImpl server;
+   private final HashMap<String, String> immutableStateValues = new HashMap<>();
+   private JsonObject globalStatus = JsonLoader.createObjectBuilder().build();
+
+   public ServerStatus(ActiveMQServerImpl activeMQServer) {
+      this.server = activeMQServer;
+      immutableStateValues.put("version", server.getVersion().getFullVersion());
+   }
+
+   public synchronized String asJson() {
+      updateServerStatus();
+      return globalStatus.toString();
+   }
+
+   private synchronized void updateServerStatus() {
+      HashMap<String, String> snapshotOfServerStatusAttributes = new HashMap<>();
+      snapshotOfServerStatusAttributes.putAll(immutableStateValues);
+      snapshotOfServerStatusAttributes.put("identity", server.getIdentity());
+      SimpleString nodeId = server.getNodeID();
+      snapshotOfServerStatusAttributes.put("nodeId", nodeId == null ? null : nodeId.toString());
+      snapshotOfServerStatusAttributes.put("uptime", server.getUptime());
+      snapshotOfServerStatusAttributes.put("state", server.getState().toString());
+
+      update("server", JsonUtil.toJsonObject(snapshotOfServerStatusAttributes));
+   }
+
+   public synchronized void update(String component, String statusJson) {
+      JsonObjectBuilder jsonObjectBuilder = JsonLoader.createObjectBuilder();
+      jsonObjectBuilder.add(component, JsonUtil.readJsonObject(statusJson));
+      globalStatus = JsonUtil.mergeAndUpdate(globalStatus, jsonObjectBuilder.build());
+   }
+
+   public synchronized void update(String component, JsonObject componentStatus) {
+      JsonObjectBuilder jsonObjectBuilder = JsonLoader.createObjectBuilder();
+      jsonObjectBuilder.add(component, componentStatus);
+      globalStatus = JsonUtil.mergeAndUpdate(globalStatus, jsonObjectBuilder.build());
+   }
+
+}
diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImplTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImplTest.java
index 84e90704c5..fb10c84f66 100644
--- a/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImplTest.java
+++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImplTest.java
@@ -644,6 +644,7 @@ public class ConfigurationImplTest extends ActiveMQTestBase {
             }
          }
       }
+      properties.remove("status"); // this is not a simple symmetric property
       // now parse
       configuration.parsePrefixedProperties(properties, null);
 
@@ -1289,6 +1290,55 @@ public class ConfigurationImplTest extends ActiveMQTestBase {
       Assert.assertEquals(SimpleString.toSimpleString("moreImportant"), configuration.getAddressSettings().get("Name.With.Dots").getExpiryAddress());
    }
 
+   @Test
+   public void testStatusOnErrorApplyingProperties() throws Exception {
+      ConfigurationImpl configuration = new ConfigurationImpl();
+
+      Properties properties = new Properties();
+
+      properties.put("clusterConfigurations.cc.bonkers", "bla");
+
+      properties.put("notValid.#.expiryAddress", "sharedExpiry");
+      properties.put("addressSettings.#.bla", "bla");
+      properties.put("addressSettings.#.expiryAddress", "good");
+
+      String SHA = "34311";
+      // status field json blob gives possibility for two-way interaction
+      // this value is reflected back but can be augmented
+      properties.put("status", "{ \"properties\": { \"sha\": \"" + SHA + "\"}}");
+
+      configuration.parsePrefixedProperties(properties, null);
+
+      String jsonStatus = configuration.getStatus();
+
+      // errors reported
+      assertTrue(jsonStatus.contains("notValid"));
+      assertTrue(jsonStatus.contains("Unknown"));
+      assertTrue(jsonStatus.contains("bonkers"));
+      assertTrue(jsonStatus.contains("bla"));
+
+      // input status reflected
+      assertTrue(jsonStatus.contains(SHA));
+      // only errors reported, good property goes unmentioned
+      assertFalse(jsonStatus.contains("good"));
+
+      // apply again with only good values, new sha.... verify no errors
+      properties.clear();
+
+      String UPDATED_SHA = "66666";
+      // status field json blob gives possibility for two-way interaction
+      // this value is reflected back but can be augmented
+      properties.put("status", "{ \"properties\": { \"sha\": \"" + UPDATED_SHA + "\"}}");
+      properties.put("addressSettings.#.expiryAddress", "changed");
+
+      configuration.parsePrefixedProperties(properties, null);
+
+      jsonStatus = configuration.getStatus();
+
+      assertTrue(jsonStatus.contains(UPDATED_SHA));
+      assertFalse(jsonStatus.contains(SHA));
+   }
+
    /**
     * To test ARTEMIS-926
     * @throws Throwable
diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/server/ConfigurationTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/server/ConfigurationTest.java
index b3457f641a..641c9a4008 100644
--- a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/server/ConfigurationTest.java
+++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/server/ConfigurationTest.java
@@ -95,8 +95,6 @@ public class ConfigurationTest extends ActiveMQTestBase {
 
       Assert.assertTrue(propsFile.exists());
 
-      System.out.println("props: " + propsFile.getAbsolutePath());
-
       ActiveMQServer server = getActiveMQServer("duplicate-queues.xml");
       server.setProperties(propsFile.getAbsolutePath());
       try {
@@ -127,6 +125,10 @@ public class ConfigurationTest extends ActiveMQTestBase {
          // verify round trip apply
          Assert.assertTrue(server.getActiveMQServerControl().getStatus().contains("2"));
 
+         // verify some server attributes
+         Assert.assertNotNull(server.getActiveMQServerControl().getStatus().contains("version"));
+         Assert.assertNotNull(server.getActiveMQServerControl().getStatus().contains("uptime"));
+
       } finally {
          try {
             server.stop();