source: main/waeup.sirp/trunk/src/waeup/sirp/utils/logger.py @ 6748

Last change on this file since 6748 was 6745, checked in by uli, 14 years ago

Add safety belt.

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