/*
* ALMA - Atacama Large Millimiter Array
* (c) European Southern Observatory, 2004
* Copyright by ESO (in the framework of the ALMA collaboration),
* All rights reserved
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston,
* MA 02111-1307 USA
*/
package alma.acs.logging.config;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
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;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.exolab.castor.core.exceptions.CastorException;
import org.exolab.castor.xml.Unmarshaller;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import com.cosylab.CDB.DAL;
import com.cosylab.CDB.DALOperations;
import alma.Logging.LoggingConfigurable;
import alma.ACSErrTypeCommon.wrappers.AcsJIllegalArgumentEx;
import alma.acs.logging.AcsLogLevel;
import alma.acs.logging.level.AcsLogLevelDefinition;
import alma.acs.util.IsoDateFormat;
import alma.cdbErrType.CDBRecordDoesNotExistEx;
import alma.cdbErrType.CDBXMLErrorEx;
import alma.maci.loggingconfig.LoggingConfig;
import alma.maci.loggingconfig.NamedLogger;
import alma.maci.loggingconfig.UnnamedLogger;
import alma.maci.loggingconfig.types.LogLevel;
/**
* Class that encapsulates all configuration sources (defaults, properties, CDB) for Java logging,
* and supports runtime updates of logging configuration based on these sources.
* This class can be used by a process such as the Java container or the manager,
* in order to configure all its loggers (e.g. container logger, component loggers, ORB logger).
* @author hsommer
*/
public class LogConfig {
// for the often-used min levels we offer properties, to avoid having an otherwise not needed CDB configuration
public final static String PROPERTYNAME_MIN_LOG_LEVEL_LOCAL = "ACS.logstdout";
public final static String PROPERTYNAME_MIN_LOG_LEVEL = "ACS.log.minlevel.remote"; // from env var ACS_LOG_CENTRAL
/**
* Using this property, we can configure named loggers for processes other than containers and manager,
* for which there is no logging configuration in the CDB.
* It is not expected to be used widely, which excuses the not so intuitive stuffing of data into a single property.
* <p>
* Example: <pre>-DACS.log.minlevel.namedloggers="hibernateSQL@CDB-RDB=2,3:hibernate@CDB-RDB=5,5"</pre>
* configures two loggers: "hibernateSQL@CDB-RDB" will use local log level 2 and remote level 3,
* while "hibernate@CDB-RDB" will use local and remote level 5.
*/
public final static String PROPERTYNAME_NAMED_LOGGER_LEVELS = "ACS.log.minlevel.namedloggers";
/**
* In CDB queries for the <code>LoggingConfig</code> child of a container or manager configuration
* we need the "LoggingConfig" string.
* <p>
* For type safety we get that name from the schema-generated castor class.
*/
final static String CDBNAME_LoggingConfig = LoggingConfig.class.getSimpleName();
/**
* In CDB queries for the <code>ComponentLogger</code> child of a component configuration
* we need the "LoggingConfig" string.
* <p>
* In the schema <code>Components.xsd</code> we are interested in the grand-child element
* <code>ComponentLogger</code>. The generated Java class corresponds to the type
* <code>UnnamedLogger</code> though.
* The element name is only present in the method name
* <code>alma.maci.componentconfig.components.ComponentInfo#getComponentLogger</code>
* which cannot be used to define a constant. Therefore we hardcode the name here.
*/
final static String CDBNAME_ComponentLogger = "ComponentLogger";
/**
* The logger for messages logged by this class.
* Note that this field will be null at first, until the Logger gets provided,
* so better use {@link #log(Level, String, Throwable)} for safe access.
*/
private Logger logger;
/**
* The ACS CDB. This value is null unless it gets set in {@link #setCDB(DAL)}.
*/
private DALOperations cdb;
/**
* Path in the CDB to the element (container, manager etc) that contains a <code>LoggingConfig</code> child.
* From here we get process-wide settings, default log levels, and optional named logger levels.
* Note that named logger levels found here override those in {@link #cdbComponentPaths} in case of conflict.
*/
private String cdbLoggingConfigPath;
/**
* Paths in the CDB to the element that contains component configuration with child elements of type <code>UnnamedLogger</code>.
* key = component logger name, value = path in the CDB to the Component configuration.
* Note that log levels given in the component configuration get overridden by named logger levels
* found in the container configuration, in case of conflicts.
* <p>
* This only applies to container log config, since for example the manager logging config does not deal with components.
*/
private Map<String, String> cdbComponentPaths;
/**
* The schema-generated logging config, either with default values, or read from the CDB.
* The log levels can also be changed by properties (env var peers) and dynamically.
*/
private LoggingConfig loggingConfig;
/**
* Logger names and optionally associated log levels.
* Loggers that use default levels are stored with their name (map key),
* but a null value for the configuration is associated with them.
* <p>
* Actual named logger configurations are stored as the map values, and can come from
* <ul>
* <li>properties {@link #PROPERTYNAME_NAMED_LOGGER_LEVELS_LOCAL} or {@link #PROPERTYNAME_NAMED_LOGGER_LEVELS_REMOTE},
* <li>optional named logger children in the process configuration ({@link #loggingConfig}),
* which are of subclass {@link NamedLogger}, or
* <li>optional log config children of CDB component configuration, or
* <li>dynamically set values that can create or override the above, see {@link #setNamedLoggerConfig(String, UnnamedLogger)}.
* </ul>
*
* key = [String] logger name, value = [LockableUnnamedLogger or null] level config
*/
private final Map<String, LockableUnnamedLogger> namedLoggerConfigs;
/**
* Subscribers get notified of logging config changes
*/
private final List<LogConfigSubscriber> subscriberList;
public LogConfig() {
cdbComponentPaths = new HashMap<String, String>();
namedLoggerConfigs = new HashMap<String, LockableUnnamedLogger>();
subscriberList = new ArrayList<LogConfigSubscriber>();
loggingConfig = new LoggingConfig(); // comes with schema default values
configureDefaultLevelsFromProperties();
}
/**
* Reads the properties <code>ACS.logstdout</code> (name defined as {@link #PROPERTYNAME_MIN_LOG_LEVEL}) and
* <code>ACS.log.minlevel.remote</code> (name defined as {@link #PROPERTYNAME_MIN_LOG_LEVEL_LOCAL}) and, if the
* property is defined, sets the respective default level. Prior values are lost.
*/
private void configureDefaultLevelsFromProperties() {
// env vars / properties can override the schema defaults
Integer minLevelLocalValue = Integer.getInteger(PROPERTYNAME_MIN_LOG_LEVEL_LOCAL);
if (minLevelLocalValue != null) {
try {
loggingConfig.setMinLogLevelLocal(LogLevel.valueOf(minLevelLocalValue.toString()));
} catch (IllegalArgumentException ex) {
log(Level.INFO, "failed to pick up default stdout log level from property " + PROPERTYNAME_MIN_LOG_LEVEL_LOCAL, ex);
}
}
Integer minLevelValue = Integer.getInteger(PROPERTYNAME_MIN_LOG_LEVEL);
if (minLevelValue != null) {
try {
loggingConfig.setMinLogLevel(LogLevel.valueOf(minLevelValue.toString()));
} catch (IllegalArgumentException ex) {
log(Level.INFO, "failed to pick up default remote log level from property " + PROPERTYNAME_MIN_LOG_LEVEL, ex);
}
}
}
/**
* Reads the property <code>ACS.log.minlevel.namedloggers</code> (name defined as {@link #PROPERTYNAME_NAMED_LOGGER_LEVELS})
* and, if the property is defined, sets the respective named logger levels. Prior values are lost.
*/
private void configureNamedLoggerLevelsFromProperties() {
String propVal = System.getProperty(PROPERTYNAME_NAMED_LOGGER_LEVELS);
if (propVal != null) {
try {
for (String levelDef : propVal.split(":")) {
String[] levelDefSplit = levelDef.split("=");
try {
String loggerName = levelDefSplit[0].trim();
String[] levels = levelDefSplit[1].split(",");
UnnamedLogger loggerConfig = new UnnamedLogger();
loggerConfig.setMinLogLevelLocal(AcsLogLevelDefinition.xsdLevelFromInteger(Integer.parseInt(levels[0])));
loggerConfig.setMinLogLevel(AcsLogLevelDefinition.xsdLevelFromInteger(Integer.parseInt(levels[1])));
storeNamedLoggerConfig(loggerName, new LockableUnnamedLogger(loggerConfig));
log(Level.INFO, "Set named logger levels from property. Name=" + loggerName +
" local=" + loggerConfig.getMinLogLevelLocal() + " remote=" + loggerConfig.getMinLogLevel(), null);
} catch (Exception ex) {
log(Level.WARNING, "Failed to process named logger level definition '" + levelDef +
"' given in property '" + PROPERTYNAME_NAMED_LOGGER_LEVELS + "'. ", ex);
}
}
} catch (Exception ex) {
log(Level.WARNING, "Failed to process named loggers from property " + PROPERTYNAME_NAMED_LOGGER_LEVELS, ex);
}
}
else {
log(Level.FINEST, PROPERTYNAME_NAMED_LOGGER_LEVELS + " not defined.", null);
}
}
/**
* Sets the reference to the CDB, which will then be used for configuration.
* Before this method is called, default values are used instead of CDB
* values.
* <p>
* Note that the reference is only set, but the CDB is not read
* automatically; for this, call {@link #initialize(boolean)}.
*
* @param cdb a reference to the CDB. Will be ignored if == null.
*/
public void setCDB(DALOperations cdb) {
if (cdb != null) {
this.cdb = cdb;
} else {
log(Level.FINE, "Ignoring call to setCDB(null)", null);
}
}
/**
* Sets the path to the CDB's node that is <em>parent of</em> the
* <LoggingConfig> node, such as a container or the manager node.
* This call does not access the CDB.
*
* @param path for example "MACI/Containers/frodoContainer"
*/
public void setCDBLoggingConfigPath(String path) {
cdbLoggingConfigPath = path;
}
/**
* Sets the path to the CDB's node that is <em>parent of</em> the
* <log:UnnamedLogger/> component logger node.
* This call does not access the CDB.
*
* @param compLoggerName the component logger name, e.g. <code>MOUNT1</code>.
* @param path e.g. <code>MACI/Components</code>.
*/
public void setCDBComponentPath(String compLoggerName, String path) {
cdbComponentPaths.put(compLoggerName, path);
}
/**
* Initializes the values based on CDB settings, logging properties, etc. All subscribing classes are notified of
* the new configuration, see {@link LogConfigSubscriber#configureLogging(LogConfig)}.
* <p>
* This method can be called more than once: if some settings have changed, should be read in, and the logging
* classes should be notified of these changes. For example, the container could call this method when it gets
* notified that the logging configuration in the CDB has been changed at runtime.
*
* @param cdbBeatsProperties
* if true then the default logger level values from the CDB override the properties (env vars).
* <code>True</code> is foreseen for dynamic updates from the CDB, whereas for the initial
* configuration it should be a <code>false</code>.
*
* @throws LogConfigException
* if reading the configuration data failed and thus default values were used, or if there were problems
* during configuration even though some of the configuring went ok (best-effort approach).
*/
public void initialize(boolean cdbBeatsProperties) throws LogConfigException {
StringBuffer errMsg = new StringBuffer();
LoggingConfig newLoggingConfig = null; // schema binding class generated from LogggingConfig.xsd
if (cdb != null) {
try {
if (cdbLoggingConfigPath != null) {
String loggingConfigXml = getLogConfigXml(cdbLoggingConfigPath, "//" + CDBNAME_LoggingConfig);
if (loggingConfigXml == null || loggingConfigXml.trim().isEmpty()) {
// the LoggingConfig child is mandatory for containers and manager
throw new LogConfigException("Node " + cdbLoggingConfigPath + " does not contain one LoggingConfig element.");
}
try {
newLoggingConfig = LoggingConfig.unmarshalLoggingConfig(new StringReader(loggingConfigXml));
} catch (Throwable thr) {
log(Level.FINE, "Failed to unmarshal logging config xml '" + loggingConfigXml + "'.", thr);
throw thr;
}
}
else {
errMsg.append("CDB reference was set, but not the path to the logging configuration. ");
}
if (newLoggingConfig != null) {
loggingConfig = newLoggingConfig;
// named logger configs under LoggingConfig we process right away, while other separate configs (those from component configurations) we do later.
// @TODO: check if we really want to lose named logger settings that had been added dynamically before this refresh from CDB.
// If not, then we must distinguish between dynamic API and from-CDB config, and leave those objects that have no CDB-equivalent.
synchronized (namedLoggerConfigs) {
// We don't call namedLoggerConfigs.clear() because we don't want to lose logger names
// but only null their configurations.
for (String loggerName : namedLoggerConfigs.keySet()) {
storeNamedLoggerConfig(loggerName, null);
}
// named logger levels from children of the <LoggingConfig/>
NamedLogger[] namedLoggers = loggingConfig.get();
for (int i = 0; i < namedLoggers.length; i++) {
storeNamedLoggerConfig(namedLoggers[i].getName(), new LockableUnnamedLogger(namedLoggers[i]));
}
// Named logger levels from separate component config:
// check CDB config for all component loggers who got a CDB path configured
for (String loggerName : cdbComponentPaths.keySet()) {
// skip named logger if it's been already configured from the main XML, since those values have precedence
if (!namedLoggerConfigs.containsKey(loggerName)) {
String cdbPath = cdbComponentPaths.get(loggerName);
String xpath = "//_[@Name='" + loggerName + "']/" + CDBNAME_ComponentLogger;
String componentConfigXML = getLogConfigXml(cdbPath, xpath);
// the ComponentLogger xml child element is optional, we get a null if it's missing.
if (componentConfigXML != null) {
UnnamedLogger compLoggerConfig;
try {
compLoggerConfig = UnnamedLogger.unmarshalUnnamedLogger(new StringReader(componentConfigXML));
} catch (Throwable thr) {
log(Level.FINE, "Failed to unmarshal component config xml '" + componentConfigXML + "'.", thr);
throw thr;
}
storeNamedLoggerConfig(loggerName, new LockableUnnamedLogger(compLoggerConfig));
}
}
}
}
}
else {
throw new LogConfigException("LoggingConfig binding class obtained from CDB node '" + cdbLoggingConfigPath + "' was null.");
}
} catch (CDBXMLErrorEx ex) {
errMsg.append("Failed to read node " + cdbLoggingConfigPath + " from the CDB (msg='"
+ ex.errorTrace.shortDescription + "'). ");
} catch (CDBRecordDoesNotExistEx ex) {
errMsg.append("Node " + cdbLoggingConfigPath + " does not exist in the CDB (msg='"
+ ex.errorTrace.shortDescription + "'). ");
} catch (CastorException ex) {
errMsg.append("Failed to parse XML for CDB node " + cdbLoggingConfigPath + " into binding classes (ex="
+ ex.getClass().getName() + ", msg='" + ex.getMessage() + "'). ");
} catch (Throwable thr) {
errMsg.append("Failed to read node " + cdbLoggingConfigPath + " from the CDB (ex=" + thr.getClass().getName() + ", msg='" + thr.getMessage() + "'). ");
}
}
// consider the env var based properties only if the CDB was not considered or if the CDB settings should not override the env vars
if (cdb == null || !cdbBeatsProperties) {
configureDefaultLevelsFromProperties();
configureNamedLoggerLevelsFromProperties();
}
notifySubscribers();
// now that the subscribers had a chance to adjust their log levels according to the changes from the CDB (if
// present), we can publish a trace log
if (newLoggingConfig != null) {
StringWriter writer = new StringWriter();
String newXML = null;
try {
newLoggingConfig.marshal(writer);
newXML = writer.toString().trim();
} catch (Throwable thr) {
; // nothing
}
String msg = "Updated logging configuration based on CDB entry " + newXML;
msg += " with " + (cdbBeatsProperties ? "CDB" : "env vars" ) + " having precedence over " + (cdbBeatsProperties ? "env vars" : "CDB" );
log(Level.FINER, msg, null);
// @TODO: also log something for named component loggers if any were considered
}
else {
log(Level.FINER, "Logging configuration has been initialized, but not from CDB settings.", null);
}
if (errMsg.length() > 0) {
throw new LogConfigException("Log config initialization at least partially failed. " + errMsg.toString());
}
}
/**
* Reads the CDB element from the given path as XML,
* and extracts the child element that must be uniquely identified
* by the XPath expression in <code>xpathLogConfigNode</code>.
* <p>
* TODO: move the Node-to-XML-String code to a general utility package
* and return just the Node -- any client can then easily turn it into a String if needed.
* For Castor-parsing, a node is fine, see {@link Unmarshaller#unmarshal(Node)}.
*
* @param cdbPathParent path to container, manager, or component config node
* @param xpathExpression For example, for containers and managers <code>//LoggingConfig</code>,
* for multiple-components-xml <code>//_[@Name='myCompName']/ComponentLogger</code>
* @return XML String for the requested child, or <code>null</code> if no unique xml child element was found.
* @throws CDBRecordDoesNotExistEx
* @throws CDBXMLErrorEx
* @throws ParserConfigurationException
* @throws IOException
* @throws SAXException
* @throws XPathExpressionException
* @throws TransformerException
* @throws IllegalStateException if the cdb ref has not been set
*/
String getLogConfigXml(String cdbPathParent, String xpathLogConfigNode)
throws CDBXMLErrorEx, CDBRecordDoesNotExistEx, ParserConfigurationException, SAXException, IOException, XPathExpressionException, TransformerException {
if (cdb == null) {
throw new IllegalStateException("CDB reference has not been set.");
}
String parentConfigXML = cdb.get_DAO(cdbPathParent);
Node loggingConfigElement = null;
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(false);
DocumentBuilder builder = factory.newDocumentBuilder();
// parse the XML file into a DOM
Document parentDoc;
try {
parentDoc = builder.parse(new InputSource(new StringReader(parentConfigXML)));
} catch (SAXException ex) {
String msg = "Failed to parse the following XML retrieved from CDB#get_DAO(" + cdbPathParent + "):\n" + parentConfigXML;
log(Level.FINE, msg, ex);
throw ex;
}
String encoding = parentDoc.getXmlEncoding();
Element rootElement = parentDoc.getDocumentElement();
// eval xpath expression
XPath xpath = XPathFactory.newInstance().newXPath();
Object xpathResult = xpath.evaluate(xpathLogConfigNode, rootElement, XPathConstants.NODE);
// debug output, remove later
// try {
// log(Level.FINER, "XML for logging config parent node " + cdbPathParent + " and xpath result:\n" +
// parentConfigXML + "\n" +
// xpathResult,
// null);
// } catch (Throwable thr) {
// }
// end debug output
if (xpathResult == null || !(xpathResult instanceof Node)) {
return null;
}
loggingConfigElement = (Node) xpathResult;
Document childDoc = builder.newDocument();
loggingConfigElement = childDoc.importNode(loggingConfigElement, true);
childDoc.appendChild(loggingConfigElement);
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
transformer.setOutputProperty(OutputKeys.ENCODING, encoding);
StringWriter sw = new StringWriter();
StreamResult result = new StreamResult(sw);
transformer.transform(new DOMSource(childDoc), result);
return sw.toString();
}
public String getCentralizedLogger() {
return loggingConfig.getCentralizedLogger();
}
public int getDispatchPacketSize() {
return loggingConfig.getDispatchPacketSize();
}
public AcsLogLevelDefinition getImmediateDispatchLevel() {
return convertLegalLogLevel(loggingConfig.getImmediateDispatchLevel());
}
public int getFlushPeriodSeconds() {
return loggingConfig.getFlushPeriodSeconds();
}
public int getMaxLogQueueSize() {
return loggingConfig.getMaxLogQueueSize();
}
public int getMaxLogsPerSecond() {
return loggingConfig.getMaxLogsPerSecond();
}
/**
* Limits the total number of logs emitted by the whole process,
* separately for local and remote logging.
* <p>
* Introduced with ACS 9.0 for only reading the log throttle configuration data,
* we add this setter method in preparation for future exposure in the LoggingConfigurable interface,
* and also for test setups.
*/
public void setMaxLogsPerSecond(int maxLogsPerSecond) {
loggingConfig.setMaxLogsPerSecond(maxLogsPerSecond);
notifySubscribers();
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// Getter and setter methods for default logger and named logger levels, local and remote logging.
///////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Helper method that converts an integer log level to the matching enum literal.
* It suppresses the AcsJIllegalArgumentEx because the level must have been validated during the config init
* (and we wouldn't be bothered about exceptions here if we had stored the converted enum literal instead of the castor class).
* Therefore a lame log and runtime ex are thrown just in case, but no AcsJIllegalArgumentEx gets thrown on.
*/
private AcsLogLevelDefinition convertLegalLogLevel(LogLevel legalLogLevel) {
try {
return AcsLogLevelDefinition.fromXsdLogLevel(legalLogLevel);
} catch (AcsJIllegalArgumentEx ex) {
log(Level.WARNING, "Failed to convert to AcsLogLevelDefinition the level integer " + legalLogLevel, ex);
throw new RuntimeException(ex);
}
}
/**
* Gets the log level for stdout printing for default loggers
* (those that don't have custom levels set).
*/
public AcsLogLevelDefinition getDefaultMinLogLevelLocal() {
return convertLegalLogLevel(loggingConfig.getMinLogLevelLocal());
}
/**
* Sets the given log level for stdout printing of log records
* for all loggers that don't have a custom configuration.
* All log config listeners.get notified of this change.
* The call is ignored for negative values of <code>newLevel</code>.
* @param newLevel
*/
public void setDefaultMinLogLevelLocal(AcsLogLevelDefinition newLevel) {
if (newLevel.value >= 0) {
loggingConfig.setMinLogLevelLocal(newLevel.toXsdLevel());
notifySubscribers();
}
}
/**
* Gets the log level for centralized logging for default loggers
* (those that don't have custom levels set).
*/
public AcsLogLevelDefinition getDefaultMinLogLevel() {
return convertLegalLogLevel(loggingConfig.getMinLogLevel());
}
/**
* Sets the given log level for centralized logging of log records
* for all loggers that don't have a custom configuration.
* All log config listeners.get notified of this change.
* The call is ignored for negative values of <code>newLevel</code>.
* @param newLevel
*/
public void setDefaultMinLogLevel(AcsLogLevelDefinition newLevel) {
if (newLevel.value >= 0) {
loggingConfig.setMinLogLevel(newLevel.toXsdLevel());
notifySubscribers();
}
}
/**
* Gets the names of all known loggers, no matter if they use default or custom levels.
* <p>
* The loggers are registered automatically by calls to
* {@link #getNamedLoggerConfig(String)} and {@link #setNamedLoggerConfig(String, UnnamedLogger)}.
*/
public Set<String> getLoggerNames() {
synchronized (namedLoggerConfigs) { // to avoid map change during copy-ctor
return new HashSet<String>(namedLoggerConfigs.keySet());
}
}
/**
* Checks if the given logger name is known, either for default or custom config.
* <p>
* The current {@link LoggingConfigurable} interface semantics don't allow
* configuration of a logger that does not yet exist. Therefore this method can be used
* to check first before calling {@link #getNamedLoggerConfig(String)}
* or {@link #setNamedLoggerConfig(String, alma.acs.logging.config.LogConfig.LockableUnnamedLogger)},
* which would automatically add a previously unknown logger.
*
* @param loggerName
* @since ACS 7.0.2
*/
public boolean isKnownLogger(String loggerName) {
synchronized (namedLoggerConfigs) {
return namedLoggerConfigs.containsKey(loggerName);
}
}
/**
* Checks if there is a level configuration for this particular logger.
* @param loggerName
* @return false if the logger is not known or uses the default configuration
*/
public boolean hasCustomConfig(String loggerName) {
synchronized (namedLoggerConfigs) {
return (namedLoggerConfigs.get(loggerName) != null);
}
}
/**
* Gets the (log level) configuration data for a named logger.
* Resorts to the default configuration if a specialized configuration is not available.
* <p>
* Note that a copy of the config data is returned, so changes to it will
* not affect any other object's configuration.
* <p>
* A previously unknown logger gets registered by its name.
* If this is not intended, check first with {@link #isKnownLogger(String)}.
*/
public LockableUnnamedLogger getNamedLoggerConfig(String loggerName) {
LockableUnnamedLogger ret = new LockableUnnamedLogger();
synchronized (namedLoggerConfigs) {
if (loggerName == null || loggerName.toLowerCase().equals("default") || namedLoggerConfigs.get(loggerName)==null) {
ret.setMinLogLevel(loggingConfig.getMinLogLevel());
ret.setMinLogLevelLocal(loggingConfig.getMinLogLevelLocal());
if (loggerName != null && !loggerName.toLowerCase().equals("default")) {
// register new logger, with a null config which means "apply default values"
storeNamedLoggerConfig(loggerName, null);
}
}
else {
ret = new LockableUnnamedLogger(namedLoggerConfigs.get(loggerName));
}
}
// @TODO: resolve the int / short mismatch between xsd and idl definitions
// ret.setMinLogLevel(Math.min(ret.getMinLogLevel(), Short.MAX_VALUE));
// ret.setMinLogLevelLocal(Math.min(ret.getMinLogLevelLocal(), Short.MAX_VALUE));
return ret;
}
/**
* Sets the given log levels for the named logger and notifies all listeners.
* Ignores this call if any of the parameters are <code>null</code>.
* <p>
* A copy of the supplied <code>config</code> is made
* to isolate the stored data from later modifications.
* <p>
* If <code>loggerName</code> is previously unknown, then this logger is added automatically
* to the list of known loggers. If this is not intended, check first using {@link #isKnownLogger(String)}.
*
* @throws AcsJIllegalArgumentEx if the log level integers inside <code>config</code> are illegal.
*/
public void setNamedLoggerConfig(String loggerName, LockableUnnamedLogger config) throws AcsJIllegalArgumentEx {
if (loggerName != null && config != null) {
AcsLogLevelDefinition.fromXsdLogLevel(config.getMinLogLevel());
AcsLogLevelDefinition.fromXsdLogLevel(config.getMinLogLevelLocal());
LockableUnnamedLogger config2 = new LockableUnnamedLogger(config);
storeNamedLoggerConfig(loggerName, config2);
notifySubscribers();
}
}
/**
* We keep this method next to {@link #setNamedLoggerConfig(String, alma.acs.logging.config.LogConfig.LockableUnnamedLogger)}
* in order to keep the LockableUnnamedLogger confined to this class. It may be exposed later,
* and this method could then be removed.
* @param loggerName
* @param config
* @since ACS 7.0
*/
public void setNamedLoggerConfig(String loggerName, UnnamedLogger config) throws AcsJIllegalArgumentEx {
if (loggerName != null && config != null) {
LockableUnnamedLogger config2 = new LockableUnnamedLogger(config); // unlocked by default
setNamedLoggerConfig(loggerName, config2);
}
}
/**
* Clears log level settings for the given named logger,
* so that it uses default log levels.
* Notifies all listeners.
* Ignores this call if <code>loggerName</code> is <code>null</code>.
*/
public void clearNamedLoggerConfig(String loggerName) {
if (loggerName != null) {
storeNamedLoggerConfig(loggerName, null);
notifySubscribers();
}
}
public void setMinLogLevelLocal(AcsLogLevelDefinition newLevel, String loggerName) {
LockableUnnamedLogger config = getNamedLoggerConfig(loggerName); // new object, with cached or default values
config.setMinLogLevelLocal(newLevel.toXsdLevel());
try {
setNamedLoggerConfig(loggerName, config);
} catch (AcsJIllegalArgumentEx ex) {
// cannot happen because the level integer comes from a valid AcsLogLevelDefinition
}
}
public void setMinLogLevel(AcsLogLevelDefinition newLevel, String loggerName) {
LockableUnnamedLogger config = getNamedLoggerConfig(loggerName); // new object, with cached or default values
config.setMinLogLevel(newLevel.toXsdLevel());
try {
setNamedLoggerConfig(loggerName, config);
} catch (AcsJIllegalArgumentEx ex) {
// cannot happen because the level integer comes from a valid AcsLogLevelDefinition
}
}
/**
* Locking a remote log level is currently needed only for the container to ensure that the ORB logger of the container in which
* the archive logger component runs is guaranteed to not produce any logs
* (which would lead to log record explosion through positive feedback).
* <p>
* The more abstract concept of locking log levels was chosen to keep the special scenario described above
* out of the logging config code.
*
* @param newLevel small integer log level (IDL value wrapped by a Java enum)
* @param loggerName
*/
public void setAndLockMinLogLevel(AcsLogLevelDefinition newLevel, String loggerName) {
synchronized (namedLoggerConfigs) {
LockableUnnamedLogger config = getNamedLoggerConfig(loggerName); // new object, with cached or default values
if (config.isLockedRemote()) {
// only if the level is different we log the warning. This removes the need to check the lock status and level before calling this method.
if (!newLevel.isEqualXsdLevel(config.getMinLogLevel())) {
log(Level.WARNING, "Ignoring attempt to lock logger " + loggerName + " to level " + newLevel + " because it is already locked to remote level " + config.getMinLogLevel(), null);
}
}
else if (!newLevel.isEqualXsdLevel(config.getMinLogLevel())) {
config.setMinLogLevel(newLevel.toXsdLevel());
config.lockRemote();
try {
setNamedLoggerConfig(loggerName, config);
} catch (AcsJIllegalArgumentEx ex) {
// cannot happen because the level integer comes from a valid AcsLogLevelDefinition
}
log(Level.INFO, "Locked logger " + loggerName + " to remote level " + newLevel.value, null);
}
}
}
/**
* Method that guards <code>namedLoggerConfigs.put(loggerName, config)</code>
* and denies access if the old config is locked.
* @param loggerName
* logger name, must not be null
* @param config
* new level values, or <code>null</code> to "link" to default log levels.
* @see #namedLoggerConfigs
*/
private void storeNamedLoggerConfig(String loggerName, LockableUnnamedLogger config) {
if (loggerName == null) {
throw new IllegalArgumentException("loggerName must not be null");
}
synchronized (namedLoggerConfigs) {
LockableUnnamedLogger oldConfig = namedLoggerConfigs.get(loggerName);
if (oldConfig == null) {
namedLoggerConfigs.put(loggerName, config);
}
else if (config == null) {
// null clears the named logger levels
if (!oldConfig.isLockedLocal && !oldConfig.isLockedRemote) {
namedLoggerConfigs.put(loggerName, null);
}
else {
log(Level.INFO, "Ignoring attempt to clear locked logger config for " + loggerName, null);
}
}
else {
// need to selectively update the values, watching out for locked levels
if (!oldConfig.isLockedRemote()) {
oldConfig.setMinLogLevel(config.getMinLogLevel());
}
if (!oldConfig.isLockedLocal()) {
oldConfig.setMinLogLevelLocal(config.getMinLogLevelLocal());
}
}
}
}
/**
* Renaming of a logger can occur for example when an ORB logger undergoes a name change
* as soon as the process name is determined by the container logger.
* <p>
* Renaming is not a regular part of a logger's life though. By the time that tools can look at the list of loggers,
* no renaming should occur any more.
*
* @param oldLoggerName
* @param newLoggerName
* @return true if the logger config was renamed. False for bad or unmatching parameters which result in no change.
*/
public boolean renameNamedLoggerConfig(String oldLoggerName, String newLoggerName) {
synchronized (namedLoggerConfigs) {
if (oldLoggerName == null || newLoggerName == null || !namedLoggerConfigs.containsKey(oldLoggerName)) {
return false;
}
else {
LockableUnnamedLogger config = namedLoggerConfigs.get(oldLoggerName);
namedLoggerConfigs.put(newLoggerName, config);
namedLoggerConfigs.remove(oldLoggerName);
return true;
}
}
}
public static class LockableUnnamedLogger extends UnnamedLogger {
private boolean isLockedRemote = false;
private boolean isLockedLocal = false;
LockableUnnamedLogger() {
init(null);
}
LockableUnnamedLogger(UnnamedLogger config) {
init(config);
}
LockableUnnamedLogger(LockableUnnamedLogger config) {
init(config);
isLockedRemote = config.isLockedRemote;
isLockedLocal = config.isLockedLocal;
}
private void init(UnnamedLogger config) {
unlockRemote();
if (config != null) {
setMinLogLevel(config.getMinLogLevel());
setMinLogLevelLocal(config.getMinLogLevelLocal());
}
}
void lockRemote() {
isLockedRemote = true;
}
void lockLocal() {
isLockedLocal = true;
}
void unlockRemote() {
isLockedRemote = false;
}
void unlockLocal() {
isLockedLocal = false;
}
boolean isLockedRemote() {
return isLockedRemote;
}
boolean isLockedLocal() {
return isLockedLocal;
}
}
// ///////////////////////////////////////////////////////////////////
// Propagation of configuration updates to various logging classes
/////////////////////////////////////////////////////////////////////
public void addSubscriber(LogConfigSubscriber subscriber) {
synchronized (subscriberList) {
if (!subscriberList.contains(subscriber)) {
subscriberList.add(subscriber);
}
}
}
void notifySubscribers() {
synchronized (subscriberList) {
for (LogConfigSubscriber subscriber : subscriberList) {
subscriber.configureLogging(this);
}
}
}
public void removeSubscriber(LogConfigSubscriber subscriber) {
synchronized (subscriberList) {
subscriberList.remove(subscriber);
}
}
// ///////////////////////////////////////////////////////////////////
// Logging done by this class
/////////////////////////////////////////////////////////////////////
/**
* Sets the Logger to be used by this class and dependent classes for internal tracing.
* <p>
* Note that in the current design of ClientLogManager and LogConfig,
* the Logger can not be provided already in the constructor,
* because the Logger first must be configured, which in turn requires a LogConfig instance.
* That's why we have this setter method.
*/
public void setInternalLogger(Logger logger) {
this.logger = logger;
}
/**
* Logs to the Logger given in {@link #setInternalLogger(Logger)}, or to System.out if no Logger has been provided.
*/
protected void log(Level level, String msg, Throwable thr) {
if (logger != null) {
logger.log(level, msg, thr);
}
else {
if (AcsLogLevel.getNativeLevel(level).getAcsLevel().compareTo(getDefaultMinLogLevelLocal()) >= 0) {
System.out.println(IsoDateFormat.formatCurrentDate()+ " "+ AcsLogLevel.getNativeLevel(level).getAcsLevel().toString() + " [alma.acs.logging.config.LogConfig] " + msg + (thr != null ? thr.toString() : ""));
}
}
}
}