Practical use of Logback context selectors

2020-07-17 07:23发布

问题:

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?

回答1:

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.



回答2:

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;
        }
    }
}

}



回答3:

Although I'm familiar with log4j, I'm not as familiar with slf4j and logback. However, reading the documentation of logback I see that there are quite a few similarities with log4j so I think I may be able to provide some insight into your questions.

EDIT:

I am correcting my previous statements about the context selector feature in logback (see struck out text below). I arrived at my original conclusion too quickly and now believe it would in fact be possible to create a selector that would fulfill the scenario in question #1. The selector would be somewhat similar to the ContextJNDISelector in that it would need to map threads to their corresponding configuration as well as their context instance somehow. Perhaps a simple solution would be provide a properties file that maps thread name or ID to the appropriate configuration file path, then have the selector read this file. The selector would, when asked for a context, look up the thread (either by name or ID) in this mapping and return the appropriate context object. The returned context may be an existing context that was already initialized previously or a new one that was just created for that thread by using the file path obtained from the properties read earlier. Based on the logback documentation it appears that the selector itself is reused throughout the life of the application - meaning it is used the moment the application starts until it exits. I believe this to be the case because the selector is specified by using a JVM parameter:

You can specify a different context selector by setting the logback.ContextSelector system property. Suppose you would like to specify that context selector to an instance of the myPackage.myContextSelector class, you would add the following system property: -Dlogback.ContextSelector=myPackage.myContextSelector

Regarding your question #1, logback appears to operate like log4j in that it expects exactly one configuration file. Reading the logback configuration page I see that it looks for this file on the classpath just like log4j. Therefore it does not matter that you provide multiple configuration files because only one will be loaded no matter what - whichever one the classloader finds first will be loaded. So your question about how you could use ContextSelector to load different xml config files for different threads is really unanswerable since you would need to rewrite the logging framework in order to add such a feature. The ContextSelector appears to be intended to be used with separate applications not with separate threads within the same application.

EDIT:

I just want to add that my comments in the section below were from the perspective of practical use of logback. I suggested these alternative approaches because I believe they are more in line with the intended use of the logback framework.

Alternative approaches to #1:


Now, if you wanted to have different loggers for different threads that shouldn't be too difficult as you can pass a String into the getLogger method call like the example in Chapter 9 of logback documentation:

Logger logger = LoggerFactory.getLogger("foo");

So, if you name each of your threads in a meaningful way you could set up your configuration file with a separate logger for each thread and then call getLogger passing in the name of the thread as the parameter. However, you would lose some of the functionality - the hierarchical logger structure for instance.

If you want more than a single logger per thread then a better approach would be to use filters. You could create a filter that filters based on thread name and then configure appenders to accept messages from specific threads. Take a look at the filter page in the logback manual


As for your second question about having an ILoggerFactory that returns loggers from a programmatic configuration I think your best bet would be to modify the LoggerContext similar to the answers in this question

Hope this helps!