18

I am successfully using Spring's @Scheduled annotation to execute a method every few seconds. The only issue is that I get a lot of log messages about transactions, etc from Hibernate and Spring because of this method.

I want to keep the logging levels the same because I like to receive this information for other transactions in the application.

Is there a way in logback to temporarily suppress another library's logging while a specific method is executing?

durron597
  • 31,968
  • 17
  • 99
  • 158
johnktims
  • 581
  • 2
  • 7
  • 14
  • Though there **is** answer, I would love to hear a way to turn off logging for specific method/class, which will work okay in a concurrent application. – om-nom-nom May 21 '14 at 16:46
  • 4
    Good thing you waited. For concurrent code, Durron597's answer is better than mine, and I'm happy to have had a chance to learn about it. – Warren Dew May 22 '14 at 18:43

3 Answers3

23

Yes this can be done on a per-thread basis.

You need to use Filters and MDC (Mapped Diagnostic Context). This solution will only affect the logging that occurs on the thread that is executing the @Scheduled method.

Overview of steps

  1. In your @Scheduled method, add an entry to the MDC of that thread.
  2. Create an implementation of ch.qos.logback.core.filter.Filter<ILoggingEvent>, which will return FilterReply.DENY if that entry is set in the MDC
  3. Add a reference to that filter in your <appender> entry in logback.xml

Step 1

Make your @Scheduled method look like this:

@Scheduled(fixedRate=30000)
public void scheduledMethod () {
    try{
        MDC.put("scheduled", "true");

        // Your code here
    }finally{
        MDC.remove("scheduled");
    }
}

I should mention that removing the key is important because Spring may reuse the thread and the MDC would keep the value otherwise.

Step 2

Your filter should look something like this:

package my.domain.application.logging;

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

public class MDCFilter extends Filter<ILoggingEvent> {

  @Override
  public FilterReply decide(ILoggingEvent event) {    
    String scheduled = event.getMDCPropertyMap().get("scheduled");
    if ("true".equals(scheduled)) {
      return FilterReply.DENY;
    } else {
      return FilterReply.NEUTRAL;
    }
  }
}

Step 3

Add the following to your logback.xml

<appender name="myAppender" class="ch.qos.logback.core.ConsoleAppender">
    <filter class="my.domain.application.logging.MDCFilter" />
    <!-- the rest of your appender -->
</appender>
ratchet freak
  • 47,288
  • 5
  • 68
  • 106
durron597
  • 31,968
  • 17
  • 99
  • 158
5

Yes: you can set loggers' log levels dynamically, and thus change it over time.

Ideally all you will need to do is get the logger at the top of the relevant class hierarchy and set the log level to whatever you want. For example, with hibernate:

import ch.qos.logback.classic.Level
import ch.qos.logback.classic.Logger;
import org.slf4j.LoggerFactory;

// When you want to temporarily change the log level
Logger orgHibernateLogger = LoggerFactory.getLogger("org.hibernate");
Level oldLogLevel = orgHibernateLogger.getLevel();
orgHibernateLogger.setLevel(Level.ERROR); // or whatever level you want

// When you want to return to the old log level
orgHibernateLogger.setLevel(oldLogLevel);

This should work as long as no other loggers in the relevant hierarchy have explicitly configured log levels. If other loggers in the hierarchy have explicitly configured log levels, you'll have to temporarily adjust all the explicitly set log levels, and then return them all to "normal" at the end.

Note that this is a time based approach, not a context based approach: if some other piece of code simultaneously uses the library while your temporary log level is in use, it will also get that temporary log level. Log levels only return to "normal" when you return then to normal. I don't think there's a simple way to avoid that without preventing the other code from running while you are using your temporary log level.

Edit: See the answer by Durron597 for a method that allow turning off the unwanted logging in the desired thread only. It's slightly less simple, but it looks like it should work.

Warren Dew
  • 8,790
  • 3
  • 30
  • 44
  • Your last statements make me sad: changing log level globally is quite fragile approach, especially in case of multi-threaded application (which are prevalent this days) – om-nom-nom May 15 '14 at 18:39
  • Well, the question asks about "temporarily" rather than "for certain threads". Perhaps when enough people demonstrate a need for the latter - and I agree there is one - the logback team will do something about it. – Warren Dew May 15 '14 at 19:04
5

Generally logging framework supports package level log configuration. You could do something like below in your logback.xml file:

<configuration>
    <logger name="org.hibernate.transaction" level="OFF"/>   // change log level to suppress here
     <logger name="org.springframework.transaction" level="OFF"/>

    <root level="ALL">
        <appender class="ch.qos.logback.core.ConsoleAppender">
            <encoder>
                <pattern>%d{ISO8601} | %-5level | %thread | %logger{1} | %m%n</pattern>
            </encoder>
        </appender>
    </root>
</configuration>

In jboss application server, we have same issue previously and filtered unwanted log with package level log configuration as below:

<logger category="com.lftechnology.sbworkbench">
    <level name="DEBUG"/>
</logger>
<logger category="org.quartz">
    <level name="WARN"/>
</logger>
<logger category="com.liferay.portal">
    <level name="INFO"/>
</logger>
<logger category="org.jboss.as.server.deployment">
    <level name="ERROR"/>
</logger> 

As you are interested in suppressing library's log in specific method you will have to do it programmatically and restore on exit. But i am not sure if it will affect the particular code block.

More over If your application run concurrently then you can use ThreadLocal like below

public Class A implements Runnable {
    //you can later dynamically configure this variable also
    private static final ThreadLocal<Logger> CLASS_LOGGER = new ThreadLocal<Logger>(){
       Logger LOGGER = LoggerFactory.getLogger(A.class);
         return LOGGER;
       }

     @Override
 public void run() {
   //before calling your function change log level
   //as we declare local variable here this most likely  to affect log here only
   Logger Logger = LoggerFactory.getLogger("org.hibernate.transaction");
   Level oldLogLevel = Logger.getLevel(); 
   Logger.setLevel(Level.OFF); // 

   callYourFunction();

   //restore log level to previous
   Logger.setLevel(oldLogLevel);

 }

I have similar answer here

Community
  • 1
  • 1
gyanu
  • 945
  • 1
  • 8
  • 20