source: main/waeup.sirp/trunk/src/waeup/sirp/utils/logger.py @ 6578

Last change on this file since 6578 was 6578, checked in by uli, 13 years ago

Implement a new logging technique and use it.
Details are explained in waeup.sirp.utils.logger.

File size: 12.8 KB
RevLine 
[6371]1##
2## logging.py
3## Login : <uli@pu.smp.net>
4## Started on  Mon Jun 13 01:25:07 2011 Uli Fouquet
5## $Id$
6##
7## Copyright (C) 2011 Uli Fouquet
8## This program is free software; you can redistribute it and/or modify
9## it under the terms of the GNU General Public License as published by
10## the Free Software Foundation; either version 2 of the License, or
11## (at your option) any later version.
12##
13## This program is distributed in the hope that it will be useful,
14## but WITHOUT ANY WARRANTY; without even the implied warranty of
15## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16## GNU General Public License for more details.
17##
18## You should have received a copy of the GNU General Public License
19## along with this program; if not, write to the Free Software
20## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21##
22"""
23Convenience stuff for logging.
[6578]24
25Main component of :mod:`waeup.sirp.utils.logging` is a mix-in class
26:class:`waeup.sirp.utils.logging.Logger`. Classes derived (also) from
27that mix-in provide a `logger` attribute that returns a regular Python
28logger logging to a rotating log file stored in the datacenter storage
29path.
30
31Deriving components (classes) should set their own `logger_name` and
32`logger_filename` attribute.
33
34The `logger_name` tells under which name the logger should be
35registered Python-wise. This is usually a dotted name string like
36``waeup.sirp.${sitename}.mycomponent`` which should be unique. If you
37pick a name already used by another component, trouble is ahead. The
38``${sitename}`` chunk of the name can be set literally like this. The
39logger machinery will turn it into some real site name at time of
40logging.
41
42The `logger_filename` attribute tells how the logfile should be
43named. This should be some base filename like
44``mycomponent.log``. Please note, that some logfile names are already
45used: ``main.log``, ``applications.log``, and ``datacenter.log``.
46
47The `Logger` mix-in also cares for updating the logging handlers when
48a datacenter location moves. That means you do not have to write your
49own event handlers for the purpose. Just derive from `Logger`, set
50your `logger_name` and `logger_filename` attribute and off you go::
51
52  from waeup.sirp.utils.logger import Logger
53
54  class MyComponent(object, Logger):
55      # Yes that's a complete working class
56      logger_name = 'waeup.sirp.${sitename}.mycomponent
57      logger_filename = 'mycomponent.log'
58
59      def do_something(self):
60           # demomstrate how to use logging from methods
61           self.logger.info('About to do something')
62           try:
63               # Do something here
64           except IOError:
65               self.logger.warn('Something went wrong')
66               return
67           self.logger.info('I did it')
68
69As you can see from that example, methods of the class can log
70messages simply by calling `self.logger`.
71
72The datacenter and its storage are created automatically when you
73create a :class:`waeup.sirp.app.University`. This also means that
74logging with the `Logger` mix-in will work only inside so-called sites
75(`University` instances put into ZODB are such `sites`).
76
77Other components in this module help to make everything work.
[6371]78"""
79import os
80import grok
81import logging
[6578]82from string import Template
83from zope import schema
84from zope.component import queryUtility
85from zope.interface import Interface, Attribute, implements
86from waeup.sirp.interfaces import IDataCenter, IDataCenterStorageMovedEvent
[6371]87
[6578]88#: Default logfile size
89MAX_BYTES = 5 * 1024 ** 2
[6371]90
[6578]91#: Default num of backup files
92BACKUP_COUNT = 5
[6371]93
[6578]94class ILogger(Interface):
95    logger_name = schema.TextLine(
96        title = u'A Python logger name')
97    logger_filename = schema.TextLine(
98        title = u'A filename for the log file to use (basename)')
99    logger = Attribute("Get a Python logger instance already set up")
100    def logger_setup(logger):
101        """Setup a logger.
[6371]102
[6578]103        `logger` is an instance of :class:`logging.Logger`.
104        """
[6371]105
[6578]106    def logger_get_logfile_path():
107        """Get path to logfile used.
[6371]108
[6578]109        Return `None` if the file path cannot be computed.
110        """
[6371]111
[6578]112    def logger_get_logdir():
113        """Get the directory of the logfile.
[6371]114
[6578]115        Return `None` if the directory path cannot be computed.
116        """
[6399]117
[6578]118class Logger(object):
119    """Mixin-class that for logging support.
[6371]120
[6578]121    Classes that (also) inherit from this class provide support for
122    logging with waeup sites.
[6371]123
[6578]124    By default a `logger` attribute is provided which returns a
125    regular Python logger. This logger has already registered a file
126    rotating log handler that writes log messages to a file `main.log`
127    in datacenters ``log/`` directory. This is the main log file also
128    used by other components. Therefore you can pick another filename
129    by setting the `logger_filename` attribute.
[6371]130
[6578]131    All methods and attributes of this mix-in start with ``logger_``
132    in order not to interfere with already existing names of a class.
[6371]133
[6578]134    Method names do not follow the usual Zope habit (CamelCase) but
135    PEP8 convention (lower_case_with_underscores).
136    """
137
138    #: The filename to use when logging.
139    logger_filename = 'main.log'
140
141    #: The Python logger name used when
142    #: logging. ``'waeup.sirp.${sitename}'`` by default. You can use the
143    #: ``${sitename}`` placeholder in that string, which will be
144    #: replaced by the actual used site name.
145    logger_name = 'waeup.sirp.${sitename}'
146    implements(ILogger)
147
[6371]148    @property
149    def logger(self):
[6578]150        """Get a logger instance.
[6371]151
[6578]152        Returns a standard logger object as provided by :mod:`logging`
153        module from the standard library.
[6371]154
[6578]155        Other components can use this logger to perform log entries
156        into the logfile of this component.
[6371]157
[6578]158        The logger is initialized the first time it is called.
[6371]159
[6578]160        The logger is only available when used inside a site.
[6371]161
[6578]162        .. note:: The logger default level is
163                  :data:`logging.WARN`. Use
164                  :meth:`logging.Logger.setLevel` to set a different level.
165        """
166        site = grok.getSite()
167        sitename = '%s' % getattr(site, '__name__', None)
168        loggername = Template(self.logger_name).substitute(
169            dict(sitename=sitename))
170        logger = logging.getLogger(loggername)
171        if site is None:
172            return logger
173        if len(logger.handlers) != 1:
174            handlers = [x for x in logger.handlers]
175            for handler in handlers:
176                handler.flush()
177                handler.close()
178                logger.removeHandler(handler)
179            logger = self.logger_setup(logger)
180        return logger
[6371]181
[6578]182    def logger_setup(self, logger):
183        """Setup logger.
[6371]184
[6578]185        The logfile will be stored in the datacenter logs/ dir.
186        """
187        filename = self.logger_get_logfile_path()
188        if filename is None:
189            return
190        collector = queryUtility(ILoggerCollector)
191        if collector is not None:
192            site = grok.getSite()
193            collector.registerLogger(site, self)
[6371]194
[6578]195        # Create a rotating file handler logger.
196        handler = logging.handlers.RotatingFileHandler(
197            filename, maxBytes=MAX_BYTES, backupCount=BACKUP_COUNT)
198        formatter = logging.Formatter(
[6371]199            '%(asctime)s - %(levelname)s - %(message)s')
[6578]200        handler.setFormatter(formatter)
[6371]201
[6578]202        # Don't send log msgs to ancestors. This stops displaying
203        # logmessages on the commandline.
204        logger.propagate = False
205        logger.addHandler(handler)
206        return logger
[6399]207
[6578]208    def logger_get_logfile_path(self):
209        """Get the path to the logfile used.
[6399]210
[6578]211        Returns the path to a file in local sites datacenter ``log/``
212        directory (dependent on :meth:`logger_get_logdir`) and with
213        :attr:`logger_filename` as basename.
[6399]214
[6578]215        Override this method if you want a complete different
216        computation of the logfile path. If you only want a different
217        logfile name, set :attr:`logger_filename`. If you only want a
218        different path to the logfile override
219        :meth:`logger_get_logdir` instead.
220
221        Returns ``None`` if no logdir can be fetched.
222
223        .. note:: creates the logfile dir if it does not exist.
224
225        """
226        logdir = self.logger_get_logdir()
227        if logdir is None:
228            return None
229        return os.path.join(logdir, self.logger_filename)
230
231    def logger_get_logdir(self):
232        """Get log dir where logfile should be put.
233
234        Returns the path to the logfile directory. If no site is set,
235        ``None`` is returned. The same applies, if the site has no
236        datacenter.
237
238        If the dir dies not exist already it will be created. Only the
239        last part of the directory path will be created.
240        """
241        site = grok.getSite()
[6399]242        if site is None:
[6578]243            return None
244        datacenter = site.get('datacenter', None)
245        if datacenter is None:
246            return None
247        logdir = os.path.join(datacenter.storage, 'logs')
248        if not os.path.exists(logdir):
249            os.mkdir(logdir)
250        return logdir
[6399]251
[6578]252    def logger_logfile_changed(self):
253        """React on logfile location change.
254
255        If the logfile changed, we can set a different logfile. While
256        changing the logfile is a rather critical operation you might
257        not do often in production use, we have to cope with that
258        especially in tests.
259
260        What this method does by default (unless you override it):
261
262        - It fetches the current logger and
263
264          - Removes flushes, closes, and removes all handlers
265
266          - Sets up new handler(s).
267
268        All this, of course, requires to be 'in a site'.
269
270        Use this method to handle moves of datacenters, for instance
271        by writing an appropriate event handler.
272        """
273        logger = self.logger
274        self.logger_shutdown()
275        self.logger_setup(logger)
276        return
277
278    def logger_shutdown(self):
279        """Remove all specific logger setup.
280        """
281        logger = self.logger
282        handlers = [x for x in logger.handlers]
283        for handler in handlers:
284            handler.flush()
285            handler.close()
286            logger.removeHandler(handler)
287        collector = queryUtility(ILoggerCollector)
288        if collector is not None:
289            collector.unregisterLogger(grok.getSite(), self)
290        return
291
292
293class ILoggerCollector(Interface):
294
295    def getLoggers(site):
296        """Return all loggers registered for `site`.
297        """
298
299    def registerLogger(site, logging_component):
300        """Register a logging component residing in `site`.
301        """
302
303    def unregisterLogger(site, logging_component):
304        """Unregister a logger.
305        """
306
307class LoggerCollector(dict, grok.GlobalUtility):
308    """A global utility providing `ILoggerCollector`.
309
310    A logging collector collects logging components. This helps to
311    inform them when a logfile location changes.
312
313    Logging components are registered per site they belong to.
314    """
315
316    implements(ILoggerCollector)
317
318    def getLoggers(self, site):
319        name = getattr(site, '__name__', None)
320        if name is None:
321            return []
322        if name not in self.keys():
323            return []
324        return self[name]
325
326    def registerLogger(self, site, logging_component):
327        name = getattr(site, '__name__', None)
328        if name is None:
329            return
330        if not name in self.keys():
331            # new component
332            self[name] = []
333        if logging_component in self[name]:
334            # already registered
335            return
336        self[name].append(logging_component)
337        return
338
339    def unregisterLogger(self, site, logging_component):
340        name = getattr(site, '__name__', None)
341        if name is None or name not in self.keys():
342            return
343        if logging_component not in self[name]:
344            return
345        self[name].remove(logging_component)
346        return
347
348@grok.subscribe(IDataCenter, IDataCenterStorageMovedEvent)
349def handle_datacenter_storage_move(obj, event):
350    """Event handler, in case datacenter storage moves.
351
352    By default all our logfiles (yes, we produce a whole bunch of it)
353    are located in a ``log/`` dir of a local datacenter, the
354    datacenter 'storage'. If this path changes because the datacenter
355    is moved an appropriate event is triggered and we can react.
356
357    Via the global ILoggerCollector utility, a small piece that allows
358    self-registering of logging components, we can lookup components
359    whose logfile path has to be set up anew.
360
361    Each component we call has to provide ILogger or, more specific,
362    the :meth:`logger_logfile_changed` method of this interface.
363    """
364    site = grok.getSite()
365    if site is None:
366        return
367    collector = queryUtility(ILoggerCollector)
368    loggers = collector.getLoggers(site)
369    for logger in loggers:
370        if hasattr(logger, 'logger_logfile_changed'):
371            logger.logger_logfile_changed()
372    return
Note: See TracBrowser for help on using the repository browser.