Index: main/waeup.kofa/trunk/src/waeup/kofa/datacenter.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/datacenter.py	(revision 8511)
+++ main/waeup.kofa/trunk/src/waeup/kofa/datacenter.py	(revision 8512)
@@ -22,4 +22,5 @@
 """
 import os
+import re
 import shutil
 import grok
@@ -32,4 +33,7 @@
 from waeup.kofa.utils.helpers import copy_filesystem_tree
 from waeup.kofa.utils.logger import Logger
+
+#: Regular expression describing a logfile name with backup extension
+RE_LOGFILE_NAME = re.compile('^.+\.\d+$')
 
 class DataCenter(grok.Container, Logger):
@@ -186,4 +190,84 @@
         return
 
+    def _logfiles(self, basename):
+        """Get sorted logfiles starting with `basename`.
+        """
+        def numerical_suffix(name):
+            # return numerical suffix in `name` as number or 0.
+            suffix = name.rsplit('.', 1)[-1]
+            try:
+                return int(suffix)
+            except ValueError:
+                return 0
+            pass
+        files = [basename,]
+        for name in os.listdir(os.path.join(self.storage, 'logs')):
+            if RE_LOGFILE_NAME.match(name):
+                files.append(name)
+        return sorted(files, key=numerical_suffix)
+
+    def queryLogfiles(self, basename, query=None, limit=0, start=0):
+        """Search `query` in all logfiles starting with `basename`.
+
+        Returns an iterator of those lines in logfiles starting with
+        `basename` that match `query`. If you want the result as a
+        list, simply list() the iterator.
+
+        All logfiles with name `basename` and maybe some numerical
+        extension ('.1', '.2', ...) are searched for the `query` term
+        in correct order. So, if you ask for a basename 'app.log',
+        then any file named 'app.log', 'app.log.1', 'app.log.2',
+        etc. will be searched in that order.
+
+        The `query` is expected to be a string containing a regular
+        expression.
+
+        If `limit` is set to some numerical value, at most this number
+        of lines is returned.
+
+        With `start` you can give the number of first matched line to
+        return. `start` is zero-based, i.e. the first match has number
+        0, the scond one 1, etc.
+
+        Together with `limit` this allows some basic
+        batching. Please keep in mind that batching might give
+        unpredictable results, when logfiles change between two
+        requests. This is not a problem when only one file is searched
+        and changes include only appending new log messages.
+
+        Matches are found per line only (no multiline matches).
+
+        This method raises ValueError if some basic condition is not
+        met, for instance if the given query string is not a valid
+        regular expression.
+
+        Please note, that this exception will happen not before you
+        really fetch a result line.
+        """
+        try:
+            re_query = re.compile(query)
+        except:
+            raise ValueError('Invalid query string: %s' % query)
+
+        basename = basename.replace('/../', '')
+        files = self._logfiles(basename)
+
+        # Search the log files
+        num = 0
+        for name in files:
+            path = os.path.join(self.storage, 'logs', name)
+            if not os.path.isfile(path):
+                continue
+            for line in open(path, 'rb'):
+                if not re_query.match(line):
+                    continue
+                num += 1
+                if (num - 1) < start:
+                    continue
+                yield line
+
+                if limit and (num - limit >= start):
+                    raise StopIteration
+        pass
 
 class DataCenterFile(object):
Index: main/waeup.kofa/trunk/src/waeup/kofa/tests/test_datacenter.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/tests/test_datacenter.py	(revision 8512)
+++ main/waeup.kofa/trunk/src/waeup/kofa/tests/test_datacenter.py	(revision 8512)
@@ -0,0 +1,156 @@
+## Tests for datacenter
+##
+## XXX: Most tests for datacenter are still in doctest datacenter.txt
+##
+import os
+import shutil
+import tempfile
+import unittest
+from zope.component import getUtility, getGlobalSiteManager
+from zope.interface.verify import verifyObject, verifyClass
+from waeup.kofa.datacenter import DataCenter
+from waeup.kofa.interfaces import IDataCenter, IDataCenterConfig
+
+
+class DataCenterTests(unittest.TestCase):
+
+    def setUp(self):
+        # create a temporary place to store files
+        self.workdir = tempfile.mkdtemp()
+        self.storage = os.path.join(self.workdir, 'storage')
+        os.mkdir(self.storage)
+        self.logpath = os.path.join(self.storage, 'logs', 'myapp.log')
+        # register a datacenter config that provides the set up location
+        self.config = {'path': self.storage,}
+        self.gsm = getGlobalSiteManager()
+        self.gsm.registerUtility(self.config, IDataCenterConfig)
+        pass
+
+    def tearDown(self):
+        self.gsm.unregisterUtility(self.config, IDataCenterConfig)
+        shutil.rmtree(self.workdir)
+        return
+
+    def fill_logfile(self, num=1):
+        # write 100 messages into logfile, with 50 containing 'Msg'
+        path = self.logpath
+        for m in range(num):
+            fp = open(path, 'wb')
+            for n in range(50*m, 50*m+50):
+                fp.write('Msg %d\n' % (n + 1))
+                fp.write('Other Line %d\n' % (n + 1))
+            fp.close()
+            path = self.logpath + '.%d' % (m + 1)
+        return
+
+    def test_util_available(self):
+        # a self-test
+        config = getUtility(IDataCenterConfig)
+        self.assertTrue(config is not None)
+        return
+
+    def test_iface(self):
+        # we comply with interfaces
+        obj = DataCenter()
+        verifyClass(IDataCenter, DataCenter)
+        verifyObject(IDataCenter, obj)
+        return
+
+    def test_query_logfiles(self):
+        # We can find entries in logfiles
+        datacenter = DataCenter()
+        open(self.logpath, 'wb').write('Message 1\n')
+        result = list(datacenter.queryLogfiles('myapp.log', 'Message'))
+        self.assertEqual(result, ['Message 1\n'])
+        return
+
+    def test_query_logfiles_multi_logs(self):
+        # We can find entries in multiple logfiles (backups)
+        datacenter = DataCenter()
+        open(self.logpath, 'wb').write('Msg 1\n')
+        open(self.logpath + '.2', 'wb').write('Msg 2\n')
+        open(self.logpath + '.10', 'wb').write('Msg 3\n')
+        result = list(datacenter.queryLogfiles('myapp.log', 'Msg'))
+        # entry of logfile .10 comes after entry of logfile .2
+        self.assertEqual(result, ['Msg 1\n', 'Msg 2\n', 'Msg 3\n'])
+        return
+
+    def test_query_logfiles_ignores_other_logs(self):
+        # We look only for the basename specified
+        datacenter = DataCenter()
+        open(self.logpath, 'wb').write('Msg 1\n')
+        open(self.logpath + '-not-a-real-log', 'wb').write('Msg 2\n')
+        result = list(datacenter.queryLogfiles('myapp.log', 'Msg'))
+        # Msg 2 won't show up in results.
+        self.assertEqual(result, ['Msg 1\n'])
+        return
+
+    def test_query_logfiles_not_existant(self):
+        # We make sure only existing logs are searched
+        datacenter = DataCenter()
+        open(self.logpath + '.1', 'wb').write('Msg 1\n')
+        result = list(datacenter.queryLogfiles('myapp.log', 'Msg'))
+        # works, although there is no myapp.log, only myapp.log.1
+        self.assertEqual(result, ['Msg 1\n'])
+        return
+
+    def test_query_logfiles_invalid_regexp(self):
+        # Invalid regular expressions give a ValueError
+        datacenter = DataCenter()
+        open(self.logpath, 'wb').write('Msg 1\n')
+        result = datacenter.queryLogfiles('myapp.log', '(a')
+        self.assertRaises(ValueError, list, result)
+        return
+
+    def test_query_logfiles_batching_limit(self):
+        # we can use `limit` for batching
+        datacenter = DataCenter()
+        self.fill_logfile()
+        result = list(datacenter.queryLogfiles(
+            'myapp.log', 'Msg', limit=10))
+        self.assertEqual(len(result), 10)
+        self.assertEqual(result[-1], 'Msg 10\n')
+        return
+
+    def test_query_logfiles_batching_start(self):
+        # `start` is respected when batching
+        datacenter = DataCenter()
+        self.fill_logfile()
+        result = list(datacenter.queryLogfiles(
+            'myapp.log', 'Msg', start=25))
+        self.assertEqual(len(result), 25)
+        self.assertEqual(result[0], 'Msg 26\n')
+        return
+
+    def test_query_logfiles_batching_limit_and_start(self):
+        # we can use `start` and `limit` simultanously
+        datacenter = DataCenter()
+        self.fill_logfile()
+        result = list(datacenter.queryLogfiles(
+            'myapp.log', 'Msg', start=25, limit=10))
+        self.assertEqual(len(result), 10)
+        self.assertEqual(result[0], 'Msg 26\n')
+        self.assertEqual(result[-1], 'Msg 35\n')
+        return
+
+    def test_query_logfiles_batching_edge_cases(self):
+        # we can find last matches if found num < limit.
+        datacenter = DataCenter()
+        self.fill_logfile()
+        result = list(datacenter.queryLogfiles(
+            'myapp.log', 'Msg', start=45, limit=10))
+        self.assertEqual(len(result), 5)
+        self.assertEqual(result[0], 'Msg 46\n')
+        self.assertEqual(result[-1], 'Msg 50\n')
+        return
+
+    def test_query_logfiles_batching_multiple_files(self):
+        # batching works also with multiple log files
+        datacenter = DataCenter()
+        self.fill_logfile(num=2)
+        result = list(datacenter.queryLogfiles(
+            'myapp.log', 'Msg', start=45, limit=10))
+        self.assertEqual(len(result), 10)
+        self.assertEqual(result[0], 'Msg 46\n')
+        self.assertEqual(result[-1], 'Msg 55\n')
+        return
