source: main/waeup.kofa/trunk/src/waeup/kofa/utils/logger.py @ 8573

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

Use DatedRotatingFileHandler? for logging.

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