Search code examples
springlogginglogbackmdclog-rotation

How to manage log file switching through custom user event in Spring?


Suppose we use Logback for logging.

It’s required to change the path to the log file every time a certain event (i.e. function call) occurs.

For example, somewhere we call a function.

startNewLogSegment("A")

After this event, the logger is expected to start writing to the logs/mySegment_A.log file. Then, again a call performed:

startNewLogSegment("B")

After this event, the logger is expected to finish writing to the previous file and start writing to the logs/mySegment_B.log file.

Let's assume that a state changed by startNewLogSegment should be visible in the whole application (all threads).


I tried to apply the approach with MDC:

logback.xml

...
<appender name="SIFTING_BY_ID" class="ch.qos.logback.classic.sift.SiftingAppender">
    <discriminator>
        <key>id</key>
        <defaultValue>initial</defaultValue>
    </discriminator>

    <sift>
        <appender name="FULL-${id}" class="ch.qos.logback.core.FileAppender">
            <file>logs/mySegment_${id}.log</file>
            <append>false</append>
            <encoder >
                <pattern>%d{dd-MM-yyyy HH:mm:ss.SSS} [%thread] [%-5level] %logger{36}.%M - %msg%n</pattern>
            </encoder>
        </appender>
    </sift>
</appender>
...

and calling MDC.put("id", "A") when a custom event appears.

But it works a different way than I need.

It’s known that the MDC manages contextual information on a per thread basis, so at least we need a control over threads creation to accomplish the goal described above.

I wonder if this approach could be used with Spring, and in particular with async operations performed by Spring Reactor. I’ve found no information about using a custom thread pool for internal Spring activities.

Possibly, I hope, there’s a more simple way to tune logging that way without abusing Spring internals.


Solution

  • I ended up with a cusom implementation of discriminator AbstractDiscriminator<ILoggingEvent> allowing uasge of globally visible values.

    GVC.java

    /**
     * Global values context.
     * Allows to sift log files globally independent from a thread calling log operation.
     * <p>
     * Used API analogous to standard {@link org.slf4j.MDC}.
     */
    public final class GVC {
    
        private static Map<String, String> STORED = new HashMap<>();
    
        private GVC() {
        }
    
        public static synchronized void put(String key, String value) {
            STORED.put(key, value);
        }
    
        public static synchronized String get(String key) {
            return STORED.get(key);
        }
    }
    

    GVCBasedDiscriminator.java

    /**
     * Customized analogue of MDCBasedDiscriminator.
     * <p>
     * GVCBasedDiscriminator essentially returns the value mapped to an GVC key.
     * If the value is null, then a default value is returned.
     * <p>
     * Both Key and the DefaultValue are user specified properties.
     */
    public class GVCBasedDiscriminator extends AbstractDiscriminator<ILoggingEvent> {
    
        private String key;
        private String defaultValue;
    
        public String getDiscriminatingValue(ILoggingEvent event) {
            String value = GVC.get(key);
    
            if (value == null) {
                return defaultValue;
            } else {
                return value;
            }
        }
    
        @Override
        public String getKey() {
            return key;
        }
    
        @Override
        public void start() {
            int errors = 0;
            if (OptionHelper.isEmpty(key)) {
                errors++;
                addError("The \"Key\" property must be set");
            }
            if (OptionHelper.isEmpty(defaultValue)) {
                errors++;
                addError("The \"DefaultValue\" property must be set");
            }
            if (errors == 0) {
                started = true;
            }
        }
    
        /**
         * Key for this discriminator instance
         *
         * @param key
         */
        public void setKey(String key) {
            this.key = key;
        }
    
        /**
         * The default GVC value in case the GVC is not set for
         * {@link #setKey(String) mdcKey}.
         * <p/>
         * <p> For example, if {@link #setKey(String) Key} is set to the value
         * "someKey", and the MDC is not set for "someKey", then this appender will
         * use the default value, which you can set with the help of this method.
         *
         * @param defaultValue
         */
        public void setDefaultValue(String defaultValue) {
            this.defaultValue = defaultValue;
        }
    }
    

    logback.xml

    <appender name="TRACES_PER_SESSION_FILE" class="ch.qos.logback.classic.sift.SiftingAppender">
      <!-- here the custom discriminator implementation is applied -->
      <discriminator class="internal.paxport.misc.logging.GVCBasedDiscriminator">             
        <key>id</key>
        <defaultValue>initial</defaultValue>
      </discriminator>
    
      <sift>
        <appender name="FULL-${id}" class="ch.qos.logback.core.FileAppender">
          <file>logs/mySegment_${id}.log</file>
          <append>false</append>
          <encoder>
            <pattern>%d{dd-MM-yyyy HH:mm:ss.SSS} [%thread] [%-5level] %logger{36}.%M - %msg%n</pattern>
          </encoder>
        </appender>
      </sift>
    </appender>