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

Last change on this file since 6776 was 6754, checked in by uli, 13 years ago

Logger-related stuff.

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 (
87    IDataCenter, IDataCenterStorageMovedEvent, ILoggerCollector)
88
89#: Default logfile size (5 KB)
90MAX_BYTES = 5 * 1024 ** 2
91
92#: Default num of backup files (5)
93BACKUP_COUNT = 5
94
95#: Default logging level (`logging.INFO')
96LEVEL = logging.INFO
97
98class ILogger(Interface):
99    logger_name = schema.TextLine(
100        title = u'A Python logger name')
101    logger_filename = schema.TextLine(
102        title = u'A filename for the log file to use (basename)')
103    logger = Attribute("Get a Python logger instance already set up")
104    def logger_setup(logger):
105        """Setup a logger.
106
107        `logger` is an instance of :class:`logging.Logger`.
108        """
109
110    def logger_get_logfile_path():
111        """Get path to logfile used.
112
113        Return `None` if the file path cannot be computed.
114        """
115
116    def logger_get_logdir():
117        """Get the directory of the logfile.
118
119        Return `None` if the directory path cannot be computed.
120        """
121
122class Logger(object):
123    """Mixin-class that for logging support.
124
125    Classes that (also) inherit from this class provide support for
126    logging with waeup sites.
127
128    By default a `logger` attribute is provided which returns a
129    regular Python logger. This logger has already registered a file
130    rotating log handler that writes log messages to a file `main.log`
131    in datacenters ``log/`` directory. This is the main log file also
132    used by other components. Therefore you can pick another filename
133    by setting the `logger_filename` attribute.
134
135    All methods and attributes of this mix-in start with ``logger_``
136    in order not to interfere with already existing names of a class.
137
138    Method names do not follow the usual Zope habit (CamelCase) but
139    PEP8 convention (lower_case_with_underscores).
140    """
141
142    #: The filename to use when logging.
143    logger_filename = 'main.log'
144
145    #: The Python logger name used when
146    #: logging. ``'waeup.sirp.${sitename}'`` by default. You can use the
147    #: ``${sitename}`` placeholder in that string, which will be
148    #: replaced by the actual used site name.
149    logger_name = 'waeup.sirp.${sitename}'
150    implements(ILogger)
151
152    @property
153    def logger(self):
154        """Get a logger instance.
155
156        Returns a standard logger object as provided by :mod:`logging`
157        module from the standard library.
158
159        Other components can use this logger to perform log entries
160        into the logfile of this component.
161
162        The logger is initialized the first time it is called.
163
164        The logger is only available when used inside a site.
165
166        .. note:: The logger default level is
167                  :data:`logging.WARN`. Use
168                  :meth:`logging.Logger.setLevel` to set a different level.
169        """
170        site = grok.getSite()
171        sitename = getattr(site, '__name__', None)
172        loggername = Template(self.logger_name).substitute(
173            dict(sitename='%s' % sitename))
174        logger = logging.getLogger(loggername)
175        if site is None or sitename is None:
176            # Site not added to ZODB yet. Log to commandline
177            return logger
178        if len(logger.handlers) != 1:
179            handlers = [x for x in logger.handlers]
180            for handler in handlers:
181                handler.flush()
182                handler.close()
183                logger.removeHandler(handler)
184            logger = self.logger_setup(logger)
185        if logger is None:
186            # It might happen, that we have no logger now.
187            logger = logging.getLogger(loggername)
188        return logger
189
190    def logger_setup(self, logger):
191        """Setup logger.
192
193        The logfile will be stored in the datacenter logs/ dir.
194        """
195        filename = self.logger_get_logfile_path()
196        if filename is None:
197            return
198        collector = queryUtility(ILoggerCollector)
199        if collector is not None:
200            site = grok.getSite()
201            collector.registerLogger(site, self)
202
203        # Create a rotating file handler logger.
204        handler = logging.handlers.RotatingFileHandler(
205            filename, maxBytes=MAX_BYTES, backupCount=BACKUP_COUNT)
206        handler.setLevel(LEVEL)
207        formatter = logging.Formatter(
208            '%(asctime)s - %(levelname)s - %(message)s')
209        handler.setFormatter(formatter)
210
211        # Don't send log msgs to ancestors. This stops displaying
212        # logmessages on the commandline.
213        logger.propagate = False
214        logger.addHandler(handler)
215        logger.setLevel(LEVEL)
216        return logger
217
218    def logger_get_logfile_path(self):
219        """Get the path to the logfile used.
220
221        Returns the path to a file in local sites datacenter ``log/``
222        directory (dependent on :meth:`logger_get_logdir`) and with
223        :attr:`logger_filename` as basename.
224
225        Override this method if you want a complete different
226        computation of the logfile path. If you only want a different
227        logfile name, set :attr:`logger_filename`. If you only want a
228        different path to the logfile override
229        :meth:`logger_get_logdir` instead.
230
231        Returns ``None`` if no logdir can be fetched.
232
233        .. note:: creates the logfile dir if it does not exist.
234
235        """
236        logdir = self.logger_get_logdir()
237        if logdir is None:
238            return None
239        return os.path.join(logdir, self.logger_filename)
240
241    def logger_get_logdir(self):
242        """Get log dir where logfile should be put.
243
244        Returns the path to the logfile directory. If no site is set,
245        ``None`` is returned. The same applies, if the site has no
246        datacenter.
247
248        If the dir dies not exist already it will be created. Only the
249        last part of the directory path will be created.
250        """
251        site = grok.getSite()
252        if site is None:
253            return None
254        datacenter = site.get('datacenter', None)
255        if datacenter is None:
256            return None
257        logdir = os.path.join(datacenter.storage, 'logs')
258        if not os.path.exists(logdir):
259            os.mkdir(logdir)
260        return logdir
261
262    def logger_logfile_changed(self):
263        """React on logfile location change.
264
265        If the logfile changed, we can set a different logfile. While
266        changing the logfile is a rather critical operation you might
267        not do often in production use, we have to cope with that
268        especially in tests.
269
270        What this method does by default (unless you override it):
271
272        - It fetches the current logger and
273
274          - Removes flushes, closes, and removes all handlers
275
276          - Sets up new handler(s).
277
278        All this, of course, requires to be 'in a site'.
279
280        Use this method to handle moves of datacenters, for instance
281        by writing an appropriate event handler.
282        """
283        logger = self.logger
284        self.logger_shutdown()
285        self.logger_setup(logger)
286        return
287
288    def logger_shutdown(self):
289        """Remove all specific logger setup.
290        """
291        logger = self.logger
292        handlers = [x for x in logger.handlers]
293        for handler in handlers:
294            handler.flush()
295            handler.close()
296            logger.removeHandler(handler)
297        collector = queryUtility(ILoggerCollector)
298        if collector is not None:
299            collector.unregisterLogger(grok.getSite(), self)
300        return
301
302
303class LoggerCollector(dict, grok.GlobalUtility):
304    """A global utility providing `ILoggerCollector`.
305
306    A logging collector collects logging components. This helps to
307    inform them when a logfile location changes.
308
309    Logging components are registered per site they belong to.
310    """
311
312    implements(ILoggerCollector)
313
314    def getLoggers(self, site):
315        name = getattr(site, '__name__', None)
316        if name is None:
317            return []
318        if name not in self.keys():
319            return []
320        return self[name]
321
322    def registerLogger(self, site, logging_component):
323        name = getattr(site, '__name__', None)
324        if name is None:
325            return
326        if not name in self.keys():
327            # new component
328            self[name] = []
329        if logging_component in self[name]:
330            # already registered
331            return
332        self[name].append(logging_component)
333        return
334
335    def unregisterLogger(self, site, logging_component):
336        name = getattr(site, '__name__', None)
337        if name is None or name not in self.keys():
338            return
339        if logging_component not in self[name]:
340            return
341        self[name].remove(logging_component)
342        return
343
344@grok.subscribe(IDataCenter, IDataCenterStorageMovedEvent)
345def handle_datacenter_storage_move(obj, event):
346    """Event handler, in case datacenter storage moves.
347
348    By default all our logfiles (yes, we produce a whole bunch of it)
349    are located in a ``log/`` dir of a local datacenter, the
350    datacenter 'storage'. If this path changes because the datacenter
351    is moved an appropriate event is triggered and we can react.
352
353    Via the global ILoggerCollector utility, a small piece that allows
354    self-registering of logging components, we can lookup components
355    whose logfile path has to be set up anew.
356
357    Each component we call has to provide ILogger or, more specific,
358    the :meth:`logger_logfile_changed` method of this interface.
359    """
360    site = grok.getSite()
361    if site is None:
362        return
363    collector = queryUtility(ILoggerCollector)
364    loggers = collector.getLoggers(site)
365    for logger in loggers:
366        if hasattr(logger, 'logger_logfile_changed'):
367            logger.logger_logfile_changed()
368    return
369
370from waeup.sirp.interfaces import IUniversity
371@grok.subscribe(IUniversity, grok.IObjectRemovedEvent)
372def handle_site_removal(obj, event):
373    collector = queryUtility(ILoggerCollector)
374    name = getattr(obj, '__name__', None)
375    if name is None:
376        return
377    if name not in collector.keys():
378        return
379    del collector[name]
380    return
Note: See TracBrowser for help on using the repository browser.