You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by gg...@apache.org on 2022/02/08 01:54:52 UTC

[logging-log4j2] branch release-2.x updated: [LOG4J2-3391] Add optional additional fields to NoSQLAppender.

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

ggregory pushed a commit to branch release-2.x
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git


The following commit(s) were added to refs/heads/release-2.x by this push:
     new 6db1c07  [LOG4J2-3391] Add optional additional fields to NoSQLAppender.
6db1c07 is described below

commit 6db1c07108a91d1ba5851be831b0c75a04111052
Author: Gary Gregory <ga...@gmail.com>
AuthorDate: Mon Feb 7 20:54:45 2022 -0500

    [LOG4J2-3391] Add optional additional fields to NoSQLAppender.
---
 log4j-core/revapi.json                             |   1 +
 .../log4j/core/appender/AbstractManager.java       |  46 ++++++++++
 .../core/appender/db/AbstractDatabaseManager.java  |  50 ++++++++--
 .../log4j/core/appender/nosql/NoSqlAppender.java   |  19 ++--
 .../core/appender/nosql/NoSqlDatabaseManager.java  |  69 ++++++++++----
 .../appender/nosql/NoSqlDatabaseManagerTest.java   |  12 +--
 .../mongodb3/MongoDb3AdditionalFieldsTest.java     | 100 ++++++++++++++++++++
 .../resources/log4j2-mongodb-additional-fields.xml |  35 +++++++
 .../mongodb4/MongoDb4AdditionalFieldsTest.java     | 101 +++++++++++++++++++++
 .../resources/log4j2-mongodb-additional-fields.xml |  34 +++++++
 src/changes/changes.xml                            |   3 +
 11 files changed, 431 insertions(+), 39 deletions(-)

diff --git a/log4j-core/revapi.json b/log4j-core/revapi.json
index 350c4d4..a4ff02a 100644
--- a/log4j-core/revapi.json
+++ b/log4j-core/revapi.json
@@ -6,6 +6,7 @@
         "classes": {
           "exclude": [
             "org\\.apache\\.logging\\.log4j\\.corel\\.impl\\.ContextAnchor",
+            "org\\.apache\\.logging\\.log4j\\.core\\.appender\\.db\\.AbstractDatabaseManager\\.AbstractFactoryData",
             "org\\.apache\\.logging\\.log4j\\.core\\.async\\.AsyncLoggerDisruptor",
             "org\\.apache\\.logging\\.log4j\\.core\\.async\\.RingBufferLogEvent\\.Factory",
             "org\\.apache\\.logging\\.log4j\\.core\\.layout\\.AbstractJacksonLayout",
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/AbstractManager.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/AbstractManager.java
index b253f95..72a8b4a 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/AbstractManager.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/AbstractManager.java
@@ -27,7 +27,9 @@ import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.core.AbstractLifeCycle;
 import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.config.Configuration;
 import org.apache.logging.log4j.core.config.ConfigurationException;
+import org.apache.logging.log4j.core.lookup.StrSubstitutor;
 import org.apache.logging.log4j.message.Message;
 import org.apache.logging.log4j.status.StatusLogger;
 
@@ -43,6 +45,34 @@ import org.apache.logging.log4j.status.StatusLogger;
 public abstract class AbstractManager implements AutoCloseable {
 
     /**
+     * Implementations should extend this class for passing data between the getManager method and the manager factory
+     * class.
+     */
+    protected abstract static class AbstractFactoryData {
+
+        private final Configuration configuration;
+
+        /**
+         * Constructs the base factory data.
+         *
+         * @param configuration Configuration creating this instance.
+         */
+        protected AbstractFactoryData(final Configuration configuration) {
+            this.configuration = configuration;
+        }
+
+        /**
+         * Gets my configuration.
+         *
+         * @return my configuration.
+         */
+        public Configuration getConfiguration() {
+            return configuration;
+        }
+
+    }
+
+    /**
      * Allow subclasses access to the status logger without creating another instance.
      */
     protected static final Logger LOGGER = StatusLogger.getLogger();
@@ -229,6 +259,22 @@ public abstract class AbstractManager implements AutoCloseable {
         return new HashMap<>();
     }
 
+    /**
+     * Gets my configuration's StrSubstitutor or null.
+     *
+     * @return my configuration's StrSubstitutor or null.
+     */
+    protected StrSubstitutor getStrSubstitutor() {
+        if (loggerContext == null) {
+            return null;
+        }
+        final Configuration configuration = loggerContext.getConfiguration();
+        if (configuration == null) {
+            return null;
+        }
+        return configuration.getStrSubstitutor();
+    }
+
     protected void log(final Level level, final String message, final Throwable throwable) {
         final Message m = LOGGER.getMessageFactory().newMessage("{} {} {}: {}",
                 getClass().getSimpleName(), getName(), message, throwable);
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 f0f38b9..4fa9cbf 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
@@ -26,6 +26,7 @@ import org.apache.logging.log4j.core.Layout;
 import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.core.appender.AbstractManager;
 import org.apache.logging.log4j.core.appender.ManagerFactory;
+import org.apache.logging.log4j.core.config.Configuration;
 
 /**
  * Manager that allows database appenders to have their configuration reloaded without losing events.
@@ -36,7 +37,7 @@ public abstract class AbstractDatabaseManager extends AbstractManager implements
      * Implementations should extend this class for passing data between the getManager method and the manager factory
      * class.
      */
-    protected abstract static class AbstractFactoryData {
+    protected abstract static class AbstractFactoryData extends AbstractManager.AbstractFactoryData {
         private final int bufferSize;
         private final Layout<? extends Serializable> layout;
 
@@ -45,8 +46,20 @@ public abstract class AbstractDatabaseManager extends AbstractManager implements
          *
          * @param bufferSize The size of the buffer.
          * @param layout The appender-level layout
+         * @deprecated Use {@link AbstractFactoryData#AbstractFactoryData(Configuration, int, Layout)}.
          */
         protected AbstractFactoryData(final int bufferSize, final Layout<? extends Serializable> layout) {
+            this(null, bufferSize, layout);
+        }
+
+        /**
+         * Constructs the base factory data.
+         * @param configuration Configuration creating this instance.
+         * @param bufferSize The size of the buffer.
+         * @param layout The appender-level layout
+         */
+        protected AbstractFactoryData(final Configuration configuration, final int bufferSize, final Layout<? extends Serializable> layout) {
+            super(configuration);
             this.bufferSize = bufferSize;
             this.layout = layout;
         }
@@ -68,7 +81,9 @@ public abstract class AbstractDatabaseManager extends AbstractManager implements
         public Layout<? extends Serializable> getLayout() {
             return layout;
         }
+
     }
+
     /**
      * Implementations should define their own getManager method and call this method from that to create or get
      * existing managers.
@@ -81,11 +96,11 @@ public abstract class AbstractDatabaseManager extends AbstractManager implements
      * @param <T> The concrete {@link AbstractFactoryData} type.
      * @return a new or existing manager of the specified type and name.
      */
-    protected static <M extends AbstractDatabaseManager, T extends AbstractFactoryData> M getManager(
-            final String name, final T data, final ManagerFactory<M, T> factory
-    ) {
+    protected static <M extends AbstractDatabaseManager, T extends AbstractFactoryData> M getManager(final String name, final T data,
+        final ManagerFactory<M, T> factory) {
         return AbstractManager.getManager(name, factory, data);
     }
+
     private final ArrayList<LogEvent> buffer;
     private final int bufferSize;
 
@@ -94,29 +109,48 @@ public abstract class AbstractDatabaseManager extends AbstractManager implements
     private boolean running;
 
     /**
-     * Instantiates the base manager.
+     * Constructs the base manager.
      *
      * @param name The manager name, which should include any configuration details that one might want to be able to
      *             reconfigure at runtime, such as database name, username, (hashed) password, etc.
      * @param bufferSize The size of the log event buffer.
+     * @deprecated Use {@link AbstractDatabaseManager#AbstractDatabaseManager(String, int, Layout, Configuration)}.
      */
+    @Deprecated
     protected AbstractDatabaseManager(final String name, final int bufferSize) {
         this(name, bufferSize, null);
     }
 
     /**
-     * Instantiates the base manager.
+     * Constructs the base manager.
      *
      * @param name The manager name, which should include any configuration details that one might want to be able to
      *             reconfigure at runtime, such as database name, username, (hashed) password, etc.
      * @param layout the Appender-level layout.
      * @param bufferSize The size of the log event buffer.
+     * @deprecated Use {@link AbstractDatabaseManager#AbstractDatabaseManager(String, int, Layout, Configuration)}.
      */
+    @Deprecated
     protected AbstractDatabaseManager(final String name, final int bufferSize, final Layout<? extends Serializable> layout) {
-        super(null, name);
+        this(name, bufferSize, layout, null);
+    }
+
+    /**
+     * Constructs the base manager.
+     *
+     * @param name The manager name, which should include any configuration details that one might want to be able to
+     *             reconfigure at runtime, such as database name, username, (hashed) password, etc.
+     * @param layout the Appender-level layout.
+     * @param bufferSize The size of the log event buffer.
+     * @param configuration My configuration.
+     */
+    protected AbstractDatabaseManager(final String name, final int bufferSize, final Layout<? extends Serializable> layout, final Configuration configuration) {
+        // null configuration allowed for backward compatibility.
+        // TODO should super track Configuration instead of LoggerContext?
+        super(configuration != null ? configuration.getLoggerContext() : null, name);
         this.bufferSize = bufferSize;
         this.buffer = new ArrayList<>(bufferSize + 1);
-        this.layout = layout;
+        this.layout = layout; // A null layout is allowed.
     }
 
     protected void buffer(final LogEvent event) {
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/nosql/NoSqlAppender.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/nosql/NoSqlAppender.java
index 8ed9efa..1fe625d 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/nosql/NoSqlAppender.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/nosql/NoSqlAppender.java
@@ -29,6 +29,7 @@ import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
 import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
 import org.apache.logging.log4j.core.config.plugins.PluginElement;
 import org.apache.logging.log4j.core.util.Booleans;
+import org.apache.logging.log4j.core.util.KeyValuePair;
 
 /**
  * This Appender writes logging events to a NoSQL database using a configured NoSQL provider. It requires
@@ -61,6 +62,9 @@ public final class NoSqlAppender extends AbstractDatabaseAppender<NoSqlDatabaseM
         @PluginElement("NoSqlProvider")
         private NoSqlProvider<?> provider;
 
+        @PluginElement("AdditionalField")
+        private KeyValuePair[] additionalFields;
+
         @SuppressWarnings("resource")
         @Override
         public NoSqlAppender build() {
@@ -72,9 +76,8 @@ public final class NoSqlAppender extends AbstractDatabaseAppender<NoSqlDatabaseM
 
             final String managerName = "noSqlManager{ description=" + name + ", bufferSize=" + bufferSize
                     + ", provider=" + provider + " }";
-
-            final NoSqlDatabaseManager<?> manager = NoSqlDatabaseManager.getNoSqlDatabaseManager(managerName,
-                    bufferSize, provider);
+            final NoSqlDatabaseManager<?> manager = NoSqlDatabaseManager.getNoSqlDatabaseManager(managerName, bufferSize, provider, additionalFields,
+                getConfiguration());
             if (manager == null) {
                 return null;
             }
@@ -129,13 +132,13 @@ public final class NoSqlAppender extends AbstractDatabaseAppender<NoSqlDatabaseM
     @SuppressWarnings("resource")
     @Deprecated
     public static NoSqlAppender createAppender(
-    // @formatter:off
+        // @formatter:off
             final String name,
             final String ignore,
             final Filter filter,
             final String bufferSize,
             final NoSqlProvider<?> provider) {
-    // @formatter:on
+        // @formatter:on
         if (provider == null) {
             LOGGER.error("NoSQL provider not specified for appender [{}].", name);
             return null;
@@ -144,11 +147,9 @@ public final class NoSqlAppender extends AbstractDatabaseAppender<NoSqlDatabaseM
         final int bufferSizeInt = AbstractAppender.parseInt(bufferSize, 0);
         final boolean ignoreExceptions = Booleans.parseBoolean(ignore, true);
 
-        final String managerName = "noSqlManager{ description=" + name + ", bufferSize=" + bufferSizeInt + ", provider="
-                + provider + " }";
+        final String managerName = "noSqlManager{ description=" + name + ", bufferSize=" + bufferSizeInt + ", provider=" + provider + " }";
 
-        final NoSqlDatabaseManager<?> manager = NoSqlDatabaseManager.getNoSqlDatabaseManager(managerName, bufferSizeInt,
-                provider);
+        final NoSqlDatabaseManager<?> manager = NoSqlDatabaseManager.getNoSqlDatabaseManager(managerName, bufferSizeInt, provider, null, null);
         if (manager == null) {
             return null;
         }
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/nosql/NoSqlDatabaseManager.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/nosql/NoSqlDatabaseManager.java
index b270ef1..abd0da2 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/nosql/NoSqlDatabaseManager.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/nosql/NoSqlDatabaseManager.java
@@ -17,6 +17,8 @@
 package org.apache.logging.log4j.core.appender.nosql;
 
 import java.io.Serializable;
+import java.util.Objects;
+import java.util.stream.Stream;
 
 import org.apache.logging.log4j.Marker;
 import org.apache.logging.log4j.ThreadContext;
@@ -24,7 +26,9 @@ import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.core.appender.AppenderLoggingException;
 import org.apache.logging.log4j.core.appender.ManagerFactory;
 import org.apache.logging.log4j.core.appender.db.AbstractDatabaseManager;
+import org.apache.logging.log4j.core.config.Configuration;
 import org.apache.logging.log4j.core.util.Closer;
+import org.apache.logging.log4j.core.util.KeyValuePair;
 import org.apache.logging.log4j.message.MapMessage;
 import org.apache.logging.log4j.util.ReadOnlyStringMap;
 
@@ -40,10 +44,13 @@ public final class NoSqlDatabaseManager<W> extends AbstractDatabaseManager {
 
     private NoSqlConnection<W, ? extends NoSqlObject<W>> connection;
 
-    private NoSqlDatabaseManager(final String name, final int bufferSize,
-            final NoSqlProvider<NoSqlConnection<W, ? extends NoSqlObject<W>>> provider) {
-        super(name, bufferSize);
+    private final KeyValuePair[] additionalFields;
+
+    private NoSqlDatabaseManager(final String name, final int bufferSize, final NoSqlProvider<NoSqlConnection<W, ? extends NoSqlObject<W>>> provider,
+        final KeyValuePair[] additionalFields, final Configuration configuration) {
+        super(name, bufferSize, null, configuration);
         this.provider = provider;
+        this.additionalFields = additionalFields;
     }
 
     @Override
@@ -69,8 +76,7 @@ public final class NoSqlDatabaseManager<W> extends AbstractDatabaseManager {
     @Override
     protected void writeInternal(final LogEvent event, final Serializable serializable) {
         if (!this.isRunning() || this.connection == null || this.connection.isClosed()) {
-            throw new AppenderLoggingException(
-                    "Cannot write logging event; NoSQL manager not connected to the database.");
+            throw new AppenderLoggingException("Cannot write logging event; NoSQL manager not connected to the database.");
         }
 
         final NoSqlObject<W> entity = this.connection.createObject();
@@ -79,10 +85,23 @@ public final class NoSqlDatabaseManager<W> extends AbstractDatabaseManager {
         } else {
             setFields(event, entity);
         }
-
+        setAdditionalFields(entity);
         this.connection.insertObject(entity);
     }
 
+    private void setAdditionalFields(final NoSqlObject<W> entity) {
+        if (additionalFields != null) {
+            entity.set("additionalFields", Stream.of(additionalFields).map(this::convertAdditionalField).toArray());
+        }
+    }
+
+    private NoSqlObject<W> convertAdditionalField(KeyValuePair field) {
+        final NoSqlObject<W> object = connection.createObject();
+        object.set("key", field.getKey());
+        object.set("value", getStrSubstitutor().replace(field.getValue()));
+        return object;
+    }
+
     private void setFields(final MapMessage<?, ?> mapMessage, final NoSqlObject<W> noSqlObject) {
         // Map without calling org.apache.logging.log4j.message.MapMessage#getData() which makes a copy of the map.
         mapMessage.forEach((key, value) -> noSqlObject.set(key, value));
@@ -175,7 +194,7 @@ public final class NoSqlDatabaseManager<W> extends AbstractDatabaseManager {
         // also, all our NoSQL drivers use internal connection pooling and provide clients, not connections.
         // thus, we should not be closing the client until shutdown as NoSQL is very different from SQL.
         // see LOG4J2-591 and LOG4J2-676
-    	return true;
+        return true;
     }
 
     private NoSqlObject<W>[] convertStackTrace(final StackTraceElement[] stackTrace) {
@@ -202,10 +221,26 @@ public final class NoSqlDatabaseManager<W> extends AbstractDatabaseManager {
      * @param bufferSize The size of the log event buffer.
      * @param provider A provider instance which will be used to obtain connections to the chosen NoSQL database.
      * @return a new or existing NoSQL manager as applicable.
+     * @deprecated Use {@link #getNoSqlDatabaseManager(String, int, NoSqlProvider, KeyValuePair[], Configuration)}.
+     */
+    @Deprecated
+    public static NoSqlDatabaseManager<?> getNoSqlDatabaseManager(final String name, final int bufferSize, final NoSqlProvider<?> provider) {
+        return AbstractDatabaseManager.getManager(name, new FactoryData(null, bufferSize, provider, null), FACTORY);
+    }
+
+    /**
+     * Creates a NoSQL manager for use within the {@link NoSqlAppender}, or returns a suitable one if it already exists.
+     *
+     * @param name The name of the manager, which should include connection details and hashed passwords where possible.
+     * @param bufferSize The size of the log event buffer.
+     * @param provider A provider instance which will be used to obtain connections to the chosen NoSQL database.
+     * @param additionalFields Additional fields.
+     * @param configuration TODO
+     * @return a new or existing NoSQL manager as applicable.
      */
-    public static NoSqlDatabaseManager<?> getNoSqlDatabaseManager(final String name, final int bufferSize,
-                                                                  final NoSqlProvider<?> provider) {
-        return AbstractDatabaseManager.getManager(name, new FactoryData(bufferSize, provider), FACTORY);
+    public static NoSqlDatabaseManager<?> getNoSqlDatabaseManager(final String name, final int bufferSize, final NoSqlProvider<?> provider,
+        final KeyValuePair[] additionalFields, final Configuration configuration) {
+        return AbstractDatabaseManager.getManager(name, new FactoryData(configuration, bufferSize, provider, additionalFields), FACTORY);
     }
 
     /**
@@ -213,22 +248,24 @@ public final class NoSqlDatabaseManager<W> extends AbstractDatabaseManager {
      */
     private static final class FactoryData extends AbstractDatabaseManager.AbstractFactoryData {
         private final NoSqlProvider<?> provider;
+        private final KeyValuePair[] additionalFields;
 
-        protected FactoryData(final int bufferSize, final NoSqlProvider<?> provider) {
-            super(bufferSize, null);
-            this.provider = provider;
+        protected FactoryData(final Configuration configuration, final int bufferSize, final NoSqlProvider<?> provider, final KeyValuePair[] additionalFields) {
+            super(configuration, bufferSize, null); // no layout
+            this.provider = Objects.requireNonNull(provider, "provider");
+            this.additionalFields = additionalFields; // null OK
         }
     }
 
     /**
      * Creates managers.
      */
-    private static final class NoSQLDatabaseManagerFactory implements
-            ManagerFactory<NoSqlDatabaseManager<?>, FactoryData> {
+    private static final class NoSQLDatabaseManagerFactory implements ManagerFactory<NoSqlDatabaseManager<?>, FactoryData> {
         @Override
         @SuppressWarnings("unchecked")
         public NoSqlDatabaseManager<?> createManager(final String name, final FactoryData data) {
-            return new NoSqlDatabaseManager(name, data.getBufferSize(), data.provider);
+            Objects.requireNonNull(data, "data");
+            return new NoSqlDatabaseManager(name, data.getBufferSize(), data.provider, data.additionalFields, data.getConfiguration());
         }
     }
 }
diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/nosql/NoSqlDatabaseManagerTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/nosql/NoSqlDatabaseManagerTest.java
index 2be9a65..369e577 100644
--- a/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/nosql/NoSqlDatabaseManagerTest.java
+++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/nosql/NoSqlDatabaseManagerTest.java
@@ -78,7 +78,7 @@ public class NoSqlDatabaseManagerTest {
     @Test
     public void testConnection() {
         try (final NoSqlDatabaseManager<?> manager = NoSqlDatabaseManager.getNoSqlDatabaseManager("name", 0,
-            provider)) {
+            provider, null, null)) {
 
             assertNotNull("The manager should not be null.", manager);
 
@@ -91,7 +91,7 @@ public class NoSqlDatabaseManagerTest {
     @Test
     public void testWriteInternalNotConnected01() {
         try (final NoSqlDatabaseManager<?> manager = NoSqlDatabaseManager.getNoSqlDatabaseManager("name", 0,
-            provider)) {
+            provider, null, null)) {
             expectedException.expect(AppenderLoggingException.class);
             manager.writeInternal(mock(LogEvent.class), null);
         }
@@ -102,7 +102,7 @@ public class NoSqlDatabaseManagerTest {
         given(connection.isClosed()).willReturn(true);
 
         try (final NoSqlDatabaseManager<?> manager = NoSqlDatabaseManager.getNoSqlDatabaseManager("name", 0,
-            provider)) {
+            provider, null, null)) {
 
             manager.startup();
             manager.connectAndStart();
@@ -119,7 +119,7 @@ public class NoSqlDatabaseManagerTest {
         given(message.getFormattedMessage()).willReturn("My formatted message 01.");
 
         try (final NoSqlDatabaseManager<?> manager = NoSqlDatabaseManager.getNoSqlDatabaseManager("name", 0,
-            provider)) {
+            provider, null, null)) {
 
             manager.startup();
             manager.connectAndStart();
@@ -177,7 +177,7 @@ public class NoSqlDatabaseManagerTest {
         given(message.getFormattedMessage()).willReturn("Another cool message 02.");
 
         try (final NoSqlDatabaseManager<?> manager = NoSqlDatabaseManager.getNoSqlDatabaseManager("name", 0,
-            provider)) {
+            provider, null, null)) {
             manager.startup();
 
             manager.connectAndStart();
@@ -275,7 +275,7 @@ public class NoSqlDatabaseManagerTest {
         given(message.getFormattedMessage()).willReturn("Another cool message 02.");
 
         try (final NoSqlDatabaseManager<?> manager = NoSqlDatabaseManager.getNoSqlDatabaseManager("name", 0,
-            provider)) {
+            provider, null, null)) {
             manager.startup();
 
             manager.connectAndStart();
diff --git a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDb3AdditionalFieldsTest.java b/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDb3AdditionalFieldsTest.java
new file mode 100644
index 0000000..9e8225d
--- /dev/null
+++ b/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDb3AdditionalFieldsTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.mongodb3;
+
+import java.util.List;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.categories.Appenders;
+import org.apache.logging.log4j.junit.LoggerContextRule;
+import org.apache.logging.log4j.mongodb3.MongoDb3TestRule.LoggingTarget;
+import org.apache.logging.log4j.test.AvailablePortSystemPropertyTestRule;
+import org.apache.logging.log4j.test.RuleChainFactory;
+import org.bson.Document;
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.rules.RuleChain;
+
+import com.mongodb.MongoClient;
+import com.mongodb.client.FindIterable;
+import com.mongodb.client.MongoCollection;
+import com.mongodb.client.MongoDatabase;
+
+/**
+ *
+ */
+@Category(Appenders.MongoDb.class)
+public class MongoDb3AdditionalFieldsTest {
+
+    private static LoggerContextRule loggerContextTestRule = new LoggerContextRule("log4j2-mongodb-additional-fields.xml");
+
+    private static final AvailablePortSystemPropertyTestRule mongoDbPortTestRule = AvailablePortSystemPropertyTestRule
+        .create(MongoDb3TestConstants.SYS_PROP_NAME_PORT);
+
+    private static final MongoDb3TestRule mongoDbTestRule = new MongoDb3TestRule(mongoDbPortTestRule.getName(), MongoDb3Test.class, LoggingTarget.NULL);
+
+    @ClassRule
+    public static RuleChain ruleChain = RuleChainFactory.create(mongoDbPortTestRule, mongoDbTestRule, loggerContextTestRule);
+
+    @Test
+    public void test() {
+        final Logger logger = LogManager.getLogger();
+        logger.info("Hello log 1");
+        logger.info("Hello log 2", new RuntimeException("Hello ex 2"));
+        try (final MongoClient mongoClient = mongoDbTestRule.getMongoClient()) {
+            final MongoDatabase database = mongoClient.getDatabase(MongoDb3TestConstants.DATABASE_NAME);
+            Assert.assertNotNull(database);
+            final MongoCollection<Document> collection = database.getCollection(MongoDb3TestConstants.COLLECTION_NAME);
+            Assert.assertNotNull(collection);
+            final FindIterable<Document> found = collection.find();
+            final Document first = found.first();
+            Assert.assertNotNull("first", first);
+            Assert.assertEquals(first.toJson(), "Hello log 1", first.getString("message"));
+            Assert.assertEquals(first.toJson(), "INFO", first.getString("level"));
+            //
+            List<Document> list;
+            final String envPath = System.getenv("PATH");
+            //
+            list = first.getList("additionalFields", Document.class);
+            Assert.assertEquals(first.toJson(), "A", list.get(0).get("key"));
+            Assert.assertEquals(first.toJson(), "1", list.get(0).get("value"));
+            Assert.assertEquals(first.toJson(), "B", list.get(1).get("key"));
+            Assert.assertEquals(first.toJson(), "2", list.get(1).get("value"));
+            Assert.assertEquals(first.toJson(), "env1", list.get(2).get("key"));
+            Assert.assertEquals(first.toJson(), envPath, list.get(2).get("value"));
+            Assert.assertEquals(first.toJson(), "env2", list.get(3).get("key"));
+            Assert.assertEquals(first.toJson(), envPath, list.get(3).get("value"));
+            //
+            found.skip(1);
+            final Document second = found.first();
+            Assert.assertNotNull(second);
+            Assert.assertEquals(second.toJson(), "Hello log 2", second.getString("message"));
+            Assert.assertEquals(second.toJson(), "INFO", second.getString("level"));
+            final Document thrown = second.get("thrown", Document.class);
+            Assert.assertEquals(thrown.toJson(), "Hello ex 2", thrown.getString("message"));
+            //
+            list = second.getList("additionalFields", Document.class);
+            Assert.assertEquals(first.toJson(), "A", list.get(0).get("key"));
+            Assert.assertEquals(first.toJson(), "1", list.get(0).get("value"));
+            Assert.assertEquals(first.toJson(), "B", list.get(1).get("key"));
+            Assert.assertEquals(first.toJson(), "2", list.get(1).get("value"));
+        }
+    }
+}
diff --git a/log4j-mongodb3/src/test/resources/log4j2-mongodb-additional-fields.xml b/log4j-mongodb3/src/test/resources/log4j2-mongodb-additional-fields.xml
new file mode 100644
index 0000000..0c7509a
--- /dev/null
+++ b/log4j-mongodb3/src/test/resources/log4j2-mongodb-additional-fields.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+
+-->
+<Configuration status="WARN">
+  <Appenders>
+    <NoSql name="MongoDbAppender">
+      <MongoDb3 databaseName="testDb" collectionName="testCollection" server="localhost"
+        port="${sys:MongoDBTestPort:-27017}" />
+      <KeyValuePair key="A" value="1" />
+      <KeyValuePair key="B" value="2" />
+      <KeyValuePair key="env1" value="${env:PATH}" />
+      <KeyValuePair key="env2" value="$${env:PATH}" />
+    </NoSql>
+  </Appenders>
+  <Loggers>
+    <Root level="ALL">
+      <AppenderRef ref="MongoDbAppender" />
+    </Root>
+  </Loggers>
+</Configuration>
diff --git a/log4j-mongodb4/src/test/java/org/apache/logging/log4j/mongodb4/MongoDb4AdditionalFieldsTest.java b/log4j-mongodb4/src/test/java/org/apache/logging/log4j/mongodb4/MongoDb4AdditionalFieldsTest.java
new file mode 100644
index 0000000..715162f
--- /dev/null
+++ b/log4j-mongodb4/src/test/java/org/apache/logging/log4j/mongodb4/MongoDb4AdditionalFieldsTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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.mongodb4;
+
+import java.util.List;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.categories.Appenders;
+import org.apache.logging.log4j.junit.LoggerContextRule;
+import org.apache.logging.log4j.mongodb4.MongoDb4TestRule.LoggingTarget;
+import org.apache.logging.log4j.test.AvailablePortSystemPropertyTestRule;
+import org.apache.logging.log4j.test.RuleChainFactory;
+import org.bson.Document;
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.rules.RuleChain;
+
+import com.mongodb.client.FindIterable;
+import com.mongodb.client.MongoClient;
+import com.mongodb.client.MongoCollection;
+import com.mongodb.client.MongoDatabase;
+
+/**
+ *
+ */
+@Category(Appenders.MongoDb.class)
+public class MongoDb4AdditionalFieldsTest {
+
+    private static LoggerContextRule loggerContextTestRule = new LoggerContextRule("log4j2-mongodb-additional-fields.xml");
+
+    private static final AvailablePortSystemPropertyTestRule mongoDbPortTestRule = AvailablePortSystemPropertyTestRule
+        .create(MongoDb4TestConstants.SYS_PROP_NAME_PORT);
+
+    private static final MongoDb4TestRule mongoDbTestRule = new MongoDb4TestRule(mongoDbPortTestRule.getName(), MongoDb4AdditionalFieldsTest.class,
+        LoggingTarget.NULL);
+
+    @ClassRule
+    public static RuleChain ruleChain = RuleChainFactory.create(mongoDbPortTestRule, mongoDbTestRule, loggerContextTestRule);
+
+    @Test
+    public void test() {
+        final Logger logger = LogManager.getLogger();
+        logger.info("Hello log 1");
+        logger.info("Hello log 2", new RuntimeException("Hello ex 2"));
+        try (final MongoClient mongoClient = mongoDbTestRule.getMongoClient()) {
+            final MongoDatabase database = mongoClient.getDatabase(MongoDb4TestConstants.DATABASE_NAME);
+            Assert.assertNotNull(database);
+            final MongoCollection<Document> collection = database.getCollection(MongoDb4TestConstants.COLLECTION_NAME);
+            Assert.assertNotNull(collection);
+            final FindIterable<Document> found = collection.find();
+            final Document first = found.first();
+            Assert.assertNotNull("first", first);
+            Assert.assertEquals(first.toJson(), "Hello log 1", first.getString("message"));
+            Assert.assertEquals(first.toJson(), "INFO", first.getString("level"));
+            //
+            List<Document> list;
+            final String envPath = System.getenv("PATH");
+            //
+            list = first.getList("additionalFields", Document.class);
+            Assert.assertEquals(first.toJson(), "A", list.get(0).get("key"));
+            Assert.assertEquals(first.toJson(), "1", list.get(0).get("value"));
+            Assert.assertEquals(first.toJson(), "B", list.get(1).get("key"));
+            Assert.assertEquals(first.toJson(), "2", list.get(1).get("value"));
+            Assert.assertEquals(first.toJson(), "env1", list.get(2).get("key"));
+            Assert.assertEquals(first.toJson(), envPath, list.get(2).get("value"));
+            Assert.assertEquals(first.toJson(), "env2", list.get(3).get("key"));
+            Assert.assertEquals(first.toJson(), envPath, list.get(3).get("value"));
+            //
+            found.skip(1);
+            final Document second = found.first();
+            Assert.assertNotNull(second);
+            Assert.assertEquals(second.toJson(), "Hello log 2", second.getString("message"));
+            Assert.assertEquals(second.toJson(), "INFO", second.getString("level"));
+            final Document thrown = second.get("thrown", Document.class);
+            Assert.assertEquals(thrown.toJson(), "Hello ex 2", thrown.getString("message"));
+            //
+            list = second.getList("additionalFields", Document.class);
+            Assert.assertEquals(first.toJson(), "A", list.get(0).get("key"));
+            Assert.assertEquals(first.toJson(), "1", list.get(0).get("value"));
+            Assert.assertEquals(first.toJson(), "B", list.get(1).get("key"));
+            Assert.assertEquals(first.toJson(), "2", list.get(1).get("value"));
+        }
+    }
+}
diff --git a/log4j-mongodb4/src/test/resources/log4j2-mongodb-additional-fields.xml b/log4j-mongodb4/src/test/resources/log4j2-mongodb-additional-fields.xml
new file mode 100644
index 0000000..93f3285
--- /dev/null
+++ b/log4j-mongodb4/src/test/resources/log4j2-mongodb-additional-fields.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+
+-->
+<Configuration status="WARN">
+  <Appenders>
+    <NoSql name="MongoDbAppender">
+      <MongoDb4 connection="mongodb://localhost:${sys:MongoDBTestPort:-27017}/testDb.testCollection" />
+      <KeyValuePair key="A" value="1" />
+      <KeyValuePair key="B" value="2" />
+      <KeyValuePair key="env1" value="${env:PATH}" />
+      <KeyValuePair key="env2" value="$${env:PATH}" />
+    </NoSql>
+  </Appenders>
+  <Loggers>
+    <Root level="ALL">
+      <AppenderRef ref="MongoDbAppender" />
+    </Root>
+  </Loggers>
+</Configuration>
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index c59773a..f0b3f3e 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -227,6 +227,9 @@
       <action type="add" dev="mattsicker" issue="LOG4J2-3341">
         Add shorthand syntax for properties configuration format for specifying a logger level and appender refs and implicit appender names for when no name is specified.
       </action>
+      <action issue="LOG4J2-3391" type="add" dev="ggregory" due-to="Gary Gregory">
+        Add optional additional fields to NoSQLAppender.
+      </action>
       <!-- UPDATES -->
       <action issue="LOG4J2-3368" dev="vy" type="udpate">
         Bump Tomcat from 8.5.20 to 10.0.14 in log4j-appserver along with some dependency clean-up.