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

Last change on this file since 9844 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
Line 
1## $Id: logger.py 9769 2012-12-05 08:32:11Z henrik $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
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.
8##
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.
13##
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.
20
21Main component of :mod:`waeup.kofa.utils.logging` is a mix-in class
22:class:`waeup.kofa.utils.logging.Logger`. Classes derived (also) from
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
32``waeup.kofa.${sitename}.mycomponent`` which should be unique. If you
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
48  from waeup.kofa.utils.logger import Logger
49
50  class MyComponent(object, Logger):
51      # Yes that's a complete working class
52      logger_name = 'waeup.kofa.${sitename}.mycomponent
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
69create a :class:`waeup.kofa.app.University`. This also means that
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.
74"""
75import os
76import grok
77import logging
78from string import Template
79from ulif.loghandlers import DatedRotatingFileHandler
80from zope import schema
81from zope.component import queryUtility
82from zope.interface import Interface, Attribute, implements
83from waeup.kofa.interfaces import (
84    IDataCenter, IDataCenterStorageMovedEvent, ILoggerCollector)
85from waeup.kofa.utils.helpers import get_current_principal
86
87#: Default logfile size (5 KB, not relevant for DatedRotatingFileHandlers)
88MAX_BYTES = 5 * 1024 ** 2
89
90#: Default num of backup files (-1 = indefinite)
91BACKUP_COUNT = -1
92
93#: Default logging level (`logging.INFO')
94LEVEL = logging.INFO
95
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.
104
105        `logger` is an instance of :class:`logging.Logger`.
106        """
107
108    def logger_get_logfile_path():
109        """Get path to logfile used.
110
111        Return `None` if the file path cannot be computed.
112        """
113
114    def logger_get_logdir():
115        """Get the directory of the logfile.
116
117        Return `None` if the directory path cannot be computed.
118        """
119
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
131class Logger(object):
132    """Mixin-class that for logging support.
133
134    Classes that (also) inherit from this class provide support for
135    logging with waeup sites.
136
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.
143
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.
146
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
155    #: logging. ``'waeup.kofa.${sitename}'`` by default. You can use the
156    #: ``${sitename}`` placeholder in that string, which will be
157    #: replaced by the actual used site name.
158    logger_name = 'waeup.kofa.${sitename}'
159
160    #: The format to use when logging.
161    logger_format_str = '%(asctime)s - %(levelname)s - %(user)s - %(message)s'
162
163    implements(ILogger)
164
165    @property
166    def logger(self):
167        """Get a logger instance.
168
169        Returns a standard logger object as provided by :mod:`logging`
170        module from the standard library.
171
172        Other components can use this logger to perform log entries
173        into the logfile of this component.
174
175        The logger is initialized the first time it is called.
176
177        The logger is only available when used inside a site.
178
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()
184        sitename = getattr(site, '__name__', None)
185        loggername = Template(self.logger_name).substitute(
186            dict(sitename='%s' % sitename))
187        logger = logging.getLogger(loggername)
188        if site is None or sitename is None:
189            # Site not added to ZODB yet. Log to commandline
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)
198        if logger is None:
199            # It might happen, that we have no logger now.
200            logger = logging.getLogger(loggername)
201        return logger
202
203    def logger_setup(self, logger):
204        """Setup logger.
205
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)
215
216        # Create a rotating file handler logger.
217        handler = DatedRotatingFileHandler(
218            filename, when='MON', backupCount=BACKUP_COUNT)
219        handler.setLevel(LEVEL)
220        formatter = logging.Formatter(self.logger_format_str)
221        handler.setFormatter(formatter)
222
223        # Don't send log msgs to ancestors. This stops displaying
224        # logmessages on the commandline.
225        logger.propagate = False
226        logger.addHandler(handler)
227        logger.setLevel(LEVEL)
228
229        flt = ContextFilter()
230        logger.addFilter(flt)
231        return logger
232
233    def logger_get_logfile_path(self):
234        """Get the path to the logfile used.
235
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.
239
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()
267        if site is None:
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
276
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)
360def handle_datacenter_storage_moved(obj, event):
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
384
385from waeup.kofa.interfaces import IUniversity
386@grok.subscribe(IUniversity, grok.IObjectRemovedEvent)
387def handle_site_removed(obj, event):
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.