Logback: How to the the logger-name into the appenders logfile-name?

147 Views Asked by At

I am using standard logback in Spring-Boot for logging.

I want every logger in a different file. Today I need to do this way.

logback-spring.xml

<configuration>
  <appender name="FILE_LOG1" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>logs/logger1.log</file>
    ...
  </appender>
  <appender name="FILE_LOG2" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>logs/logger2.log</file>
    ...
  </appender>


  <logger name="logger1" level="DEBUG">
    <appender-ref ref="FILE_LOG1" />
  </logger>

  <logger name="logger2" level="DEBUG">
    <appender-ref ref="FILE_LOG2" />
  </logger>
  ...

Thats not very handy. If my appenders are pretty long and I have a lot of loggers it is pretty a long config.

Is there a way I can place the name of the logger into the logfile-name in the appender?

So I would only need 1 appender.

2

There are 2 best solutions below

1
jvatechs On

You can try to use the SiftingAppender in Logback which can separate logs based on runtime attributes.

<configuration>
    <appender name="SIFT" class="ch.qos.logback.classic.sift.SiftingAppender">
        <discriminator>
            <key>loggerName</key>
            <defaultValue>unknown</defaultValue>
        </discriminator>
        <sift>
            <appender name="FILE-${loggerName}" class="ch.qos.logback.core.rolling.RollingFileAppender">
                <file>logs/${loggerName}.log</file>
                ...
            </appender>
        </sift>
    </appender>

    <root level="DEBUG">
        <appender-ref ref="SIFT" />
    </root>
</configuration>

SiftingAppender uses a discriminator to determine the log file name at runtime. The ${loggerName} placeholder in the file attribute of the RollingFileAppender is replaced with the name of the logger for each log message.

!!!But take into account that SiftingAppender more resource-intensive than a regular appender.

0
lane.maxwell On

In short, no, reusability isn't something that logback does very well. There was an issue opened almost ten years ago to allow for parameterized includes, which would allow you to create a single appender and include it into separate logger configurations (it's still open). I suspect that even with this you'd still encounter issues because variables in logback exist within the scope of a configuration so even if you define an appender that's variable driven, you can only include it once or you'll get an error that an appender exists for that file already.

In effect, what I think you're trying to do is create something like this:

Define an appender that takes variables

my-appender.xml

<included>
    <appender name="${LOG_NAME}" class="ch.qos.logback.core.FileAppender">
        <file>${LOG_FILE_NAME}</file>
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
</included>

Define multiple loggers that use the appender

logger1.xml

<included>
    <property scope="context" name="LOG_NAME" value="logger1"/>
    <property scope="context" name="LOG_FILE_NAME" value="${LOG_NAME}.log"/>

    <include resource="my-appender.xml" />
    <logger name="${LOG_NAME}" level="DEBUG">
        <appender-ref ref="${LOG_NAME}" />
    </logger>
</included>

logger2.xml

<included>
    <property scope="context" name="LOG_NAME" value="logger2"/>
    <property scope="context" name="LOG_FILE_NAME" value="${LOG_NAME}.log"/>

    <include resource="my-appender.xml" />
    <logger name="${LOG_NAME}" level="DEBUG">
        <appender-ref ref="${LOG_NAME}" />
    </logger>
</included>

Include them in your logback configuration

<configuration>
    <include resource="logger1.xml" />
    <include resource="logger2.xml" />
</configuration>

This looks like it should work, any programmer will look at this and be able to make sense of it, but because those variables exist in the context of the configuration, the same values get passed into the configuration so in effect, it tries to create the same appender and bombs.

There is a programmatic solution to this, which will allow you to reuse the appender, and it looks something like this.

// 1) Create a configuration class that contains a @PostConstruct method, I'm using my main application class but any will work
@Configuration
public class LogDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(LogDemoApplication.class, args);
    }


    // 2) Create a method that creates the appender for the given pattern layout and log filename
    private FileAppender<ILoggingEvent> createAppender(PatternLayoutEncoder patternLayoutEncoder,
                                                       LoggerContext loggerContext,
                                                       String fileName) {
        FileAppender<ILoggingEvent> fileAppender = new FileAppender<>();
        fileAppender.setFile(fileName);
        fileAppender.setEncoder(patternLayoutEncoder);
        fileAppender.setContext(loggerContext);
        fileAppender.start();
        return fileAppender;
    }

    // 3) Create a method that creates the logger and applies the appender
    private void createLogger(String loggerName, Level logLevel,
                              FileAppender<ILoggingEvent> fileAppender) {
        Logger logger = (Logger) LoggerFactory.getLogger(loggerName);
        logger.setAdditive(false);
        logger.setLevel(logLevel);
        logger.addAppender(fileAppender);
    }

    // 4) Create all your loggers here, I created loggers for foo, bar, and one Service
    @PostConstruct
    public void createMyLoggers() {
        LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
        PatternLayoutEncoder patternLayoutEncoder = new PatternLayoutEncoder();

        patternLayoutEncoder.setPattern("%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n");
        patternLayoutEncoder.setContext(loggerContext);
        patternLayoutEncoder.start();

        createLogger("foo", Level.TRACE, createAppender(patternLayoutEncoder, loggerContext,
                "/Users/lane.maxwell/foo.log"));
        createLogger("bar", Level.DEBUG, createAppender(patternLayoutEncoder, loggerContext,
                "/Users/lane.maxwell/bar.log"));
        createLogger("com.terrafirmaeng.logdemo.SomeService", Level.DEBUG,
                createAppender(patternLayoutEncoder, loggerContext, "/Users/lane.maxwell/SomeService.log"));
    }
}

The beauty of this configuration, is that if you happen to use Lombok, it integrates perfectly. See this Service class as an example

@Service
@Slf4j
public class SomeService {
    public SomeService() {
        log.debug("debug info");
    }
}

After starting this service with the above configuration, I have three files where I expect them

lane.maxwell@tesla:~ $ pwd
/Users/lane.maxwell

lane.maxwell@tesla:~ $ ls *log
SomeService.log bar.log foo.log

and my log file contains what I expect

lane.maxwell@tesla:~ $ cat SomeService.log 
2024-01-15 21:01:12 [main] DEBUG c.t.l.service.SomeService - debug info
lane.maxwell@tesla:~ $