69

I want to have two log files in my application (Spring Integration), debug.log and main.log. I want to run main.log at an INFO level and debug.log at a DEBUG level. This is doable with filters on the appenders. I want to log different levels to the appenders based on the source. In other words

<logger name="org.springframework" level="ERROR">
    <appender-ref ref="main" />
</logger>
<logger name="org.springframework" level="DEBUG">
    <appender-ref ref="debug" />
</logger>
<logger name="com.myapp" level="INFO">
    <appender-ref ref="main" />
</logger>
<logger name="com.myapp" level="DEBUG">
    <appender-ref ref="debug" />
</logger>

So to summarise:

  1. Spring logger
    • main -> ERROR
    • debug -> DEBUG
  2. com.myapp logger
    • main -> INFO
    • debug -> DEBUG

Because of this I have to have the loggers running at DEBUG and a threshold filter on an appender isn't fine grained enough.

Update Added clarity to the question

John Oxley
  • 14,698
  • 18
  • 53
  • 78

6 Answers6

56

Create a ThresholdLoggerFilter class which can be put on an appender like:

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
        <level>INFO</level>
    </filter>
    <filter class="com.myapp.ThresholdLoggerFilter">
        <logger>org.springframework</logger>
        <level>ERROR</level>
    </filter>
    </appender>

The following code works

package com.myapp;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.filter.Filter;
import ch.qos.logback.core.spi.FilterReply;

public class ThresholdLoggerFilter extends Filter<ILoggingEvent> {
    private Level level;
    private String logger;

    @Override
    public FilterReply decide(ILoggingEvent event) {
        if (!isStarted()) {
            return FilterReply.NEUTRAL;
        }

        if (!event.getLoggerName().startsWith(logger))
            return FilterReply.NEUTRAL;

        if (event.getLevel().isGreaterOrEqual(level)) {
            return FilterReply.NEUTRAL;
        } else {
            return FilterReply.DENY;
        }
    }

    public void setLevel(Level level) {
        this.level = level;
    }

    public void setLogger(String logger) {
        this.logger = logger;
    }

    public void start() {
        if (this.level != null && this.logger != null) {
            super.start();
        }
    }
}
John Oxley
  • 14,698
  • 18
  • 53
  • 78
  • 4
    No need to implement yourself, use ch.qos.logback.classic.filter.ThresholdFilter – David Roussel Jun 05 '14 at 09:25
  • 2
    ThresholdFilter just filters on a Threshold. I want to filter on both a logger AND threshold – John Oxley Jun 06 '14 at 10:09
  • 1
    @JohnOxley This is a functionality that should be included in logback! Have you considered [contributing](https://github.com/qos-ch/logback/pulls)? – schnatterer Jun 06 '15 at 19:36
  • I believe it was intended to have positive filter, so should be if (!event.getLoggerName().startsWith(logger)) return FilterReply.DENY; – Alexander Chepurnoy Aug 03 '15 at 13:13
  • The "startsWith(logger)" is sufficient for most use cases but is not entirely correct. For example, if the event's logger name is "com.examples", the configured logger "com.example" would incorrectly match. – MyKey_ Oct 29 '16 at 11:51
21

You can also do this somewhat more simply if you are willing to inherit from the root logger, e.g. here we add an extra logger for errors, that logs to stderr. It's only enabled for particular loggers.

<configuration>
    <appender name="CONSOLE-stdout" class="ch.qos.logback.core.ConsoleAppender">
        <target>System.out</target> <!-- the default -->
        <encoder>
            <pattern>%d %-5level [%thread] %logger{0}: %msg%n</pattern>
        </encoder>
    </appender>
    <appender name="CONSOLE-stderr" class="ch.qos.logback.core.ConsoleAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ERROR</level>
        </filter>

        <target>System.err</target>
        <encoder>
            <pattern>%d %-5level [%thread] %logger{0}: %msg%n</pattern>
        </encoder>
    </appender>
    <root level="DEBUG">
        <appender-ref ref="CONSOLE-stdout" />
    </root>

        <!-- We want error logging from this logger to go to an extra appender 
             It still inherits CONSOLE-stdout from the root logger -->
    <logger name="org.springframework" level="INFO">
        <appender-ref ref="CONSOLE-stderr" />
    </logger>
</configuration>
rogerdpack
  • 62,887
  • 36
  • 269
  • 388
artbristol
  • 32,010
  • 5
  • 70
  • 103
  • Thanks @artbristol, but this doesn't give the granularity of control I want. I have updated the question so show why. – John Oxley May 25 '12 at 11:52
21

Adding an additional solution that is simpler than what's already here

None of these solutions worked for me, since I'm not using a framework like Spark or Spring. So I did something a bit simpler that seems to function nicely. While this solution may not work for the OP, perhaps it can be of use to someone wanting something not so bulky.

<property name="pattern" value="%d{yyyy.MMM.dd HH:mm:ss.SSS} [ProgramName] %level - %msg%n" />

<appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>/path/to/your/program.log</file>
    <append>true</append>
    <encoder>
        <pattern>${pattern}</pattern>
    </encoder>
</appender>

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <target>System.out</target>
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
        <level>INFO</level>
    </filter>
    <encoder>
        <pattern>${pattern}</pattern>
    </encoder>
</appender>

<root level="debug">
    <appender-ref ref="FILE" />
    <appender-ref ref="STDOUT" />
</root>

With this configuration, I am able to keep the console rather clean, while outputting DEBUG statements to the log file.

Jonathan E. Landrum
  • 2,748
  • 4
  • 30
  • 46
7

Just found a practical solution using logback elements only that works pretty well, essentially you need to have two appenders, one with the default config and the other one with a filter (in my example I'm using the console):

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

    <appender name="WARN_FILTER_STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>WARN</level>
        </filter>
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <logger name="org.apache.spark" level="INFO" additivity="false">
        <appender-ref ref="SPARK" /><!-- this line is not necessary, just here to ilustrate the need for the filter -->
        <appender-ref ref="WARN_FILTER_STDOUT" />
    </logger>

    <root level="info">
        <appender-ref ref="STDOUT" />
    </root>
eduardohl
  • 1,176
  • 2
  • 10
  • 23
  • This worked great for me. I needed everything INFO and up in one log and everything WARN and up in the other. Creating 1 appender for each log, then putting both appenders under the same logger like you did was 100% the solution and required zero additional components. – JoshDM May 29 '20 at 22:28
1

Using Multiple loggers for different messages will be like this:

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext; 
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.FileAppender;

public class ConfigureLogBack 
{
    public static void programmaticConfiguration()
    {
        Logger camel = getLogger("MyRoute", C:\\Users\\amrut.malaji\\Desktop\\Oracle\\logback\\camel-Log.txt");
        Logger services = getLogger("webservices", "C:\\Users\\amrut.malaji\\Desktop\\Oracle\\logback\\services-log.txt");
    }

    private static Logger getLogger(String string, String file) {
        LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
    PatternLayoutEncoder ple = new PatternLayoutEncoder();

    ple.setPattern("%date %level [%thread] %logger{10} [%file:%line] %msg%n");
    ple.setContext(lc);
    ple.start();
    FileAppender<ILoggingEvent> fileAppender = new FileAppender<ILoggingEvent>();
    fileAppender.setFile(file);
    fileAppender.setEncoder(ple);
    fileAppender.setContext(lc);
    fileAppender.start();

    Logger logger = (Logger) LoggerFactory.getLogger(string);
    logger.addAppender(fileAppender);
    logger.setLevel(Level.INFO);
    logger.setAdditive(false); /* set to true if root should log too */

    return logger;
}
Qantas 94 Heavy
  • 15,750
  • 31
  • 68
  • 83
1

a threshold filter on an appender isn't fine grained enough

You could use an EvaluatorFilter. A JaninoEventEvaluator would need a reference to janino (.jar) and a logback.xml example would be:

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
        <level>INFO</level>
    </filter>
    <filter class="ch.qos.logback.core.filter.EvaluatorFilter">      
        <evaluator class="ch.qos.logback.classic.boolex.JaninoEventEvaluator">
            <expression>
                level &lt;= ERROR &amp;&amp; logger.equals(&quot;com.myapp.ThresholdLoggerFilter&quot;)
            </expression>
        </evaluator>
        <OnMismatch>DENY</OnMismatch>
        <OnMatch>NEUTRAL</OnMatch>
    </filter>
 </appender>

This approach uses a java expression in the expression tag (must be xml escaped) to evaluate the logging event and does not need a custom java class to be written.