""" Logging Root """ from __future__ import print_function from __future__ import absolute_import from __future__ import division __RCSID__ = "45ddde113 (2021-10-01 11:19:55 +0200) Chris Burr " import logging import time import sys import six from DIRAC.FrameworkSystem.private.standardLogging.LogLevels import LogLevels from DIRAC.FrameworkSystem.private.standardLogging.Logging import Logging from DIRAC.Resources.LogBackends.StdoutBackend import StdoutBackend from DIRAC.Core.Utilities import DIRACSingleton @six.add_metaclass(DIRACSingleton.DIRACSingleton) class LoggingRoot(Logging): """ LoggingRoot is a particular Logging object: the first parent of the chain. - It is the one that initializes the root logger of the standard logging library and it configures it - As it defines the default behaviour of the Logging objects, it needs a specific class - It is unique, there is one and only one parent at the top of the chain: this justifies the usage of a Singleton """ # Boolean preventing the LoggingRoot to be configured once more __configuredLogging = False def __init__(self): """ Initialization of the LoggingRoot object. LoggingRoot : - initialize the UTC time - set the correct level defines by the user, or the default - add the custom level to logging: verbose, notice, always - register a default backend (stdout): all messages will be displayed here - update the format according to the command line argument """ super(LoggingRoot, self).__init__() # this line removes some useless information from log records and improves the performances logging._srcfile = None # pylint: disable=protected-access # initialize the root logger, which turns out to be a child of root, and disable propagation # to avoid any conflicts with external libs that would use "logging" too self._logger = logging.getLogger("dirac") self._logger.propagate = False # here we redefine the custom name to the empty string to remove the "\" in the display self._customName = "" # initialization of levels levels = LogLevels.getLevels() for level in levels: logging.addLevelName(levels[level], level) # root Logger level is set to NOTICE by default self._logger.setLevel(LogLevels.NOTICE) # initialization of the UTC time # Actually, time.gmtime is equal to UTC time: it has its DST flag to 0 which means there is no clock advance logging.Formatter.converter = time.gmtime # initialization of the default backend # use the StdoutBackend directly to avoid dependancy loop with ObjectLoader self._addBackend(StdoutBackend) # configuration of the level and update the format self.__configureLevel() def initialize(self, systemName, cfgPath, forceInit=False): """ Configure the root Logging. It can be possible to : - attach it some backends : LogBackends = stdout,stderr,file,server - attach backend options : BackendOptions { FileName = /tmp/file.log } - add colors and the path of the call : LogColor = True, LogShowLine = True - precise a level : LogLevel = DEBUG :param str systemName: / :param str cfgPath: configuration path :param bool forceInit: Force the initialization even if it had already happened. This should not be used !! The only case is LocalConfiguration.enableCS In order to take into account extensions' backends """ # we have to put the import line here to avoid a dependancy loop from DIRAC import gConfig self._lockConfig.acquire() try: if not LoggingRoot.__configuredLogging or forceInit: Logging._componentName = systemName # prepare to remove all the backends from the root Logging as in the old gLogger. # store them in a list handlersToRemove. # we will remove them later, because some components as ObjectLoader need a backend. # this can be useful to have logs only in a file for instance. handlersToRemove = [] for backend in self._backendsList: handlersToRemove.append(backend.getHandler()) del self._backendsList[:] # get the backends, the backend options and add them to the root Logging desiredBackends = self.__getBackendsFromCFG(cfgPath) for backend in desiredBackends: desiredOptions = self.__getBackendOptionsFromCFG(cfgPath, backend) self.registerBackend(desiredOptions.get("Plugin", backend), desiredOptions) # Format options self._options["color"] = gConfig.getValue("%s/LogColor" % cfgPath, False) # Remove the old backends for handler in handlersToRemove: self._logger.removeHandler(handler) levelName = gConfig.getValue("%s/LogLevel" % cfgPath, None) if levelName is not None: self.setLevel(levelName) LoggingRoot.__configuredLogging = True finally: self._lockConfig.release() def __getBackendsFromCFG(self, cfgPath): """ Get backends from the configuration and register them in LoggingRoot. This is the new way to get the backends providing a general configuration. :param str cfgPath: configuration path """ # We have to put the import line here to avoid a dependancy loop from DIRAC.ConfigurationSystem.Client.Helpers.Operations import Operations from DIRAC import gConfig # get the second last string representing the component type in the configuration # example : 'Agents', 'Services' component = cfgPath.split("/")[-2] operation = Operations() # Search desired backends in the component desiredBackends = gConfig.getValue("%s/%s" % (cfgPath, "LogBackends"), []) if not desiredBackends: # Search desired backends in the operation section according to the # component type desiredBackends = operation.getValue("Logging/Default%sBackends" % component, []) if not desiredBackends: # Search desired backends in the operation section desiredBackends = operation.getValue("Logging/DefaultBackends", []) if not desiredBackends: # Default value desiredBackends = ["stdout"] return desiredBackends def __getBackendOptionsFromCFG(self, cfgPath, backend): """ Get backend options from the configuration. :param cfgPath: configuration path :param backend: backend identifier: stdout, file, f04 """ # We have to put the import lines here to avoid a dependancy loop from DIRAC import gConfig from DIRAC.ConfigurationSystem.Client.Helpers.Resources import getBackendConfig backendOptions = {} # Search backends config in the resources section retDictRessources = getBackendConfig(backend) if retDictRessources["OK"]: backendOptions = retDictRessources["Value"] # Search backends config in the component to update some options retDictConfig = gConfig.getOptionsDict("%s/%s/%s" % (cfgPath, "LogBackendsConfig", backend)) if retDictConfig["OK"]: backendOptions.update(retDictConfig["Value"]) return backendOptions def __configureLevel(self): """ Configure the log level of the root Logging according to the argv parameter It can be : -d, -dd, -ddd Work only for clients, scripts and tests Configuration/Client/LocalConfiguration manages services,agents and executors """ debLevs = 0 for arg in sys.argv: if arg.find("-d") == 0: debLevs += arg.count("d") if debLevs == 1: self.setLevel("verbose") elif debLevs == 2: self.setLevel("verbose") self.showHeaders(True) elif debLevs >= 3: self.setLevel("debug") self.showHeaders(True) self.showThreadIDs(True) def enableLogsFromExternalLibs(self): """ Enable the display of the logs coming from external libraries """ self.__enableLogsFromExternalLibs() def disableLogsFromExternalLibs(self): """ Disable the display of the logs coming from external libraries """ self.__enableLogsFromExternalLibs(False) @staticmethod def __enableLogsFromExternalLibs(isEnabled=True): """ Configure the root logger from 'logging' for an external library use. By default the root logger is configured with: - debug level, - stderr output - custom format close to the DIRAC format :param bool isEnabled: allows logs from external libs """ rootLogger = logging.getLogger() rootLogger.handlers = [] if isEnabled: logging.basicConfig( level=logging.DEBUG, format="%(asctime)s UTC ExternalLibrary/%(name)s %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) else: rootLogger.addHandler(logging.NullHandler())