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

Last change on this file since 9815 was 9769, checked in by Henrik Bettermann, 12 years ago

Ease customization of logging format. We use this for the new payments logger.

Do not show payments.log on logfile search page.

  • Property svn:keywords set to Id
File size: 13.6 KB
RevLine 
[7196]1## $Id: logger.py 9769 2012-12-05 08:32:11Z henrik $
[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}'
[9769]159
160    #: The format to use when logging.
161    logger_format_str = '%(asctime)s - %(levelname)s - %(user)s - %(message)s'
162
[6578]163    implements(ILogger)
164
[6371]165    @property
166    def logger(self):
[6578]167        """Get a logger instance.
[6371]168
[6578]169        Returns a standard logger object as provided by :mod:`logging`
170        module from the standard library.
[6371]171
[6578]172        Other components can use this logger to perform log entries
173        into the logfile of this component.
[6371]174
[6578]175        The logger is initialized the first time it is called.
[6371]176
[6578]177        The logger is only available when used inside a site.
[6371]178
[6578]179        .. note:: The logger default level is
180                  :data:`logging.WARN`. Use
181                  :meth:`logging.Logger.setLevel` to set a different level.
182        """
183        site = grok.getSite()
[6580]184        sitename = getattr(site, '__name__', None)
[6578]185        loggername = Template(self.logger_name).substitute(
[6580]186            dict(sitename='%s' % sitename))
[6578]187        logger = logging.getLogger(loggername)
[6580]188        if site is None or sitename is None:
189            # Site not added to ZODB yet. Log to commandline
[6578]190            return logger
191        if len(logger.handlers) != 1:
192            handlers = [x for x in logger.handlers]
193            for handler in handlers:
194                handler.flush()
195                handler.close()
196                logger.removeHandler(handler)
197            logger = self.logger_setup(logger)
[6745]198        if logger is None:
199            # It might happen, that we have no logger now.
200            logger = logging.getLogger(loggername)
[6578]201        return logger
[6371]202
[6578]203    def logger_setup(self, logger):
204        """Setup logger.
[6371]205
[6578]206        The logfile will be stored in the datacenter logs/ dir.
207        """
208        filename = self.logger_get_logfile_path()
209        if filename is None:
210            return
211        collector = queryUtility(ILoggerCollector)
212        if collector is not None:
213            site = grok.getSite()
214            collector.registerLogger(site, self)
[6371]215
[6578]216        # Create a rotating file handler logger.
[8492]217        handler = DatedRotatingFileHandler(
218            filename, when='MON', backupCount=BACKUP_COUNT)
[6624]219        handler.setLevel(LEVEL)
[9769]220        formatter = logging.Formatter(self.logger_format_str)
[6578]221        handler.setFormatter(formatter)
[6371]222
[6578]223        # Don't send log msgs to ancestors. This stops displaying
224        # logmessages on the commandline.
225        logger.propagate = False
226        logger.addHandler(handler)
[6624]227        logger.setLevel(LEVEL)
[8608]228
229        flt = ContextFilter()
230        logger.addFilter(flt)
[6578]231        return logger
[6399]232
[6578]233    def logger_get_logfile_path(self):
234        """Get the path to the logfile used.
[6399]235
[6578]236        Returns the path to a file in local sites datacenter ``log/``
237        directory (dependent on :meth:`logger_get_logdir`) and with
238        :attr:`logger_filename` as basename.
[6399]239
[6578]240        Override this method if you want a complete different
241        computation of the logfile path. If you only want a different
242        logfile name, set :attr:`logger_filename`. If you only want a
243        different path to the logfile override
244        :meth:`logger_get_logdir` instead.
245
246        Returns ``None`` if no logdir can be fetched.
247
248        .. note:: creates the logfile dir if it does not exist.
249
250        """
251        logdir = self.logger_get_logdir()
252        if logdir is None:
253            return None
254        return os.path.join(logdir, self.logger_filename)
255
256    def logger_get_logdir(self):
257        """Get log dir where logfile should be put.
258
259        Returns the path to the logfile directory. If no site is set,
260        ``None`` is returned. The same applies, if the site has no
261        datacenter.
262
263        If the dir dies not exist already it will be created. Only the
264        last part of the directory path will be created.
265        """
266        site = grok.getSite()
[6399]267        if site is None:
[6578]268            return None
269        datacenter = site.get('datacenter', None)
270        if datacenter is None:
271            return None
272        logdir = os.path.join(datacenter.storage, 'logs')
273        if not os.path.exists(logdir):
274            os.mkdir(logdir)
275        return logdir
[6399]276
[6578]277    def logger_logfile_changed(self):
278        """React on logfile location change.
279
280        If the logfile changed, we can set a different logfile. While
281        changing the logfile is a rather critical operation you might
282        not do often in production use, we have to cope with that
283        especially in tests.
284
285        What this method does by default (unless you override it):
286
287        - It fetches the current logger and
288
289          - Removes flushes, closes, and removes all handlers
290
291          - Sets up new handler(s).
292
293        All this, of course, requires to be 'in a site'.
294
295        Use this method to handle moves of datacenters, for instance
296        by writing an appropriate event handler.
297        """
298        logger = self.logger
299        self.logger_shutdown()
300        self.logger_setup(logger)
301        return
302
303    def logger_shutdown(self):
304        """Remove all specific logger setup.
305        """
306        logger = self.logger
307        handlers = [x for x in logger.handlers]
308        for handler in handlers:
309            handler.flush()
310            handler.close()
311            logger.removeHandler(handler)
312        collector = queryUtility(ILoggerCollector)
313        if collector is not None:
314            collector.unregisterLogger(grok.getSite(), self)
315        return
316
317
318class LoggerCollector(dict, grok.GlobalUtility):
319    """A global utility providing `ILoggerCollector`.
320
321    A logging collector collects logging components. This helps to
322    inform them when a logfile location changes.
323
324    Logging components are registered per site they belong to.
325    """
326
327    implements(ILoggerCollector)
328
329    def getLoggers(self, site):
330        name = getattr(site, '__name__', None)
331        if name is None:
332            return []
333        if name not in self.keys():
334            return []
335        return self[name]
336
337    def registerLogger(self, site, logging_component):
338        name = getattr(site, '__name__', None)
339        if name is None:
340            return
341        if not name in self.keys():
342            # new component
343            self[name] = []
344        if logging_component in self[name]:
345            # already registered
346            return
347        self[name].append(logging_component)
348        return
349
350    def unregisterLogger(self, site, logging_component):
351        name = getattr(site, '__name__', None)
352        if name is None or name not in self.keys():
353            return
354        if logging_component not in self[name]:
355            return
356        self[name].remove(logging_component)
357        return
358
359@grok.subscribe(IDataCenter, IDataCenterStorageMovedEvent)
[6839]360def handle_datacenter_storage_moved(obj, event):
[6578]361    """Event handler, in case datacenter storage moves.
362
363    By default all our logfiles (yes, we produce a whole bunch of it)
364    are located in a ``log/`` dir of a local datacenter, the
365    datacenter 'storage'. If this path changes because the datacenter
366    is moved an appropriate event is triggered and we can react.
367
368    Via the global ILoggerCollector utility, a small piece that allows
369    self-registering of logging components, we can lookup components
370    whose logfile path has to be set up anew.
371
372    Each component we call has to provide ILogger or, more specific,
373    the :meth:`logger_logfile_changed` method of this interface.
374    """
375    site = grok.getSite()
376    if site is None:
377        return
378    collector = queryUtility(ILoggerCollector)
379    loggers = collector.getLoggers(site)
380    for logger in loggers:
381        if hasattr(logger, 'logger_logfile_changed'):
382            logger.logger_logfile_changed()
383    return
[6754]384
[7811]385from waeup.kofa.interfaces import IUniversity
[6754]386@grok.subscribe(IUniversity, grok.IObjectRemovedEvent)
[6839]387def handle_site_removed(obj, event):
[6754]388    collector = queryUtility(ILoggerCollector)
389    name = getattr(obj, '__name__', None)
390    if name is None:
391        return
392    if name not in collector.keys():
393        return
394    del collector[name]
395    return
Note: See TracBrowser for help on using the repository browser.