[elbe-devel] [PATCH v3 11/52] log.py - New logging system

dion at linutronix.de dion at linutronix.de
Thu Jun 27 14:44:25 CEST 2019


From: Olivier Dion <dion at linutronix.de>

* Logger objects

  By default, all logged message go to "log.txt" and to the SOAP
  server.  Thus, using 'logging.debug' is enough in most cases.

  However, we might sometime only send to the SOAP server, or write to
  a different files than "log.txt".  This is done using different
  loggers.

** logging

   Using 'logging' allows to write to "log.txt" and the SOAP server.

** log

   Using 'log' only write to "log.txt".

** validation

   Using 'validation' only write to "validation.txt".

** report

   Using 'report' only write to "elbe-report.txt".

** soap

   Using 'soap' only write to the SOAP server.

** Example

   ```````````````````````````````````````````````````````````````````
   import logging

   log = logging.getLogger("log")
   validation = logging.getLogger("validation")
   report = logging.getLogger("report")
   soap = logging.getLogger("soap")

   logging.debug("logging")
   log.debug("log")
   validation.debug("validation")
   report.debug("report")
   soap.debug("soap")
   ```````````````````````````````````````````````````````````````````

   log.txt
   -------------------------------------------------------------------
   logging
   log
   -------------------------------------------------------------------

   validation.txt
   -------------------------------------------------------------------
   validation
   -------------------------------------------------------------------

   elbe-report.txt
   -------------------------------------------------------------------
   report
   -------------------------------------------------------------------

   SOAP
   -------------------------------------------------------------------
   logging
   soap
   -------------------------------------------------------------------

* elbe_logging context manager

  Since multiple threads can work on multiple projects and since
  logging is shared between threads, this context manager allows to
  register handlers for that thread and remove them after.

* QHandler

  Simply push a record' message to a queue.  It can then be read
  later, e.g. with /wait_busy/.

* async_logging

  This function take a pipe and two logging objects, one streaming and
  one block, it then launch another thread.

  That thread proceed to read from the reading end of the pipe and
  write to the streaming logging object every time a newline is
  encountered.

  When the writting end of the pipe is closed, everything that has
  been readed through the pipe is logged to the block logging object
  in one message.

Signed-off-by: Olivier Dion <dion at linutronix.de>
Acked-by: Torben Hohn <torben.hohn at linutronix.de>
---
 elbepack/log.py | 176 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 176 insertions(+)
 create mode 100644 elbepack/log.py

diff --git a/elbepack/log.py b/elbepack/log.py
new file mode 100644
index 00000000..53d41e64
--- /dev/null
+++ b/elbepack/log.py
@@ -0,0 +1,176 @@
+# ELBE - Debian Based Embedded Rootfilesystem Builder
+# Copyright (c) 2019 Olivier Dion <dion at linutronix.de>
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+import os
+import logging
+import threading
+import collections
+from contextlib import contextmanager
+
+class QHandler(logging.Handler):
+
+    queues = {}
+
+    def __init__(self, target, *args, **kwargs):
+        super(QHandler, self).__init__(*args, **kwargs)
+        if target not in QHandler.queues:
+            QHandler.queues[target] = collections.deque(maxlen=1024)
+        self.Q = QHandler.queues[target]
+
+    def emit(self, record):
+        self.Q.append(self.format(record))
+
+    @classmethod
+    def pop(cls, target):
+        try:
+            return cls.queues[target].popleft()
+        except (IndexError, KeyError):
+            return ''
+
+class ThreadFilter(logging.Filter):
+
+    def __init__(self, allowed, *args, **kwargs):
+        super(ThreadFilter, self).__init__(*args, **kwargs)
+        self.allowed = allowed
+        self.thread = threading.current_thread().ident
+
+    def filter(self, record):
+        if hasattr(record, '_thread'):
+            thread = record._thread
+        else:
+            thread = record.thread
+        retval = record.name in self.allowed and thread == self.thread
+        if retval and not hasattr(record, 'context'):
+            record.context = "[%s] " % record.levelname
+        return retval
+
+root = logging.getLogger()
+root.setLevel(logging.DEBUG)
+local = threading.local()
+normal_fmt = logging.Formatter("%(context)s%(message)s")
+
+
+ at contextmanager
+def elbe_logging(*args, **kwargs):
+    try:
+        open_logging(*args, **kwargs)
+        yield
+    finally:
+        close_logging()
+
+def open_logging(builddir=None, stdout=False):
+
+    if stdout:
+        out = logging.StreamHandler()
+        out.setFormatter(normal_fmt)
+        out.addFilter(ThreadFilter(['root' 'log', 'report', 'validation', 'echo', 'soap']))
+        root.addHandler(out)
+        local.handlers = [out]
+        return
+
+    # Handlers
+    validation = logging.FileHandler(os.path.join(builddir, "validation.txt"))
+    report = logging.FileHandler(os.path.join(builddir, "elbe-report.txt"))
+    log = logging.FileHandler(os.path.join(builddir, "log.txt"))
+    echo = QHandler(builddir)
+    soap = QHandler(builddir)
+    local.handlers = [validation, report, log, echo, soap]
+
+    # Filter
+    validation.addFilter(ThreadFilter(['validation']))
+    report.addFilter(ThreadFilter(['report']))
+    log.addFilter(ThreadFilter(['root', 'log']))
+    echo.addFilter(ThreadFilter(['root', 'report', 'validation']))
+    soap.addFilter(ThreadFilter(['soap']))
+
+    # Fmt
+    validation.setFormatter(normal_fmt)
+    report.setFormatter(normal_fmt)
+    log.setFormatter(normal_fmt)
+    echo.setFormatter(normal_fmt)
+    soap.setFormatter(normal_fmt)
+
+    # Registering
+    root.addHandler(validation)
+    root.addHandler(report)
+    root.addHandler(log)
+    root.addHandler(echo)
+    root.addHandler(soap)
+
+
+def close_logging():
+    for h in local.handlers:
+        root.removeHandler(h)
+    local.handlers = []
+
+
+def read_loggingQ(builddir):
+    return QHandler.pop(builddir)
+
+
+import select
+
+def async_logging(r, w, stream, block, atmost=80):
+    t = threading.Thread(target=AsyncLogging(atmost, stream, block),
+                         args=(r, w))
+    t.daemon = True
+    t.start()
+
+
+class AsyncLogging(object):
+
+    def __init__(self, atmost, stream, block):
+        self.lines = []
+        self.epoll = select.epoll()
+        self.atmost = atmost
+        self.fd = None
+        calling_thread = threading.current_thread().ident
+        extra = {"_thread":calling_thread}
+        extra["context"] = ""
+        self.stream = logging.LoggerAdapter(stream, extra)
+        self.block = logging.LoggerAdapter(block, extra)
+
+    def __call__(self, r, w):
+        os.close(w)
+        self.epoll.register(r, select.EPOLLIN | select.EPOLLHUP)
+        self.fd = r
+        try:
+            self.run()
+        finally:
+            os.close(r)
+
+    def run(self):
+        alive = True
+        rest = ""
+        while alive:
+            events = self.epoll.poll()
+            for fd, event in events:
+                if event & select.EPOLLIN:
+                    rest = self.read(rest)
+                if event & select.EPOLLHUP:
+                    alive = False
+
+        # Reading rest after pipe hang up
+        while True:
+            rest = self.read(rest)
+            if not rest:
+                break
+
+        if self.lines:
+            self.lines[-1] += rest
+            self.block.info("\n".join(self.lines))
+
+    def read(self, rest):
+        buff = rest + os.read(self.fd, self.atmost)
+        j = 0
+        count = 0
+        for i in xrange(len(buff)):
+            if buff[i] == '\n':
+                self.lines.append(buff[j:i])
+                count += 1
+                j = i + 1
+        if count:
+            self.stream.info("\n".join(self.lines[-count:]))
+        return buff[j:]
-- 
2.11.0




More information about the elbe-devel mailing list