'Programmatic Custom Json Layout Log4j2

I have Java desktop client application that utilizes Log4j2 framework. JSON structure is used when logging the exceptions or infos with custom parameters. I am trying to configure the Log4j2 programmatically for this reason. I am aware that the file based configuration is recommended but it seems like it'd be better to programmatically initialize the framework for my case.

This is the structure I am trying to build.

  • Custom Json Layout
  • HttpAppender that will send the logs over HTTP
  • If HttpAppender fails, it will use JdbcAppender to write to the database
  • If JdbcAppender fails, it will use FileAppender to write to a log file

I am able to generate JsonLayout but I cannot add custom fields and remove existing ones from the default structure. I've probably checked over 50 articles/questions but none of them worked for me. This is the current code I have.

    public static void initializeLogger() 
    {
       ConfigurationBuilder<BuiltConfiguration> builder = 
               ConfigurationBuilderFactory.newConfigurationBuilder();

       builder.setStatusLevel(Level.DEBUG);
       builder.setConfigurationName("DefaultLogger");
            
       //Creating console appender just to see logging is there.
       AppenderComponentBuilder appenderBuilder = builder.newAppender("Console", "CONSOLE")
            .addAttribute("target", ConsoleAppender.Target.SYSTEM_OUT);
       appenderBuilder.add(builder.newLayout("PatternLayout")
            .addAttribute("pattern", pattern)
            .addAttribute("AdditionalField.key", "asd")
            .addAttribute("AdditionalField.value", "qwe"));
       RootLoggerComponentBuilder rootLogger = builder.newRootLogger(Level.DEBUG);
       rootLogger.add(builder.newAppenderRef("Console"));
    
       builder.add(appenderBuilder);

       //Creating the file appender
       LayoutComponentBuilder layoutBuilder = builder.newLayout("JsonLayout")
            .addAttribute("compact", "false")
            .addAttribute("AdditionalField", builder.newKeyValuePair("asd", "qwe"));

       ComponentBuilder triggeringPolicy = builder.newComponent("Policies")
            .addComponent(builder.newComponent("SizeBasedTriggeringPolicy").addAttribute("size", "1KB"));
       appenderBuilder = builder.newAppender("LogToRollingFile", "RollingFile")
            .addAttribute("fileName", fileName)
            .addAttribute("filePattern", "applog-%d{MM-dd-yy-HH-mm-ss}.log.")
            .add(layoutBuilder)
            .addComponent(triggeringPolicy);
       builder.add(appenderBuilder);
    
       rootLogger.add(builder.newAppenderRef("LogToRollingFile"));
       builder.add(rootLogger);
    
       Configurator.reconfigure(builder.build());
}

This code prints to the console and writes Json formatted logging to a file. The custom values are not added to the Json. I tried to create JsonLayout via its builder but I cannot add it to the builder itself.

//Probably creating a new layout instance with additional fields
//might be helpful but I cannot use this in the builder above.
JsonLayout layout = JsonLayout.newBuilder()
    .setAdditionalFields(new KeyValuePair [] {
         new KeyValuePair("asd", "qwe"),
         new KeyValuePair("zxc", "rty"),
     }).build();

The parameters that I need to add to the Json is dynamic and I do not know the keys and values of the custom nodes. At this stage, I am trying to create a custom Json Layout at write it to the file.

I might be doing this completely wrong so please advice! I'm open to suggestions. Thanks.



Solution 1:[1]

A custom value as a key-value pair can be added as a component by calling the addComponent method of LayoutComponentBuilder. The following code snippet shows an example:

ConfigurationBuilder<BuiltConfiguration> builder = ConfigurationBuilderFactory.newConfigurationBuilder();
        AppenderComponentBuilder console =
                builder.newAppender("stdout", "Console")
                        .addAttribute("target", ConsoleAppender.Target.SYSTEM_OUT);
        LayoutComponentBuilder jsonLayout
                = builder.newLayout("JsonLayout")
                .addAttribute("complete", false)
                .addAttribute("compact", false)
                .addComponent(builder.newKeyValuePair("key1", "value1"))
                .addComponent(builder.newKeyValuePair("key2", "value2"));
        console.add(jsonLayout);
        builder.add(console);
        RootLoggerComponentBuilder rootLogger
                = builder.newRootLogger(Level.INFO);
        rootLogger.add(builder.newAppenderRef("stdout"));
        builder.add(rootLogger);
        builder.writeXmlConfiguration(System.out);
        return builder;

Here is the corresponding xml ouput:

<?xml version='1.0' encoding='UTF-8'?>
<Configuration>
    <Appenders>
        <Console name="stdout" target="SYSTEM_OUT">
            <JsonLayout complete="false" compact="false">
                <KeyValuePair key="key1" value="value1"/>
                <KeyValuePair key="key2" value="value2"/>
            </JsonLayout>
        </Console>
    </Appenders>
    <Loggers>
        <Root level="INFO">
            <AppenderRef ref="stdout"/>
        </Root>
    </Loggers>
</Configuration>

Tested with log4j 2.17.1

Solution 2:[2]

You can use the following class to subtract any of the attributes you want in the class from the log.

     import com.fanap.midhco.applicationUtil.JsonUtil;
     import com.fasterxml.jackson.core.util.MinimalPrettyPrinter;
     import com.fasterxml.jackson.databind.ObjectWriter;
     import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
     import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
     import org.apache.logging.log4j.core.Layout;
     import org.apache.logging.log4j.core.LogEvent;
     import org.apache.logging.log4j.core.config.Node;
     import org.apache.logging.log4j.core.config.plugins.Plugin;
     import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
     import org.apache.logging.log4j.core.config.plugins.PluginFactory;
     import org.apache.logging.log4j.core.impl.Log4jLogEvent;
     import org.apache.logging.log4j.core.jackson.JsonConstants;
     import org.apache.logging.log4j.core.jackson.Log4jJsonObjectMapper;
     import org.apache.logging.log4j.core.layout.AbstractStringLayout;
     import org.apache.logging.log4j.core.util.StringBuilderWriter;
     import org.apache.logging.log4j.util.Strings;
     
     import java.io.IOException;
     import java.nio.charset.StandardCharsets;
     import java.util.HashSet;
     import java.util.Set;
     
     @Plugin(name = "MineCustLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
     public class MineCustLayout extends AbstractStringLayout {
     
         private final ObjectWriter objectWriter;
     
         @PluginFactory
         public static MineCustLayout createLayout(
                 @PluginAttribute(value = "locationInfo", defaultBoolean = false) final boolean locationInfo, @PluginAttribute(value = "eventEol", defaultBoolean = true) final boolean eventEol
         ) {
             final SimpleFilterProvider filters = new SimpleFilterProvider();
             final Set<String> except = new HashSet<>();
             if (!locationInfo) {
                 except.add(JsonConstants.ELT_SOURCE);
             }
             except.add("loggerFqcn");
             except.add("level");
             except.add("timeMillis");
             except.add("instant");
             except.add("thread");
             except.add("loggerName");
             except.add("threadId");
             except.add("threadPriority");
             except.add("contextMap");
             except.add("endOfBatch");
             except.add(JsonConstants.ELT_NANO_TIME);
             filters.addFilter(Log4jLogEvent.class.getName(), SimpleBeanPropertyFilter.serializeAllExcept(except));
             final ObjectWriter writer = new Log4jJsonObjectMapper().writer(new MinimalPrettyPrinter());
             return new MineCustLayout(writer.with(filters));
         }
     
         public MineCustLayout(ObjectWriter objectWriter) {
             super(StandardCharsets.UTF_8, null, null);
             this.objectWriter = objectWriter;
         }
     
     
         @Override
         public String toSerializable(LogEvent event) {
             final StringBuilderWriter writer = new StringBuilderWriter();
             try {
                 objectWriter.writeValue(writer, event);
     //            writer.write('\n');
                 return JsonUtil.getJsonObject(writer.toString()).get("message").toString() + '\n';
             } catch (final IOException e) {
                 LOGGER.error(e);
                 return Strings.EMPTY;
             }
         }
     }

And also inside the config:

     <RollingFile name="logFile_json" fileName="${sys:catalina.home}/logs/logFile_json.json"
                 filePattern="${sys:catalina.home}/logs/logFile_json-%d{MM-dd-yyyy}-%i.json">
        <MineCustLayout>
        </MineCustLayout>
        <Policies>
            <SizeBasedTriggeringPolicy size="50 MB"/>
        </Policies>
        <DefaultRolloverStrategy min="1" max="100"/>
     </RollingFile>

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 kamimanzoor
Solution 2 hani