Log4j2 has a lot of features but if you want to configure it from the environment (or system properties), you need to put placeholders everywhere. This is not very convenient since you don't always know which loggers will need to be tuned and things like that. Let see how to make the configuration taken from the environment in 1 class!

The first thing to do to make it is to create a custom configuration factory, it is a class extending ConfigurationFactory:

public class EnvironmentConfigurationFactory extends ConfigurationFactory {
    @Override
    protected String[] getSupportedTypes() {
        return new String[]{".env", "*"}; // 1
    }

    @Override // 2
    public Configuration getConfiguration(final LoggerContext loggerContext, final ConfigurationSource source) {
        // ...
    }
}
  1. The factory is responsible ot take a file as input and map it to a configuration (2). The file is determined from a static name (log4j2log4j2-test etc) and a suffix provided by getSupportedTypes() method.

Since we don't have a file but log4j requires one, we will just put one empty file in our project (log4j2.env) to pass this test.

At that point, getConfiguration() will be called and we can read the config as we want to map it to a log4j2 Configuration.

The idea of the implementation will be to:

  1. Extract all environment variables and system properties starting with a prefix (app.logging. for system properties, APP_LOGGING_ for env variables),
  2. Convert the keys to log4j2 format (lowercase, replacing _ by dots),
  3. Merge all these entries in a Properties instance,
  4. Add defaults in the properties instance,
  5. Delegate the creation of the configuration to the built-in PropertiesConfigurationFactory.

Here what it can look like:

public class EnvironmentConfigurationFactory extends ConfigurationFactory {
    @Override
    protected String[] getSupportedTypes() {
        return new String[]{".env", "*"}; // env is just a placeholder to enter in log4j2 logic
    }

    @Override
    public Configuration getConfiguration(final LoggerContext loggerContext, final ConfigurationSource source) {
        try {
            final Properties properties = Stream.concat(
                    System.getProperties().stringPropertyNames().stream()
                            .filter(e -> e.startsWith("app.logging."))
                            .map(e -> new AbstractMap.SimpleImmutableEntry<>(e, System.getProperty(e))),
                    System.getenv().entrySet().stream()
                            .filter(e -> sanitizeKey(e).startsWith("app.logging.")))
                            .collect(Properties::new, (p, e) -> p.setProperty(sanitizeKey(e).substring("app.logging.".length()), e.getValue()), Properties::putAll);
            setDefaults(properties);
            final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            try (final OutputStream os = byteArrayOutputStream) {
                properties.store(os, "");
            }
            return new PropertiesConfigurationFactory().getConfiguration(loggerContext, new ConfigurationSource(
                    new ByteArrayInputStream(byteArrayOutputStream.toByteArray())));
        } catch (final IOException e) {
            throw new IllegalStateException(e);
        }
    }

    private String sanitizeKey(final Map.Entry<String, String> e) {
        return e.getKey().toLowerCase(Locale.ROOT)
                .replace('_', '.')
                // here case is used by PropertiesConfiguration
                .replace("customlevel", "customLevel")
                .replace("rootlogger", "rootLogger")
                .replace("appenderref", "appenderRef");
    }

    private void setDefaults(final Properties properties) {
        properties.putIfAbsent("status", "INFO");
        properties.putIfAbsent("appender.stdout.type", "Console");
        properties.putIfAbsent("appender.stdout.name", "stdout");
        properties.putIfAbsent("appender.stdout.layout.type", "PatternLayout");
        properties.putIfAbsent("appender.stdout.layout.pattern", "[%d][%highlight{%-5level}][%15.15t][%30.30logger] %msg%n");
        properties.putIfAbsent("rootLogger.level", "INFO");
        properties.putIfAbsent("rootLogger.appenderRef.stdout.ref", "stdout");
    }
}

The only trick here is to normalize appenderRefrootLogger and customLevel entries which are case sensitive.

Once done, you just have to enable this factory. There are mainly two options:

  1. Use a custom flag and initialize the log4j2 system properties to enable this factory,
  2. Make this factory a plugin.

Last option is cleaner if you intend to reuse it but most of the time, you will want to enable it in some cases (in containers but not in dev for example) so let's go with option 1.

-Dlog4j.configurationFactory=com.github.rmannibucau.log4j2.EnvironmentConfigurationFactory

Then retart your software and you will be able to use environment configuration:

APP_LOGGING_ROOTLOGGER_LEVEL=DEBUG
APP_LOGGING_LOGGER_VERBOSELOGGER_NAME=org.foo.Bar
APP_LOGGING_LOGGER_VERBOSELOGGER_LEVEL=OFF

In docker containers (and kubernetes) it is really an added value to be able to fully configure the logging without having to rebuild an image or mount any file in the container.

 

 

From the same author:

In the same category: