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

Last change on this file since 6631 was 6624, checked in by uli, 13 years ago

Set default level for loggers.

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