logging as of 2022-02-01
Change-Id: I63c681bea9553cd822b214075b163ca6c42fe0cc
This commit is contained in:
127
secop/logging.py
Normal file
127
secop/logging.py
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# *****************************************************************************
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU General Public License as published by the Free Software
|
||||||
|
# Foundation; either version 2 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
#
|
||||||
|
# This program 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 General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
# Module authors:
|
||||||
|
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||||
|
#
|
||||||
|
# *****************************************************************************
|
||||||
|
|
||||||
|
import os
|
||||||
|
from os.path import dirname, join
|
||||||
|
import mlzlog
|
||||||
|
from secop.lib import getGeneralConfig
|
||||||
|
|
||||||
|
|
||||||
|
OFF = 99
|
||||||
|
|
||||||
|
LOG_LEVELS = dict(mlzlog.LOGLEVELS, off=OFF)
|
||||||
|
LEVEL_NAMES = {v: k for k, v in LOG_LEVELS.items()}
|
||||||
|
log = None
|
||||||
|
rootlogdir = None
|
||||||
|
|
||||||
|
|
||||||
|
def checkLevel(level):
|
||||||
|
try:
|
||||||
|
if isinstance(level, str):
|
||||||
|
return LOG_LEVELS[level.lower()]
|
||||||
|
if level in LEVEL_NAMES:
|
||||||
|
return level
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
raise ValueError('%r is not a valid level' % level)
|
||||||
|
|
||||||
|
|
||||||
|
def initLogging(loglevel='info'):
|
||||||
|
global log, rootlogdir # pylint: disable-global-statement
|
||||||
|
|
||||||
|
loglevel = checkLevel(loglevel)
|
||||||
|
genConfig = getGeneralConfig()
|
||||||
|
rootname = genConfig.get('rootname', 'secop')
|
||||||
|
logdir = genConfig.get('logdir')
|
||||||
|
rootlogdir = join(logdir, rootname)
|
||||||
|
mlzlog.initLogging(rootname, 'debug', logdir)
|
||||||
|
for hdl in mlzlog.log.handlers:
|
||||||
|
hdl.setLevel(loglevel)
|
||||||
|
return mlzlog.log
|
||||||
|
|
||||||
|
|
||||||
|
class ComlogHandler(mlzlog.LogfileHandler):
|
||||||
|
"""handler for logging communication
|
||||||
|
|
||||||
|
communication is
|
||||||
|
"""
|
||||||
|
|
||||||
|
def format(self, record):
|
||||||
|
return '%s %s' % (self.formatter.formatTime(record), record.getMessage())
|
||||||
|
|
||||||
|
def doRollover(self):
|
||||||
|
super().doRollover()
|
||||||
|
max_days = getGeneralConfig().get('comlog_days', 31)
|
||||||
|
# keep only the last max_days files
|
||||||
|
with os.scandir(dirname(self.baseFilename)) as it:
|
||||||
|
files = sorted(entry.path for entry in it if entry.name != 'current')
|
||||||
|
for filepath in files[-max_days:]:
|
||||||
|
os.remove(filepath)
|
||||||
|
|
||||||
|
|
||||||
|
def add_comlog_handler(modobj):
|
||||||
|
global rootlogdir # pylint: disable-global-statement
|
||||||
|
comlog = getGeneralConfig().get('comlog')
|
||||||
|
if comlog:
|
||||||
|
comlog = join(rootlogdir, comlog)
|
||||||
|
modobj.log.addHandler(ComlogHandler(comlog, modobj.name))
|
||||||
|
|
||||||
|
|
||||||
|
class RemoteLogHandler(mlzlog.Handler):
|
||||||
|
"""handler for remote logging"""
|
||||||
|
def __init__(self, modobj):
|
||||||
|
super().__init__()
|
||||||
|
self.subscriptions = {} # dict [conn] of level
|
||||||
|
self.modobj = modobj
|
||||||
|
self.modobj.log.addHandler(self)
|
||||||
|
self.used_by = set()
|
||||||
|
|
||||||
|
def handle(self, record, name=None):
|
||||||
|
result = False
|
||||||
|
for conn, lev in self.subscriptions.items():
|
||||||
|
if record.levelno >= lev:
|
||||||
|
msg = record.getMessage()
|
||||||
|
if self.modobj.DISPATCHER.send_log_msg(
|
||||||
|
conn, name or self.modobj.name, LEVEL_NAMES[record.levelno], msg):
|
||||||
|
result = True
|
||||||
|
if result:
|
||||||
|
return True
|
||||||
|
for master in self.used_by:
|
||||||
|
# this is an iodev, try to handle by one of our masters
|
||||||
|
if master.remoteLogHandler.handle(record, self.modobj.name):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def set_conn_level(self, conn, level):
|
||||||
|
level = checkLevel(level)
|
||||||
|
if level == mlzlog.DEBUG:
|
||||||
|
iodev = getattr(self.modobj, '_iodev', None)
|
||||||
|
if iodev:
|
||||||
|
# we want also to see debug messages of iodev
|
||||||
|
if iodev.remoteLogHandler is None:
|
||||||
|
iodev.remoteLogHandler = RemoteLogHandler(self)
|
||||||
|
iodev.remoteLogHandler.used_by.add(self.modobj)
|
||||||
|
level = checkLevel(level)
|
||||||
|
if level == OFF:
|
||||||
|
self.subscriptions.pop(conn, None)
|
||||||
|
else:
|
||||||
|
self.subscriptions[conn] = level
|
||||||
@@ -31,13 +31,21 @@ from secop.datatypes import ArrayOf, BoolType, EnumType, FloatRange, \
|
|||||||
IntRange, StatusType, StringType, TextType, TupleOf
|
IntRange, StatusType, StringType, TextType, TupleOf
|
||||||
from secop.errors import BadValueError, ConfigError, \
|
from secop.errors import BadValueError, ConfigError, \
|
||||||
ProgrammingError, SECoPError, SilentError, secop_error
|
ProgrammingError, SECoPError, SilentError, secop_error
|
||||||
from secop.lib import formatException, mkthread
|
from secop.lib import formatException, mkthread, UniqueObject
|
||||||
from secop.lib.enum import Enum
|
from secop.lib.enum import Enum
|
||||||
from secop.params import Accessible, Command, Parameter
|
from secop.params import Accessible, Command, Parameter
|
||||||
from secop.poller import BasicPoller, Poller
|
from secop.poller import BasicPoller, Poller
|
||||||
from secop.properties import HasProperties, Property
|
from secop.properties import HasProperties, Property
|
||||||
|
from secop.logging import RemoteLogHandler, add_comlog_handler
|
||||||
|
|
||||||
Done = object() #: a special return value for a read/write function indicating that the setter is triggered already
|
|
||||||
|
class DoneClass:
|
||||||
|
@classmethod
|
||||||
|
def __repr__(cls):
|
||||||
|
return 'Done'
|
||||||
|
|
||||||
|
|
||||||
|
Done = UniqueObject('Done')
|
||||||
|
|
||||||
|
|
||||||
class HasAccessibles(HasProperties):
|
class HasAccessibles(HasProperties):
|
||||||
@@ -124,20 +132,20 @@ class HasAccessibles(HasProperties):
|
|||||||
|
|
||||||
def wrapped_rfunc(self, pname=pname, rfunc=rfunc):
|
def wrapped_rfunc(self, pname=pname, rfunc=rfunc):
|
||||||
if rfunc:
|
if rfunc:
|
||||||
self.log.debug("calling %r" % rfunc)
|
self.log.debug("call read_%s" % pname)
|
||||||
try:
|
try:
|
||||||
value = rfunc(self)
|
value = rfunc(self)
|
||||||
self.log.debug("rfunc(%s) returned %r" % (pname, value))
|
self.log.debug("read_%s returned %r" % (pname, value))
|
||||||
if value is Done: # the setter is already triggered
|
if value is Done: # the setter is already triggered
|
||||||
return getattr(self, pname)
|
return getattr(self, pname)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.debug("rfunc(%s) failed %r" % (pname, e))
|
self.log.debug("read_%s failed %r" % (pname, e))
|
||||||
self.announceUpdate(pname, None, e)
|
self.announceUpdate(pname, None, e)
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
# return cached value
|
# return cached value
|
||||||
self.log.debug("rfunc(%s): return cached value" % pname)
|
|
||||||
value = self.accessibles[pname].value
|
value = self.accessibles[pname].value
|
||||||
|
self.log.debug("return cached %s = %r" % (pname, value))
|
||||||
setattr(self, pname, value) # important! trigger the setter
|
setattr(self, pname, value) # important! trigger the setter
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@@ -158,16 +166,19 @@ class HasAccessibles(HasProperties):
|
|||||||
if not wrapped:
|
if not wrapped:
|
||||||
|
|
||||||
def wrapped_wfunc(self, value, pname=pname, wfunc=wfunc):
|
def wrapped_wfunc(self, value, pname=pname, wfunc=wfunc):
|
||||||
self.log.debug("check validity of %s = %r" % (pname, value))
|
|
||||||
pobj = self.accessibles[pname]
|
pobj = self.accessibles[pname]
|
||||||
value = pobj.datatype(value)
|
|
||||||
if wfunc:
|
if wfunc:
|
||||||
self.log.debug('calling %s %r(%r)' % (wfunc.__name__, wfunc, value))
|
self.log.debug("check and call write_%s(%r)" % (pname, value))
|
||||||
|
value = pobj.datatype(value)
|
||||||
returned_value = wfunc(self, value)
|
returned_value = wfunc(self, value)
|
||||||
|
self.log.debug('write_%s returned %r' % (pname, returned_value))
|
||||||
if returned_value is Done: # the setter is already triggered
|
if returned_value is Done: # the setter is already triggered
|
||||||
return getattr(self, pname)
|
return getattr(self, pname)
|
||||||
if returned_value is not None: # goodie: accept missing return value
|
if returned_value is not None: # goodie: accept missing return value
|
||||||
value = returned_value
|
value = returned_value
|
||||||
|
else:
|
||||||
|
self.log.debug("check %s = %r" % (pname, value))
|
||||||
|
value = pobj.datatype(value)
|
||||||
setattr(self, pname, value)
|
setattr(self, pname, value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@@ -176,11 +187,17 @@ class HasAccessibles(HasProperties):
|
|||||||
setattr(cls, 'write_' + pname, wrapped_wfunc)
|
setattr(cls, 'write_' + pname, wrapped_wfunc)
|
||||||
wrapped_wfunc.__wrapped__ = True
|
wrapped_wfunc.__wrapped__ = True
|
||||||
|
|
||||||
# check information about Command's
|
# check for programming errors
|
||||||
for attrname in cls.__dict__:
|
for attrname in cls.__dict__:
|
||||||
if attrname.startswith('do_'):
|
prefix, _, pname = attrname.partition('_')
|
||||||
|
if not pname:
|
||||||
|
continue
|
||||||
|
if prefix == 'do':
|
||||||
raise ProgrammingError('%r: old style command %r not supported anymore'
|
raise ProgrammingError('%r: old style command %r not supported anymore'
|
||||||
% (cls.__name__, attrname))
|
% (cls.__name__, attrname))
|
||||||
|
elif prefix in ('read', 'write') and not isinstance(accessibles.get(pname), Parameter):
|
||||||
|
raise ProgrammingError('%s.%s defined, but %r is no parameter'
|
||||||
|
% (cls.__name__, attrname, pname))
|
||||||
|
|
||||||
res = {}
|
res = {}
|
||||||
# collect info about properties
|
# collect info about properties
|
||||||
@@ -434,6 +451,10 @@ class Module(HasAccessibles):
|
|||||||
errors.append('%s: %s' % (aname, e))
|
errors.append('%s: %s' % (aname, e))
|
||||||
if errors:
|
if errors:
|
||||||
raise ConfigError(errors)
|
raise ConfigError(errors)
|
||||||
|
self.remoteLogHandler = None
|
||||||
|
self._earlyInitDone = False
|
||||||
|
self._initModuleDone = False
|
||||||
|
self._startModuleDone = False
|
||||||
|
|
||||||
# helper cfg-editor
|
# helper cfg-editor
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
@@ -579,6 +600,11 @@ class Module(HasAccessibles):
|
|||||||
started_callback()
|
started_callback()
|
||||||
self.startModuleDone = True
|
self.startModuleDone = True
|
||||||
|
|
||||||
|
def setRemoteLogging(self, conn, level):
|
||||||
|
if self.remoteLogHandler is None:
|
||||||
|
self.remoteLogHandler = RemoteLogHandler(self)
|
||||||
|
self.remoteLogHandler.set_conn_level(conn, level)
|
||||||
|
|
||||||
|
|
||||||
class Readable(Module):
|
class Readable(Module):
|
||||||
"""basic readable module"""
|
"""basic readable module"""
|
||||||
@@ -697,6 +723,10 @@ class Drivable(Writable):
|
|||||||
class Communicator(Module):
|
class Communicator(Module):
|
||||||
"""basic abstract communication module"""
|
"""basic abstract communication module"""
|
||||||
|
|
||||||
|
def initModule(self):
|
||||||
|
super().initModule()
|
||||||
|
add_comlog_handler(self)
|
||||||
|
|
||||||
@Command(StringType(), result=StringType())
|
@Command(StringType(), result=StringType())
|
||||||
def communicate(self, command):
|
def communicate(self, command):
|
||||||
"""communicate command
|
"""communicate command
|
||||||
@@ -708,7 +738,7 @@ class Communicator(Module):
|
|||||||
|
|
||||||
|
|
||||||
class Attached(Property):
|
class Attached(Property):
|
||||||
"""a special property, defining an attached modle
|
"""a special property, defining an attached module
|
||||||
|
|
||||||
assign a module name to this property in the cfg file,
|
assign a module name to this property in the cfg file,
|
||||||
and the server will create an attribute with this module
|
and the server will create an attribute with this module
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ class Dispatcher:
|
|||||||
self._connections.remove(conn)
|
self._connections.remove(conn)
|
||||||
for _evt, conns in list(self._subscriptions.items()):
|
for _evt, conns in list(self._subscriptions.items()):
|
||||||
conns.discard(conn)
|
conns.discard(conn)
|
||||||
|
self.set_all_log_levels(conn, 'off')
|
||||||
self._active_connections.discard(conn)
|
self._active_connections.discard(conn)
|
||||||
|
|
||||||
def register_module(self, moduleobj, modulename, export=True):
|
def register_module(self, moduleobj, modulename, export=True):
|
||||||
@@ -375,11 +376,33 @@ class Dispatcher:
|
|||||||
# XXX: also check all entries in self._subscriptions?
|
# XXX: also check all entries in self._subscriptions?
|
||||||
return (DISABLEEVENTSREPLY, None, None)
|
return (DISABLEEVENTSREPLY, None, None)
|
||||||
|
|
||||||
|
def send_log_msg(self, conn, modname, level, msg):
|
||||||
|
"""send log message """
|
||||||
|
if conn in self._connections:
|
||||||
|
conn.send_reply(('log', '%s:%s' % (modname, level), msg))
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def set_all_log_levels(self, conn, level):
|
||||||
|
for modobj in self._modules.values():
|
||||||
|
modobj.setRemoteLogging(conn, level)
|
||||||
|
|
||||||
|
def handle_logging(self, conn, specifier, level):
|
||||||
|
if specifier and specifier != '.':
|
||||||
|
modobj = self._modules[specifier]
|
||||||
|
iodev = getattr(modobj, '_iodev', None)
|
||||||
|
if iodev and iodev.remoteLogHandler is None:
|
||||||
|
iodev.setRemoteLogging(conn, 'off')
|
||||||
|
iodev.remoteLogHandler.used_by.add(modobj)
|
||||||
|
modobj.setRemoteLogging(conn, level)
|
||||||
|
else:
|
||||||
|
self.set_all_log_levels(conn, level)
|
||||||
|
return 'logging', specifier, level
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
for conn in self._connections:
|
for conn in self._connections:
|
||||||
try:
|
try:
|
||||||
# - may be used for the 'closed' message in serial interface
|
# - may be used for the 'closed' message in serial interface
|
||||||
# - is used in frappy history for indicating the close time
|
|
||||||
conn.close_message((ERRORCLOSED, None, None))
|
conn.close_message((ERRORCLOSED, None, None))
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
|||||||
Reference in New Issue
Block a user