diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/FailoverAppender.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/FailoverAppender.java index 659caf124..7a6234e38 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/FailoverAppender.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/FailoverAppender.java @@ -20,6 +20,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; +import java.util.Collections; +import java.util.Objects; import org.apache.logging.log4j.LoggingException; import org.apache.logging.log4j.core.Appender; @@ -28,6 +30,7 @@ import org.apache.logging.log4j.core.Filter; import org.apache.logging.log4j.core.LogEvent; import org.apache.logging.log4j.core.config.AppenderControl; import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.ConfigurationStoppingAware; import org.apache.logging.log4j.core.config.plugins.Plugin; import org.apache.logging.log4j.core.config.plugins.PluginAliases; import org.apache.logging.log4j.core.config.plugins.PluginAttribute; @@ -43,7 +46,7 @@ import org.apache.logging.log4j.core.util.Constants; * to not suppress exceptions for the FailoverAppender to work. */ @Plugin(name = "Failover", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE, printObject = true) -public final class FailoverAppender extends AbstractAppender { +public final class FailoverAppender extends AbstractAppender implements ConfigurationStoppingAware { private static final int DEFAULT_INTERVAL_SECONDS = 60; @@ -113,7 +116,7 @@ public final class FailoverAppender extends AbstractAppender { if (localCheckNanos == 0 || System.nanoTime() - localCheckNanos > 0) { callAppender(event); } else { - failover(event, null); + failover(new FailoverContext(Collections.singletonList(event))); } } @@ -123,18 +126,29 @@ public final class FailoverAppender extends AbstractAppender { nextCheckNanos = 0; } catch (final Exception ex) { nextCheckNanos = System.nanoTime() + intervalNanos; - failover(event, ex); + failover(new FailoverContext(resolveFailoverEvents(event, ex), ex)); } } - - private void failover(final LogEvent event, final Exception ex) { - final RuntimeException re = ex != null ? - (ex instanceof LoggingException ? (LoggingException) ex : new LoggingException(ex)) : null; + + private List resolveFailoverEvents(LogEvent event, Exception exception) { + List events; + Appender appender = primary.getAppender(); + if (appender instanceof FailoverAware) { + events = ((FailoverAware) appender).onFailover(event, exception); + } else { + events = Collections.singletonList(event); + } + return events; + } + + private void failover(final FailoverContext failoverContext) { boolean written = false; Exception failoverException = null; for (final AppenderControl control : failoverAppenders) { try { - control.callAppender(event); + for(LogEvent event : failoverContext.getEvents()) { + control.callAppender(event); + } written = true; break; } catch (final Exception fex) { @@ -144,14 +158,27 @@ public final class FailoverAppender extends AbstractAppender { } } if (!written && !ignoreExceptions()) { - if (re != null) { - throw re; - } - throw new LoggingException("Unable to write to failover appenders", failoverException); + throw failoverContext.getException(failoverException); } } @Override + public void onBeforeStopConfiguration() + { + Appender appender = primary.getAppender(); + if (appender instanceof FailoverAware) { + try { + ((FailoverAware) appender).onBeforeFailoverAppenderStop(); + } catch (Exception e) { + List events = ((FailoverAware) appender).onBeforeFailoverAppenderStopException(e); + if (!events.isEmpty()) { + failover(new FailoverContext(events, e)); + } + } + } + } + + @Override public String toString() { final StringBuilder sb = new StringBuilder(getName()); sb.append(" primary=").append(primary).append(", failover={"); @@ -215,4 +242,37 @@ public final class FailoverAppender extends AbstractAppender { return new FailoverAppender(name, filter, primary, failovers, retryIntervalMillis, config, ignoreExceptions); } + + private static class FailoverContext { + + private final List events; + //nullable + private final Exception exception; + + private FailoverContext(List events) { + this.events = Objects.requireNonNull(events, "events nust not be null"); + this.exception = null; + } + + private FailoverContext(List events, Exception exception) { + this.events = Objects.requireNonNull(events, "events nust not be null"); + this.exception = Objects.requireNonNull(exception, "exception nust not be null"); + } + + List getEvents() { + return events; + } + + RuntimeException getException(Exception cause) { + RuntimeException resolvedException; + if(exception == null) { + resolvedException = new LoggingException("Unable to write to failover appenders", cause); + } else if (exception instanceof LoggingException) { + resolvedException = (LoggingException) exception; + } else { + resolvedException = new LoggingException(exception); + } + return resolvedException; + } + } } diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/FailoverAware.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/FailoverAware.java new file mode 100644 index 000000000..5c1deed8f --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/FailoverAware.java @@ -0,0 +1,49 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.logging.log4j.core.appender; + +import org.apache.logging.log4j.core.LogEvent; + +import java.util.List; + +/** + * Primary appenders used with {@link FailoverAppender}s that implement this interface will be aware of events in the + * parent {@link FailoverAppender}. + */ +public interface FailoverAware { + + /** + * Invoked when failing over from a primary appender to a secondary appender. + * @param event the event that triggered the failover + * @param exception the exception that caused the failover + * @return a {@link List} of {@link LogEvent}s to be passed to the secondary appenders + */ + List onFailover(LogEvent event, Exception exception); + + /** + * Invoked when the parent {@link FailoverAppender} is preparing to stop. Nothing is actually stopped at this point. + */ + void onBeforeFailoverAppenderStop(); + + /** + * Invoked if an error occurred when {@link #onBeforeFailoverAppenderStop()} was invoked. + * @param exception the exception thrown from {@link #onBeforeFailoverAppenderStop()} + * @return a {@link List} of {@link LogEvent}s to be passed to the secondary appenders + */ + List onBeforeFailoverAppenderStopException(Exception exception); + +} diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/AbstractDatabaseAppender.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/AbstractDatabaseAppender.java index 538b6444d..1226948ab 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/AbstractDatabaseAppender.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/AbstractDatabaseAppender.java @@ -20,6 +20,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.List; import org.apache.logging.log4j.LoggingException; import org.apache.logging.log4j.core.Filter; @@ -27,6 +28,7 @@ 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.appender.AppenderLoggingException; +import org.apache.logging.log4j.core.appender.FailoverAware; /** * An abstract Appender for writing events to a database of some type, be it relational or NoSQL. All database appenders @@ -36,7 +38,7 @@ import org.apache.logging.log4j.core.appender.AppenderLoggingException; * * @param Specifies which type of {@link AbstractDatabaseManager} this Appender requires. */ -public abstract class AbstractDatabaseAppender extends AbstractAppender { +public abstract class AbstractDatabaseAppender extends AbstractAppender implements FailoverAware { private final ReadWriteLock lock = new ReentrantReadWriteLock(); private final Lock readLock = lock.readLock(); @@ -119,6 +121,21 @@ public abstract class AbstractDatabaseAppender onFailover(LogEvent event, Exception exception) { + return manager.onFailover(event); + } + + @Override + public void onBeforeFailoverAppenderStop() { + manager.onBeforeFailoverAppenderStop(); + } + + @Override + public List onBeforeFailoverAppenderStopException(Exception exception) { + return manager.onBeforeFailoverAppenderStopException(); + } + /** * Replaces the underlying manager in use within this appender. This can be useful for manually changing the way log * events are written to the database without losing buffered or in-progress events. The existing manager is diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/AbstractDatabaseManager.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/AbstractDatabaseManager.java index c36c4d8f7..cfa5c8bb0 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/AbstractDatabaseManager.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/AbstractDatabaseManager.java @@ -17,6 +17,8 @@ package org.apache.logging.log4j.core.appender.db; +import java.util.Collections; +import java.util.List; import java.io.Flushable; import java.util.ArrayList; import java.util.concurrent.TimeUnit; @@ -175,6 +177,48 @@ public abstract class AbstractDatabaseManager extends AbstractManager implements } } } + + /** + * Handles a failover when used with a failover appender. + * @param event the event that triggered the failover + * @return the content of the buffer, including the triggering event + */ + public synchronized List onFailover(LogEvent event) { + final List events; + if (bufferSize > 0) { + events = new ArrayList<>(buffer); + if (!events.contains(event)) { + events.add(event); + } + buffer.clear(); + } else { + events = Collections.singletonList(event); + } + return events; + } + + /** + * When used with a failover appender, handles the failover appender stopping. Attempts to flush the buffer. + */ + public void onBeforeFailoverAppenderStop() { + flush(); + } + + /** + * When used with a failover appender, handles any exceptions thrown by {@link #onBeforeFailoverAppenderStop()}. + * @param exception the exception thrown from {@link #onBeforeFailoverAppenderStop()} + * @return the content of the buffer + */ + public synchronized List onBeforeFailoverAppenderStopException() { + final List events; + if (bufferSize > 0) { + events = new ArrayList<>(buffer); + buffer.clear(); + } else { + events = Collections.emptyList(); + } + return events; + } @Override public final boolean releaseSub(final long timeout, final TimeUnit timeUnit) { diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/AbstractConfiguration.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/AbstractConfiguration.java index 3c0e81089..4d2cb66fe 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/AbstractConfiguration.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/AbstractConfiguration.java @@ -282,12 +282,21 @@ public abstract class AbstractConfiguration extends AbstractFilterable implement } return false; } + + private void beforeStop() { + for (Appender appender : appenders.values()) { + if (appender instanceof ConfigurationStoppingAware) { + ((ConfigurationStoppingAware) appender).onBeforeStopConfiguration(); + } + } + } /** * Tear down the configuration. */ @Override public boolean stop(final long timeout, final TimeUnit timeUnit) { + beforeStop(); this.setStopping(); super.stop(timeout, timeUnit, false); LOGGER.trace("Stopping {}...", this); diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationStoppingAware.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationStoppingAware.java new file mode 100644 index 000000000..423fda782 --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationStoppingAware.java @@ -0,0 +1,28 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.logging.log4j.core.config; + +/** + * Implementing appenders will be notified when the configuration is preparing to stop, but not actually stopped. + */ +public interface ConfigurationStoppingAware { + + /** + * Invoked when the configuration is preparing to stop, but not actually stopped. + */ + void onBeforeStopConfiguration(); +} diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/FailoverAwareTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/FailoverAwareTest.java new file mode 100644 index 000000000..6ab119085 --- /dev/null +++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/FailoverAwareTest.java @@ -0,0 +1,143 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.logging.log4j.core.appender; + +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.ErrorHandler; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.config.Configuration; +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verify; + + +public class FailoverAwareTest { + + private Configuration configuration = null; + private FailoverAppender failoverAppender = null; + private FailoverAwareAppender primaryAppender = null; + private Appender secondaryAppender = null; + private ErrorHandler primaryErrorHandler = null; + + @Before + public void setup() { + + primaryAppender = mock(FailoverAwareAppender.class); + secondaryAppender = mock(Appender.class); + primaryErrorHandler = mock(ErrorHandler.class); + + when(primaryAppender.isStarted()).thenReturn(true); + when(primaryAppender.getHandler()).thenReturn(primaryErrorHandler); + when(secondaryAppender.isStarted()).thenReturn(true); + + + Map appenders = new HashMap<>(); + appenders.put("primary", primaryAppender); + appenders.put("secondary", secondaryAppender); + + configuration = mock(Configuration.class); + when(configuration.getAppenders()).thenReturn(Collections.unmodifiableMap(appenders)); + + failoverAppender = FailoverAppender.createAppender( + "failover", + "primary", + new String[]{"secondary"}, + "0", + configuration, + null, + "false"); + failoverAppender.start(); + + } + + @Test + public void testOnBeforeStop() throws Exception { + + failoverAppender.onBeforeStopConfiguration(); + + verify(primaryAppender).onBeforeFailoverAppenderStop(); + verify(primaryAppender, never()).onBeforeFailoverAppenderStopException(any(Exception.class)); + verify(secondaryAppender, never()).append(any(LogEvent.class)); + } + + @Test + public void testOnBeforeStopExceptionNoResults() throws Exception { + + RuntimeException exception = new RuntimeException("test"); + List events = Collections.emptyList(); + doThrow(exception).when(primaryAppender).onBeforeFailoverAppenderStop(); + when(primaryAppender.onBeforeFailoverAppenderStopException(exception)).thenReturn(events); + + failoverAppender.onBeforeStopConfiguration(); + + verify(primaryAppender).onBeforeFailoverAppenderStop(); + verify(primaryAppender).onBeforeFailoverAppenderStopException(exception); + verify(secondaryAppender, never()).append(any(LogEvent.class)); + } + + @Test + public void testOnBeforeStopExceptionWithResults() throws Exception { + + LogEvent event1 = mock(LogEvent.class); + LogEvent event2 = mock(LogEvent.class); + + RuntimeException exception = new RuntimeException("test"); + List events = Arrays.asList(event1, event2); + doThrow(exception).when(primaryAppender).onBeforeFailoverAppenderStop(); + when(primaryAppender.onBeforeFailoverAppenderStopException(exception)).thenReturn(events); + + failoverAppender.onBeforeStopConfiguration(); + + verify(primaryAppender).onBeforeFailoverAppenderStop(); + verify(primaryAppender).onBeforeFailoverAppenderStopException(exception); + verify(secondaryAppender).append(event1); + verify(secondaryAppender).append(event2); + } + + @Test + public void testFailover() throws Exception { + + LogEvent event1 = mock(LogEvent.class); + LogEvent event2 = mock(LogEvent.class); + List events = Arrays.asList(event1, event2); + + RuntimeException exception = new RuntimeException("test"); + when(primaryAppender.onFailover(event1, exception)).thenReturn(events); + + doThrow(exception).when(primaryAppender).append(event1); + + failoverAppender.append(event1); + + verify(secondaryAppender).append(event1); + verify(secondaryAppender).append(event2); + + } + + private interface FailoverAwareAppender extends Appender, FailoverAware {} +} diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/AbstractDatabaseAppenderTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/AbstractDatabaseAppenderTest.java index cf33cea7c..5c4f9ad42 100644 --- a/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/AbstractDatabaseAppenderTest.java +++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/AbstractDatabaseAppenderTest.java @@ -31,6 +31,7 @@ import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; @RunWith(MockitoJUnitRunner.class) public class AbstractDatabaseAppenderTest { @@ -105,6 +106,30 @@ public class AbstractDatabaseAppenderTest { then(manager).should().writeInternal(same(event2)); then(manager).should().commitAndClose(); } + + @Test + public void testOnBeforeFailoverAppenderStop() throws Exception { + setUp("name"); + appender.onBeforeFailoverAppenderStop(); + verify(manager).onBeforeFailoverAppenderStop(); + } + + @Test + public void testOnBeforeFailoverAppenderStopException() throws Exception { + setUp("name"); + Exception exception = mock(Exception.class); + appender.onBeforeFailoverAppenderStopException(exception); + verify(manager).onBeforeFailoverAppenderStopException(); + } + + @Test + public void testOnFailover() throws Exception { + setUp("name"); + LogEvent event = mock(LogEvent.class); + Exception exception = mock(Exception.class); + appender.onFailover(event, exception); + verify(manager).onFailover(event); + } private static abstract class LocalAbstractDatabaseManager extends AbstractDatabaseManager { public LocalAbstractDatabaseManager(final String name, final int bufferSize) { diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/AbstractDatabaseManagerTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/AbstractDatabaseManagerTest.java index 04d7699d0..09a757e99 100644 --- a/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/AbstractDatabaseManagerTest.java +++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/AbstractDatabaseManagerTest.java @@ -19,12 +19,19 @@ package org.apache.logging.log4j.core.appender.db; import org.apache.logging.log4j.core.LogEvent; import org.junit.Test; +import java.util.ArrayList; +import java.util.List; + import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.same; import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; public class AbstractDatabaseManagerTest { private AbstractDatabaseManager manager; @@ -184,7 +191,110 @@ public class AbstractDatabaseManagerTest { then(manager).should().shutdownInternal(); then(manager).shouldHaveNoMoreInteractions(); } + + @Test + public void testBufferFlushOnBeforeFailoverAppenderStop() throws Exception { + setUp("name", 10); + + final LogEvent event1 = mock(LogEvent.class); + final LogEvent event2 = mock(LogEvent.class); + final LogEvent event3 = mock(LogEvent.class); + + manager.startup(); + manager.write(event1); + manager.write(event2); + manager.write(event3); + + manager.onBeforeFailoverAppenderStop(); + verify(manager).writeInternal(event1); + verify(manager).writeInternal(event2); + verify(manager).writeInternal(event3); + } + @Test + public void testOnBeforeFailoverAppenderStopExceptionWithBuffer() throws Exception { + int bufferSize = 10; + setUp("name", bufferSize); + manager.startup(); + + final List expectedEvents = new ArrayList<>(); + for (int i = 0; i < bufferSize - 1; i++) { + LogEvent event = mock(LogEvent.class); + expectedEvents.add(event); + manager.write(event); + } + + verify(manager, never()).writeInternal(any(LogEvent.class)); + + final List events = manager.onBeforeFailoverAppenderStopException(); + assertEquals("exception events do not match expected", expectedEvents, events); + + //buffer should be cleared, test by refilling and verify that flush was not called + for (int i = 0; i < bufferSize - 1; i++) { + LogEvent event = mock(LogEvent.class); + manager.write(event); + } + + verify(manager, never()).writeInternal(any(LogEvent.class)); + } + + @Test + public void testOnBeforeFailoverAppenderStopExceptionWithoutBuffer() throws Exception { + setUp("name", 0); + + final LogEvent event1 = mock(LogEvent.class); + final LogEvent event2 = mock(LogEvent.class); + final LogEvent event3 = mock(LogEvent.class); + + manager.startup(); + manager.write(event1); + manager.write(event2); + manager.write(event3); + + assertTrue("onBeforeStopConfigurationException returned unexpected events", manager.onBeforeFailoverAppenderStopException().isEmpty()); + } + + @Test + public void testOnFailover() throws Exception { + int bufferSize = 10; + setUp("name", bufferSize); + manager.startup(); + + final List expectedEvents = new ArrayList<>(); + for (int i = 0; i < bufferSize - 1; i++) { + LogEvent event = mock(LogEvent.class); + expectedEvents.add(event); + manager.write(event); + } + final LogEvent causalEvent = mock(LogEvent.class); + final RuntimeException exception = new RuntimeException("test"); + doThrow(exception).when(manager).connectAndStart(); + expectedEvents.add(causalEvent); + + Exception caught = null; + try { + manager.write(causalEvent); + } catch (Exception e) { + caught = e; + } + assertNotNull("write did not throw expected exception", caught); + assertEquals("exception thrown by write did not equal the expected one", exception, caught); + + verify(manager, never()).writeInternal(any(LogEvent.class)); + + final List events = manager.onFailover(causalEvent); + assertEquals("exception events do not match expected", expectedEvents, events); + + //buffer should be cleared, test by refilling and verify that flush was not called + reset(manager); + for (int i = 0; i < bufferSize - 1; i++) { + LogEvent event = mock(LogEvent.class); + manager.write(event); + } + + verify(manager, never()).writeInternal(any(LogEvent.class)); + } + // this stub is provided because mocking constructors is hard private static class StubDatabaseManager extends AbstractDatabaseManager { diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/config/ConfigurationStoppingAwareTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/config/ConfigurationStoppingAwareTest.java new file mode 100644 index 000000000..9cfb3cf2c --- /dev/null +++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/config/ConfigurationStoppingAwareTest.java @@ -0,0 +1,187 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.logging.log4j.core.config; + +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.ErrorHandler; +import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; +import org.apache.logging.log4j.core.config.plugins.util.PluginType; +import org.junit.Test; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + + +public class ConfigurationStoppingAwareTest +{ + + @Test + public void testBeforeStop() throws Exception { + + int numberOfAppenders = 3; + + AbstractConfiguration abstractConfiguration = new AbstractConfigurationTestImpl(numberOfAppenders); + abstractConfiguration.initialize(); + abstractConfiguration.stop(); + + List appenders = new ArrayList<>(); + for (Appender appender : abstractConfiguration.getAppenders().values()) { + if (appender instanceof AppenderTester) { + appenders.add((AppenderTester) appender); + } + } + + assertEquals("wrong number of ConfigurationStoppingAware appenders", numberOfAppenders, appenders.size()); + + for (Appender appender : appenders) { + assertEquals("appender beforeStopConfiguration() was called the wrong number of times", + 1, + ((AppenderTester) appender).getBeforeStopConfigurationCount()); + } + + } + + private static class AbstractConfigurationTestImpl extends AbstractConfiguration { + + public AbstractConfigurationTestImpl(int numberOfAppenders) { + super(null, ConfigurationSource.NULL_SOURCE); + + + PluginType appendersPlugin = mock(PluginType.class); + when(appendersPlugin.getPluginClass()).thenReturn(AppendersPlugin.class); + Node appendersNode = new Node(rootNode, "Appenders", appendersPlugin); + + for (int i = 0; i < numberOfAppenders; i++) + { + addAppenderTester(appendersNode); + } + + rootNode = new Node(); + rootNode.getChildren().add(appendersNode); + } + + private void addAppenderTester(Node appendersNode) { + PluginType appenderPlugin = mock(PluginType.class); + when(appenderPlugin.getPluginClass()).thenReturn(AppenderTester.class); + Node appenderNode = new Node(appendersNode, "Appender", appenderPlugin); + appendersNode.getChildren().add(appenderNode); + } + + } + + private static class AppenderTester implements Appender, ConfigurationStoppingAware { + + private final String name; + private boolean started = false; + private int beforeStopConfigurationCount = 0; + + public AppenderTester(String name) { + this.name = name; + } + + @Override + public void append(LogEvent event) { + throw new UnsupportedOperationException(); + } + + @Override + public String getName() { + return name; + } + + @Override + public Layout getLayout() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean ignoreExceptions() { + return false; + } + + @Override + public ErrorHandler getHandler() { + throw new UnsupportedOperationException(); + } + + @Override + public void setHandler(ErrorHandler handler) {} + + @Override + public State getState() { + throw new UnsupportedOperationException(); + } + + @Override + public void initialize() {} + + @Override + public void start() { + started = true; + } + + @Override + public void stop() { + started = false; + } + + @Override + public boolean isStarted() { + return started; + } + + @Override + public boolean isStopped() { + return !started; + } + + @Override + public void onBeforeStopConfiguration() { + beforeStopConfigurationCount++; + } + + public int getBeforeStopConfigurationCount() { + return beforeStopConfigurationCount; + } + + @PluginBuilderFactory + public static > B newBuilder() { + return new AppenderTester.Builder().asBuilder(); + } + + public static class Builder> + implements org.apache.logging.log4j.core.util.Builder { + + public B asBuilder() { + return (B) this; + } + + @Override + public AppenderTester build() { + return new AppenderTester(UUID.randomUUID().toString()); + } + } + } +}