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

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

Move the logger filter addition. Speeds up imports definitely, while I
am not sure about any side effects.

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