| /* |
| * |
| * 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.log4j.xml; |
| |
| import org.apache.log4j.Level; |
| import org.apache.log4j.Logger; |
| import org.xml.sax.ErrorHandler; |
| import org.xml.sax.SAXException; |
| import org.xml.sax.SAXParseException; |
| |
| import org.apache.qpid.server.logging.management.LoggingManagementMBean; |
| |
| import javax.xml.parsers.DocumentBuilder; |
| import javax.xml.parsers.DocumentBuilderFactory; |
| import javax.xml.parsers.ParserConfigurationException; |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.Map; |
| import java.util.concurrent.locks.ReentrantLock; |
| |
| /** |
| * Substitute for the Log4J XMLWatchdog (as used by DOMConfigurator.configureAndWatch) |
| * |
| * Extends the default behaviour with a strict parser check on the XML file before allowing the reconfiguration to proceed, |
| * ensuring that any parser error or warning prevents initiation of a configuration update by Log4J, which aborts mid-update |
| * upon fatal errors from the parser and proceeds in the event of 'regular' parser errors and warnings, in all cases allowing |
| * startup to proceed with whatever half-baked configuration then exists. |
| */ |
| public class QpidLog4JConfigurator |
| { |
| //lock to protect access to the configuration file |
| //shared with LoggingManagementMBean |
| public static final ReentrantLock LOCK = new ReentrantLock(); |
| private static Logger _logger; |
| private static DOMConfigurator domConfig = new DOMConfigurator(); |
| |
| private QpidLog4JConfigurator() |
| { |
| //no instances |
| } |
| |
| public static void configure(String filename) throws IOException, ParserConfigurationException, |
| SAXException, IllegalLoggerLevelException |
| { |
| try |
| { |
| LOCK.lock(); |
| |
| parseXMLConfigFile(filename); |
| checkLoggerLevels(filename); |
| |
| DOMConfigurator.configure(filename); |
| |
| if(_logger == null) |
| { |
| _logger = Logger.getLogger(QpidLog4JConfigurator.class); |
| } |
| } |
| finally |
| { |
| LOCK.unlock(); |
| } |
| } |
| |
| public static void configureAndWatch(String filename, long delay) throws IOException, ParserConfigurationException, |
| SAXException, IllegalLoggerLevelException |
| { |
| parseXMLConfigFile(filename); |
| checkLoggerLevels(filename); |
| |
| QpidLog4JXMLWatchdog watchdog = new QpidLog4JXMLWatchdog(filename); |
| watchdog.setDelay(delay); |
| watchdog.start(); |
| } |
| |
| private static void parseXMLConfigFile(String fileName) throws IOException, SAXException, |
| ParserConfigurationException |
| { |
| try |
| { |
| LOCK.lock(); |
| |
| //check file was specified, exists, and is readable |
| if(fileName == null) |
| { |
| throw new IOException("Provided log4j XML configuration filename was null"); |
| } |
| |
| File configFile = new File(fileName); |
| |
| if (!configFile.exists()) |
| { |
| throw new IOException("The log4j XML configuration file does not exist: " + fileName); |
| } |
| else if (!configFile.canRead()) |
| { |
| throw new IOException("The log4j XML configuration file is not readable: " + fileName); |
| } |
| |
| //parse it |
| DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); |
| DocumentBuilder docBuilder; |
| |
| ErrorHandler errHandler = new QpidLog4JSaxErrorHandler(); |
| |
| docFactory.setValidating(true); |
| docBuilder = docFactory.newDocumentBuilder(); |
| docBuilder.setErrorHandler(errHandler); |
| docBuilder.setEntityResolver(new Log4jEntityResolver()); |
| docBuilder.parse(fileName); |
| } |
| finally |
| { |
| LOCK.unlock(); |
| } |
| } |
| |
| public 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(); |
| } |
| } |
| |
| private static class QpidLog4JXMLWatchdog extends XMLWatchdog |
| { |
| public QpidLog4JXMLWatchdog(String filename) |
| { |
| super(filename); |
| } |
| |
| public void doOnChange() |
| { |
| try |
| { |
| LOCK.lock(); |
| |
| try |
| { |
| parseXMLConfigFile(filename); |
| } |
| catch (Exception e) |
| { |
| //logger will be instantiated following first configuration success, which has been pre-validated |
| //and so the null check should never actually be required. |
| if(_logger != null) |
| { |
| _logger.warn("Parsing the log4j XML configuration file generated errors/warnings. " + |
| "The new configuration was not applied. Correct the issues to prompt " + |
| "another update attempt: " + e.getMessage()); |
| } |
| return; |
| } |
| |
| try |
| { |
| checkLoggerLevels(filename); |
| } |
| catch (Exception e) |
| { |
| //logger will be instantiated following first configuration success, which has been pre-validated |
| //and so the null check should never actually be required. |
| if(_logger != null) |
| { |
| _logger.warn("Errors were found when validating the logger level values in the " + |
| "log4j XML configuration file. The new configuration was not applied. " + |
| "Correct the issues to prompt another update attempt: " + e.getMessage()); |
| } |
| return; |
| } |
| |
| //everything checked was ok, let the normal update process proceed |
| super.doOnChange(); |
| |
| //a configuration has now been applied, enable logging for future attempts |
| if(_logger == null) |
| { |
| _logger = Logger.getLogger(QpidLog4JConfigurator.class); |
| } |
| |
| _logger.info("Applied log4j configuration from: " + filename); |
| } |
| finally |
| { |
| LOCK.unlock(); |
| } |
| |
| } |
| } |
| |
| protected static void checkLoggerLevels(String filename) throws IllegalLoggerLevelException, IOException |
| { |
| //check that the logger levels specified in the XML are actually valid |
| |
| try |
| { |
| LOCK.lock(); |
| |
| //get the Logger levels to check |
| Map<String, String> loggersLevels; |
| loggersLevels = LoggingManagementMBean.retrieveConfigFileLoggersLevels(filename); |
| //add the RootLogger to the list too |
| String rootLoggerlevelString = LoggingManagementMBean.retrieveConfigFileRootLoggerLevel(filename); |
| loggersLevels.put("Root", rootLoggerlevelString); |
| |
| |
| for (Map.Entry<String, String> entry : loggersLevels.entrySet()) |
| { |
| String loggerName = entry.getKey(); |
| String levelString = entry.getValue(); |
| |
| //let log4j replace any properties in the string |
| String log4jConfiguredString = domConfig.subst(levelString); |
| |
| if(log4jConfiguredString.equals("") && ! log4jConfiguredString.equals(levelString)) |
| { |
| //log4j has returned an empty string but this isnt what we gave it. |
| //There may have been an undefined property. Unlike an incorrect |
| //literal value, we will allow this case to proceed, but warn users. |
| |
| if(_logger != null) |
| { |
| _logger.warn("Unable to detect Level value from '" + levelString |
| +"' for logger '" + loggerName + "', Log4J will default this to DEBUG"); |
| } |
| else |
| { |
| System.err.println("Unable to detect Level value from '" + levelString |
| +"' for logger " + loggerName + ", Log4J will default this to DEBUG"); |
| } |
| |
| continue; |
| } |
| |
| checkLevel(loggerName,log4jConfiguredString); |
| } |
| } |
| finally |
| { |
| LOCK.unlock(); |
| } |
| } |
| |
| private static void checkLevel(String loggerName, String levelString) throws IllegalLoggerLevelException |
| { |
| if("null".equalsIgnoreCase(levelString) || "inherited".equalsIgnoreCase(levelString)) |
| { |
| //the string "null" signals to inherit from a parent logger |
| return; |
| } |
| |
| Level level = Level.toLevel(levelString); |
| |
| //above Level.toLevel call returns a DEBUG Level if the request fails. Check the result. |
| if (level.equals(Level.DEBUG) && !(levelString.equalsIgnoreCase("debug"))) |
| { |
| //received DEBUG but we did not ask for it, the Level request failed. |
| throw new IllegalLoggerLevelException("Level '" + levelString + "' specified for Logger '" + loggerName + "' is invalid"); |
| } |
| } |
| |
| public static class IllegalLoggerLevelException extends Exception |
| { |
| private static final long serialVersionUID = 1L; |
| |
| public IllegalLoggerLevelException(String msg) |
| { |
| super(msg); |
| } |
| } |
| } |
| |