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

Last change on this file since 6620 was 6580, checked in by uli, 13 years ago

Log to commandline while site not added to ZODB.

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