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

Last change on this file since 8549 was 8492, checked in by uli, 12 years ago

Use DatedRotatingFileHandler? for logging.

  • Property svn:keywords set to Id
File size: 13.5 KB
Line 
1## $Id: logger.py 8492 2012-05-22 23:30:17Z 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        flt = ContextFilter()
185        logger.addFilter(flt)
186        if site is None or sitename is None:
187            # Site not added to ZODB yet. Log to commandline
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)
196        if logger is None:
197            # It might happen, that we have no logger now.
198            logger = logging.getLogger(loggername)
199        return logger
200
201    def logger_setup(self, logger):
202        """Setup logger.
203
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)
213
214        # Create a rotating file handler logger.
215        handler = DatedRotatingFileHandler(
216            filename, when='MON', backupCount=BACKUP_COUNT)
217        handler.setLevel(LEVEL)
218        formatter = logging.Formatter(
219            '%(asctime)s - %(levelname)s - %(user)s - %(message)s')
220        handler.setFormatter(formatter)
221
222        # Don't send log msgs to ancestors. This stops displaying
223        # logmessages on the commandline.
224        logger.propagate = False
225        logger.addHandler(handler)
226        logger.setLevel(LEVEL)
227        return logger
228
229    def logger_get_logfile_path(self):
230        """Get the path to the logfile used.
231
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.
235
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()
263        if site is None:
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
272
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)
356def handle_datacenter_storage_moved(obj, event):
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
380
381from waeup.kofa.interfaces import IUniversity
382@grok.subscribe(IUniversity, grok.IObjectRemovedEvent)
383def handle_site_removed(obj, event):
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.