com.netflix.blitz4j.LoggingConfiguration.java Source code

Java tutorial

Introduction

Here is the source code for com.netflix.blitz4j.LoggingConfiguration.java

Source

/*
 * Copyright 2012 Netflix, Inc.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package com.netflix.blitz4j;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.apache.commons.configuration.ConfigurationException;
import org.apache.log4j.Appender;
import org.apache.log4j.LogManager;
import org.apache.log4j.PropertyConfigurator;
import org.apache.log4j.spi.LoggerFactory;
import org.slf4j.Logger;

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.netflix.config.ConfigurationManager;
import com.netflix.config.ExpandedConfigurationListenerAdapter;
import com.netflix.config.PropertyListener;
import com.netflix.logging.messaging.BatcherFactory;
import com.netflix.logging.messaging.MessageBatcher;

/**
 * The main configuration class that bootstraps the <em>blitz4j</em>
 * implementation.
 * 
 * <p>
 * The users can either use {@link #configure()} or
 * {@link #configure(Properties)} to kick start the configuration. If the
 * <code>log4j.configuration</code> is provided, the properties are additionally
 * loaded from the provided {@link URL}.
 * </p>
 * 
 * <p>
 * The list of appenders to be automatically converted can be provided by the
 * property <code>log4j.logger.asyncAppenders</code>. The configuration takes
 * these appenders and automatically enables them for asynchronous logging.
 * </p>
 * 
 * @author Karthik Ranganathan
 * 
 */
public class LoggingConfiguration implements PropertyListener {

    private static final String BLITZ_LOGGER_FACTORY = "com.netflix.blitz4j.NFCategoryFactory";
    private static final String PROP_LOG4J_CONFIGURATION = "log4j.configuration";
    private static final Object guard = new Object();
    private static final String PROP_LOG4J_LOGGER_FACTORY = "log4j.loggerFactory";
    private static final String LOG4J_FACTORY_IMPL = "com.netflix.logging.log4jAdapter.NFCategoryFactory";

    private static final String LOG4J_LOGGER_FACTORY = "log4j.loggerFactory";
    private static final BlitzConfig blitz4jConfig = new DefaultBlitz4jConfig();

    private static final String PROP_LOG4J_ORIGINAL_APPENDER_NAME = "originalAppenderName";
    private static final String LOG4J_PREFIX = "log4j.logger";
    private static final String LOG4J_APPENDER_DELIMITER = ".";
    private static final String LOG4J_APPENDER_PREFIX = "log4j.appender";
    private static final String ASYNC_APPENDERNAME_SUFFIX = "_ASYNC";
    private static final String ROOT_CATEGORY = "rootCategory";
    private static final String ROOT_LOGGER = "rootLogger";
    private Map<String, String> originalAsyncAppenderNameMap = new HashMap<String, String>();
    private Properties props = new Properties();
    Properties updatedProps = new Properties();
    private final ExecutorService executorPool;
    private Logger logger;
    private static final int SLEEP_TIME_MS = 200;
    private static final CharSequence PROP_LOG4J_ASYNC_APPENDERS = "log4j.logger.asyncAppenders";

    private static LoggingConfiguration instance = new LoggingConfiguration();

    private LoggingConfiguration() {
        ThreadFactory threadFactory = new ThreadFactoryBuilder().setDaemon(false)
                .setNameFormat("DynamicLog4jListener").build();

        this.executorPool = new ThreadPoolExecutor(0, 1, 15 * 60, TimeUnit.SECONDS, new SynchronousQueue(),
                threadFactory);
    }

    /**
     * Kick start the blitz4j implementation
     */
    public void configure() {
        this.configure(null);
    }

    /**
     * Kick start the blitz4j implementation.
     * 
     * @param props
     *            - The overriding <em>log4j</em> properties if any.
     */
    public void configure(Properties props) {
        this.originalAsyncAppenderNameMap.clear();
        // First try to load the log4j configuration file from the classpath
        String log4jConfigurationFile = System.getProperty(PROP_LOG4J_CONFIGURATION);

        if (log4jConfigurationFile != null) {
            InputStream in = null;
            try {
                URL url = new URL(log4jConfigurationFile);
                in = url.openStream();
                this.props.load(in);
            } catch (Throwable t) {
                throw new RuntimeException(
                        "Cannot load log4 configuration file specified in " + PROP_LOG4J_CONFIGURATION, t);
            } finally {

                if (in != null) {
                    try {
                        in.close();
                    } catch (IOException ignore) {

                    }
                }
            }

        }
        if (props != null) {
            Enumeration enumeration = props.propertyNames();
            while (enumeration.hasMoreElements()) {
                String key = (String) enumeration.nextElement();
                this.props.setProperty(key, props.getProperty(key));
                ConfigurationManager.getConfigInstance().setProperty(key, props.getProperty(key));
            }
        }

        NFHierarchy nfHierarchy = null;
        // Make log4j use blitz4j implementations
        if (blitz4jConfig.shouldUseLockFree()
                && (!NFHierarchy.class.equals(LogManager.getLoggerRepository().getClass()))) {
            nfHierarchy = new NFHierarchy(new NFRootLogger(org.apache.log4j.Level.INFO));
            org.apache.log4j.LogManager.setRepositorySelector(new NFRepositorySelector(nfHierarchy), guard);
        }
        String log4jLoggerFactory = System.getProperty(PROP_LOG4J_LOGGER_FACTORY);
        if (log4jLoggerFactory != null) {
            this.props.setProperty(PROP_LOG4J_LOGGER_FACTORY, log4jLoggerFactory);
            if (nfHierarchy != null) {
                try {
                    LoggerFactory loggerFactory = (LoggerFactory) Class.forName(log4jLoggerFactory).newInstance();
                    nfHierarchy.setLoggerFactory(loggerFactory);
                } catch (Throwable e) {
                    System.err.println("Cannot set the logger factory. Hence reverting to default.");
                    e.printStackTrace();
                }
            }
        } else {
            if (blitz4jConfig.shouldUseLockFree()) {
                this.props.setProperty(PROP_LOG4J_LOGGER_FACTORY, BLITZ_LOGGER_FACTORY);
            }
        }

        String[] asyncAppenderArray = blitz4jConfig.getAsyncAppenders();
        if (asyncAppenderArray == null) {
            return;
        }
        for (int i = 0; i < asyncAppenderArray.length; i++) {
            String oneAppenderName = asyncAppenderArray[i];
            if (i == 0) {
                continue;
            }
            String oneAsyncAppenderName = oneAppenderName + ASYNC_APPENDERNAME_SUFFIX;
            originalAsyncAppenderNameMap.put(oneAppenderName, oneAsyncAppenderName);
        }
        try {
            convertConfiguredAppendersToAsync(this.props);
        } catch (Throwable e) {
            throw new RuntimeException("Could not configure async appenders ", e);
        }
        PropertyConfigurator.configure(this.props);
        closeNonexistingAsyncAppenders();
        this.logger = org.slf4j.LoggerFactory.getLogger(LoggingConfiguration.class);
        ConfigurationManager.getConfigInstance()
                .addConfigurationListener(new ExpandedConfigurationListenerAdapter(this));
    }

    public static LoggingConfiguration getInstance() {
        return instance;
    }

    public BlitzConfig getConfiguration() {
        return this.blitz4jConfig;
    }

    /**
     * Shuts down blitz4j cleanly by flushing out all the async related
     * messages.
     */
    public void stop() {
        MessageBatcher batcher = null;
        for (String originalAppenderName : originalAsyncAppenderNameMap.keySet()) {
            String batcherName = AsyncAppender.class.getName() + "." + originalAppenderName;
            batcher = BatcherFactory.getBatcher(batcherName);
            if (batcher == null) {
                continue;
            }
            batcher.stop();
        }
        for (String originalAppenderName : originalAsyncAppenderNameMap.keySet()) {
            String batcherName = AsyncAppender.class.getName() + "." + originalAppenderName;
            batcher = BatcherFactory.getBatcher(batcherName);
            if (batcher == null) {
                continue;
            }
            BatcherFactory.removeBatcher(batcherName);
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.netflix.config.PropertyListener#addProperty(java.lang.Object,
     * java.lang.String, java.lang.Object, boolean)
     */
    public void addProperty(Object source, String name, Object value, boolean beforeUpdate) {
        if (shouldProcessProperty(name, beforeUpdate)) {
            updatedProps.put(name, value);
            reConfigureAsynchronously();
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.netflix.config.PropertyListener#clear(java.lang.Object, boolean)
     */
    public void clear(Object source, boolean beforeUpdate) {
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.netflix.config.PropertyListener#clearProperty(java.lang.Object,
     * java.lang.String, java.lang.Object, boolean)
     */
    public void clearProperty(Object source, String name, Object value, boolean beforeUpdate) {
        if (shouldProcessProperty(name, beforeUpdate)) {
            updatedProps.remove(name);
            reConfigureAsynchronously();
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.netflix.config.PropertyListener#configSourceLoaded(java.lang.Object)
     */
    public void configSourceLoaded(Object source) {
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.netflix.config.PropertyListener#setProperty(java.lang.Object,
     * java.lang.String, java.lang.Object, boolean)
     */
    public void setProperty(Object source, String name, Object value, boolean beforeUpdate) {
        if (shouldProcessProperty(name, beforeUpdate)) {
            updatedProps.put(name, value);
            reConfigureAsynchronously();
        }
    }

    /**
     * Reconfigure log4j at run-time.
     * 
     * @param name
     *            - The name of the property that changed
     * @param value
     *            - The new value of the property
     * @throws FileNotFoundException
     * @throws ConfigurationException
     */
    private void reConfigure() throws ConfigurationException, FileNotFoundException {

        Properties consolidatedProps = new Properties();
        consolidatedProps.putAll(props);
        logger.info("Updated properties is :" + updatedProps);
        consolidatedProps.putAll(updatedProps);
        logger.info("The root category for log4j.rootCategory now is "
                + consolidatedProps.getProperty("log4j.rootCategory"));
        logger.info("The root category for log4j.rootLogger now is "
                + consolidatedProps.getProperty("log4j.rootLogger"));

        // Pause the async appenders so that the appenders are not accessed
        for (String originalAppenderName : originalAsyncAppenderNameMap.keySet()) {
            MessageBatcher asyncBatcher = BatcherFactory
                    .getBatcher(AsyncAppender.class.getName() + "." + originalAppenderName);
            if (asyncBatcher == null) {
                continue;
            }
            asyncBatcher.pause();
        }

        // Configure log4j using the new set of properties
        configureLog4j(consolidatedProps);
        // Resume all the batchers to continue logging
        for (String originalAppenderName : originalAsyncAppenderNameMap.keySet()) {
            MessageBatcher asyncBatcher = BatcherFactory
                    .getBatcher(AsyncAppender.class.getName() + "." + originalAppenderName);
            if (asyncBatcher == null) {
                continue;
            }
            asyncBatcher.resume();
        }
    }

    /**
     * Configure log4j with the given properties.
     * 
     * @param props
     *            The properties that needs to be configured for log4j
     * @throws ConfigurationException
     * @throws FileNotFoundException
     */
    private void configureLog4j(Properties props) throws ConfigurationException, FileNotFoundException {
        if (blitz4jConfig.shouldUseLockFree() && (props.getProperty(LOG4J_LOGGER_FACTORY) == null)) {
            props.setProperty(LOG4J_LOGGER_FACTORY, LOG4J_FACTORY_IMPL);
        }
        convertConfiguredAppendersToAsync(props);
        logger.info("Configuring log4j with properties :" + props);
        PropertyConfigurator.configure(props);
    }

    /**
     * Asynchronously reconfigure <code>log4j</code>. This make sure there is
     * only one configuration currently in progress and rejects multiple
     * reconfigurations at the same time there by causing logging contentions.
     */
    private void reConfigureAsynchronously() {
        try {
            executorPool.submit(new Runnable() {

                public void run() {
                    try {
                        Thread.sleep(SLEEP_TIME_MS);
                        logger.info("Configuring log4j dynamically");
                        reConfigure();
                    } catch (Throwable th) {
                        logger.error("Cannot dynamically configure log4j :", th);
                    }
                }
            });
        } catch (RejectedExecutionException re) {
            throw re;
        }
    }

    /**
     * Check if the property that is being changed is something that this
     * configuration cares about.
     * 
     * The implementation only cares about changes related to <code>log4j</code>
     * properties.
     * 
     * @param name
     *            -The name of the property which should be checked.
     * @param beforeUpdate
     *            -true, if this call is made before the property has been
     *            updated, false otherwise.
     * @return
     */
    private boolean shouldProcessProperty(String name, boolean beforeUpdate) {
        if (name == null) {
            logger.warn("The listener got a null value for name");
            return false;
        }
        if (beforeUpdate) {
            return false;
        }
        return name.startsWith(LOG4J_PREFIX);
    }

    /**
     * Convert appenders specified by the property
     * <code>log4j.logger.asyncAppender</code> to the blitz4j Asynchronous
     * appenders.
     * 
     * @param props
     *            - The properties that need to be passed into the log4j for
     *            configuration.
     * @throws ConfigurationException
     * @throws FileNotFoundException
     */
    private void convertConfiguredAppendersToAsync(Properties props)
            throws ConfigurationException, FileNotFoundException {
        for (Map.Entry<String, String> originalAsyncAppenderMapEntry : originalAsyncAppenderNameMap.entrySet()) {
            String asyncAppenderName = originalAsyncAppenderMapEntry.getValue();
            props.setProperty(LOG4J_APPENDER_PREFIX + LOG4J_APPENDER_DELIMITER + asyncAppenderName,
                    AsyncAppender.class.getName());
            // Set the original appender so that it can be fetched later after
            // configuration
            String originalAppenderName = originalAsyncAppenderMapEntry.getKey();
            props.setProperty(LOG4J_APPENDER_PREFIX + LOG4J_APPENDER_DELIMITER + asyncAppenderName
                    + LOG4J_APPENDER_DELIMITER + PROP_LOG4J_ORIGINAL_APPENDER_NAME, originalAppenderName);
            // Set the batcher to reject the collector request instead of it
            // participating in processing
            ConfigurationManager.getConfigInstance().setProperty("batcher." + AsyncAppender.class.getName() + "."
                    + originalAppenderName + "." + "rejectWhenFull", true);
            // Set the default value of the processing max threads to 1, if a
            // value is not specified
            int maxThreads = ConfigurationManager.getConfigInstance().getInt(
                    "batcher." + AsyncAppender.class.getName() + "." + originalAppenderName + "." + "maxThreads",
                    0);
            if (maxThreads == 0) {
                ConfigurationManager.getConfigInstance().setProperty("batcher." + AsyncAppender.class.getName()
                        + "." + originalAppenderName + "." + "maxThreads", "1");
            }

            for (Map.Entry mapEntry : props.entrySet()) {
                String key = mapEntry.getKey().toString();
                if ((key.contains(LOG4J_PREFIX) || key.contains(ROOT_CATEGORY) || key.contains(ROOT_LOGGER))
                        && !key.contains(PROP_LOG4J_ASYNC_APPENDERS)
                        && !key.contains(PROP_LOG4J_ORIGINAL_APPENDER_NAME)) {
                    Object value = mapEntry.getValue();
                    if (value != null && !((String) value).contains(asyncAppenderName)) {
                        String convertedString = value.toString().replace(originalAppenderName, asyncAppenderName);
                        mapEntry.setValue(convertedString);
                    }

                }
            }
        }
    }

    /**
     * Closes any asynchronous appenders that were not removed during configuration.
     */
    private void closeNonexistingAsyncAppenders() {
        org.apache.log4j.Logger rootLogger = LogManager.getRootLogger();
        if (NFLockFreeLogger.class.isInstance(rootLogger)) {
            ((NFLockFreeLogger) rootLogger).reconcileAppenders();
        }
        Enumeration enums = LogManager.getCurrentLoggers();
        while (enums.hasMoreElements()) {
            Object myLogger = enums.nextElement();
            if (NFLockFreeLogger.class.isInstance(myLogger)) {
                ((NFLockFreeLogger) myLogger).reconcileAppenders();
            }
        }
    }

}