Index: main/waeup.sirp/trunk/src/waeup/sirp/utils/logger.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/utils/logger.py	(revision 6531)
+++ main/waeup.sirp/trunk/src/waeup/sirp/utils/logger.py	(revision 6578)
@@ -22,136 +22,351 @@
 """
 Convenience stuff for logging.
+
+Main component of :mod:`waeup.sirp.utils.logging` is a mix-in class
+:class:`waeup.sirp.utils.logging.Logger`. Classes derived (also) from
+that mix-in provide a `logger` attribute that returns a regular Python
+logger logging to a rotating log file stored in the datacenter storage
+path.
+
+Deriving components (classes) should set their own `logger_name` and
+`logger_filename` attribute.
+
+The `logger_name` tells under which name the logger should be
+registered Python-wise. This is usually a dotted name string like
+``waeup.sirp.${sitename}.mycomponent`` which should be unique. If you
+pick a name already used by another component, trouble is ahead. The
+``${sitename}`` chunk of the name can be set literally like this. The
+logger machinery will turn it into some real site name at time of
+logging.
+
+The `logger_filename` attribute tells how the logfile should be
+named. This should be some base filename like
+``mycomponent.log``. Please note, that some logfile names are already
+used: ``main.log``, ``applications.log``, and ``datacenter.log``.
+
+The `Logger` mix-in also cares for updating the logging handlers when
+a datacenter location moves. That means you do not have to write your
+own event handlers for the purpose. Just derive from `Logger`, set
+your `logger_name` and `logger_filename` attribute and off you go::
+
+  from waeup.sirp.utils.logger import Logger
+
+  class MyComponent(object, Logger):
+      # Yes that's a complete working class
+      logger_name = 'waeup.sirp.${sitename}.mycomponent
+      logger_filename = 'mycomponent.log'
+
+      def do_something(self):
+           # demomstrate how to use logging from methods
+           self.logger.info('About to do something')
+           try:
+               # Do something here
+           except IOError:
+               self.logger.warn('Something went wrong')
+               return
+           self.logger.info('I did it')
+
+As you can see from that example, methods of the class can log
+messages simply by calling `self.logger`.
+
+The datacenter and its storage are created automatically when you
+create a :class:`waeup.sirp.app.University`. This also means that
+logging with the `Logger` mix-in will work only inside so-called sites
+(`University` instances put into ZODB are such `sites`).
+
+Other components in this module help to make everything work.
 """
 import os
 import grok
 import logging
-from zope.interface import Interface, Attribute
-from waeup.sirp.interfaces import ISiteLoggers, ILogger
-
-class SiteLoggers(object):
-
-    grok.implements(ISiteLoggers)
-
-    @property
-    def loggers(self):
-        return self._loggers.values()
-
-    def __init__(self):
-        self._loggers = dict()
-
-    def getLogger(self, filename):
-        waeup_logger = self._loggers.get(filename, None)
-        return waeup_logger.logger
-
-    def register(self, loggername, filename=None, site=None, **kw):
-        if loggername in self._loggers.keys():
-            raise KeyError('Logger "%s" exists already.' % loggername)
-        self._loggers[loggername] = WAeUPLogger(
-            loggername, filename=filename, site=site, **kw)
-        return
-
-    def unregister(self, name):
-        if name in self._loggers.keys():
-            del self._loggers[name]
-        return
-
-    def keys(self):
-        return self._loggers.keys()
-
-class WAeUPLogger(object):
-
-    grok.implements(ILogger)
-
-    _logger = None
-
-    def __init__(self, name, filename=None, site=None, **options):
-        self.name = name
-        self.filename = filename
-        if filename is None:
-            if '.' in name:
-                name = name.rsplit('.', 1)[1]
-            self.filename = "%s.log" % name
-        self.site = site
-        self.max_bytes = options.get('maxBytes', 5 * 1024 ** 2)
-        self.backup_count = options.get('backupCount', 5)
-        self.options = options
-        return
+from string import Template
+from zope import schema
+from zope.component import queryUtility
+from zope.interface import Interface, Attribute, implements
+from waeup.sirp.interfaces import IDataCenter, IDataCenterStorageMovedEvent
+
+#: Default logfile size
+MAX_BYTES = 5 * 1024 ** 2
+
+#: Default num of backup files
+BACKUP_COUNT = 5
+
+class ILogger(Interface):
+    logger_name = schema.TextLine(
+        title = u'A Python logger name')
+    logger_filename = schema.TextLine(
+        title = u'A filename for the log file to use (basename)')
+    logger = Attribute("Get a Python logger instance already set up")
+    def logger_setup(logger):
+        """Setup a logger.
+
+        `logger` is an instance of :class:`logging.Logger`.
+        """
+
+    def logger_get_logfile_path():
+        """Get path to logfile used.
+
+        Return `None` if the file path cannot be computed.
+        """
+
+    def logger_get_logdir():
+        """Get the directory of the logfile.
+
+        Return `None` if the directory path cannot be computed.
+        """
+
+class Logger(object):
+    """Mixin-class that for logging support.
+
+    Classes that (also) inherit from this class provide support for
+    logging with waeup sites.
+
+    By default a `logger` attribute is provided which returns a
+    regular Python logger. This logger has already registered a file
+    rotating log handler that writes log messages to a file `main.log`
+    in datacenters ``log/`` directory. This is the main log file also
+    used by other components. Therefore you can pick another filename
+    by setting the `logger_filename` attribute.
+
+    All methods and attributes of this mix-in start with ``logger_``
+    in order not to interfere with already existing names of a class.
+
+    Method names do not follow the usual Zope habit (CamelCase) but
+    PEP8 convention (lower_case_with_underscores).
+    """
+
+    #: The filename to use when logging.
+    logger_filename = 'main.log'
+
+    #: The Python logger name used when
+    #: logging. ``'waeup.sirp.${sitename}'`` by default. You can use the
+    #: ``${sitename}`` placeholder in that string, which will be
+    #: replaced by the actual used site name.
+    logger_name = 'waeup.sirp.${sitename}'
+    implements(ILogger)
 
     @property
     def logger(self):
-        if self._needSetup():
-            self._setup()
-        return logging.getLogger(self._loggername)
-
-    def setup(self):
-        return self._setup()
-
-    def update(self, **options):
-        self._logger = None # unset to be set up again on next access.
-        # XXX: set options
-        pass
-
-    def _needSetup(self):
-        if self._logger is None:
-            return True
-        if not '__app' in self._logger.name:
-            return False
-        if getattr(self.site, '__name__', None) is not None:
-            return True
-        return False
-
-    def _removeHandlers(self):
-        handlers = self._logger.handlers
+        """Get a logger instance.
+
+        Returns a standard logger object as provided by :mod:`logging`
+        module from the standard library.
+
+        Other components can use this logger to perform log entries
+        into the logfile of this component.
+
+        The logger is initialized the first time it is called.
+
+        The logger is only available when used inside a site.
+
+        .. note:: The logger default level is
+                  :data:`logging.WARN`. Use
+                  :meth:`logging.Logger.setLevel` to set a different level.
+        """
+        site = grok.getSite()
+        sitename = '%s' % getattr(site, '__name__', None)
+        loggername = Template(self.logger_name).substitute(
+            dict(sitename=sitename))
+        logger = logging.getLogger(loggername)
+        if site is None:
+            return logger
+        if len(logger.handlers) != 1:
+            handlers = [x for x in logger.handlers]
+            for handler in handlers:
+                handler.flush()
+                handler.close()
+                logger.removeHandler(handler)
+            logger = self.logger_setup(logger)
+        return logger
+
+    def logger_setup(self, logger):
+        """Setup logger.
+
+        The logfile will be stored in the datacenter logs/ dir.
+        """
+        filename = self.logger_get_logfile_path()
+        if filename is None:
+            return
+        collector = queryUtility(ILoggerCollector)
+        if collector is not None:
+            site = grok.getSite()
+            collector.registerLogger(site, self)
+
+        # Create a rotating file handler logger.
+        handler = logging.handlers.RotatingFileHandler(
+            filename, maxBytes=MAX_BYTES, backupCount=BACKUP_COUNT)
+        formatter = logging.Formatter(
+            '%(asctime)s - %(levelname)s - %(message)s')
+        handler.setFormatter(formatter)
+
+        # Don't send log msgs to ancestors. This stops displaying
+        # logmessages on the commandline.
+        logger.propagate = False
+        logger.addHandler(handler)
+        return logger
+
+    def logger_get_logfile_path(self):
+        """Get the path to the logfile used.
+
+        Returns the path to a file in local sites datacenter ``log/``
+        directory (dependent on :meth:`logger_get_logdir`) and with
+        :attr:`logger_filename` as basename.
+
+        Override this method if you want a complete different
+        computation of the logfile path. If you only want a different
+        logfile name, set :attr:`logger_filename`. If you only want a
+        different path to the logfile override
+        :meth:`logger_get_logdir` instead.
+
+        Returns ``None`` if no logdir can be fetched.
+
+        .. note:: creates the logfile dir if it does not exist.
+
+        """
+        logdir = self.logger_get_logdir()
+        if logdir is None:
+            return None
+        return os.path.join(logdir, self.logger_filename)
+
+    def logger_get_logdir(self):
+        """Get log dir where logfile should be put.
+
+        Returns the path to the logfile directory. If no site is set,
+        ``None`` is returned. The same applies, if the site has no
+        datacenter.
+
+        If the dir dies not exist already it will be created. Only the
+        last part of the directory path will be created.
+        """
+        site = grok.getSite()
+        if site is None:
+            return None
+        datacenter = site.get('datacenter', None)
+        if datacenter is None:
+            return None
+        logdir = os.path.join(datacenter.storage, 'logs')
+        if not os.path.exists(logdir):
+            os.mkdir(logdir)
+        return logdir
+
+    def logger_logfile_changed(self):
+        """React on logfile location change.
+
+        If the logfile changed, we can set a different logfile. While
+        changing the logfile is a rather critical operation you might
+        not do often in production use, we have to cope with that
+        especially in tests.
+
+        What this method does by default (unless you override it):
+
+        - It fetches the current logger and
+
+          - Removes flushes, closes, and removes all handlers
+
+          - Sets up new handler(s).
+
+        All this, of course, requires to be 'in a site'.
+
+        Use this method to handle moves of datacenters, for instance
+        by writing an appropriate event handler.
+        """
+        logger = self.logger
+        self.logger_shutdown()
+        self.logger_setup(logger)
+        return
+
+    def logger_shutdown(self):
+        """Remove all specific logger setup.
+        """
+        logger = self.logger
+        handlers = [x for x in logger.handlers]
         for handler in handlers:
-            self._logger.removeHandler(handler)
-
-    def _setup(self):
-        if self.site is None:
-            self.site = grok.getSite()
-        self._loggername = self.name
-        if not '.' in self._loggername:
-            self._loggername = 'waeup.sirp.%s.%s' % ('%s', self._loggername)
-        if '%s' in self._loggername:
-            site_name = getattr(self.site, '__name__', '__app')
-            if site_name is None:
-                site_name = '__app'
-            self._loggername = self._loggername % (site_name,)
-        self._logger = logging.getLogger(self._loggername)
-        return self._update()
-
-    def _update(self):
-        self._removeHandlers() # Remove any old handlers
-        self._logdir = os.path.join(self.site['datacenter'].storage, 'logs')
-        if not os.path.exists(self._logdir):
-            os.mkdir(self._logdir)
-        filename = os.path.join(self._logdir, self.filename)
-
-        # Create a rotating file handler logger for datacenter.
-        default_handler = logging.handlers.RotatingFileHandler(
-            filename, maxBytes=self.max_bytes, backupCount=self.backup_count)
-        self.handler = self.options.get('handler', default_handler)
-
-        default_formatter = logging.Formatter(
-            '%(asctime)s - %(levelname)s - %(message)s')
-        formatter = self.options.get('formatter', default_formatter)
-        self.handler.setFormatter(formatter)
-
-        self._logger.addHandler(self.handler)
-        self._logger.propagate = self.options.get('propagate', False)
-        self._logger.setLevel(self.options.get('level', logging.DEBUG))
-        return self._logger
-
-#: A place where we store seen loggers.
-SiteLoggersRepository = dict()
-
-class SiteLoggersFinder(object):
-    grok.provides(ISiteLoggers)
-
-    def getLoggers(self, site=None):
-        if site is None:
-            site = grok.getSite()
-        if site not in SiteLoggersRepository.keys():
-            SiteLoggersRepository[site] = SiteLoggers()
-        return SiteLoggersRepository[site]
-
-grok.global_utility(SiteLoggersFinder, provides=ISiteLoggers)
+            handler.flush()
+            handler.close()
+            logger.removeHandler(handler)
+        collector = queryUtility(ILoggerCollector)
+        if collector is not None:
+            collector.unregisterLogger(grok.getSite(), self)
+        return
+
+
+class ILoggerCollector(Interface):
+
+    def getLoggers(site):
+        """Return all loggers registered for `site`.
+        """
+
+    def registerLogger(site, logging_component):
+        """Register a logging component residing in `site`.
+        """
+
+    def unregisterLogger(site, logging_component):
+        """Unregister a logger.
+        """
+
+class LoggerCollector(dict, grok.GlobalUtility):
+    """A global utility providing `ILoggerCollector`.
+
+    A logging collector collects logging components. This helps to
+    inform them when a logfile location changes.
+
+    Logging components are registered per site they belong to.
+    """
+
+    implements(ILoggerCollector)
+
+    def getLoggers(self, site):
+        name = getattr(site, '__name__', None)
+        if name is None:
+            return []
+        if name not in self.keys():
+            return []
+        return self[name]
+
+    def registerLogger(self, site, logging_component):
+        name = getattr(site, '__name__', None)
+        if name is None:
+            return
+        if not name in self.keys():
+            # new component
+            self[name] = []
+        if logging_component in self[name]:
+            # already registered
+            return
+        self[name].append(logging_component)
+        return
+
+    def unregisterLogger(self, site, logging_component):
+        name = getattr(site, '__name__', None)
+        if name is None or name not in self.keys():
+            return
+        if logging_component not in self[name]:
+            return
+        self[name].remove(logging_component)
+        return
+
+@grok.subscribe(IDataCenter, IDataCenterStorageMovedEvent)
+def handle_datacenter_storage_move(obj, event):
+    """Event handler, in case datacenter storage moves.
+
+    By default all our logfiles (yes, we produce a whole bunch of it)
+    are located in a ``log/`` dir of a local datacenter, the
+    datacenter 'storage'. If this path changes because the datacenter
+    is moved an appropriate event is triggered and we can react.
+
+    Via the global ILoggerCollector utility, a small piece that allows
+    self-registering of logging components, we can lookup components
+    whose logfile path has to be set up anew.
+
+    Each component we call has to provide ILogger or, more specific,
+    the :meth:`logger_logfile_changed` method of this interface.
+    """
+    site = grok.getSite()
+    if site is None:
+        return
+    collector = queryUtility(ILoggerCollector)
+    loggers = collector.getLoggers(site)
+    for logger in loggers:
+        if hasattr(logger, 'logger_logfile_changed'):
+            logger.logger_logfile_changed()
+    return
Index: main/waeup.sirp/trunk/src/waeup/sirp/utils/tests/test_logger.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/utils/tests/test_logger.py	(revision 6531)
+++ main/waeup.sirp/trunk/src/waeup/sirp/utils/tests/test_logger.py	(revision 6578)
@@ -1,44 +1,193 @@
-##
-## test_logger.py
-## Login : <uli@pu.smp.net>
-## Started on  Mon Jun 13 14:40:04 2011 Uli Fouquet
-## $Id$
-## 
-## Copyright (C) 2011 Uli Fouquet
-## This program is free software; you can redistribute it and/or modify
-## it under the terms of the GNU General Public License as published by
-## the Free Software Foundation; either version 2 of the License, or
-## (at your option) any later version.
-## 
-## This program is distributed in the hope that it will be useful,
-## but WITHOUT ANY WARRANTY; without even the implied warranty of
-## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-## GNU General Public License for more details.
-## 
-## You should have received a copy of the GNU General Public License
-## along with this program; if not, write to the Free Software
-## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-##
+# Tests for waeup.sirp.utils.logger
 import logging
 import os
-import re
 import shutil
 import tempfile
-from zope.app.testing.functional import FunctionalTestCase
 from zope.component import queryUtility
 from zope.component.hooks import setSite, clearSite
 from zope.interface.verify import verifyClass, verifyObject
 from waeup.sirp.app import University
-from waeup.sirp.interfaces import ISiteLoggers, ILogger
-from waeup.sirp.testing import FunctionalLayer, doctestsuite_for_module
+from waeup.sirp.testing import (
+    FunctionalLayer, FunctionalTestCase, get_all_loggers,
+    remove_new_loggers, remove_logger)
+
 from waeup.sirp.utils.logger import (
-    SiteLoggers, WAeUPLogger, SiteLoggersRepository)
-
-class SiteLoggersTests(FunctionalTestCase):
+    Logger, MAX_BYTES, BACKUP_COUNT, ILoggerCollector, LoggerCollector,
+    ILogger)
+
+class FakeComponent(object):
+    # A component that can use the Logger mixin-class
+    def do_something(self):
+        self.logger.info('msg from do_something')
+
+class LoggingComponent(FakeComponent, Logger):
+    # A component that supports logging by mixing in Logger
+    logger_name = 'sample.${sitename}'
+    logger_filename = 'sample.log'
+
+class LoggersTests(FunctionalTestCase):
 
     layer = FunctionalLayer
 
     def setUp(self):
-        super(SiteLoggersTests, self).setUp()
+        super(LoggersTests, self).setUp()
+        self.old_loggers = get_all_loggers()
+        # Setup a sample site for each test
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        self.dc_root2 = None
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        self.workdir = tempfile.mkdtemp()
+        return
+
+    def tearDown(self):
+        super(LoggersTests, self).tearDown()
+        remove_new_loggers(self.old_loggers)
+        shutil.rmtree(self.workdir)
+        shutil.rmtree(self.dc_root)
+        clearSite()
+        return
+
+    def clear_logger(self, logger):
+        del_list = [x for x in logger.handlers]
+        for handler in del_list:
+            logger.removeHandler(handler)
+        return
+
+    def test_iface(self):
+        setSite(self.app)
+        logger = LoggingComponent()
+        verifyClass(ILogger, Logger)
+        verifyObject(ILogger, logger)
+        return
+
+    def test_logger_prop_no_site(self):
+        # If not in a site, we will get a logger without handlers
+        logger = Logger().logger
+        self.assertTrue(isinstance(logger, logging.Logger))
+        self.assertEqual(len(logger.handlers), 0)
+        return
+
+    def test_site_no_site_name(self):
+        # If a site has not name we will get a logger anyway
+        setSite(self.app)
+        del self.app.__name__
+        logger = Logger().logger
+        self.assertTrue(logger is not None)
+        return
+
+    def test_get_logdir(self):
+        # We can get a logdir
+        setSite(self.app)
+        logger = Logger()
+        logdir = logger.logger_get_logdir()
+        expected_logdir = os.path.join(
+            self.app['datacenter'].storage, 'logs')
+        self.assertEqual(logdir, expected_logdir)
+        return
+
+    def test_get_logdir_no_site(self):
+        # We cannot get a logdir without a site
+        logger = Logger()
+        logdir = logger.logger_get_logdir()
+        self.assertTrue(logdir is None)
+        return
+
+    def test_get_logdir_no_datacenter(self):
+        # Having a site, is not enough. We also need a datacenter in it.
+        setSite(self.app)
+        del self.app['datacenter']
+        logger = Logger()
+        logdir = logger.logger_get_logdir()
+        self.assertTrue(logdir is None)
+        return
+
+    def test_get_logdir_create_dir(self):
+        # If the logdirectory does not exist, we will create it
+        setSite(self.app)
+        logger = Logger()
+        expected_logdir = os.path.join(
+            self.app['datacenter'].storage, 'logs')
+        if os.path.exists(expected_logdir):
+            shutil.rmtree(expected_logdir)
+        logdir = logger.logger_get_logdir()
+        exists_after = os.path.exists(expected_logdir)
+        self.assertEqual(logdir, expected_logdir)
+        self.assertTrue(exists_after is True)
+        return
+
+    def test_get_logfile(self):
+        # We can get a logfile
+        setSite(self.app)
+        logger = Logger()
+        expected_filepath = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        path = logger.logger_get_logfile_path()
+        self.assertEqual(expected_filepath, path)
+        return
+
+    def test_get_logfile_no_dir(self):
+        # We cannot get a logfilepath if there is no dir for it
+        setSite(self.app)
+        logger = Logger()
+        del self.app['datacenter']
+        path = logger.logger_get_logfile_path()
+        self.assertTrue(path is None)
+        return
+
+    def test_setup(self):
+        # We can setup a logger.
+        mylogger = logging.getLogger('test.sample')
+        setSite(self.app)
+        logger = Logger()
+        result = logger.logger_setup(mylogger)
+        self.assertEqual(len(result.handlers), 1)
+        handler = result.handlers[0]
+        self.assertEqual(handler.maxBytes, MAX_BYTES)
+        self.assertEqual(handler.backupCount, BACKUP_COUNT)
+        self.assertTrue(result.propagate is False)
+        return
+
+    def test_setup_no_site(self):
+        # Without a site we get no logger from setup
+        mylogger = logging.getLogger('test.sample')
+        logger = Logger()
+        result = logger.logger_setup(mylogger)
+        self.assertTrue(result is None)
+        return
+
+    def test_logfile_change(self):
+        # When the logfile location changes, logger_logfile_changed can react
+        setSite(self.app)
+        logger = Logger()
+        logger.logger.warn('Warning 1')  # Log something to old logfile
+        filename1 = logger.logger.handlers[0].baseFilename
+        content1 = open(filename1, 'r').read()
+        # Now move the logfile
+        self.dc_root2 = tempfile.mkdtemp()
+        self.app['datacenter'].setStoragePath(self.dc_root2)
+        logger.logger_logfile_changed()
+        logger.logger.warn('Warning 2')  # Log something to new logfile
+        filename2 = logger.logger.handlers[0].baseFilename
+        content2 = open(filename2, 'r').read()
+        self.assertTrue('Warning 1' in content1)
+        self.assertTrue('Warning 2' in content2)
+        self.assertTrue('Warning 1' not in content2)
+        self.assertTrue('Warning 2' not in content1)
+        self.assertTrue(filename1 != filename2)
+        return
+
+
+class LoggersFunctionalTests(FunctionalTestCase):
+    # Check loggers with real components using them
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(LoggersFunctionalTests, self).setUp()
 
         # Setup a sample site for each test
@@ -50,11 +199,15 @@
         self.getRootFolder()['app'] = app
         self.app = self.getRootFolder()['app']
-
         self.workdir = tempfile.mkdtemp()
-        SiteLoggersRepository.clear()
+
+        self.app['mycomponent'] = LoggingComponent()
+        self.component = self.app['mycomponent']
+        setSite(self.app)
+        self.old_loggers = get_all_loggers()
         return
 
     def tearDown(self):
-        super(SiteLoggersTests, self).tearDown()
+        super(LoggersFunctionalTests, self).tearDown()
+        remove_new_loggers(self.old_loggers)
         shutil.rmtree(self.workdir)
         shutil.rmtree(self.dc_root)
@@ -62,75 +215,39 @@
         return
 
-    def test_iface(self):
-        loggers = SiteLoggers()
-        verifyClass(ISiteLoggers, SiteLoggers)
-        verifyObject(ISiteLoggers, loggers)
-        return
-
-    def test_loggers(self):
-        loggers = SiteLoggers()
-        loggers.register('sample1')
-        loggers.register('sample2')
-        result = loggers.loggers
-        assert len(result) == 2
-
-    def test_register(self):
-        loggers = SiteLoggers()
-        loggers.register('sample')
-        assert 'sample' in loggers._loggers.keys()
-
-    def test_register_duplicate(self):
-        loggers = SiteLoggers()
-        loggers.register('sample')
-        self.assertRaises(
-            KeyError,
-            loggers.register, 'sample')
-
-    def test_unregister(self):
-        loggers = SiteLoggers()
-        loggers.register('sample')
-        loggers.unregister('sample')
-        self.assertEqual(loggers._loggers, {})
-
-    def test_utility_no_site(self):
-        # make sure we get loggers even if no site is defined/set
-        util = queryUtility(ISiteLoggers)
-        loggers = util.getLoggers()
-        assert util is not None
-        assert loggers is not None
-        assert None in SiteLoggersRepository.keys()
-
-    def test_utility(self):
-        # make sute we get loggers for a certain site
-        setSite(self.app)
-        util = queryUtility(ISiteLoggers)
-        loggers = util.getLoggers()
-        assert util is not None
-        assert loggers is not None
-        assert self.app in SiteLoggersRepository.keys()
-
-    def test_utility_multiple(self):
-        # make sure we get the same loggers for same site
-        setSite(self.app)
-        self.assertEqual(SiteLoggersRepository.keys(), [])
-        util = queryUtility(ISiteLoggers)
-        loggers1 = util.getLoggers()
-        loggers2 = util.getLoggers()
-        assert loggers1 is loggers2
-
-    def test_utility_given_site(self):
-        # make sure a passed site is respected
-        util = queryUtility(ISiteLoggers)
-        loggers = util.getLoggers(site=self.app)
-        assert loggers is not None
-        assert self.app in SiteLoggersRepository.keys()
-
-class LoggerTests(FunctionalTestCase):
+    def test_component_can_get_logger(self):
+        # Components can get a logger
+        logger = self.component.logger
+        self.assertTrue(logger is not None)
+        self.assertEqual(len(logger.handlers), 1)
+        self.assertEqual(logger.name, 'sample.app')
+        return
+
+    def test_component_handler_setup(self):
+        # handlers of components are setup
+        handler = self.component.logger.handlers[0]
+        self.assertTrue(isinstance(
+                handler, logging.handlers.RotatingFileHandler))
+        self.assertEqual(handler.maxBytes, MAX_BYTES)
+        self.assertEqual(handler.backupCount, BACKUP_COUNT)
+        self.assertTrue(handler.baseFilename.endswith('logs/sample.log'))
+        return
+
+    def test_component_can_log(self):
+        # a method of some 'loggerized' instance can log like this.
+        self.component.logger.setLevel(logging.INFO)
+        self.component.do_something()
+        handler = logging.getLogger('sample.app').handlers[0]
+        logfile = handler.baseFilename
+        content = open(logfile, 'rb').read()
+        self.assertTrue(content.endswith(' - INFO - msg from do_something\n'))
+        return
+
+class LoggerCollectorTests(FunctionalTestCase):
 
     layer = FunctionalLayer
 
     def setUp(self):
-        super(LoggerTests, self).setUp()
-
+        super(LoggerCollectorTests, self).setUp()
+        self.old_loggers = get_all_loggers()
         # Setup a sample site for each test
         app = University()
@@ -141,10 +258,18 @@
         self.getRootFolder()['app'] = app
         self.app = self.getRootFolder()['app']
-
         self.workdir = tempfile.mkdtemp()
+
+        self.app['mycomponent'] = LoggingComponent()
+        self.component = self.app['mycomponent']
+        setSite(self.app)
+
+        self.component = LoggingComponent()
+
+
         return
 
     def tearDown(self):
-        super(LoggerTests, self).tearDown()
+        super(LoggerCollectorTests, self).tearDown()
+        remove_new_loggers(self.old_loggers)
         shutil.rmtree(self.workdir)
         shutil.rmtree(self.dc_root)
@@ -152,81 +277,113 @@
         return
 
-    def test_iface(self):
-        setSite(self.app)
-        logger = WAeUPLogger('sample')
-        verifyClass(ILogger, WAeUPLogger)
-        verifyObject(ILogger, logger)
-        return
-
-    def test_logger_attr(self):
-        setSite(self.app)
-        logger = WAeUPLogger('sample')
-        raw_logger = logger.logger
-        assert isinstance(raw_logger, logging.Logger)
-
-    def test_site_name(self):
-        setSite(self.app)
-        logger = WAeUPLogger('waeup.sirp.%s.sample', site=self.app)
-        self.assertEqual(logger.logger.name, 'waeup.sirp.app.sample')
-
-    def test_site_no_name(self):
-        # While a site has no name, we get __app as placeholder
-        setSite(self.app)
-        self.app.__name__ = None
-        logger = WAeUPLogger('waeup.sirp.%s.sample', site=self.app)
-        self.assertEqual(logger.logger.name, 'waeup.sirp.__app.sample')
-
-    def test_site_changing_name(self):
-        # Changing the sitename (from None to a value) will be reflected
-        setSite(self.app)
-        self.app.__name__ = None
-        logger = WAeUPLogger('waeup.sirp.%s.sample', site=self.app)
-        self.assertEqual(logger.logger.name, 'waeup.sirp.__app.sample')
-        self.app.__name__ = 'myapp'
-        self.assertEqual(logger.logger.name, 'waeup.sirp.myapp.sample')
-        assert len(logger.logger.handlers) > 0
-
-    def test_loggername_expansion(self):
-        setSite(self.app)
-        logger = WAeUPLogger('sample', site=self.app)
-        logger.logger # Trigger setup
-        self.assertEqual(logger._loggername, 'waeup.sirp.app.sample')
-
-    def test_filename(self):
-        setSite(self.app)
-        logger = WAeUPLogger('sample')
-        logger.logger # Trigger setup
-        self.assertEqual(logger.filename, 'sample.log')
-
-    def test_filename_from_dotted_name(self):
-        setSite(self.app)
-        logger = WAeUPLogger('waeup.sirp.%s.sample')
-        logger.logger # Trigger setup
-        logfile = os.path.join(self.dc_root, 'logs', 'sample.log')
-        self.assertEqual(logger.filename, 'sample.log')
-        assert os.path.isfile(logfile)
-
-    def test_option_filename(self):
-        setSite(self.app)
-        logger = WAeUPLogger('sample', filename='my.log')
-        logger.logger # Trigger setup
-        logfile = os.path.join(self.dc_root, 'logs', 'my.log')
-        self.assertEqual(logger.filename, 'my.log')
-        assert os.path.isfile(logfile)
-
-    def test_option_propagate(self):
-        logger = WAeUPLogger('sample', site=self.app)
-        assert logger.logger.propagate is False  # default
-        logger = WAeUPLogger('sample', site=self.app, propagate=True)
-        assert logger.logger.propagate is True
-        logger = WAeUPLogger('sample', site=self.app, propagate=False)
-        assert logger.logger.propagate is False
-
-    def test_logging(self):
-        logger = WAeUPLogger('sample', site=self.app)
-        logger.logger.info('Hi there!')
-        logfile = os.path.join(self.dc_root, 'logs', 'sample.log')
-        assert os.path.isfile(logfile)
-        contents = open(logfile, 'rb').read()
-        self.assertTrue(re.match(
-                '^....-..-.. ..:..:..,... - INFO - Hi there!\n$', contents))
+    def test_ifaces(self):
+        collector = LoggerCollector()
+        verifyClass(ILoggerCollector, LoggerCollector)
+        verifyObject(ILoggerCollector, collector)
+        return
+
+    def test_get_utility(self):
+        # We can get a logger collector via utility lookup
+        util1 = queryUtility(ILoggerCollector, default=None)
+        util2 = queryUtility(ILoggerCollector, default=None)
+        self.assertTrue(util1 is util2)
+        self.assertTrue(isinstance(util1, LoggerCollector))
+        return
+
+    def test_get_loggers(self):
+        # We can get loggers from a logger collector
+        collector = LoggerCollector()
+        result0 = collector.getLoggers(None)
+        result1 = collector.getLoggers('not-a-site')
+        result2 = collector.getLoggers(self.app)
+        collector.registerLogger(self.app, self.component)
+        result3 = collector.getLoggers(self.app)
+        self.assertEqual(result0, [])
+        self.assertEqual(result1, [])
+        self.assertEqual(result2, [])
+        self.assertEqual(result3, [self.component])
+        self.assertTrue(result3[0] is self.component)
+        return
+
+    def test_register_logger(self):
+        # We can register loggers by name
+        collector = LoggerCollector()
+        collector.registerLogger(None, self.component) # Should have no effect
+        collector.registerLogger(object(), self.component) # Same here
+        collector.registerLogger(self.app, self.component)
+        self.assertEqual(['app'], collector.keys())
+        return
+
+    def test_register_logger_double(self):
+        # Registering the same component twice gives one entry, not two
+        collector = LoggerCollector()
+        collector.registerLogger(self.app, self.component)
+        collector.registerLogger(self.app, self.component)
+        self.assertEqual(len(collector['app']), 1)
+        return
+
+    def test_unregister_logger(self):
+        # We can also unregister loggers
+        collector = LoggerCollector()
+        collector.registerLogger(self.app, self.component)
+        collector.unregisterLogger(self.app, self.component)
+        result = collector.getLoggers(self.app)
+        self.assertEqual(result, [])
+        return
+
+    def test_unregister_logger_invalid(self):
+        # We can cope with unregistration of non-existing entries.
+        collector = LoggerCollector()
+        collector.registerLogger(self.app, self.component)
+        collector.unregisterLogger(None, self.component)
+        collector.unregisterLogger(self.app, 'nonsense')
+        self.assertEqual(len(collector), 1)
+        return
+
+class LogfileChangeTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(LogfileChangeTests, self).setUp()
+        remove_logger('waeup.sirp.app') # In case it lingers around
+        self.old_loggers = get_all_loggers()
+        # Setup a sample site for each test
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        self.dc_root_new = None
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        self.workdir = tempfile.mkdtemp()
+
+        self.app['mycomponent'] = LoggingComponent()
+        self.component = self.app['mycomponent']
+        setSite(self.app)
+
+        self.component = LoggingComponent()
+        return
+
+    def tearDown(self):
+        super(LogfileChangeTests, self).tearDown()
+        remove_new_loggers(self.old_loggers)
+        shutil.rmtree(self.workdir)
+        shutil.rmtree(self.dc_root)
+        if self.dc_root_new is not None:
+            shutil.rmtree(self.dc_root_new)
+        clearSite()
+        return
+
+    def test_storage_move(self):
+        # When a datacenter storage (its path) changes, we have to
+        # move the logfiles as well.
+        #return
+        filename1 = self.component.logger.handlers[0].baseFilename
+        self.dc_root_new = tempfile.mkdtemp()
+        self.app['datacenter'].setStoragePath(self.dc_root_new)
+        filename2 = self.component.logger.handlers[0].baseFilename
+        self.assertTrue(filename1 != filename2)
+        self.assertTrue(filename2.startswith(self.dc_root_new))
+        return
