diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/logging/CollectedLogMessage.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/logging/CollectedLogMessage.java
new file mode 100644
index 00000000..ad96eac5
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/logging/CollectedLogMessage.java
@@ -0,0 +1,153 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.logging;
+
+
+import org.apache.logging.log4j.Level;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+
+/*******************************************************************************
+ ** A log message, which can be "collected" by the QCollectingLogger.
+ *******************************************************************************/
+public class CollectedLogMessage
+{
+ private Level level;
+ private String message;
+ private Throwable exception;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public CollectedLogMessage()
+ {
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for message
+ *******************************************************************************/
+ public String getMessage()
+ {
+ return (this.message);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for message
+ *******************************************************************************/
+ public void setMessage(String message)
+ {
+ this.message = message;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for message
+ *******************************************************************************/
+ public CollectedLogMessage withMessage(String message)
+ {
+ this.message = message;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for exception
+ *******************************************************************************/
+ public Throwable getException()
+ {
+ return (this.exception);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for exception
+ *******************************************************************************/
+ public void setException(Throwable exception)
+ {
+ this.exception = exception;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for exception
+ *******************************************************************************/
+ public CollectedLogMessage withException(Throwable exception)
+ {
+ this.exception = exception;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for level
+ **
+ *******************************************************************************/
+ public Level getLevel()
+ {
+ return level;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for level
+ **
+ *******************************************************************************/
+ public void setLevel(Level level)
+ {
+ this.level = level;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for level
+ **
+ *******************************************************************************/
+ public CollectedLogMessage withLevel(Level level)
+ {
+ this.level = level;
+ return (this);
+ }
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public JSONObject getMessageAsJSONObject() throws JSONException
+ {
+ return (new JSONObject(getMessage()));
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/logging/QCollectingLogger.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/logging/QCollectingLogger.java
new file mode 100644
index 00000000..b10d3c07
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/logging/QCollectingLogger.java
@@ -0,0 +1,155 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.logging;
+
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.Marker;
+import org.apache.logging.log4j.message.Message;
+import org.apache.logging.log4j.message.ObjectMessage;
+import org.apache.logging.log4j.message.SimpleMessageFactory;
+import org.apache.logging.log4j.simple.SimpleLogger;
+import org.apache.logging.log4j.util.PropertiesUtil;
+
+
+/*******************************************************************************
+ ** QQQ log4j implementation, used within a QLogger, to "collect" log messages
+ ** in an internal list - the idea being - for tests, to assert that logs happened.
+ *******************************************************************************/
+public class QCollectingLogger extends SimpleLogger
+{
+ private List collectedMessages = new ArrayList<>();
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // just in case one of these gets activated, and left on, put a limit on how many messages we'll collect //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////
+ private int capacity = 100;
+
+ private Logger logger;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public QCollectingLogger(Logger logger)
+ {
+ super(logger.getName(), logger.getLevel(), false, false, true, false, "", new SimpleMessageFactory(), new PropertiesUtil(new Properties()), System.out);
+ this.logger = logger;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void logMessage(String fqcn, Level level, Marker marker, Message message, Throwable throwable)
+ {
+ ////////////////////////////////////////////
+ // add this log message to our collection //
+ ////////////////////////////////////////////
+ collectedMessages.add(new CollectedLogMessage()
+ .withLevel(level)
+ .withMessage(message.getFormattedMessage())
+ .withException(throwable));
+
+ ////////////////////////////////////////////////////////////////////////////////////////
+ // if we've gone over our capacity, remove the 1st entry until we're back at capacity //
+ ////////////////////////////////////////////////////////////////////////////////////////
+ while(collectedMessages.size() > capacity)
+ {
+ collectedMessages.remove(0);
+ }
+
+ //////////////////////////////////////////////////////////////////////
+ // update the message that we log to indicate that we collected it. //
+ // if it looks like JSON, insert as a name:value pair; else text. //
+ //////////////////////////////////////////////////////////////////////
+ String formattedMessage = message.getFormattedMessage();
+ String updatedMessage;
+ if(formattedMessage.startsWith("{"))
+ {
+ updatedMessage = """
+ {"collected":true,""" + formattedMessage.substring(1);
+ }
+ else
+ {
+ updatedMessage = "[Collected] " + formattedMessage;
+ }
+ ObjectMessage myMessage = new ObjectMessage(updatedMessage);
+
+ ///////////////////////////////////////////////////////////////////////////////////////
+ // log the message with the original log4j logger, with our slightly updated message //
+ ///////////////////////////////////////////////////////////////////////////////////////
+ logger.logMessage(level, marker, fqcn, null, myMessage, throwable);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for logger
+ **
+ *******************************************************************************/
+ public void setLogger(Logger logger)
+ {
+ this.logger = logger;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for collectedMessages
+ **
+ *******************************************************************************/
+ public List getCollectedMessages()
+ {
+ return collectedMessages;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void clear()
+ {
+ this.collectedMessages.clear();
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for capacity
+ **
+ *******************************************************************************/
+ public void setCapacity(int capacity)
+ {
+ this.capacity = capacity;
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/logging/QLogger.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/logging/QLogger.java
index 3683c62a..a00dfb0d 100755
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/logging/QLogger.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/logging/QLogger.java
@@ -119,6 +119,34 @@ public class QLogger
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static QCollectingLogger activateCollectingLoggerForClass(Class> c)
+ {
+ Logger loggerFromLogManager = LogManager.getLogger(c);
+ QCollectingLogger collectingLogger = new QCollectingLogger(loggerFromLogManager);
+
+ QLogger qLogger = getLogger(c);
+ qLogger.setLogger(collectingLogger);
+
+ return collectingLogger;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static void deactivateCollectingLoggerForClass(Class> c)
+ {
+ Logger loggerFromLogManager = LogManager.getLogger(c);
+ QLogger qLogger = getLogger(c);
+ qLogger.setLogger(loggerFromLogManager);
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
@@ -518,7 +546,7 @@ public class QLogger
/*******************************************************************************
**
*******************************************************************************/
- private String makeJsonString(String message, Throwable t, List logPairList)
+ protected String makeJsonString(String message, Throwable t, List logPairList)
{
if(logPairList == null)
{
@@ -620,4 +648,15 @@ public class QLogger
exceptionList.get(0).setHasLoggedLevel(level);
return (level);
}
+
+
+
+ /*******************************************************************************
+ ** Setter for logger
+ **
+ *******************************************************************************/
+ private void setLogger(Logger logger)
+ {
+ this.logger = logger;
+ }
}
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/logging/QCollectingLoggerTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/logging/QCollectingLoggerTest.java
new file mode 100644
index 00000000..8a70203e
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/logging/QCollectingLoggerTest.java
@@ -0,0 +1,92 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.logging;
+
+
+import com.kingsrook.qqq.backend.core.BaseTest;
+import org.apache.logging.log4j.Level;
+import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+
+/*******************************************************************************
+ ** Unit test for QCollectingLogger
+ *******************************************************************************/
+class QCollectingLoggerTest extends BaseTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void test()
+ {
+ ClassThatLogsThings classThatLogsThings = new ClassThatLogsThings();
+ classThatLogsThings.logAnInfo("1");
+
+ QCollectingLogger collectingLogger = QLogger.activateCollectingLoggerForClass(ClassThatLogsThings.class);
+ classThatLogsThings.logAnInfo("2");
+ classThatLogsThings.logAWarn("3");
+ QLogger.deactivateCollectingLoggerForClass(ClassThatLogsThings.class);
+
+ classThatLogsThings.logAWarn("4");
+
+ assertEquals(2, collectingLogger.getCollectedMessages().size());
+
+ assertThat(collectingLogger.getCollectedMessages().get(0).getMessage()).contains("""
+ "message":"2",""");
+ assertEquals("2", collectingLogger.getCollectedMessages().get(0).getMessageAsJSONObject().getString("message"));
+ assertEquals(Level.INFO, collectingLogger.getCollectedMessages().get(0).getLevel());
+
+ assertThat(collectingLogger.getCollectedMessages().get(1).getMessage()).contains("""
+ "message":"3",""");
+ assertEquals(Level.WARN, collectingLogger.getCollectedMessages().get(1).getLevel());
+ assertEquals("3", collectingLogger.getCollectedMessages().get(1).getMessageAsJSONObject().getString("message"));
+ }
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static class ClassThatLogsThings
+ {
+ private static final QLogger LOG = QLogger.getLogger(ClassThatLogsThings.class);
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void logAnInfo(String message)
+ {
+ LOG.info(message);
+ }
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void logAWarn(String message)
+ {
+ LOG.warn(message);
+ }
+ }
+
+}
\ No newline at end of file