Separate method logging in Java / logback

Bart writing some logs







Task



Let's say we wanted to log each method of a certain Java class differently:









This article shows how to fulfill these requirements. In order to maintain simplicity, separation of logging is performed only by methods; in reality, you might want to have a hierarchical qualifying config, like















... A link to the full source code will be below.







Client code



  class ThingService { log = LoggerFactory.getLogger(); getThing() { log.debug("getThing..."); // => one.log } listThings() { log.debug("listThings..."); // => another.log } }
      
      





Logback



For the implementation, a solid "logback" logging library was selected, which provides interesting possibilities for customization:







 ch.qos.logback:logback-classic:1.2.3
      
      





It is configured both from an XML config and directly from Java, approaches can be combined:







  public void configureLogback() throws JoranException { LoggerContext lc = LoggerFactory.getILoggerFactory(); lc.reset(); // reset prev config JoranConfigurator configurator = new JoranConfigurator(); configurator.setContext(lc); configurator.doConfigure("config.xml"); // any data source StatusPrinter.printInCaseOfErrorsOrWarnings(lc); //   : Logger root = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); root.setLevel(Level.INFO); //  }
      
      





Briefly about logging:







  1. The programmer pulls the logger ,
  2. The logger pulls the appenders assigned to him,
  3. The appender thinks and calls the encoder ,
  4. The encoder formats exactly one line of the log,
  5. To do this, he pulls a chain of converters , each of which reveals its %



    ,
  6. Success.


For simplicity, a pure Java configuration was chosen. Everything is pretty obvious here if you keep in mind the XML config. The main task is to create your own appender / encoder and register them - they will be called by the logback from their bowels. Almost every object you create must be remembered to start using the start()



method. Abstract example:







  Logger rootLogger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); LoggerContext lc = rootLogger.getLoggerContext(); lc.reset(); // reset prev config var encoder = new PatternLayoutEncoder(); encoder.setContext(lc); encoder.setPattern("%-5level %message%n"); encoder.start(); var appender = new ConsoleAppender<ILoggingEvent>(); appender.setContext(lc); appender.setEncoder(encoder); appender.start(); rootLogger.setLevel(Level.DEBUG); rootLogger.addAppender(appender);
      
      





Separate logged methods from each other



So that the logback can distinguish one method from another, before calling the method, save its name in ThreadLocal



Mapped Diagnostic Context. Further, during analysis, we don’t get these values ​​directly from the MDC



class, since the logging code will be executed in another thread and this data will not be there - we get them through ILoggingEvent.getMDCPropertyMap()



.







In the general case, as vooft correctly noted, you need to support the call stack and not overwrite the MDC value, but return it to the previous frame, which is done through the introduction of a new ThreadLocal



. Schematic example:







  try { MDC.put(MDC_KEY_METHOD, currentMethod); // 1.  currentMethod    // 2.    // 3.       AOP, . } finally { String previousMethod = //     MDC.put(previousMethod); }
      
      





Own log file for each method



Create and do not forget to register your own appender:







  class MultiAppender extends AppenderBase<ILoggingEvent> { @Override protected void append(ILoggingEvent event) { method = event.getMDCPropertyMap().get(MDC_KEY_METHOD); Appender appender = getOrCreateAppender(method); appender.doAppend(event); }
      
      





He himself does almost nothing, only delegates logging to a pack of real file appenders, one for each method. Delegated to one, the most suitable. "Real" appenders are created on demand, as follows:







  fileAppender = new FileAppender<ILoggingEvent>(); fileAppender.setContext(lc); fileAppender.setAppend(false); fileAppender.setEncoder(getOrCreateEncoderByMethod(lc, method)); fileAppender.setFile(logFileByMethod.get(method)); fileAppender.start();
      
      





Own format for each method



To do this, keep the cache of auto-created objects of the Encoder



type:







  Map<String, String> patternByMethod = new HashMap<>(); //  ;  Encoder getOrCreateEncoderByMethod(LoggerContext lc, String method) { String pattern = patternByMethod.get(method); encoder = new PatternLayoutEncoder(); encoder.setContext(lc); encoder.setPattern(pattern); encoder.start(); return encoder; }
      
      





Each method has its own level of logging



MultiAppender



add a check to the MultiAppender



class: if the event level MultiAppender



threshold specified for the method, only then we log it:







  Map<String, Level> levelByMethod = new HashMap<>(); protected void append(ILoggingEvent event) { Level minLevel = levelByMethod.get(methodName); if (event.getLevel().levelInt >= minLevel.levelInt) { appender.doAppend(event); }
      
      





In principle, this logic can be put into the filter.







Extending the format with your variables



In order not to fence your garden, but to take advantage of the proven infrastructure of the log-back, you need to define your converter class, which must be entirely public so that it can be instantiated from the outside. If you need MDC



, take it from the event. The %custom



variable handler starts here:







  public class CustomConverter extends ClassicConverter { public String convert(ILoggingEvent event) { // mdc = event.getMDCPropertyMap(); return "variable-expanded"; } }
      
      





During the general configuration process, register the handler:







  void configurePatterns(LoggerContext lc) { Map<String, String> rules = lc.getObject(CoreConstants.PATTERN_RULE_REGISTRY); if (rules == null) { rules = new HashMap<String, String>(); lc.putObject(CoreConstants.PATTERN_RULE_REGISTRY, rules); } rules.put("custom", CustomConverter.class.getName()); }
      
      





And we will use, as an encoder, for example, PatternLayoutEncoder



, which will pick it all up. In this case, the %custom



variable will expand to the string "variable-expanded"



.







Config update on the fly



There is such an opportunity out of the box: it is enough to call the configurator function again, not forgetting to do LoggerContext::reset()



and clear the accumulated cache.







Multithreading



If the method configured by us brings new threads to life, then, of course, the specified logging rules will not apply to them - thread local'y will not appear by themselves in the new thread. So, if you want to apply the method settings to a new stream, you have to copy the MDC



there:







  Map<String, String> mdcOrig = MDC.getCopyOfContextMap(); ExecutorService es = Executors.newFixedThreadPool(1); es.submit(() -> threadWorker(mdcOrig)); void threadWorker(Map<String, String> parentMdc) { MDC.setContextMap(parentMdc); log.error("expected to appear in method2*.log"); }
      
      





Whole example



https://github.com/zencd/logback-setup







Literature



Official Logback Manual








All Articles