From 6b590324beadcdc4e5d0993e8eaabc378ee30282 Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Mon, 12 Jun 2023 16:04:46 -0500 Subject: [PATCH] initial version of attempting to downgrade logs if a warning or error has already been logged from the stack of throwables --- .../backend/core/exceptions/QException.java | 105 ++++++++++ .../qqq/backend/core/logging/QLogger.java | 54 ++++- .../backend/core/utils/ExceptionUtils.java | 41 ++++ .../qqq/backend/core/logging/QLoggerTest.java | 190 ++++++++++++++++++ 4 files changed, 382 insertions(+), 8 deletions(-) create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/logging/QLoggerTest.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QException.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QException.java index 651919fa..67b4aba6 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QException.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QException.java @@ -22,12 +22,19 @@ package com.kingsrook.qqq.backend.core.exceptions; +import org.apache.logging.log4j.Level; + + /******************************************************************************* * Base class for checked exceptions thrown in qqq. * *******************************************************************************/ public class QException extends Exception { + private boolean hasLoggedWarning; + private boolean hasLoggedError; + + /******************************************************************************* ** Constructor of message @@ -59,4 +66,102 @@ public class QException extends Exception { super(message, cause); } + + + + /******************************************************************************* + ** Getter for hasLoggedWarning + *******************************************************************************/ + public boolean getHasLoggedWarning() + { + return (this.hasLoggedWarning); + } + + + + /******************************************************************************* + ** Setter for hasLoggedWarning + *******************************************************************************/ + public void setHasLoggedWarning(boolean hasLoggedWarning) + { + this.hasLoggedWarning = hasLoggedWarning; + } + + + + /******************************************************************************* + ** Fluent setter for hasLoggedWarning + *******************************************************************************/ + public QException withHasLoggedWarning(boolean hasLoggedWarning) + { + this.hasLoggedWarning = hasLoggedWarning; + return (this); + } + + + + /******************************************************************************* + ** Getter for hasLoggedError + *******************************************************************************/ + public boolean getHasLoggedError() + { + return (this.hasLoggedError); + } + + + + /******************************************************************************* + ** Setter for hasLoggedError + *******************************************************************************/ + public void setHasLoggedError(boolean hasLoggedError) + { + this.hasLoggedError = hasLoggedError; + } + + + + /******************************************************************************* + ** Fluent setter for hasLoggedError + *******************************************************************************/ + public QException withHasLoggedError(boolean hasLoggedError) + { + this.hasLoggedError = hasLoggedError; + return (this); + } + + + + /******************************************************************************* + ** helper function for getting if level logged + *******************************************************************************/ + public boolean hasLoggedLevel(Level level) + { + if(Level.WARN.equals(level)) + { + return (hasLoggedWarning); + } + if(Level.ERROR.equals(level)) + { + return (hasLoggedError); + } + return (false); + } + + + + /******************************************************************************* + ** helper function for setting if level logged + *******************************************************************************/ + public void setHasLoggedLevel(Level level) + { + if(Level.WARN.equals(level)) + { + setHasLoggedWarning(true); + } + if(Level.ERROR.equals(level)) + { + setHasLoggedError(true); + } + } + } 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 d057b8fb..5135b836 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 @@ -29,10 +29,12 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter; import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.ExceptionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; -import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -392,7 +394,7 @@ public class QLogger *******************************************************************************/ public void warn(String message, Throwable t) { - logger.warn(makeJsonString(message, t)); + logger.log(determineIfShouldDowngrade(t, Level.WARN), makeJsonString(message, t)); } @@ -402,7 +404,7 @@ public class QLogger *******************************************************************************/ public void warn(String message, Throwable t, LogPair... logPairs) { - logger.warn(makeJsonString(message, t, logPairs)); + logger.log(determineIfShouldDowngrade(t, Level.WARN), makeJsonString(message, t, logPairs)); } @@ -412,7 +414,7 @@ public class QLogger *******************************************************************************/ public void warn(Throwable t) { - logger.warn(makeJsonString(null, t)); + logger.log(determineIfShouldDowngrade(t, Level.WARN), makeJsonString(null, t)); } @@ -452,7 +454,7 @@ public class QLogger *******************************************************************************/ public void error(String message, Throwable t) { - logger.error(makeJsonString(message, t)); + logger.log(determineIfShouldDowngrade(t, Level.ERROR), makeJsonString(message, t)); } @@ -462,7 +464,7 @@ public class QLogger *******************************************************************************/ public void error(String message, Throwable t, LogPair... logPairs) { - logger.error(makeJsonString(message, t, logPairs)); + logger.log(determineIfShouldDowngrade(t, Level.ERROR), makeJsonString(message, t, logPairs)); } @@ -472,7 +474,7 @@ public class QLogger *******************************************************************************/ public void error(Throwable t) { - logger.error(makeJsonString(null, t)); + logger.log(determineIfShouldDowngrade(t, Level.ERROR), makeJsonString(null, t)); } @@ -532,7 +534,7 @@ public class QLogger if(t != null) { - logPairList.add(logPair("stackTrace", LogUtils.filterStackTrace(ExceptionUtils.getStackTrace(t)))); + logPairList.add(logPair("stackTrace", LogUtils.filterStackTrace(org.apache.commons.lang3.exception.ExceptionUtils.getStackTrace(t)))); } return (LogUtils.jsonLog(logPairList)); @@ -582,4 +584,40 @@ public class QLogger } } } + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected Level determineIfShouldDowngrade(Throwable t, Level level) + { + ////////////////////////////////////////////////////////////////////////////////////// + // look for QExceptions in the chain, if none found, return the log level passed in // + ////////////////////////////////////////////////////////////////////////////////////// + List exceptionList = ExceptionUtils.getClassListFromRootChain(t, QException.class); + if(CollectionUtils.nullSafeIsEmpty(exceptionList)) + { + return (level); + } + + //////////////////////////////////////////////////////////////////// + // check if any QException in this chain to see if it has already // + // logged this level, if so, downgrade to INFO // + //////////////////////////////////////////////////////////////////// + for(QException qException : exceptionList) + { + if(qException.hasLoggedLevel(level)) + { + log(Level.INFO, "Downgrading log message from " + level.toString() + " to " + Level.INFO, t); + return (Level.INFO); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////////////// + // if it has not logged at this level, set that it has in QException, and return passed in level // + /////////////////////////////////////////////////////////////////////////////////////////////////// + exceptionList.get(0).setHasLoggedLevel(level); + return (level); + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ExceptionUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ExceptionUtils.java index c4d71714..b9809b88 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ExceptionUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ExceptionUtils.java @@ -22,7 +22,9 @@ package com.kingsrook.qqq.backend.core.utils; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Set; @@ -60,6 +62,45 @@ public class ExceptionUtils + /******************************************************************************* + ** Find a list of exceptions of the given class in an exception's caused-by chain. + ** Returns empty list if none found. + ** + *******************************************************************************/ + public static List getClassListFromRootChain(Throwable e, Class targetClass) + { + List throwableList = new ArrayList<>(); + if(targetClass.isInstance(e)) + { + throwableList.add(targetClass.cast(e)); + } + + /////////////////////////////////////////////////// + // iterate through the chain with a limit of 100 // + /////////////////////////////////////////////////// + int counter = 0; + while(counter++ < 100) + { + //////////////////////////////////////////////////////////////////////// + // look for the same class from the last throwable found of that type // + //////////////////////////////////////////////////////////////////////// + e = findClassInRootChain(e.getCause(), targetClass); + if(e == null) + { + break; + } + + //////////////////////////////////////////////////////////////////////// + // if we did not break, higher one must have been found, keep looking // + //////////////////////////////////////////////////////////////////////// + throwableList.add(targetClass.cast(e)); + } + + return (throwableList); + } + + + /******************************************************************************* ** Get the root exception in a caused-by-chain. ** diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/logging/QLoggerTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/logging/QLoggerTest.java new file mode 100644 index 00000000..1c28982c --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/logging/QLoggerTest.java @@ -0,0 +1,190 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. 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.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.Core; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.Property; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; +import org.apache.logging.log4j.core.filter.LevelRangeFilter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + + +/******************************************************************************* + ** Unit test for com.kingsrook.qqq.backend.core.logging.QLogger + ** + *******************************************************************************/ +@Disabled // disabled because could never get the custom appender class to receive logEvents that have their levels set (always null) +class QLoggerTest extends BaseTest +{ + private static final QLogger LOG = QLogger.getLogger(QLoggerTest.class); + private static final ListAppender listAppender = ListAppender.createAppender(); + + + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + void beforeAll() throws Exception + { + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testDowngradingWarnings() throws Exception + { + org.apache.logging.log4j.core.Logger logger = (org.apache.logging.log4j.core.Logger) LogManager.getLogger(QLoggerTest.class); + logger.addAppender(listAppender); + listAppender.start(); + + try + { + try + { + try + { + try + { + throw (new QException("Some deepest exception...")); + } + catch(Exception e) + { + String warning = "Less deep warning"; + LOG.warn(warning, e); + throw (new QException(warning, e)); + } + } + catch(Exception e2) + { + String warning = "Middle warning"; + LOG.warn(warning, e2); + throw (new QException(warning, e2)); + } + } + catch(Exception e2) + { + String warning = "Last warning"; + LOG.warn(warning, e2); + throw (new QException(warning, e2)); + } + } + catch(Exception e3) + { + ///////////////////////// + // check results below // + ///////////////////////// + } + + assertThat(listAppender.getEventList()).isNotNull(); + assertThat(listAppender.getEventList().size()).isEqualTo(5); + int counter = 0; + for(LogEvent logEvent : listAppender.getEventList()) + { + if(counter == 0) + { + assertThat(logEvent.getLevel()).isEqualTo(Level.WARN); + } + else + { + assertThat(logEvent.getLevel()).isEqualTo(Level.INFO); + } + counter++; + } + } + + + + /******************************************************************************* + ** appender to add to logger to keep a list of log events + *******************************************************************************/ + @Plugin(name = "ListAppender", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE) + public static class ListAppender extends AbstractAppender + { + private List eventList = new ArrayList<>(); + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected ListAppender(final String name, final Filter filter, final Layout layout, final boolean ignoreExceptions, final Property[] properties) + { + super(name, filter, layout, ignoreExceptions, properties); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @PluginFactory + public static ListAppender createAppender() + { + LevelRangeFilter levelRangeFilter = LevelRangeFilter.createFilter(Level.TRACE, Level.ERROR, Filter.Result.ACCEPT, Filter.Result.ACCEPT); + // return (new ListAppender("ListApppender", levelRangeFilter, null, true, null)); + return (new ListAppender("ListApppender", null, null, true, null)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void append(LogEvent event) + { + eventList.add(event); + } + + + + /******************************************************************************* + ** Getter for eventList + *******************************************************************************/ + public List getEventList() + { + return (this.eventList); + } + } + +}