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

Last change on this file since 7318 was 7196, checked in by Henrik Bettermann, 13 years ago

More copyright adjustments (job finished).

  • Property svn:keywords set to Id
File size: 13.1 KB
Line 
1## $Id: logger.py 7196 2011-11-25 07:44:52Z henrik $
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.sirp.utils.logging` is a mix-in class
22:class:`waeup.sirp.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.sirp.${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.sirp.utils.logger import Logger
49
50  class MyComponent(object, Logger):
51      # Yes that's a complete working class
52      logger_name = 'waeup.sirp.${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.sirp.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 zope import schema
80from zope.component import queryUtility
81from zope.interface import Interface, Attribute, implements
82from waeup.sirp.interfaces import (
83    IDataCenter, IDataCenterStorageMovedEvent, ILoggerCollector)
84
85#: Default logfile size (5 KB)
86MAX_BYTES = 5 * 1024 ** 2
87
88#: Default num of backup files (5)
89BACKUP_COUNT = 5
90
91#: Default logging level (`logging.INFO')
92LEVEL = logging.INFO
93
94class ILogger(Interface):
95    logger_name = schema.TextLine(
96        title = u'A Python logger name')
97    logger_filename = schema.TextLine(
98        title = u'A filename for the log file to use (basename)')
99    logger = Attribute("Get a Python logger instance already set up")
100    def logger_setup(logger):
101        """Setup a logger.
102
103        `logger` is an instance of :class:`logging.Logger`.
104        """
105
106    def logger_get_logfile_path():
107        """Get path to logfile used.
108
109        Return `None` if the file path cannot be computed.
110        """
111
112    def logger_get_logdir():
113        """Get the directory of the logfile.
114
115        Return `None` if the directory path cannot be computed.
116        """
117
118class Logger(object):
119    """Mixin-class that for logging support.
120
121    Classes that (also) inherit from this class provide support for
122    logging with waeup sites.
123
124    By default a `logger` attribute is provided which returns a
125    regular Python logger. This logger has already registered a file
126    rotating log handler that writes log messages to a file `main.log`
127    in datacenters ``log/`` directory. This is the main log file also
128    used by other components. Therefore you can pick another filename
129    by setting the `logger_filename` attribute.
130
131    All methods and attributes of this mix-in start with ``logger_``
132    in order not to interfere with already existing names of a class.
133
134    Method names do not follow the usual Zope habit (CamelCase) but
135    PEP8 convention (lower_case_with_underscores).
136    """
137
138    #: The filename to use when logging.
139    logger_filename = 'main.log'
140
141    #: The Python logger name used when
142    #: logging. ``'waeup.sirp.${sitename}'`` by default. You can use the
143    #: ``${sitename}`` placeholder in that string, which will be
144    #: replaced by the actual used site name.
145    logger_name = 'waeup.sirp.${sitename}'
146    implements(ILogger)
147
148    @property
149    def logger(self):
150        """Get a logger instance.
151
152        Returns a standard logger object as provided by :mod:`logging`
153        module from the standard library.
154
155        Other components can use this logger to perform log entries
156        into the logfile of this component.
157
158        The logger is initialized the first time it is called.
159
160        The logger is only available when used inside a site.
161
162        .. note:: The logger default level is
163                  :data:`logging.WARN`. Use
164                  :meth:`logging.Logger.setLevel` to set a different level.
165        """
166        site = grok.getSite()
167        sitename = getattr(site, '__name__', None)
168        loggername = Template(self.logger_name).substitute(
169            dict(sitename='%s' % sitename))
170        logger = logging.getLogger(loggername)
171        if site is None or sitename is None:
172            # Site not added to ZODB yet. Log to commandline
173            return logger
174        if len(logger.handlers) != 1:
175            handlers = [x for x in logger.handlers]
176            for handler in handlers:
177                handler.flush()
178                handler.close()
179                logger.removeHandler(handler)
180            logger = self.logger_setup(logger)
181        if logger is None:
182            # It might happen, that we have no logger now.
183            logger = logging.getLogger(loggername)
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 LoggerCollector(dict, grok.GlobalUtility):
300    """A global utility providing `ILoggerCollector`.
301
302    A logging collector collects logging components. This helps to
303    inform them when a logfile location changes.
304
305    Logging components are registered per site they belong to.
306    """
307
308    implements(ILoggerCollector)
309
310    def getLoggers(self, site):
311        name = getattr(site, '__name__', None)
312        if name is None:
313            return []
314        if name not in self.keys():
315            return []
316        return self[name]
317
318    def registerLogger(self, site, logging_component):
319        name = getattr(site, '__name__', None)
320        if name is None:
321            return
322        if not name in self.keys():
323            # new component
324            self[name] = []
325        if logging_component in self[name]:
326            # already registered
327            return
328        self[name].append(logging_component)
329        return
330
331    def unregisterLogger(self, site, logging_component):
332        name = getattr(site, '__name__', None)
333        if name is None or name not in self.keys():
334            return
335        if logging_component not in self[name]:
336            return
337        self[name].remove(logging_component)
338        return
339
340@grok.subscribe(IDataCenter, IDataCenterStorageMovedEvent)
341def handle_datacenter_storage_moved(obj, event):
342    """Event handler, in case datacenter storage moves.
343
344    By default all our logfiles (yes, we produce a whole bunch of it)
345    are located in a ``log/`` dir of a local datacenter, the
346    datacenter 'storage'. If this path changes because the datacenter
347    is moved an appropriate event is triggered and we can react.
348
349    Via the global ILoggerCollector utility, a small piece that allows
350    self-registering of logging components, we can lookup components
351    whose logfile path has to be set up anew.
352
353    Each component we call has to provide ILogger or, more specific,
354    the :meth:`logger_logfile_changed` method of this interface.
355    """
356    site = grok.getSite()
357    if site is None:
358        return
359    collector = queryUtility(ILoggerCollector)
360    loggers = collector.getLoggers(site)
361    for logger in loggers:
362        if hasattr(logger, 'logger_logfile_changed'):
363            logger.logger_logfile_changed()
364    return
365
366from waeup.sirp.interfaces import IUniversity
367@grok.subscribe(IUniversity, grok.IObjectRemovedEvent)
368def handle_site_removed(obj, event):
369    collector = queryUtility(ILoggerCollector)
370    name = getattr(obj, '__name__', None)
371    if name is None:
372        return
373    if name not in collector.keys():
374        return
375    del collector[name]
376    return
Note: See TracBrowser for help on using the repository browser.