Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Practical use of Logback context selectors

The documentation on Logback logging separation indicates that I can use context selectors to create different logging configurations on the same JVM. Somehow a context selector will allow me to call LoggerFactory.getLogger(Foo.class) and, based upon the context, I will get a differently configured logger.

Unfortunately the examples only deal with JNDI in the context of a specially configured web server such as Tomcat or Jetty. I want to know how I can practically use a context selector myself, for example in a non-web application.

My goal is to have more than one logging configuration on the same JVM. Here is one scenario:

  • I want one thread to get loggers using the default logback.xml configuration on the classpath.
  • I want another thread to get loggers using another logback.xml from a custom directory.
  • I want a third thread to get loggers from a programmatic configuration.

I provide these example scenarios just to get an idea of practical use of context selectors---how I would do something useful with them in real life.

  1. How could I use context selectors to implement the scenarios above, so that LoggerFactory.getLogger(Foo.class) returns a logger from the correct configuration based on the thread?
  2. If context selectors are not up to the task, how could I manually get an ILoggerFactory instance that would give me loggers from a programmatic configuration?
like image 674
Garret Wilson Avatar asked Jul 05 '16 16:07

Garret Wilson


People also ask

What is Logback context?

Logback provides a mechanism for dealing with multiple contexts, without corruption of data, nor collision between logger context instances.

Why do we use Logback xml?

Logback executes an async appender in a separate thread to decouple the logging overhead from the thread executing your code. Using the async appender is incredibly easy. Refer the appender that should be asynchronously invoked within an <appender> element.

Is Log4j2 better than Logback?

Log4j, Logback, and Log4j2 are good logging frameworks that are broadly used. So which one should you use? I recommend using Log4j2 because it's the fastest and most advanced of the three frameworks. Logback is still a good option, if performance is not your highest priority.

What is context name in Logback xml?

Setting the context name is a simple and straightforward method in order to distinguish between multiple applications logging to the same target. This last example illustrates naming of the logger context. Adding the contextName conversion word in layout's pattern will output the said name.


2 Answers

I had asked this question to prevent my needing to trace through the Logback source code, but as the initial answers were inadequate I wound up having to do so anyway. So let me explain the initialization of the SLF4J+Logback system and how it relates to context selectors.

SLF4J is a logging API that allows various implementations, one of them being Logback.

  1. When the first request is made to org.slf4j.LoggerFactory.getLogger(...), the SLF4J framework is initialized by creating a org.slf4j.impl.StaticLoggerBinder. The trick is that StaticLoggerBinder is not distributed with SLF4J; it is actually implemented in whichever logging implementation is being used (e.g. Logback). This is a somewhat cumbersome approach to loading a specific implementation (a service loader might have been a better choice), but that's somewhat beside the point here.

  2. Logback's implementation of StaticLoggerBinder creates a singleton ch.qos.logback.classic.util.ContextSelectorStaticBinder. This is the class that sets up the context selector. The logic goes something like is outlined below.

    a. If the "logback.ContextSelector" system property contains "JNDI", use a ContextJNDISelector.

    b. If the "logback.ContextSelector" system property contains anything else, assume the value is the name of a context selector class and try to instantiate that.

    c. Otherwise if there is no "logback.ContextSelector" system property, use a DefaultContextSelector.

  3. If a custom ContextSelector is used, ContextSelectorStaticBinder will instantiate it using a constructor that takes a LoggerContext as a parameter, and will pass it the default LoggerContext which StaticLoggerBinder has created and auto-configured. (Changing the default configuration strategy is a separate subject I won't cover here.)

As Pieter points out in another answer, the way to install a custom context selector is to provide the name of the implementing class in the "logback.ContextSelector" system property. Unfortunately this approach is a bit precarious, and it obviously has to be done 1) manually and 2) before any SLF4J calls are made. (Here again a service loader mechanism would be been much better; I have filed issue LOGBACK-1196 for this improvement.)

If you manage to get your custom Logback context selector installed, you'll probably want to store the LoggerContext you receive in the constructor so that you can return it in ContextSelector.getDefaultLoggerContext(). Other than this, the most important method in ContextSelector is ContextSelector.getLoggerContext(); from this method you will determine what logger context is appropriate for the current context and return it.

The LoggerContext which is so important here is ch.qos.logback.classic.LoggerContext, which implements ILoggerFactory. When you access the main org.slf4j.LoggerFactory.getLogger(...) method, it uses the singleton StaticLoggerBinder (discussed above) to look up the logger factory. For Logback StaticLoggerBinder will use the singleton ContextSelectorStaticBinder (also discussed above) which hopefully will return to your now installed custom LoggerContext.

(The ContextSelector.getLoggerContext(String name), ContextSelector.detachLoggerContext(String loggerContextName), and ContextSelector.getContextNames() methods seem to only be used in a situation such as the JNDI context selector in which you wish to keep track of context selectors using names. If you don't need named context selectors, it appears you can safely return null and an empty list as appropriate for these methods.)

Thus the custom ContextSelector need simply provide some LoggerContext appropriately configured for the calling thread; this LoggerContext will serve as the ILoggerFactory that creates a logger based upon the LoggerContext configuration. Configuration is covered in the Logback documentation; perhaps this discussion here makes clearer what the Logback logger context is all about.

As for the actual mechanics of associating a LoggerContext with a thread, that was never an issue for me. It's simple: I'll be using my own Csar library, which handles such things with ease. And now that I've figured out how to hook into the logger context selection process, I've already implemented this in a logging assistance library named Clogr which uses Csar and which I have now publicly released.

Because I wound up having to do all the research myself, I'll be marking my own answer as the accepted answer unless someone points out something important in one of the other answers which I didn't cover in my own.

like image 151
Garret Wilson Avatar answered Sep 28 '22 03:09

Garret Wilson


Based on a quick look at the logback source code I think you should be able implement all of the scenario's you mentioned by plugging in your own ContextSelector via a java system property. I'm not sure if this feature is documented or not but it's certainly there: ContextSelector initialization

When logback bootstraps itself it will first create the default logger context. The default logger context is typically configured via logback.xml or logback.groovy. More info on configuring the default logger context can be found here: Logback configuration

As you already read logback has the notion of context selectors that decide which logger context to use when creating a logger. The default context selector simply returns the default logger context but by plugging in your own context selector you can do pretty much anything you want.

The following example shows you how to plug in you're own ContextSelector. The selector itself doesn't do much; implementing it so that if fulfills your needs is up to you ;)

import ch.qos.logback.classic.ClassicConstants;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.selector.ContextSelector;
import org.slf4j.LoggerFactory;

import java.util.Arrays;
import java.util.List;

public class LoggingExperiment {

public static void main(String[] args) {
    System.getProperties().setProperty(
            ClassicConstants.LOGBACK_CONTEXT_SELECTOR,
            MyCustomContextSelector.class.getName()
    );
    LoggerFactory.getLogger(LoggingExperiment.class).info("test");
}

// this is implementation is just a copy of ch.qos.logback.classic.selector.DefaultContextSelector
// but it shows you how to bootstrap you're own context selector
public static class MyCustomContextSelector implements ContextSelector {

    private LoggerContext defaultLoggerContext;

    public MyCustomContextSelector(LoggerContext context) {
        System.out.println("You're custom ContextSelector is being constructed!");
        this.defaultLoggerContext = context;
    }

    public LoggerContext getDefaultLoggerContext() {
        return defaultLoggerContext;
    }

    public LoggerContext getLoggerContext() {
        //TODO create and return the LoggerContext that should be used
        //if ("A".equals(Thread.currentThread().getName())){
        //    //return LoggerContext x and create it if necessary
        //   // Take a look at ch.qos.logback.classic.selector.ContextJNDISelector for an example of how to create & cache LoggerContexts.
        //   // Also note that when using multiple contexts you'll also have the adjust the other methods of this class appropriately.
        //}else {
        //    return getDefaultLoggerContext();
        //}

        return getDefaultLoggerContext();
    }

    public LoggerContext detachLoggerContext(String loggerContextName) {
        return defaultLoggerContext;
    }

    public List<String> getContextNames() {
        return Arrays.asList(defaultLoggerContext.getName());
    }

    public LoggerContext getLoggerContext(String name) {
        if (defaultLoggerContext.getName().equals(name)) {
            return defaultLoggerContext;
        } else {
            return null;
        }
    }
}

}

like image 44
Pieter Avatar answered Sep 28 '22 01:09

Pieter