blob: 931171b175ca396c1d581162d6c6b70145c9030f [file] [log] [blame]
/*
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.qpid.server.logging.log4j;
import org.apache.log4j.Level;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.apache.log4j.xml.DOMConfigurator;
import org.apache.log4j.xml.Log4jEntityResolver;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
/**
* A facade over log4j that allows both the control of the runtime logging behaviour (that is, the ability to
* turn {@link Logger} on, off and control their {@link Level}, and the manipulation and reload
* of the log4j configuration file.
*/
public class LoggingFacade
{
private static Logger LOGGER;
private static transient LoggingFacade _instance;
private final String _filename;
private final int _delay;
public static LoggingFacade configure(String filename) throws LoggingFacadeException
{
_instance = new LoggingFacade(filename);
return _instance;
}
public static LoggingFacade configureAndWatch(String filename, int delay) throws LoggingFacadeException
{
_instance = new LoggingFacade(filename, delay);
return _instance;
}
public static LoggingFacade getCurrentInstance()
{
return _instance;
}
private LoggingFacade(String filename)
{
DOMConfigurator.configure(filename);
if(LOGGER == null)
{
LOGGER = Logger.getLogger(LoggingFacade.class);
}
_filename = filename;
_delay = 0;
}
private LoggingFacade(String filename, int delay)
{
DOMConfigurator.configureAndWatch(filename, delay);
if(LOGGER == null)
{
LOGGER = Logger.getLogger(LoggingFacade.class);
}
_filename = filename;
_delay = delay;
}
public int getLog4jLogWatchInterval()
{
return _delay;
}
public synchronized void reload() throws LoggingFacadeException
{
DOMConfigurator.configure(_filename);
}
/** The log4j XML configuration file DTD defines three possible element
* combinations for specifying optional logger+level settings.
* Must account for the following:
*
* <category name="x"> <priority value="y"/> </category> OR
* <category name="x"> <level value="y"/> </category> OR
* <logger name="x"> <level value="y"/> </logger>
*
* Noting also that the level/priority child element is optional too,
* and not the only possible child element.
*/
public synchronized Map<String,String> retrieveConfigFileLoggersLevels() throws LoggingFacadeException
{
try
{
Map<String,String> loggerLevelList = new HashMap<String,String>();
LOGGER.info("Getting logger levels from log4j configuration file");
Document doc = parseConfigFile(_filename);
List<Element> categoryOrLoggerElements = buildListOfCategoryOrLoggerElements(doc);
for (Element categoryOrLogger : categoryOrLoggerElements)
{
Element priorityOrLevelElement;
try
{
priorityOrLevelElement = getPriorityOrLevelElement(categoryOrLogger);
}
catch (LoggingFacadeException lfe)
{
//there is no exiting priority or level to view, move onto next category/logger
continue;
}
String categoryName = categoryOrLogger.getAttribute("name");
String priorityOrLevelValue = priorityOrLevelElement.getAttribute("value");
loggerLevelList.put(categoryName, priorityOrLevelValue);
}
return loggerLevelList;
}
catch (IOException e)
{
throw new LoggingFacadeException(e);
}
}
/**
* The log4j XML configuration file DTD defines 2 possible element
* combinations for specifying the optional root logger level settings
* Must account for the following:
*
* <root> <priority value="y"/> </root> OR
* <root> <level value="y"/> </root>
*
* Noting also that the level/priority child element is optional too,
* and not the only possible child element.
*/
public synchronized String retrieveConfigFileRootLoggerLevel() throws LoggingFacadeException
{
try
{
Document doc = parseConfigFile(_filename);
//retrieve the optional 'root' element node
NodeList rootElements = doc.getElementsByTagName("root");
if (rootElements.getLength() == 0)
{
//there is no root logger definition
return "N/A";
}
Element rootElement = (Element) rootElements.item(0);
Element levelElement = getPriorityOrLevelElement(rootElement);
if(levelElement != null)
{
return levelElement.getAttribute("value");
}
else
{
return "N/A";
}
}
catch (IOException e)
{
throw new LoggingFacadeException(e);
}
}
public synchronized void setConfigFileLoggerLevel(String logger, String level) throws LoggingFacadeException
{
LOGGER.info("Setting level to " + level + " for logger '" + logger
+ "' in log4j xml configuration file: " + _filename);
try
{
Document doc = parseConfigFile(_filename);
List<Element> logElements = buildListOfCategoryOrLoggerElements(doc);
//try to locate the specified logger/category in the elements retrieved
Element logElement = null;
for (Element e : logElements)
{
if (e.getAttribute("name").equals(logger))
{
logElement = e;
break;
}
}
if (logElement == null)
{
throw new LoggingFacadeException("Can't find logger " + logger);
}
Element levelElement = getPriorityOrLevelElement(logElement);
//update the element with the new level/priority
levelElement.setAttribute("value", level);
//output the new file
writeUpdatedConfigFile(_filename, doc);
}
catch (IOException ioe)
{
throw new LoggingFacadeException(ioe);
}
catch (TransformerConfigurationException e)
{
throw new LoggingFacadeException(e);
}
}
public synchronized void setConfigFileRootLoggerLevel(String level) throws LoggingFacadeException
{
try
{
LOGGER.info("Setting level to " + level + " for the Root logger in " +
"log4j xml configuration file: " + _filename);
Document doc = parseConfigFile(_filename);
//retrieve the optional 'root' element node
NodeList rootElements = doc.getElementsByTagName("root");
if (rootElements.getLength() == 0)
{
throw new LoggingFacadeException("Configuration contains no root element");
}
Element rootElement = (Element) rootElements.item(0);
Element levelElement = getPriorityOrLevelElement(rootElement);
//update the element with the new level/priority
levelElement.setAttribute("value", level);
//output the new file
writeUpdatedConfigFile(_filename, doc);
}
catch (IOException e)
{
throw new LoggingFacadeException(e);
}
catch (TransformerConfigurationException e)
{
throw new LoggingFacadeException(e);
}
}
public List<String> getAvailableLoggerLevels()
{
return new ArrayList<String>()
{{
add(Level.ALL.toString());
add(Level.TRACE.toString());
add(Level.DEBUG.toString());
add(Level.INFO.toString());
add(Level.WARN.toString());
add(Level.ERROR.toString());
add(Level.FATAL.toString());
add(Level.OFF.toString());
}};
}
public String retrieveRuntimeRootLoggerLevel()
{
Logger rootLogger = Logger.getRootLogger();
return rootLogger.getLevel().toString();
}
public void setRuntimeRootLoggerLevel(String level)
{
Level newLevel = Level.toLevel(level);
LOGGER.info("Setting RootLogger level to " + level);
Logger log = Logger.getRootLogger();
log.setLevel(newLevel);
}
public void setRuntimeLoggerLevel(String loggerName, String level) throws LoggingFacadeException
{
Level newLevel = level == null ? null : Level.toLevel(level);
Logger targetLogger = findRuntimeLogger(loggerName);
if(targetLogger == null)
{
throw new LoggingFacadeException("Can't find logger " + loggerName);
}
LOGGER.info("Setting level to " + newLevel + " for logger '" + targetLogger.getName() + "'");
targetLogger.setLevel(newLevel);
}
public Map<String,String> retrieveRuntimeLoggersLevels()
{
LOGGER.info("Getting levels for currently active log4j loggers");
Map<String, String> levels = new HashMap<String, String>();
@SuppressWarnings("unchecked")
Enumeration<Logger> loggers = LogManager.getCurrentLoggers();
while (loggers.hasMoreElements())
{
Logger logger = loggers.nextElement();
levels.put(logger.getName(), logger.getEffectiveLevel().toString());
}
return levels;
}
private void writeUpdatedConfigFile(String log4jConfigFileName, Document doc) throws IOException, TransformerConfigurationException
{
File log4jConfigFile = new File(log4jConfigFileName);
if (!log4jConfigFile.canWrite())
{
LOGGER.warn("Specified log4j XML configuration file is not writable: " + log4jConfigFile);
throw new IOException("Specified log4j XML configuration file is not writable");
}
Transformer transformer = null;
transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, "log4j.dtd");
DOMSource source = new DOMSource(doc);
File tmp;
Random r = new Random();
do
{
tmp = new File(log4jConfigFile.getAbsolutePath() + r.nextInt() + ".tmp");
}
while(tmp.exists());
tmp.deleteOnExit();
try
{
StreamResult result = new StreamResult(new FileOutputStream(tmp));
transformer.transform(source, result);
}
catch (TransformerException e)
{
LOGGER.warn("Could not transform the XML into new file: ", e);
throw new IOException("Could not transform the XML into new file: ", e);
}
// Swap temp file in to replace existing configuration file.
File old = new File(log4jConfigFile.getAbsoluteFile() + ".old");
if (old.exists())
{
old.delete();
}
if(!log4jConfigFile.renameTo(old))
{
//unable to rename the existing file to the backup name
LOGGER.error("Could not backup the existing log4j XML file");
throw new IOException("Could not backup the existing log4j XML file");
}
if(!tmp.renameTo(log4jConfigFile))
{
//failed to rename the new file to the required filename
if(!old.renameTo(log4jConfigFile))
{
//unable to return the backup to required filename
LOGGER.error("Could not rename the new log4j configuration file into place, and unable to restore original file");
throw new IOException("Could not rename the new log4j configuration file into place, and unable to restore original file");
}
LOGGER.error("Could not rename the new log4j configuration file into place");
throw new IOException("Could not rename the new log4j configuration file into place");
}
}
//method to parse the XML configuration file, validating it in the process, and returning a DOM Document of the content.
private static Document parseConfigFile(String fileName) throws IOException
{
//check file was specified, exists, and is readable
if(fileName == null)
{
LOGGER.warn("Provided log4j XML configuration filename is null");
throw new IOException("Provided log4j XML configuration filename is null");
}
File configFile = new File(fileName);
if (!configFile.exists())
{
LOGGER.warn("The log4j XML configuration file could not be found: " + fileName);
throw new IOException("The log4j XML configuration file could not be found");
}
else if (!configFile.canRead())
{
LOGGER.warn("The log4j XML configuration file is not readable: " + fileName);
throw new IOException("The log4j XML configuration file is not readable");
}
//parse it
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder;
Document doc;
ErrorHandler errHandler = new QpidLog4JSaxErrorHandler();
try
{
docFactory.setValidating(true);
docBuilder = docFactory.newDocumentBuilder();
docBuilder.setErrorHandler(errHandler);
docBuilder.setEntityResolver(new Log4jEntityResolver());
doc = docBuilder.parse(fileName);
}
catch (ParserConfigurationException e)
{
LOGGER.warn("Unable to parse the log4j XML file due to possible configuration error: ", e);
throw new IOException("Unable to parse the log4j XML file due to possible configuration error: ", e);
}
catch (SAXException e)
{
LOGGER.warn("The specified log4j XML file is invalid: ", e);
throw new IOException("The specified log4j XML file is invalid: ", e);
}
catch (IOException e)
{
LOGGER.warn("Unable to parse the specified log4j XML file", e);
throw new IOException("Unable to parse the specified log4j XML file: ", e);
}
return doc;
}
private Logger findRuntimeLogger(String loggerName)
{
Logger targetLogger = null;
@SuppressWarnings("unchecked")
Enumeration<Logger> loggers = LogManager.getCurrentLoggers();
while(loggers.hasMoreElements())
{
targetLogger = loggers.nextElement();
if (targetLogger.getName().equals(loggerName))
{
return targetLogger;
}
}
return null;
}
private List<Element> buildListOfCategoryOrLoggerElements(Document doc)
{
//retrieve the 'category' and 'logger' element nodes
NodeList categoryElements = doc.getElementsByTagName("category");
NodeList loggerElements = doc.getElementsByTagName("logger");
//collect them into a single elements list
List<Element> logElements = new ArrayList<Element>();
for (int i = 0; i < categoryElements.getLength(); i++)
{
logElements.add((Element) categoryElements.item(i));
}
for (int i = 0; i < loggerElements.getLength(); i++)
{
logElements.add((Element) loggerElements.item(i));
}
return logElements;
}
private Element getPriorityOrLevelElement(Element categoryOrLogger) throws LoggingFacadeException
{
//retrieve the optional 'priority' or 'level' sub-element value.
//It may not be the only child node, so request by tag name.
NodeList priorityElements = categoryOrLogger.getElementsByTagName("priority");
NodeList levelElements = categoryOrLogger.getElementsByTagName("level");
Element levelElement = null;
if (priorityElements.getLength() != 0)
{
levelElement = (Element) priorityElements.item(0);
}
else if (levelElements.getLength() != 0)
{
levelElement = (Element) levelElements.item(0);
}
else
{
throw new LoggingFacadeException("Configuration " + categoryOrLogger.getNodeName()
+ " element contains neither priority nor level child");
}
return levelElement;
}
private static class QpidLog4JSaxErrorHandler implements ErrorHandler
{
public void error(SAXParseException e) throws SAXException
{
if(LOGGER != null)
{
LOGGER.warn(constructMessage("Error parsing XML file", e));
}
else
{
System.err.println(constructMessage("Error parsing XML file", e));
}
}
public void fatalError(SAXParseException e) throws SAXException
{
throw new SAXException(constructMessage("Fatal error parsing XML file", e));
}
public void warning(SAXParseException e) throws SAXException
{
if(LOGGER != null)
{
LOGGER.warn(constructMessage("Warning parsing XML file", e));
}
else
{
System.err.println(constructMessage("Warning parsing XML file", e));
}
}
private static String constructMessage(final String msg, final SAXParseException ex)
{
return msg + ": Line " + ex.getLineNumber()+" column " +ex.getColumnNumber() + ": " + ex.getMessage();
}
}
}