chiark / gitweb /
Spelling fixes
[elogind.git] / src / python-systemd / journal.py
index d94934cfa80e817f855235f08a2c471e57b586be..9e40cbc57e9384f7710dee0b68b66a174615633e 100644 (file)
@@ -23,27 +23,48 @@ from __future__ import division
 
 import sys as _sys
 import datetime as _datetime
 
 import sys as _sys
 import datetime as _datetime
-import functools as _functools
 import uuid as _uuid
 import traceback as _traceback
 import os as _os
 import uuid as _uuid
 import traceback as _traceback
 import os as _os
-from os import SEEK_SET, SEEK_CUR, SEEK_END
 import logging as _logging
 import logging as _logging
-if _sys.version_info >= (3,):
+if _sys.version_info >= (3,3):
     from collections import ChainMap as _ChainMap
 from syslog import (LOG_EMERG, LOG_ALERT, LOG_CRIT, LOG_ERR,
                     LOG_WARNING, LOG_NOTICE, LOG_INFO, LOG_DEBUG)
 from ._journal import sendv, stream_fd
     from collections import ChainMap as _ChainMap
 from syslog import (LOG_EMERG, LOG_ALERT, LOG_CRIT, LOG_ERR,
                     LOG_WARNING, LOG_NOTICE, LOG_INFO, LOG_DEBUG)
 from ._journal import sendv, stream_fd
-from ._reader import (_Journal, NOP, APPEND, INVALIDATE,
-                      LOCAL_ONLY, RUNTIME_ONLY, SYSTEM_ONLY)
+from ._reader import (_Reader, NOP, APPEND, INVALIDATE,
+                      LOCAL_ONLY, RUNTIME_ONLY, SYSTEM_ONLY,
+                      _get_catalog)
 from . import id128 as _id128
 
 from . import id128 as _id128
 
-_MONOTONIC_CONVERTER = lambda x: _datetime.timedelta(microseconds=x)
-_REALTIME_CONVERTER = lambda x: _datetime.datetime.fromtimestamp(x / 1E6)
+if _sys.version_info >= (3,):
+    from ._reader import Monotonic
+else:
+    Monotonic = tuple
+
+def _convert_monotonic(m):
+    return Monotonic((_datetime.timedelta(microseconds=m[0]),
+                      _uuid.UUID(bytes=m[1])))
+
+def _convert_source_monotonic(s):
+    return _datetime.timedelta(microseconds=int(s))
+
+def _convert_realtime(t):
+    return _datetime.datetime.fromtimestamp(t / 1000000)
+
+def _convert_timestamp(s):
+    return _datetime.datetime.fromtimestamp(int(s) / 1000000)
+
+if _sys.version_info >= (3,):
+    def _convert_uuid(s):
+        return _uuid.UUID(s.decode())
+else:
+    _convert_uuid = _uuid.UUID
+
 DEFAULT_CONVERTERS = {
 DEFAULT_CONVERTERS = {
-    'MESSAGE_ID': _uuid.UUID,
-    '_MACHINE_ID': _uuid.UUID,
-    '_BOOT_ID': _uuid.UUID,
+    'MESSAGE_ID': _convert_uuid,
+    '_MACHINE_ID': _convert_uuid,
+    '_BOOT_ID': _convert_uuid,
     'PRIORITY': int,
     'LEADER': int,
     'SESSION_ID': int,
     'PRIORITY': int,
     'LEADER': int,
     'SESSION_ID': int,
@@ -62,57 +83,69 @@ DEFAULT_CONVERTERS = {
     'CODE_LINE': int,
     'ERRNO': int,
     'EXIT_STATUS': int,
     'CODE_LINE': int,
     'ERRNO': int,
     'EXIT_STATUS': int,
-    '_SOURCE_REALTIME_TIMESTAMP': _REALTIME_CONVERTER,
-    '__REALTIME_TIMESTAMP': _REALTIME_CONVERTER,
-    '_SOURCE_MONOTONIC_TIMESTAMP': _MONOTONIC_CONVERTER,
-    '__MONOTONIC_TIMESTAMP': _MONOTONIC_CONVERTER,
+    '_SOURCE_REALTIME_TIMESTAMP': _convert_timestamp,
+    '__REALTIME_TIMESTAMP': _convert_realtime,
+    '_SOURCE_MONOTONIC_TIMESTAMP': _convert_source_monotonic,
+    '__MONOTONIC_TIMESTAMP': _convert_monotonic,
     'COREDUMP': bytes,
     'COREDUMP_PID': int,
     'COREDUMP_UID': int,
     'COREDUMP_GID': int,
     'COREDUMP_SESSION': int,
     'COREDUMP_SIGNAL': int,
     'COREDUMP': bytes,
     'COREDUMP_PID': int,
     'COREDUMP_UID': int,
     'COREDUMP_GID': int,
     'COREDUMP_SESSION': int,
     'COREDUMP_SIGNAL': int,
-    'COREDUMP_TIMESTAMP': _REALTIME_CONVERTER,
+    'COREDUMP_TIMESTAMP': _convert_timestamp,
 }
 
 }
 
-if _sys.version_info >= (3,):
-    _convert_unicode = _functools.partial(str, encoding='utf-8')
-else:
-    _convert_unicode = _functools.partial(unicode, encoding='utf-8')
+_IDENT_LETTER = set('ABCDEFGHIJKLMNOPQRTSUVWXYZ_')
 
 
-class Journal(_Journal):
-    """Journal allows the access and filtering of systemd journal
+def _valid_field_name(s):
+    return not (set(s) - _IDENT_LETTER)
+
+class Reader(_Reader):
+    """Reader allows the access and filtering of systemd journal
     entries. Note that in order to access the system journal, a
     entries. Note that in order to access the system journal, a
-    non-root user must be in the `adm` group.
+    non-root user must be in the `systemd-journal` group.
 
 
-    Example usage to print out all error or higher level messages
-    for systemd-udevd for the boot:
+    Example usage to print out all informational or higher level
+    messages for systemd-udevd for this boot:
 
 
-    >>> myjournal = journal.Journal()
-    >>> myjournal.add_boot_match(journal.CURRENT_BOOT)
-    >>> myjournal.add_loglevel_matches(journal.LOG_ERR)
-    >>> myjournal.add_match(_SYSTEMD_UNIT="systemd-udevd.service")
-    >>> for entry in myjournal:
+    >>> j = journal.Reader()
+    >>> j.this_boot()
+    >>> j.log_level(journal.LOG_INFO)
+    >>> j.add_match(_SYSTEMD_UNIT="systemd-udevd.service")
+    >>> for entry in j:
     ...    print(entry['MESSAGE'])
 
     See systemd.journal-fields(7) for more info on typical fields
     found in the journal.
     """
     ...    print(entry['MESSAGE'])
 
     See systemd.journal-fields(7) for more info on typical fields
     found in the journal.
     """
-    def __init__(self, converters=None, flags=LOCAL_ONLY, path=None):
-        """Creates instance of Journal, which allows filtering and
+    def __init__(self, flags=0, path=None, converters=None):
+        """Create an instance of Reader, which allows filtering and
         return of journal entries.
         return of journal entries.
-        Argument `converters` is a dictionary which updates the
-        DEFAULT_CONVERTERS to convert journal field values.
+
         Argument `flags` sets open flags of the journal, which can be one
         of, or ORed combination of constants: LOCAL_ONLY (default) opens
         journal on local machine only; RUNTIME_ONLY opens only
         volatile journal files; and SYSTEM_ONLY opens only
         journal files of system services and the kernel.
         Argument `flags` sets open flags of the journal, which can be one
         of, or ORed combination of constants: LOCAL_ONLY (default) opens
         journal on local machine only; RUNTIME_ONLY opens only
         volatile journal files; and SYSTEM_ONLY opens only
         journal files of system services and the kernel.
+
         Argument `path` is the directory of journal files. Note that
         Argument `path` is the directory of journal files. Note that
-        currently flags are ignored when `path` is present as they are
-        currently not relevant.
+        `flags` and `path` are exclusive.
+
+        Argument `converters` is a dictionary which updates the
+        DEFAULT_CONVERTERS to convert journal field values. Field
+        names are used as keys into this dictionary. The values must
+        be single argument functions, which take a `bytes` object and
+        return a converted value. When there's no entry for a field
+        name, then the default UTF-8 decoding will be attempted. If
+        the conversion fails with a ValueError, unconverted bytes
+        object will be returned. (Note that ValueEror is a superclass
+        of UnicodeDecodeError).
+
+        Reader implements the context manager protocol: the journal
+        will be closed when exiting the block.
         """
         """
-        super(Journal, self).__init__(flags, path)
+        super(Reader, self).__init__(flags, path)
         if _sys.version_info >= (3,3):
             self.converters = _ChainMap()
             if converters is not None:
         if _sys.version_info >= (3,3):
             self.converters = _ChainMap()
             if converters is not None:
@@ -124,18 +157,19 @@ class Journal(_Journal):
                 self.converters.update(converters)
 
     def _convert_field(self, key, value):
                 self.converters.update(converters)
 
     def _convert_field(self, key, value):
-        """Convert value based on callable from self.converters
-        based of field/key"""
+        """Convert value using self.converters[key]
+
+        If `key` is not present in self.converters, a standard unicode
+        decoding will be attempted.  If the conversion (either
+        key-specific or the default one) fails with a ValueError, the
+        original bytes object will be returned.
+        """
+        convert = self.converters.get(key, bytes.decode)
         try:
         try:
-            result = self.converters[key](value)
-        except:
-            # Default conversion in unicode
-            try:
-                result = _convert_unicode(value)
-            except UnicodeDecodeError:
-                # Leave in default bytes
-                result = value
-        return result
+            return convert(value)
+        except ValueError:
+            # Leave in default bytes
+            return value
 
     def _convert_entry(self, entry):
         """Convert entire journal entry utilising _covert_field"""
 
     def _convert_entry(self, entry):
         """Convert entire journal entry utilising _covert_field"""
@@ -147,6 +181,25 @@ class Journal(_Journal):
                 result[key] = self._convert_field(key, value)
         return result
 
                 result[key] = self._convert_field(key, value)
         return result
 
+    def __iter__(self):
+        """Part of iterator protocol.
+        Returns self.
+        """
+        return self
+
+    if _sys.version_info >= (3,):
+        def __next__(self):
+            """Part of iterator protocol.
+            Returns self.get_next().
+            """
+            return self.get_next()
+    else:
+        def next(self):
+            """Part of iterator protocol.
+            Returns self.get_next().
+            """
+            return self.get_next()
+
     def add_match(self, *args, **kwargs):
         """Add one or more matches to the filter journal log entries.
         All matches of different field are combined in a logical AND,
     def add_match(self, *args, **kwargs):
         """Add one or more matches to the filter journal log entries.
         All matches of different field are combined in a logical AND,
@@ -158,29 +211,60 @@ class Journal(_Journal):
         args = list(args)
         args.extend(_make_line(key, val) for key, val in kwargs.items())
         for arg in args:
         args = list(args)
         args.extend(_make_line(key, val) for key, val in kwargs.items())
         for arg in args:
-            super(Journal, self).add_match(arg)
+            super(Reader, self).add_match(arg)
 
     def get_next(self, skip=1):
 
     def get_next(self, skip=1):
-        """Return the next log entry as a dictionary of fields.
+        """Return the next log entry as a mapping type, currently
+        a standard dictionary of fields.
 
         Optional skip value will return the `skip`\-th log entry.
 
         Entries will be processed with converters specified during
 
         Optional skip value will return the `skip`\-th log entry.
 
         Entries will be processed with converters specified during
-        Journal creation.
+        Reader creation.
+        """
+        if super(Reader, self)._next(skip):
+            entry = super(Reader, self)._get_all()
+            if entry:
+                entry['__REALTIME_TIMESTAMP'] =  self._get_realtime()
+                entry['__MONOTONIC_TIMESTAMP']  = self._get_monotonic()
+                entry['__CURSOR']  = self._get_cursor()
+                return self._convert_entry(entry)
+        return dict()
+
+    def get_previous(self, skip=1):
+        """Return the previous log entry as a mapping type,
+        currently a standard dictionary of fields.
+
+        Optional skip value will return the -`skip`\-th log entry.
+
+        Entries will be processed with converters specified during
+        Reader creation.
+
+        Equivalent to get_next(-skip).
         """
         """
-        return self._convert_entry(
-            super(Journal, self).get_next(skip))
+        return self.get_next(-skip)
 
     def query_unique(self, field):
 
     def query_unique(self, field):
-        """Return unique values appearing in the Journal for given `field`.
+        """Return unique values appearing in the journal for given `field`.
 
         Note this does not respect any journal matches.
 
         Entries will be processed with converters specified during
 
         Note this does not respect any journal matches.
 
         Entries will be processed with converters specified during
-        Journal creation.
+        Reader creation.
         """
         return set(self._convert_field(field, value)
         """
         return set(self._convert_field(field, value)
-            for value in super(Journal, self).query_unique(field))
+            for value in super(Reader, self).query_unique(field))
+
+    def wait(self, timeout=None):
+        """Wait for a change in the journal. `timeout` is the maximum
+        time in seconds to wait, or None, to wait forever.
+
+        Returns one of NOP (no change), APPEND (new entries have been
+        added to the end of the journal), or INVALIDATE (journal files
+        have been added or removed).
+        """
+        us = -1 if timeout is None else int(timeout * 1000000)
+        return super(Reader, self).wait(us)
 
     def seek_realtime(self, realtime):
         """Seek to a matching journal entry nearest to `realtime` time.
 
     def seek_realtime(self, realtime):
         """Seek to a matching journal entry nearest to `realtime` time.
@@ -189,8 +273,8 @@ class Journal(_Journal):
         or datetime.datetime instance.
         """
         if isinstance(realtime, _datetime.datetime):
         or datetime.datetime instance.
         """
         if isinstance(realtime, _datetime.datetime):
-            realtime = float(realtime.strftime("%s.%f"))
-        return super(Journal, self).seek_realtime(realtime)
+            realtime = float(realtime.strftime("%s.%f")) * 1000000
+        return super(Reader, self).seek_realtime(int(realtime))
 
     def seek_monotonic(self, monotonic, bootid=None):
         """Seek to a matching journal entry nearest to `monotonic` time.
 
     def seek_monotonic(self, monotonic, bootid=None):
         """Seek to a matching journal entry nearest to `monotonic` time.
@@ -202,16 +286,17 @@ class Journal(_Journal):
         """
         if isinstance(monotonic, _datetime.timedelta):
             monotonic = monotonic.totalseconds()
         """
         if isinstance(monotonic, _datetime.timedelta):
             monotonic = monotonic.totalseconds()
+        monotonic = int(monotonic * 1000000)
         if isinstance(bootid, _uuid.UUID):
             bootid = bootid.get_hex()
         if isinstance(bootid, _uuid.UUID):
             bootid = bootid.get_hex()
-        return super(Journal, self).seek_monotonic(monotonic, bootid)
+        return super(Reader, self).seek_monotonic(monotonic, bootid)
 
     def log_level(self, level):
         """Set maximum log `level` by setting matches for PRIORITY.
         """
         if 0 <= level <= 7:
             for i in range(level+1):
 
     def log_level(self, level):
         """Set maximum log `level` by setting matches for PRIORITY.
         """
         if 0 <= level <= 7:
             for i in range(level+1):
-                self.add_match(PRIORITY="%s" % i)
+                self.add_match(PRIORITY="%d" % i)
         else:
             raise ValueError("Log level must be 0 <= level <= 7")
 
         else:
             raise ValueError("Log level must be 0 <= level <= 7")
 
@@ -254,6 +339,11 @@ class Journal(_Journal):
         self.add_match(_MACHINE_ID=machineid)
 
 
         self.add_match(_MACHINE_ID=machineid)
 
 
+def get_catalog(mid):
+    if isinstance(mid, _uuid.UUID):
+        mid = mid.get_hex()
+    return _get_catalog(mid)
+
 def _make_line(field, value):
         if isinstance(value, bytes):
                 return field.encode('utf-8') + b'=' + value
 def _make_line(field, value):
         if isinstance(value, bytes):
                 return field.encode('utf-8') + b'=' + value
@@ -365,12 +455,6 @@ class JournalHandler(_logging.Handler):
 
         >>> log.setLevel(logging.DEBUG)
 
 
         >>> log.setLevel(logging.DEBUG)
 
-        To attach journal MESSAGE_ID, an extra field is supported:
-
-        >>> import uuid
-        >>> mid = uuid.UUID('0123456789ABCDEF0123456789ABCDEF')
-        >>> log.warn("Message with ID", extra={'MESSAGE_ID': mid})
-
         To redirect all logging messages to journal regardless of where
         they come from, attach it to the root logger:
 
         To redirect all logging messages to journal regardless of where
         they come from, attach it to the root logger:
 
@@ -381,12 +465,36 @@ class JournalHandler(_logging.Handler):
         handler class.  Only standard handler configuration options
         are supported: `level`, `formatter`, `filters`.
 
         handler class.  Only standard handler configuration options
         are supported: `level`, `formatter`, `filters`.
 
+        To attach journal MESSAGE_ID, an extra field is supported:
+
+        >>> import uuid
+        >>> mid = uuid.UUID('0123456789ABCDEF0123456789ABCDEF')
+        >>> log.warn("Message with ID", extra={'MESSAGE_ID': mid})
+
+        Fields to be attached to all messages sent through this
+        handler can be specified as keyword arguments. This probably
+        makes sense only for SYSLOG_IDENTIFIER and similar fields
+        which are constant for the whole program:
+
+        >>> journal.JournalHandler(SYSLOG_IDENTIFIER='my-cool-app')
+
         The following journal fields will be sent:
         `MESSAGE`, `PRIORITY`, `THREAD_NAME`, `CODE_FILE`, `CODE_LINE`,
         `CODE_FUNC`, `LOGGER` (name as supplied to getLogger call),
         The following journal fields will be sent:
         `MESSAGE`, `PRIORITY`, `THREAD_NAME`, `CODE_FILE`, `CODE_LINE`,
         `CODE_FUNC`, `LOGGER` (name as supplied to getLogger call),
-        `MESSAGE_ID` (optional, see above).
+        `MESSAGE_ID` (optional, see above), `SYSLOG_IDENTIFIER` (defaults
+        to sys.argv[0]).
         """
 
         """
 
+        def __init__(self, level=_logging.NOTSET, **kwargs):
+                super(JournalHandler, self).__init__(level)
+
+                for name in kwargs:
+                        if not _valid_field_name(name):
+                                raise ValueError('Invalid field name: ' + name)
+                if 'SYSLOG_IDENTIFIER' not in kwargs:
+                        kwargs['SYSLOG_IDENTIFIER'] = _sys.argv[0]
+                self._extra = kwargs
+
         def emit(self, record):
                 """Write record as journal event.
 
         def emit(self, record):
                 """Write record as journal event.
 
@@ -407,7 +515,8 @@ class JournalHandler(_logging.Handler):
                              THREAD_NAME=record.threadName,
                              CODE_FILE=record.pathname,
                              CODE_LINE=record.lineno,
                              THREAD_NAME=record.threadName,
                              CODE_FILE=record.pathname,
                              CODE_LINE=record.lineno,
-                             CODE_FUNC=record.funcName)
+                             CODE_FUNC=record.funcName,
+                             **self._extra)
                 except Exception:
                         self.handleError(record)
 
                 except Exception:
                         self.handleError(record)