Home » Java » java – Logback – can you define appender name and class from env variables?-Exceptionshub

java – Logback – can you define appender name and class from env variables?-Exceptionshub

Posted by: admin February 25, 2020 Leave a comment

Questions:

I want to have dynamic logback appender properties which can easily be added to the logback config file, however trying to set the class and name of the appender(which are in a xml attributes instead of elements under the appender element).

So here is my application.yml(hardcoded the values for the example, but in the real world use case those will be passed as env variables from Helm, during the deploy to K8s cluster):

log:
  config:
    appender:
      name: CONSOLE
      class: ch.qos.logback.core.ConsoleAppender

And here is how I try to access those in logback-spring.xml(spring boot version – 2.2.4.RELEASE

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- use Spring default values like patterns -->
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <!-- declaration of ENV properties:   -->
    <springProperty name="LOG_CONFIG_APPENDER_NAME" source="log.config.appender.name"/>
    <springProperty name="LOG_CONFIG_APPENDER_CLASS" source="log.config.appender.class"/>

    <appender name="${LOG_CONFIG_APPENDER_NAME}" class="${LOG_CONFIG_APPENDER_CLASS}">
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="${LOG_CONFIG_APPENDER_NAME}"/>
    </root>
</configuration>

This leads to the following exception:

ERROR in ch.qos.logback.core.joran.action.AppenderAction - Could not create an Appender of type [${LOG_CONFIG_APPENDER_CLASS}]. ch.qos.logback.core.util.DynamicClassLoadingException: Failed to instantiate type ${LOG_CONFIG_APPENDER_CLASS}

So my question is: is it possible to define the appender name and class dynamically ?

How to&Answers:

For the local env I want to have simple console log appender. For anything else I will use the LayoutWrappingEncoder and JacksonJsonFormatter to add custom fields for our ELK stack.

So you want to use the <springProfile> tag in your logback-spring.xml:

The <springProfile> tag lets you optionally include or exclude sections of configuration based on the active Spring profiles. Profile sections are supported anywhere within the <configuration> element. Use the name attribute to specify which profile accepts the configuration. The <springProfile> tag can contain a simple profile name (for example staging) or a profile expression. A profile expression allows for more complicated profile logic to be expressed, for example production & (eu-central | eu-west). Check the reference guide for more details.

See the example below:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>

    <springProfile name="staging">
        <!-- configuration to be enabled when the "staging" profile is active -->
    </springProfile>

    <springProfile name="dev | staging">
        <!-- configuration to be enabled when the "dev" or "staging" profiles are active -->
    </springProfile>

    <springProfile name="!production">
        <!-- configuration to be enabled when the "production" profile is not active -->
    </springProfile>

</configuration>

Answer:

I have ended up doing it in Groovy, as mapping the config to the Spring profiles does not work for me and my team, as we are using the dev profile for both our local and our shared dev env(hosted on K8s cluster).

I am not proud of it, but ended doing it with a simple if:

def loggingType = System.getenv('LOGGING_TYPE')
def loggingLevelEnvVar = System.getenv('CUSTOM_LOGGING_LEVEL')
def loggingLevel = loggingLevelEnvVar == null ? INFO : Level.valueOf(loggingLevelEnvVar)
// please do not use a coloured pattern for consoles that will be scrapped
def loggingPattern = System.getenv('LOGGING_LEVEL_PATTERN')
...
if ('JSON'.equalsIgnoreCase(loggingType)) {
    appender('CONSOLE', ConsoleAppender) {
        encoder(LayoutWrappingEncoder) {
            layout(JsonLayout) {
                timestampFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS"
                timestampFormatTimezoneId = 'Etc/UTC'
                appendLineSeparator = true
                jsonFormatter(JacksonJsonFormatter) {
                    prettyPrint = false
                }
            }
        }
    }
} else {
    appender('CONSOLE', ConsoleAppender) {
        encoder(PatternLayoutEncoder) {
            pattern = loggingPattern
        }
    }
}
...