/* * 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.management; import static org.apache.log4j.xml.QpidLog4JConfigurator.LOCK; import java.io.File; 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; import org.apache.qpid.management.common.mbeans.LoggingManagement; import org.apache.qpid.management.common.mbeans.annotations.MBeanDescription; import org.apache.qpid.server.management.AMQManagedObject; import org.apache.qpid.util.FileUtils; import org.apache.log4j.Level; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.apache.log4j.xml.Log4jEntityResolver; import org.apache.log4j.xml.QpidLog4JConfigurator; import org.apache.log4j.xml.QpidLog4JConfigurator.QpidLog4JSaxErrorHandler; import org.apache.log4j.xml.QpidLog4JConfigurator.IllegalLoggerLevelException; 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.management.JMException; import javax.management.openmbean.CompositeData; import javax.management.openmbean.CompositeDataSupport; import javax.management.openmbean.CompositeType; import javax.management.openmbean.OpenDataException; import javax.management.openmbean.OpenType; import javax.management.openmbean.SimpleType; import javax.management.openmbean.TabularData; import javax.management.openmbean.TabularDataSupport; import javax.management.openmbean.TabularType; 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.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; /** MBean class for BrokerLoggingManagerMBean. It implements all the management features exposed for managing logging. */ @MBeanDescription("Logging Management Interface") public class LoggingManagementMBean extends AMQManagedObject implements LoggingManagement { private static final Logger _logger = Logger.getLogger(LoggingManagementMBean.class); private String _log4jConfigFileName; private int _log4jLogWatchInterval; private static final String INHERITED = "INHERITED"; private static final String[] LEVELS = new String[]{Level.ALL.toString(), Level.TRACE.toString(), Level.DEBUG.toString(), Level.INFO.toString(), Level.WARN.toString(), Level.ERROR.toString(), Level.FATAL.toString(),Level.OFF.toString(), INHERITED}; static TabularType _loggerLevelTabularType; static CompositeType _loggerLevelCompositeType; static { try { OpenType[] loggerLevelItemTypes = new OpenType[]{SimpleType.STRING, SimpleType.STRING}; _loggerLevelCompositeType = new CompositeType("LoggerLevelList", "Logger Level Data", COMPOSITE_ITEM_NAMES.toArray(new String[COMPOSITE_ITEM_NAMES.size()]), COMPOSITE_ITEM_DESCRIPTIONS.toArray(new String[COMPOSITE_ITEM_DESCRIPTIONS.size()]), loggerLevelItemTypes); _loggerLevelTabularType = new TabularType("LoggerLevel", "List of loggers with levels", _loggerLevelCompositeType, TABULAR_UNIQUE_INDEX.toArray(new String[TABULAR_UNIQUE_INDEX.size()])); } catch (OpenDataException e) { _logger.error("Tabular data setup for viewing logger levels was incorrect."); _loggerLevelTabularType = null; } } public LoggingManagementMBean(String log4jConfigFileName, int log4jLogWatchInterval) throws JMException { super(LoggingManagement.class, LoggingManagement.TYPE); _log4jConfigFileName = log4jConfigFileName; _log4jLogWatchInterval = log4jLogWatchInterval; } public String getObjectInstanceName() { return LoggingManagement.TYPE; } public Integer getLog4jLogWatchInterval() { return _log4jLogWatchInterval; } public String[] getAvailableLoggerLevels() { return LEVELS; } @SuppressWarnings("unchecked") public synchronized boolean setRuntimeLoggerLevel(String logger, String level) { //check specified level is valid Level newLevel; try { newLevel = getLevel(level); } catch (Exception e) { return false; } //check specified logger exists Enumeration loggers = LogManager.getCurrentLoggers(); Boolean loggerExists = false; while(loggers.hasMoreElements()) { Logger log = (Logger) loggers.nextElement(); if (log.getName().equals(logger)) { loggerExists = true; break; } } if(!loggerExists) { return false; } //set the logger to the new level _logger.info("Setting level to " + level + " for logger: " + logger); Logger log = Logger.getLogger(logger); log.setLevel(newLevel); return true; } @SuppressWarnings("unchecked") public synchronized TabularData viewEffectiveRuntimeLoggerLevels() { if (_loggerLevelTabularType == null) { _logger.warn("TabluarData type not set up correctly"); return null; } _logger.info("Getting levels for currently active log4j loggers"); Enumeration loggers = LogManager.getCurrentLoggers(); TabularData loggerLevelList = new TabularDataSupport(_loggerLevelTabularType); Logger logger; String loggerName; String level; try { while(loggers.hasMoreElements()){ logger = (Logger) loggers.nextElement(); loggerName = logger.getName(); level = logger.getEffectiveLevel().toString(); Object[] itemData = {loggerName, level}; CompositeData loggerData = new CompositeDataSupport(_loggerLevelCompositeType, COMPOSITE_ITEM_NAMES.toArray(new String[COMPOSITE_ITEM_NAMES.size()]), itemData); loggerLevelList.put(loggerData); } } catch (OpenDataException e) { _logger.warn("Unable to create logger level list due to :" + e); return null; } return loggerLevelList; } public synchronized String getRuntimeRootLoggerLevel() { Logger rootLogger = Logger.getRootLogger(); return rootLogger.getLevel().toString(); } public synchronized boolean setRuntimeRootLoggerLevel(String level) { Level newLevel; try { newLevel = getLevel(level); } catch (Exception e) { return false; } if(newLevel == null) { //A null Level reference implies inheritance. Setting the runtime RootLogger //to null is catastrophic (and prevented by Log4J at startup and runtime anyway). return false; } _logger.info("Setting RootLogger level to " + level); Logger log = Logger.getRootLogger(); log.setLevel(newLevel); return true; } //method to convert from a string to a log4j Level, throws exception if the given value is invalid private Level getLevel(String level) throws Exception { if("null".equalsIgnoreCase(level) || INHERITED.equalsIgnoreCase(level)) { //the string "null" or "inherited" signals to inherit from a parent logger, //using a null Level reference for the logger. return null; } Level newLevel = Level.toLevel(level); //above Level.toLevel call returns a DEBUG Level if the request fails. Check the result. if (newLevel.equals(Level.DEBUG) && !(level.equalsIgnoreCase("debug"))) { //received DEBUG but we did not ask for it, the Level request failed. throw new Exception("Invalid level name"); } return newLevel; } //method to parse the XML configuration file, validating it in the process, and returning a DOM Document of the content. private static synchronized Document parseConfigFile(String fileName) throws IOException { try { LOCK.lock(); //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); //recommended that MBeans should use java.* and javax.* exceptions only throw new IOException("Unable to parse the log4j XML file due to possible configuration error: " + e.getMessage()); } catch (SAXException e) { _logger.warn("The specified log4j XML file is invalid: " + e); //recommended that MBeans should use standard java.* and javax.* exceptions only throw new IOException("The specified log4j XML file is invalid: " + e.getMessage()); } 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.getMessage()); } return doc; } finally { LOCK.unlock(); } } private static synchronized boolean writeUpdatedConfigFile(String log4jConfigFileName, Document doc) throws IOException { try { LOCK.lock(); 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; try { transformer = TransformerFactory.newInstance().newTransformer(); } catch (Exception e) { _logger.warn("Could not create an XML transformer: " +e); return false; } 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.getPath() + r.nextInt() + ".tmp"); } while(tmp.exists()); tmp.deleteOnExit(); try { StreamResult result = new StreamResult(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"); } return true; } finally { LOCK.unlock(); } } /* The log4j XML configuration file DTD defines three possible element * combinations for specifying optional logger+level settings. * Must account for the following: * * OR * OR * * * Noting also that the level/priority child element is optional too, * and not the only possible child element. */ public static synchronized Map retrieveConfigFileLoggersLevels(String fileName) throws IOException { try { LOCK.lock(); Document doc = parseConfigFile(fileName); HashMap loggerLevelList = new HashMap(); //retrieve the 'category' element nodes NodeList categoryElements = doc.getElementsByTagName("category"); String categoryName; String priority = null; for (int i = 0; i < categoryElements.getLength(); i++) { Element categoryElement = (Element) categoryElements.item(i); categoryName = categoryElement.getAttribute("name"); //retrieve the category's mandatory 'priority' or 'level' element's value. //It may not be the only child node, so request by tag name. NodeList priorityElements = categoryElement.getElementsByTagName("priority"); NodeList levelElements = categoryElement.getElementsByTagName("level"); if (priorityElements.getLength() != 0) { Element priorityElement = (Element) priorityElements.item(0); priority = priorityElement.getAttribute("value"); } else if (levelElements.getLength() != 0) { Element levelElement = (Element) levelElements.item(0); priority = levelElement.getAttribute("value"); } else { //there is no exiting priority or level to view, move onto next category/logger continue; } loggerLevelList.put(categoryName, priority); } //retrieve the 'logger' element nodes NodeList loggerElements = doc.getElementsByTagName("logger"); String loggerName; String level; for (int i = 0; i < loggerElements.getLength(); i++) { Element loggerElement = (Element) loggerElements.item(i); loggerName = loggerElement.getAttribute("name"); //retrieve the logger's mandatory 'level' element's value //It may not be the only child node, so request by tag name. NodeList levelElements = loggerElement.getElementsByTagName("level"); Element levelElement = (Element) levelElements.item(0); level = levelElement.getAttribute("value"); loggerLevelList.put(loggerName, level); } return loggerLevelList; } finally { LOCK.unlock(); } } public synchronized TabularData viewConfigFileLoggerLevels() throws IOException { try { LOCK.lock(); if (_loggerLevelTabularType == null) { _logger.warn("TabluarData type not set up correctly"); return null; } _logger.info("Getting logger levels from log4j configuration file"); TabularData loggerLevelList = new TabularDataSupport(_loggerLevelTabularType); Map levels = retrieveConfigFileLoggersLevels(_log4jConfigFileName); for (Map.Entry entry : levels.entrySet()) { String loggerName = entry.getKey(); String level = entry.getValue(); try { Object[] itemData = {loggerName, level.toUpperCase()}; CompositeData loggerData = new CompositeDataSupport(_loggerLevelCompositeType, COMPOSITE_ITEM_NAMES.toArray(new String[COMPOSITE_ITEM_NAMES.size()]), itemData); loggerLevelList.put(loggerData); } catch (OpenDataException e) { _logger.warn("Unable to create logger level list due to :" + e); return null; } } return loggerLevelList; } finally { LOCK.unlock(); } } public synchronized boolean setConfigFileLoggerLevel(String logger, String level) throws IOException { try { LOCK.lock(); //check that the specified level is a valid log4j Level try { getLevel(level); } catch (Exception e) { //it isnt a valid level return false; } _logger.info("Setting level to " + level + " for logger '" + logger + "' in log4j xml configuration file: " + _log4jConfigFileName); Document doc = parseConfigFile(_log4jConfigFileName); //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 logElements = new ArrayList(); 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)); } //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) { //no loggers/categories with given name found, does not exist to update _logger.warn("Specified logger does not exist in the configuration file: " +logger); return false; } //retrieve the optional 'priority' or 'level' sub-element value. //It may not be the only child node, so request by tag name. NodeList priorityElements = logElement.getElementsByTagName("priority"); NodeList levelElements = logElement.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 { //there is no exiting priority or level element to update return false; } //update the element with the new level/priority levelElement.setAttribute("value", level.toLowerCase()); //output the new file return writeUpdatedConfigFile(_log4jConfigFileName, doc); } finally { LOCK.unlock(); } } /* The log4j XML configuration file DTD defines 2 possible element * combinations for specifying the optional root logger level settings * Must account for the following: * * OR * * * Noting also that the level/priority child element is optional too, * and not the only possible child element. */ public static synchronized String retrieveConfigFileRootLoggerLevel(String fileName) throws IOException { try { LOCK.lock(); 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); //retrieve the optional 'priority' or 'level' element value. //It may not be the only child node, so request by tag name. NodeList priorityElements = rootElement.getElementsByTagName("priority"); NodeList levelElements = rootElement.getElementsByTagName("level"); String priority = null; if (priorityElements.getLength() != 0) { Element priorityElement = (Element) priorityElements.item(0); priority = priorityElement.getAttribute("value"); } else if(levelElements.getLength() != 0) { Element levelElement = (Element) levelElements.item(0); priority = levelElement.getAttribute("value"); } if(priority != null) { return priority; } else { return "N/A"; } } finally { LOCK.unlock(); } } public synchronized String getConfigFileRootLoggerLevel() throws IOException { return retrieveConfigFileRootLoggerLevel(_log4jConfigFileName).toUpperCase(); } public synchronized boolean setConfigFileRootLoggerLevel(String level) throws IOException { try { LOCK.lock(); //check that the specified level is a valid log4j Level try { Level newLevel = getLevel(level); if(newLevel == null) { //A null Level reference implies inheritance. Setting the config file RootLogger //to "null" or "inherited" just ensures it defaults to DEBUG at startup as Log4J //prevents this catastrophic situation at startup and runtime anyway. return false; } } catch (Exception e) { //it isnt a valid level return false; } _logger.info("Setting level to " + level + " for the Root logger in " + "log4j xml configuration file: " + _log4jConfigFileName); Document doc = parseConfigFile(_log4jConfigFileName); //retrieve the optional 'root' element node NodeList rootElements = doc.getElementsByTagName("root"); if (rootElements.getLength() == 0) { return false; } Element rootElement = (Element) rootElements.item(0); //retrieve the optional 'priority' or 'level' sub-element value. //It may not be the only child node, so request by tag name. NodeList priorityElements = rootElement.getElementsByTagName("priority"); NodeList levelElements = rootElement.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 { //there is no exiting priority/level to update return false; } //update the element with the new level/priority levelElement.setAttribute("value", level); //output the new file return writeUpdatedConfigFile(_log4jConfigFileName, doc); } finally { LOCK.unlock(); } } public synchronized void reloadConfigFile() throws IOException { try { LOCK.lock(); QpidLog4JConfigurator.configure(_log4jConfigFileName); _logger.info("Applied log4j configuration from: " + _log4jConfigFileName); } catch (IllegalLoggerLevelException e) { _logger.warn("The log4j configuration reload request was aborted: " + e); //recommended that MBeans should use standard java.* and javax.* exceptions only throw new IOException("The log4j configuration reload request was aborted: " + e.getMessage()); } catch (ParserConfigurationException e) { _logger.warn("The log4j configuration reload request was aborted: " + e); throw new IOException("The log4j configuration reload request was aborted: " + e.getMessage()); } catch (SAXException e) { _logger.warn("The log4j configuration reload request was aborted: " + e); //recommended that MBeans should use standard java.* and javax.* exceptions only throw new IOException("The log4j configuration reload request was aborted: " + e.getMessage()); } catch (IOException e) { _logger.warn("The log4j configuration reload request was aborted: " + e); throw new IOException("The log4j configuration reload request was aborted: " + e.getMessage()); } finally { LOCK.unlock(); } } }