/*
* ALMA - Atacama Large Millimiter Array
* (c) European Southern Observatory, 2002
* 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;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Handler;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import org.omg.CORBA.ORB;
import org.omg.CORBA.ORBPackage.InvalidName;
import org.omg.CosNaming.NameComponent;
import org.omg.CosNaming.NamingContext;
import org.omg.CosNaming.NamingContextExtHelper;
import org.omg.DsLogAdmin.LogOperations;
import org.slf4j.impl.HibernateLoggerHelper;
import org.slf4j.impl.JacorbLoggerHelper;
import si.ijs.maci.Manager;
import alma.ACSErrTypeCommon.wrappers.AcsJCouldntPerformActionEx;
import alma.Logging.AcsLogServiceHelper;
import alma.Logging.AcsLogServiceOperations;
import alma.acs.logging.adapters.AcsJacorbLoggingInitializer;
import alma.acs.logging.adapters.CommonsLoggingFactory;
import alma.acs.logging.config.LogConfig;
import alma.acs.logging.config.LogConfigException;
import alma.acs.logging.config.LogConfigSubscriber;
import alma.acs.logging.formatters.AcsBinLogFormatter;
import alma.acs.logging.formatters.AcsXMLLogFormatter;
import alma.acs.logging.formatters.ConsoleLogFormatter;
import alma.acs.logging.level.AcsLogLevelDefinition;
import alma.acs.util.ACSPorts;
import alma.maciErrType.CannotGetComponentEx;
import alma.maciErrType.ComponentConfigurationNotFoundEx;
import alma.maciErrType.ComponentNotAlreadyActivatedEx;
import alma.maciErrType.NoPermissionEx;
/**
* This class provides methods for getting the loggers for components and containers.
* Both the loggers and the handlers are organized in a hierarchical namespace so that
* children may inherit some properties from their parents in the namespace.
* <p>
* To set the policy for remote logging, either {@link #initRemoteLogging(ORB, Manager, int, boolean)}
* or {@link #suppressRemoteLogging()} must be called.
* As long as none of these methods are called, it is assumed that remote logging will be initialized
* at some point later, and all log messages that are meant for remote logging will be stored in
* a queue. <code>suppressRemoteLogging</code> will throw away this queue, while <code>initRemoteLogging</code>
* will send all queued log records to the remote logger.
* <p>
* Since ACS 7.0, this class acts pretty much as a replacement for the JDK's {@link LogManager}
* and the {@linkplain AcsLogManager} subclass. <br>
* If needed, we should investigate how to support the JMX logger management
* via {@linkplain java.util.logging.LoggingMXBean} which is useless now.
*
* @author hsommer
*/
public class ClientLogManager implements LogConfigSubscriber
{
public enum LoggerOwnerType { ContainerLogger, ComponentLogger, FrameworkLogger, OtherLogger, UnknownLogger }
/** instance of a singleton */
private volatile static ClientLogManager s_instance;
/** The logging configurator shared by various classes of this package */
protected final LogConfig sharedLogConfig;
/** The (CORBA-) name of the remote logging service to which logs will be sent */
private volatile String logServiceName;
/** The time in seconds between the periodic log queue flushings */
private volatile int flushPeriodSeconds;
private static class AcsLoggerInfo {
AcsLogger logger;
AcsLoggingHandler remoteHandler;
StdOutConsoleHandler localHandler;
LoggerOwnerType loggerOwnerType = LoggerOwnerType.UnknownLogger;
boolean needsProcessNameUpdate = false;
}
/**
* We store all loggers in this map instead of giving them to
* {@link LogManager} which does not allow to remove loggers
* and would thus prevent component loggers from being garbage collected.
*/
private final Map<String, AcsLoggerInfo> loggers = new HashMap<String, AcsLoggerInfo>();
/**
* Parent logger of all remote loggers.
* Not really needed any more now that handlers are not shared.
*/
private Logger parentRemoteLogger;
/**
* The log queue can be null if remote logging has been suppressed. Otherwise it is non-null even if remote logging
* is not active, because it stores the records to be sent off later.
*/
protected volatile DispatchingLogQueue logQueue;
private final ReentrantLock logQueueLock = new ReentrantLock();
/**
* The log dispatcher object serves as a flag for whether remote logging has been activated via
* {@link #initRemoteLogging(ORB, Manager, int, boolean)}.
*/
private RemoteLogDispatcher logDispatcher;
/**
* Logger used by the classes in this logging package.
* TODO: allow container or client to replace this logger with their own
* logger, because these classes belong to their respective process.
*/
private final Logger m_internalLogger;
/** for testing */
private boolean DEBUG = Boolean.getBoolean("alma.acs.logging.verbose");
private boolean LOG_BIN_TYPE = Boolean.getBoolean("ACS.loggingBin");
/**
* The process name gets used as a data field in the log record, and also
* gets appended to the name for the Corba logger,
* so that different ORB instances in the system can be distinguished.
*/
private volatile String processName;
private final ReentrantLock processNameLock = new ReentrantLock();
/**
* If true then the Corba (ORB) logger will not send logs to the Log service.
*/
private volatile boolean suppressCorbaRemoteLogging = false;
private final LogThrottle logThrottle;
/**
* Used to optionally raise/clear alarms from ClientLogManager or its dependent objects.
* This field is <code>null</code>, unless it gets set in {@link #enableLoggingAlarms(LogAlarmHandler)}.
*/
private volatile LogAlarmHandler logAlarmHandler;
/**
* Singleton accessor.
* <p>
* TODO: rename, because now that we have class {@link AcsLogManager}, this method name is confusing.
*
* @return ClientLogManager
*/
public static synchronized ClientLogManager getAcsLogManager() {
if (s_instance == null) {
s_instance = new ClientLogManager();
}
return s_instance;
}
/**
*
*/
protected ClientLogManager()
{
sharedLogConfig = new LogConfig();
try {
sharedLogConfig.initialize(false); // will determine the default values
configureLogging(sharedLogConfig);
} catch (LogConfigException ex) {
System.out.println("Failed to configure logging: " + ex.toString());
}
// parentRemoteLogger is not removed in method disableRemoteLogging, and thus does not need to be
// (re-) created in method prepareRemoteLogging (unlike the queue and the handler).
// Therefore we can create it once here in the ctor.
parentRemoteLogger = Logger.getLogger("AcsRemoteLogger");
parentRemoteLogger.setUseParentHandlers(false); // otherwise the default log handler on the root logger will dump all logs to stderr
prepareRemoteLogging();
m_internalLogger = getAcsLogger("alma.acs.logging", LoggerOwnerType.OtherLogger);
sharedLogConfig.setInternalLogger(m_internalLogger);
ThrottleCallback throttleCallback = new ThrottleCallback();
logThrottle = new LogThrottle(sharedLogConfig, throttleCallback);
if (DEBUG) {
m_internalLogger.fine("ClientLogManager instance is created.");
}
sharedLogConfig.addSubscriber(this); // passing "this" should only be done when this object is fully constructed.
}
/**
* @see alma.acs.logging.config.LogConfigSubscriber#configureLogging(alma.acs.logging.LogConfig)
*/
public void configureLogging(LogConfig logConfig) {
if (!logConfig.getCentralizedLogger().equals(logServiceName)) {
if (logServiceName == null) {
logServiceName = logConfig.getCentralizedLogger();
} else {
m_internalLogger.warning("Dynamic switching of Log service not yet supported!");
// @TODO: switch connection to new log service
}
}
flushPeriodSeconds = logConfig.getFlushPeriodSeconds();
logQueueLock.lock();
try {
if (logQueue != null) {
// don't call this while logQueue is not ready for remote dispatching, because it would produce an ugly error message.
if (logQueue.hasRemoteDispatcher()) {
logQueue.setPeriodicFlushing(flushPeriodSeconds * 1000);
}
// Set the log queue size.
logQueue.setMaxQueueSize(logConfig.getMaxLogQueueSize());
}
} finally {
logQueueLock.unlock();
}
}
/**
* Gets the <code>LogConfig</code> object that is shared between the ClientLogManager singleton and any other
* objects in the process (e.g. Java container or manager classes, Loggers, Handlers).
*/
public LogConfig getLogConfig() {
return sharedLogConfig;
}
/**
* If not done already, sets up remote handlers for all loggers using a shared queue.
*/
protected void prepareRemoteLogging() {
logQueueLock.lock();
try {
if (logQueue == null) {
logQueue = createDispatchingLogQueue();
logQueue.setMaxQueueSize(getLogConfig().getMaxLogQueueSize());
}
} finally {
logQueueLock.unlock();
}
synchronized (loggers) {
// attach remote handlers to all loggers
for (String loggerName : loggers.keySet()) {
AcsLoggerInfo loggerInfo = loggers.get(loggerName);
AcsLogger logger = loggerInfo.logger;
// sanity check on loggerName: map key vs. Logger
if (logger.getLoggerName() == null || !logger.getLoggerName().equals(loggerName)) {
logger.setLoggerName(loggerName);
logger.info("Logging name mismatch resolved for '" + loggerName
+ "'. Should be reported to ACS developers");
}
addRemoteHandler(logger);
}
}
}
/**
* Factory method broken out from {@link #prepareRemoteLogging()} to support tests.
*/
protected DispatchingLogQueue createDispatchingLogQueue() {
return new DispatchingLogQueue();
}
/**
* Removes, closes, and nulls all remote handlers, so that no more records are put into the queue, and queue and
* remote handlers can be garbage collected.
* <p>
* GC of the queue can be an advantage when logs have been queued for remote sending, but then instead of connecting
* to the remote logger, we get a call to {@link #suppressRemoteLogging()}. All messages have been logged locally
* anyway, so in this case we want to clean up all these <code>LogRecord</code>s.
*/
protected void disableRemoteLogging() {
synchronized (loggers) {
for (String loggerName : loggers.keySet()) {
try {
AcsLoggerInfo loggerInfo = loggers.get(loggerName);
AcsLogger logger = loggerInfo.logger;
if (loggerInfo.remoteHandler != null) {
logger.removeHandler(loggerInfo.remoteHandler);
loggerInfo.remoteHandler.close(); // also unsubscribes from sharedLogConfig
loggerInfo.remoteHandler = null;
}
} catch (Throwable thr) {
// better just print to stderr because remote logging may be in a delicate state
System.out.println("Unexpected exception while disabling remote logging for '" + loggerName + "': " + thr.toString());
}
}
}
}
/**
* Attaches a remote log handler to the given logger, if remote logging has not been suppressed.
*
* Note that this method does not require <code>logger</code> to be
* already registered in {@linkplain #loggers}, but if it is,
* then the new AcsLoggingHandler is set in the map's AcsLoggerInfo as well.
*
* @param logger logger to be set up for remote logging
* @return the remote handler that was added to the logger.
*/
private AcsLoggingHandler addRemoteHandler(AcsLogger logger) {
AcsLoggingHandler remoteHandler = null;
synchronized (loggers) {
String loggerName = logger.getLoggerName();
AcsLoggerInfo loggerInfo = loggers.get(loggerName);
// logQueue == null serves as indicator for remote log suppression
if (logQueue != null) {
// try to find an existing handler
for (Handler handler : logger.getHandlers()) {
if (handler instanceof AcsLoggingHandler) {
remoteHandler = (AcsLoggingHandler) handler;
if (loggerInfo != null && !remoteHandler.equals(loggerInfo.remoteHandler)) {
logger.info("Remote logging handler mismatch resolved for '" + loggerName + "'. Should be reported to ACS developers");
}
break;
}
}
if (remoteHandler == null) {
remoteHandler = new AcsLoggingHandler(logQueue, sharedLogConfig, loggerName, logThrottle); // subscribes to sharedLogConfig
logger.addHandler(remoteHandler);
if (loggerInfo != null && loggerInfo.remoteHandler != null) {
logger.info("Logging handler mismatch resolved for '" + loggerName + "'. Should be reported to ACS developers");
}
}
}
if (loggerInfo != null) {
loggerInfo.remoteHandler = remoteHandler;
}
}
return remoteHandler;
}
/**
* Adds a local logging handler to the provided logger. Unlike with remote handlers in
* {@link #prepareRemoteLogging()}, for local handlers we don't allow removing and re-adding the handlers later.
* <p>
* Note that this method does not require <code>logger</code> to be already registered in {@linkplain #loggers}, but
* if it is, then the new StdOutConsoleHandler is set in the map's AcsLoggerInfo as well.
*
* @param logger
* logger to be set up for local logging
* @return the local handler that was added to the logger.
*/
protected StdOutConsoleHandler addLocalHandler(AcsLogger logger) {
StdOutConsoleHandler localHandler = null;
synchronized (loggers) {
String loggerName = logger.getLoggerName();
AcsLoggerInfo loggerInfo = loggers.get(loggerName);
// try to find an existing handler (should not happen)
for (Handler handler : logger.getHandlers()) {
if (handler instanceof StdOutConsoleHandler) {
localHandler = (StdOutConsoleHandler) handler;
if (loggerInfo != null && !localHandler.equals(loggerInfo.localHandler)) {
logger.info("Stdout logging handler mismatch resolved for '" + loggerName + "'. Should be reported to ACS developers");
}
break;
}
}
if (localHandler == null) {
localHandler = createStdOutConsoleHandlerWithFormatter(sharedLogConfig, loggerName, logThrottle);
logger.addHandler(localHandler);
if (loggerInfo != null && loggerInfo.localHandler != null) {
logger.info("Logging handler mismatch resolved for '" + loggerName + "'. Should be reported to ACS developers");
}
}
if (loggerInfo != null) {
loggerInfo.localHandler = localHandler;
}
}
return localHandler;
}
/**
* Factory method broken out to allow tests to easily use an instrumented variant.
*/
protected StdOutConsoleHandler createStdOutConsoleHandlerWithFormatter(LogConfig logConfig, String loggerName, LogThrottle throttle) {
StdOutConsoleHandler localHandler = new StdOutConsoleHandler(logConfig, loggerName, throttle); // subscribes to sharedLogConfig
localHandler.setFormatter(new ConsoleLogFormatter());
return localHandler;
}
/**
* Creates or reuses a logger, normally with both a local and a remote handler attached.
* If remote logging is suppressed then no remote handler is attached.
* <p>
* Logger names must be unique. If the requested logger name matches a given logger
* of the same LoggerOwnerType then that logger is returned. If however a logger of a different
* type is matched, then a name uniqueness violation is avoided by appending
* small integer numbers to the logger name.
* Therefore the name of the returned Logger may be different from <code>loggerName</code>!
* The idea of this strategy is that uniqueness of component names will be enforced
* by the system, while a component may in sick cases have the name of a container or the orb logger.
*
* @param loggerName The unique logger name
* @param loggerOwnerType enum for the type of the logger owner.
* With ACS 7.0, this replaces the namespace strings previously prepended to the logger names.
* @return
* @throws IllegalArgumentException if loggerName is null or empty
*/
private AcsLogger getAcsLogger(String loggerName, LoggerOwnerType loggerOwnerType) {
if (loggerName == null || loggerName.trim().isEmpty()) {
throw new IllegalArgumentException("loggerName must not be null or empty");
}
AcsLoggerInfo loggerInfo = null;
// just to make sure we never throw an exception
try {
synchronized (loggers) {
loggerInfo = loggers.get(loggerName);
// try to reuse existing logger
// avoid false "reuse" by making new logger name unique
int counter = 2; // that is to make "name" into "name_2"
while (loggerInfo != null && loggerOwnerType != loggerInfo.loggerOwnerType) {
// the current loggerName exists already for a different logger type.
// Try a different logger name.
int lastIndexUnderscore = loggerName.lastIndexOf('_');
if (lastIndexUnderscore > 0 && loggerName.length() > lastIndexUnderscore + 1) {
try {
int oldCounter = Integer.parseInt(loggerName.substring(lastIndexUnderscore + 1));
counter = Math.max(counter, oldCounter+1);
loggerName = loggerName.substring(0, lastIndexUnderscore+1) + counter;
loggerInfo = loggers.get(loggerName);
continue;
} catch (NumberFormatException ex) {
// we had an '_' but no number behind it. Same as no '_' at all.
}
}
loggerName += "_" + counter;
loggerInfo = loggers.get(loggerName);
}
if (loggerInfo == null) {
// not yet in cache, so we create the logger
loggerInfo = new AcsLoggerInfo();
loggerInfo.logger = new AcsLogger(loggerName, null, sharedLogConfig); // ctor registers itself in loggers map
loggerInfo.logger.setParent(parentRemoteLogger);
loggerInfo.logger.setUseParentHandlers(false); // since ACS 7.0 all AcsLoggers have their own handlers
// set the value for the "ProcessName" and "SourceObject" fields in the produced log records
loggerInfo.logger.setProcessName(this.processName);
if (loggerOwnerType == LoggerOwnerType.ComponentLogger || loggerOwnerType == LoggerOwnerType.FrameworkLogger) {
loggerInfo.logger.setSourceObject(loggerName);
}
else {
// TODO: check why we don't always use the logger name as SourceObject
loggerInfo.logger.setSourceObject(this.processName);
}
loggerInfo.localHandler = addLocalHandler(loggerInfo.logger);
loggerInfo.remoteHandler = addRemoteHandler(loggerInfo.logger); // may be null
loggerInfo.loggerOwnerType = loggerOwnerType;
loggers.put(loggerName, loggerInfo);
}
if (DEBUG) {
System.out.println("created remote logger '" + loggerName + "' (level " + loggerInfo.logger.getLevel() + ") and separate local handler.");
}
}
} catch (Throwable thr) {
System.out.println("failed to create logger '" + loggerName + "'. Stack trace follows.");
thr.printStackTrace(System.out);
}
return loggerInfo.logger;
}
/**
* Initialize the remote logging for a ACS service.
* <P>
* The implementation of this method is the same of
* {@link #initRemoteLogging(ORB, Manager, int, boolean)} but the reference to the log service
* is read from the naming service instead of being asked to the Manager.
* <P>
* <B>Note</B>: this method must be called <EM>only</EM> by ACS services that starts before the manager.
* ACS services, in fact, do not log into the manager and therefore do not have a handle.
* <BR>Other ACS tools like containers and component clients must use {@link #initRemoteLogging(ORB, Manager, int, boolean)}
* to properly initialize the remote logging.
*
* @param orb The not <code>null</code> ORB
* @param retry if <code>true</code>, a failing connection to the log service will trigger up to 5 other attempts, every 10 seconds.
* @return <code>true</code> if remote logging was initialized successfully
* @see #initRemoteLogging(ORB, Manager, int, boolean), {@link #internalInitRemoteLogging(ORB, Manager, int, boolean)}
*/
public boolean initRemoteLoggingForService(ORB orb, boolean retry) {
return initRemoteLogging(orb, null, 0, retry);
}
/**
* {@link #initRemoteLogging(ORB, Manager, int, boolean)} and {@link #initRemoteLoggingForService(ORB, boolean)}
* delegate to this method for the initialization of the remote logging.
* <P>
* If the passed manager is <code>null</code>, <code>internalInitRemoteLogging</code> gets
* the log service reference from the naming service; if the manager is not <code>null</code>,
* it asked the manager for the reference to the log service.
*
* @param orb The not <code>null</code> ORB
* @param manager the ACS manager (it can be <code>null</code>)
* @param managerHandle the handle assigned by the ACS Manager for this client;
* ignored if the manager is <code>null</code>
* @param retry if <code>true</code>, a failing connection to the log service will trigger up to 5 other attempts, every 10 seconds.
* @return <code>true</code> if remote logging was initialized successfully
*/
private boolean internalInitRemoteLogging(ORB orb, Manager manager, int managerHandle, boolean retry) {
if (logDispatcher != null) {
System.out.println("Ignoring call to ClientLogManager#init: already initialized!");
// todo: or is there a case where we want to retrieve the log service again?
return false;
}
if (orb == null) {
System.out.println("Given ORB is null.");
return false;
}
if (manager == null) {
System.out.println("Will retrieve the log service reference from the naming service..." );
} else if (managerHandle<=0) {
System.out.println("can't connect to log service: invalid handle " + managerHandle);
return false;
}
AcsLogServiceOperations logService = null;
int count = 0;
String errMsg;
do {
errMsg = null;
count++;
try {
// normally there will be a remote handler and log queue already, which has captured all log records produced so far.
// However, if suppressRemoteLogging was called, we need to set up remote logging from scratch.
prepareRemoteLogging();
if (manager!=null) {
logService = getLogService(manager, managerHandle);
} else {
logService=getLogServiceFromNameSarvice(orb);
}
if (logService == null) {
errMsg = "Failed to obtain central log service '" + logServiceName + "': reference is 'null'. ";
}
else {
logQueueLock.lock(); // we keep the above get_service call outside this locked section in order to not block shutdown() too long
//System.out.println("ClientLogManager#initRemoteLogging got the logQueue lock");
try {
if (logQueue == null) {
// this can happen if shutdown or suppressRemoteLogging is called concurrently
System.out.println("Will abort ClientLogManager#initRemoteLogging because remote logging seems no longer needed.");
return false;
}
if (count > 1) {
// all is fine, but we report the difficulty
m_internalLogger.info("Connected to central log service after initial failure. ");
}
// make log service available to our dispatcher, and flush the records collected so far
if (LOG_BIN_TYPE){
logDispatcher = new RemoteLogDispatcher(orb, logService, new AcsBinLogFormatter());
}
else {
logDispatcher = new RemoteLogDispatcher(orb, logService, new AcsXMLLogFormatter());
}
logQueue.setRemoteLogDispatcher(logDispatcher);
logQueue.flushAllAndWait();
logQueue.setPeriodicFlushing(flushPeriodSeconds * 1000);
}
finally {
logQueueLock.unlock();
// System.out.println("ClientLogManager#initRemoteLogging released the logQueue lock");
}
}
}
catch (Throwable thr) {
errMsg = "Failed to connect to central log service with exception " + thr.toString();
// as this message must go repeatedly to the command line output regardless of local log level,
// we don't want to print the multi-line exception stack, but just the original location.
StackTraceElement[] trace = thr.getStackTrace();
if (trace != null && trace.length > 0) {
StackTraceElement traceOrigin = trace[0];
errMsg += " in file " + traceOrigin.getFileName() + ", line " + traceOrigin.getLineNumber();
}
errMsg += ". ";
}
if (errMsg != null) {
// can't use the loggers, so println is ok here
if (retry) {
System.out.println(errMsg + "Will try again in 10 seconds.");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
System.out.println("Abandoning ClientLogManager#initRemoteLogging retries because of thread interruption.");
retry = false;
}
}
else {
System.out.println(errMsg);
}
}
} while (retry && count <= 5 && errMsg != null);
return (errMsg == null);
}
/**
* Enables loggers to send log records to the central ACS logger service.
* Tries to connect to the log service using the supplied ACS manager.
* As long as this connection fails, this method can sleep for 10 seconds and then try to connect again,
* if the parameter <code>retry</code> is <code>true</code>. Total retries are limited to 5,
* to detect a permanent problem before the log queue overflows.
* <p>
* Execution time can be significant, so consider calling this method in a separate thread
* (which has no negative effect on the logging since all log records are cached and automatically sent off
* once the log service is available).
* Use a daemon thread to avoid shutdown problems if this method still hangs in the login loop.
* <p>
* When the log service is obtained, the log queue used for remote logging will be flushed periodically
* to the log service unless an otherwise triggered flush has done this already.
* The default period is 10 seconds, but can be overridden in the CDB. <br>
*
* @param orb the ORB used in this JVM
* @param manager the ACS manager
* @param managerHandle handle assigned by the ACS Manager for this client
* @param retry if <code>true</code>, a failing connection to the log service will trigger up to 5 other attempts, every 10 seconds.
* @return <code>true</code> if remote logging was initialized successfully
* @see #shutdown(boolean)
*/
public boolean initRemoteLogging(ORB orb, Manager manager, int managerHandle, boolean retry)
{
return internalInitRemoteLogging(orb, manager, managerHandle, retry);
}
/**
* This method is broken out from {@link #initRemoteLogging(ORB, Manager, int, boolean)} to allow mock implementation by tests
* without a functional manager object. Note that module acsjlog comes before jmanager.
* <p>
* Since ACS 8.0.1 we use an ACS-specific subtype of {@link LogOperations} to avoid the marshalling to Corba Any.
*/
protected AcsLogServiceOperations getLogService(Manager manager, int managerHandle) throws ComponentNotAlreadyActivatedEx, CannotGetComponentEx, NoPermissionEx, ComponentConfigurationNotFoundEx {
return AcsLogServiceHelper.narrow(manager.get_service(managerHandle, logServiceName, true));
}
/**
* Get the log service from the name server. It is used by ACS services that do
* not log into the manager and therefore can't use {@link #getLogService(Manager, int)}.
*
* @param orb The not <code>null</code> ORB
* @return The log service
* @throws InvalidName
*/
protected AcsLogServiceOperations getLogServiceFromNameSarvice(ORB orb) throws InvalidName {
if (orb==null) {
throw new IllegalArgumentException("The ORB can't be null!");
}
org.omg.CORBA.Object obj = orb.resolve_initial_references("NameService");
NamingContext context = NamingContextExtHelper.narrow(obj);
if (context==null) {
throw new NullPointerException("Got a null NamingContext");
}
try {
// try naming service first
NameComponent[] nameComponent= new NameComponent[1];
nameComponent[0]=new NameComponent("Log","");
org.omg.CORBA.Object logObj = context.resolve(nameComponent);
return AcsLogServiceHelper.narrow(logObj);
} catch (Throwable th) {
System.out.println("Failed to obtain alarm service reference from naming service, trying corbaloc...");
}
String corbaloc = "corbaloc::" + ACSPorts.getIP() + ":" + ACSPorts.getLogPort()+"/Log";
org.omg.CORBA.Object logObj = orb.string_to_object(corbaloc);
return AcsLogServiceHelper.narrow(logObj);
}
/**
* Suppresses remote logging. If log messages destined for remote logging have not been sent to the central log
* service yet (e.g. because {@link #initRemoteLogging(ORB, Manager, int, boolean) initRemoteLogging} has not been
* called, or because of sending failures), these log messages will be lost for remote logging. Log messages
* produced after this call will not even be queued for remote logging.
* <p>
* This method should only be called by special ALMA applications such as the Observation Preparation tool, which
* runs stand-alone, with all containers, managers, etc. in one JVM. In this case, no central logger is available,
* and all loggers which normally send their output to the central logger should be limited to local logging
* (stdout).
* <p>
* It is possible (probably not useful in real life) to re-enable remote logging later, by calling
* <code>initRemoteLogging</code>.
*/
public void suppressRemoteLogging() {
logQueueLock.lock();
try {
System.out.println("suppressRemoteLogging called");
disableRemoteLogging();
logQueue = null;
} finally {
logQueueLock.unlock();
}
}
/**
* Allows to suppress remote logging of Corba/ORB logger(s). Internally this suppression is handled using log level
* changes that cannot be undone by other log level changes. Generally remote logging remains enabled though, which
* makes this method quite different from {@linkplain #suppressRemoteLogging()}.
* <p>
* <strong>Application code such as components must not call this method!</strong>
* It is a very specialized feature provided for the ArchiveLogger infrastructural component.
* <p>
* TODO: Currently we do not distinguish between ORB loggers and other framework loggers.
* This comes from the now wider use of {@link #getLoggerForCorba(String, boolean)}, which initially
* was meant only for ORB loggers as the name still suggests.
* It is not a problem at the moment though, because the archiveLogger component is the only known user,
* and it does not use any other frameworks whose logs we intercept.
*/
public void suppressCorbaRemoteLogging() {
suppressCorbaRemoteLogging = true;
synchronized (loggers) {
for (AcsLoggerInfo loggerInfo : loggers.values()) {
if (loggerInfo.loggerOwnerType == LoggerOwnerType.FrameworkLogger) {
sharedLogConfig.setAndLockMinLogLevel(AcsLogLevelDefinition.OFF, loggerInfo.logger.getLoggerName());
}
}
}
}
/**
* Gets a logger to be used by ORB and POA classes, or by hibernate or another 3rd party framework whose logging we redirect.
* The logger is connected to the central ACS logger.
* <p>
* @TODO rename this method to accommodate non-corba frameworks into which we insert ACS loggers, such as hibernate,
* see {@link org.slf4j.impl.ACSLoggerFactory}.
* <p>
* For hibernate and scxml loggers, the logger automatically receives an initial custom log level configuration,
* to avoid jamming the log system with hibernate or scxml logs.
* The applied custom log level is the maximum of the default log level and WARNING.
* Note that the hibernate/scxml logger can still be set to a more verbose level by giving it a custom log config
* in the CDB, or dynamically using logLevelGUI etc.
* <p>
* @TODO Instead of this hard coded and probably confusing application of a custom log level,
* the CDB should offer a central configuration option for all jacorb, hibernate, scxml etc loggers,
* independently of the process (container or manager etc).
* <p>
* @param frameworkName
* e.g. <code>jacorb</code> or <code>laser</code>.
* @param autoConfigureContextName
* if true, the context (e.g. container name) will be appended to this logger's name as soon as it is
* available, changing the logger name to something like <code>jacorb@frodoContainer</code>.
*/
public AcsLogger getLoggerForCorba(String frameworkName, boolean autoConfigureContextName) {
String loggerName = frameworkName;
AcsLogger frameworkLogger = null;
processNameLock.lock();
try {
// if the process name is not known yet (e.g. during startup), then we need to schedule its update
if (autoConfigureContextName && processName != null) {
// if the process name is already known, we can even use it for the regular logger name instead of using a later workaround
loggerName += "@" + processName;
}
frameworkLogger = getAcsLogger(loggerName, LoggerOwnerType.FrameworkLogger);
if (frameworkName.equals(JacorbLoggerHelper.JACORB_LOGGER_NAME)) {
// Suppress logs inside the call to the Log service, which could happen e.g. when policies are set and jacorb-debug is enabled.
// As of ACS 8, that trashy log message would be "get_policy_overrides returns 1 policies"
frameworkLogger.addIgnoreLogs("org.omg.DsLogAdmin._LogStub", "write_records");
frameworkLogger.addIgnoreLogs("alma.Logging._AcsLogServiceStub", "write_records");
frameworkLogger.addIgnoreLogs("alma.Logging._AcsLogServiceStub", "writeRecords");
}
if (autoConfigureContextName && processName == null) {
// mark this logger for process name update
AcsLoggerInfo loggerInfo = loggers.get(loggerName);
loggerInfo.needsProcessNameUpdate = true;
}
}
finally {
processNameLock.unlock();
}
// fix levels if we suppress corba remote logging
if (suppressCorbaRemoteLogging) {
// TODO: Skip this for non-ORB framework loggers, see comment at #suppressCorbaRemoteLogging()
sharedLogConfig.setAndLockMinLogLevel(AcsLogLevelDefinition.OFF, loggerName);
}
else if (!sharedLogConfig.hasCustomConfig(loggerName)) {
// In the absence of their own custom logger config, some very verbose loggers
// get a minimum log level applied to ensure that a carelessly set low default log level
// does not swamp the system with log messages.
AcsLogLevelDefinition minCustomLevel = null;
if (frameworkName.startsWith(HibernateLoggerHelper.HIBERNATE_LOGGER_NAME_PREFIX) ||
frameworkName.startsWith(HibernateLoggerHelper.HIBERNATE_SQL_LOGGER_NAME_PREFIX) ||
frameworkName.startsWith(CommonsLoggingFactory.SCXML_LOGGER_NAME_PREFIX)) {
minCustomLevel = AcsLogLevelDefinition.WARNING;
}
else if (frameworkName.equals(JacorbLoggerHelper.JACORB_LOGGER_NAME)) {
// jacorb loggers have a special property that we can use as default,
// instead of blindly going for WARNING like we do with the other loggers.
AcsLogLevelDefinition level = AcsJacorbLoggingInitializer.getLogLevelFromJacorbVerbosity();
if (level != null) {
minCustomLevel = level;
}
else {
minCustomLevel = AcsLogLevelDefinition.WARNING;
m_internalLogger.info("Failed to consider jacorb log level property, will use only ACS log level mechanisms.");
}
}
// else... we currently do not give special treatment to Log4jFactory#LASER_LOGGER_NAME_PREFIX loggers
if (minCustomLevel != null) {
// We are dealing with one of the known framework loggers, and it has no other custom log level set.
// We apply as custom level the maximum of minCustomLevel and the process default level.
//
// TODO: These default values currently get lost when calling LoggingConfigurable#refresh_logging_config().
AcsLogLevelDefinition customLevel =
( minCustomLevel.compareTo(sharedLogConfig.getDefaultMinLogLevel()) > 0
? minCustomLevel
: sharedLogConfig.getDefaultMinLogLevel() );
sharedLogConfig.setMinLogLevel(customLevel, loggerName);
AcsLogLevelDefinition customLevelLocal =
( minCustomLevel.compareTo(sharedLogConfig.getDefaultMinLogLevelLocal()) > 0
? minCustomLevel
: sharedLogConfig.getDefaultMinLogLevelLocal() );
sharedLogConfig.setMinLogLevelLocal(customLevelLocal, loggerName);
m_internalLogger.info("Logger " + loggerName + " created with custom log levels local=" + customLevelLocal.name +
", remote=" + customLevel.name + " to avoid log jams due to careless default log level settings.");
}
}
return frameworkLogger;
}
/**
* Gets a logger to be used by the Java container classes. The logger is connected to the central ACS logger.
*/
public AcsLogger getLoggerForContainer(String containerName) {
setProcessName(containerName);
return getAcsLogger(containerName, LoggerOwnerType.ContainerLogger);
}
/**
* Gets a logger object to be used by a component.
* <p>
* This method is not supposed to be accessed directly in the component implementation.
* Instead, the implementation of <code>alma.acs.container.ContainerServices</code>
* should call this method.
*
* @param componentName component name (sufficiently qualified to be unique in the system or log domain)
* @return AcsLogger
*/
public AcsLogger getLoggerForComponent(String componentName) {
return getAcsLogger(componentName, LoggerOwnerType.ComponentLogger);
}
/**
* Gets a logger object from any type.
*
* @param loggerName name of the logger whose retrieval is wanted
* @return AcsLogger if logger is found, null otherwise
*/
public AcsLogger getLoggerByName(String loggerName) {
AcsLoggerInfo loggerInfo = null;
try {
loggerInfo = loggers.get(loggerName);
return loggerInfo.logger;
} catch (Exception ex) {
return null;
}
}
/**
* Gets a logger for an application (which is not an ACS component itself), e.g. a GUI application using the ACS ComponentClient.
* @param loggerName the logger name, should identify the application or the particular logger that gets requested.
* @param enableRemoteLogging if true (generally recommended), the returned logger is set up to send the logs to the remote log service,
* as it always happens for container and component loggers.
* <emph>This will only work if {@link #initRemoteLogging(ORB, Manager, int, boolean)} is also called,
* which happens automatically in <code>ComponentClient</code>.
* For a standalone application that is not ACS-aware (no manager known etc), remote logging is not available
* even with enableRemoteLogging == true.</emph>
* @return a configured Logger
*/
public AcsLogger getLoggerForApplication(String loggerName, boolean enableRemoteLogging)
{
if (loggerName == null || loggerName.trim().length() == 0) {
loggerName = "unknownClientApplication";
}
else {
setProcessName(loggerName);
}
AcsLogger logger = null;
if (enableRemoteLogging) {
logger = getAcsLogger(loggerName, LoggerOwnerType.OtherLogger);
}
else {
logger = AcsLogger.createUnconfiguredLogger(loggerName, null);
logger.setUseParentHandlers(false);
addLocalHandler(logger);
logger.configureLevels(sharedLogConfig.getNamedLoggerConfig(loggerName));
}
return logger;
}
/**
* Takes the process name and overwrites previous names,
* and updates all Loggers which are waiting to get their overly simple name enriched.
* The new name will be the old name + @ + processName.
* <p>
* For example, the Java container first uses a logger from which the process name "AcsContainerRunner"
* is derived. Only later when the the real container name has been parsed out, this gets renamed.
* <p>
* The update mechanism ensures that the process name will eventually be set also on loggers
* which were created before the process name was known, e.g. component logger created before container logger.
* @TODO check if we still need the process name appended to the logger name, now that we have a separate field for it in AcsLogger.
*
* @param name
*/
private void setProcessName(String newProcessName) {
if (newProcessName == null) {
// just ignore this call
return;
}
if (!newProcessName.equals(this.processName)) {
processNameLock.lock();
try {
String oldProcessName = this.processName;
this.processName = newProcessName;
synchronized (loggers) {
for (String oldLoggerName : new ArrayList<String>(loggers.keySet())) { // iterate over copy to avoid ConcurrentModif.Ex
AcsLoggerInfo loggerInfo = loggers.get(oldLoggerName);
if (loggerInfo.needsProcessNameUpdate) {
String newLoggerName = oldLoggerName + "@" + newProcessName;
loggerInfo.logger.setLoggerName(newLoggerName);
loggerInfo.logger.setProcessName(newProcessName);
loggerInfo.needsProcessNameUpdate = false;
AcsLoggerInfo gonner = loggers.put(newLoggerName, loggerInfo);
if (gonner != null) {
m_internalLogger.info("Process name update on logger '" + newLoggerName + "' overwrote an existing logger. This should be reported to the ACS developers.");
}
loggers.remove(oldLoggerName);
}
}
}
m_internalLogger.finer("Changed processName='" + oldProcessName + "' to '" + newProcessName + "' and updated logger names.");
} finally {
processNameLock.unlock();
}
}
}
/**
* Because of build order problems (acsjlog before jcont), we cannot pass <code>ContainerServices</code> or some
* other alarm API to <code>ClientLogManager</code>. Instead we define the alarm methods here and let the user
* delegate to the underlying alarm API.
* @see ClientLogManager#enableLoggingAlarms(LogAlarmHandler)
*/
public static interface LogAlarmHandler
{
public void raiseAlarm(String faultFamily, String faultMember, int faultCode) throws AcsJCouldntPerformActionEx;
public void clearAlarm(String faultFamily, String faultMember, int faultCode) throws AcsJCouldntPerformActionEx;
}
/**
* Enables alarms to be raised by the logging classes.
* As of ACS 9.1, there is only one type of alarm, when the log throttle gets activated and logs are lost.
* @param logAlarmHandler A user-supplied callback that actually raises and clears alarms,
* so that the logging classes do not get a dependency on the alarm system API.
* @since ACS 9.1
*/
public void enableLoggingAlarms(LogAlarmHandler logAlarmHandler) {
this.logAlarmHandler = logAlarmHandler;
}
/**
* Shuts down remote ACS logging and nulls the singleton instance.
* The loggers can still be used, but will only log locally.
* @param wait
*/
public void shutdown(boolean wait) {
if (DEBUG) {
System.out.println("ClientLogManager#shutdown(" + wait + ") called.");
}
// clean up remote logging, if it's still active
logQueueLock.lock();
try {
if (logQueue != null) {
// stop adding more log records for remote logging
disableRemoteLogging();
if (wait) {
flushAll();
} else {
// trigger one last flush, which may itself attempt to trigger more flushes,
// but only until shutDown prohibits further scheduling.
Future<Boolean> flushFuture = logQueue.flush();
// wait at most 200 milliseconds, to give the flush a chance to reach the central log service.
// we don't care about the result.
try {
flushFuture.get(200, TimeUnit.MILLISECONDS);
} catch (Exception e) {
e.printStackTrace();
}
}
logQueue.shutDown();
logQueue = null;
}
// junit classloaders don't manage to reload this class between tests, so we explicitly null the instance
s_instance = null;
// todo: check if we should release the Log service with the manager
// (probably not since currently it doesn't even require us to be logged in to access the Log service).
} finally {
logQueueLock.unlock();
}
}
/**
* Flushes the remote log queue completely and returns only when it's done.
*/
public void flushAll()
{
if (logQueue != null) {
logQueue.flushAllAndWait();
}
}
/**
* Callback that gets notified about throttle actions and raises/clears throttle alarms,
* using {@link ClientLogManager#logAlarmHandler}.
* <p>
* @TODO: Ensure that alarm gets cleared when container (or other throttled process) is shut down.
* Not clear yet if we want to do the same for crashes (though rare in Java).
* Thus maybe enough to add a shutdown method here, which gets called from {@link ClientLogManager#shutdown(boolean)},
* which clears the alarm and removes log throttling.
*/
private class ThrottleCallback implements LogThrottle.ThrottleCallback {
public static final String throttleAlarmFF = "Logging";
/**
* The FM is the container name (=process name), which becomes available to ClientLogManager only later on.
*/
private String throttleAlarmFM;
public static final int throttleAlarmFC = 10;
private final AtomicLong suppressedLocalLogCount = new AtomicLong();
private final AtomicLong suppressedRemoteLogCount = new AtomicLong();
public void suppressedLog(boolean remoteLog) {
boolean firstSuppressed = (suppressedLocalLogCount.get() + suppressedRemoteLogCount.get() == 0);
if (remoteLog) {
suppressedRemoteLogCount.incrementAndGet();
}
else {
suppressedLocalLogCount.incrementAndGet();
}
if (firstSuppressed) {
// raise alarm
if (logAlarmHandler != null && processName != null) {
// the processName should not change any more once it is != null, but just for safety, this copy will ensure
// that we later clear the alarm using the same FM, even if the process name changes in between calls.
throttleAlarmFM = processName;
try {
logAlarmHandler.raiseAlarm(throttleAlarmFF, throttleAlarmFM, throttleAlarmFC);
// If alarm fails, we skip the following log, but instead get a severe/warning log on all paths that also mention the throttle kicking in.
m_internalLogger.fine("Log throttle kicked in, suppressing logs. Alarm has been raised."); // TODO type-safe log
} catch (AcsJCouldntPerformActionEx ex) {
m_internalLogger.severe("Failed to publish alarm about dropping logs in the log throttle.");
}
}
else {
m_internalLogger.warning("Cannot raise alarm about log throttle action because alarm callback or process name is not yet available.");
}
}
}
public void clearedLogSuppression() {
// @TODO: somehow say (alarm property, or only log?) how many logs were suppressed by the throttle,
// based on suppressedLocalLogCount, suppressedRemoteLogCount
suppressedLocalLogCount.set(0);
suppressedRemoteLogCount.set(0);
if (logAlarmHandler != null) {
try {
// this method will not be called more than once a second because only a new time interval can "free" the logs
// (see LogThrottle.LogStreamThrottle#intervalLengthMillis),
// so that we don't have to worry about raising/clearing this alarm on a very short time scale.
logAlarmHandler.clearAlarm(throttleAlarmFF, throttleAlarmFM, throttleAlarmFC);
m_internalLogger.fine("Log throttle disengaged. Alarm has been cleared."); // TODO type-safe log
} catch (AcsJCouldntPerformActionEx ex) {
m_internalLogger.severe("Failed to clear alarm about dropping logs in the log throttle."); // TODO type-safe log
}
}
}
}
}