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