chiark / gitweb /
Move bus_error to dbus-common and remove bus_error_message_or_strerror
[elogind.git] / src / python-systemd / journal.py
1 #  -*- Mode: python; coding:utf-8; indent-tabs-mode: nil -*- */
2 #
3 #  This file is part of systemd.
4 #
5 #  Copyright 2012 David Strauss <david@davidstrauss.net>
6 #  Copyright 2012 Zbigniew JÄ™drzejewski-Szmek <zbyszek@in.waw.pl>
7 #  Copyright 2012 Marti Raudsepp <marti@juffo.org>
8 #
9 #  systemd is free software; you can redistribute it and/or modify it
10 #  under the terms of the GNU Lesser General Public License as published by
11 #  the Free Software Foundation; either version 2.1 of the License, or
12 #  (at your option) any later version.
13 #
14 #  systemd is distributed in the hope that it will be useful, but
15 #  WITHOUT ANY WARRANTY; without even the implied warranty of
16 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17 #  Lesser General Public License for more details.
18 #
19 #  You should have received a copy of the GNU Lesser General Public License
20 #  along with systemd; If not, see <http://www.gnu.org/licenses/>.
21
22 from __future__ import division
23
24 import sys as _sys
25 import datetime as _datetime
26 import uuid as _uuid
27 import traceback as _traceback
28 import os as _os
29 import logging as _logging
30 if _sys.version_info >= (3,):
31     from collections import ChainMap as _ChainMap
32 from syslog import (LOG_EMERG, LOG_ALERT, LOG_CRIT, LOG_ERR,
33                     LOG_WARNING, LOG_NOTICE, LOG_INFO, LOG_DEBUG)
34 from ._journal import sendv, stream_fd
35 from ._reader import (_Reader, NOP, APPEND, INVALIDATE,
36                       LOCAL_ONLY, RUNTIME_ONLY, SYSTEM_ONLY,
37                       _get_catalog)
38 from . import id128 as _id128
39
40 if _sys.version_info >= (3,):
41     from ._reader import Monotonic
42 else:
43     Monotonic = tuple
44
45 def _convert_monotonic(m):
46     return Monotonic((_datetime.timedelta(microseconds=m[0]),
47                       _uuid.UUID(bytes=m[1])))
48
49 def _convert_source_monotonic(s):
50     return _datetime.timedelta(microseconds=int(s))
51
52 def _convert_realtime(t):
53     return _datetime.datetime.fromtimestamp(t / 1000000)
54
55 def _convert_timestamp(s):
56     return _datetime.datetime.fromtimestamp(int(s) / 1000000)
57
58 if _sys.version_info >= (3,):
59     def _convert_uuid(s):
60         return _uuid.UUID(s.decode())
61 else:
62     _convert_uuid = _uuid.UUID
63
64 DEFAULT_CONVERTERS = {
65     'MESSAGE_ID': _convert_uuid,
66     '_MACHINE_ID': _convert_uuid,
67     '_BOOT_ID': _convert_uuid,
68     'PRIORITY': int,
69     'LEADER': int,
70     'SESSION_ID': int,
71     'USERSPACE_USEC': int,
72     'INITRD_USEC': int,
73     'KERNEL_USEC': int,
74     '_UID': int,
75     '_GID': int,
76     '_PID': int,
77     'SYSLOG_FACILITY': int,
78     'SYSLOG_PID': int,
79     '_AUDIT_SESSION': int,
80     '_AUDIT_LOGINUID': int,
81     '_SYSTEMD_SESSION': int,
82     '_SYSTEMD_OWNER_UID': int,
83     'CODE_LINE': int,
84     'ERRNO': int,
85     'EXIT_STATUS': int,
86     '_SOURCE_REALTIME_TIMESTAMP': _convert_timestamp,
87     '__REALTIME_TIMESTAMP': _convert_realtime,
88     '_SOURCE_MONOTONIC_TIMESTAMP': _convert_source_monotonic,
89     '__MONOTONIC_TIMESTAMP': _convert_monotonic,
90     'COREDUMP': bytes,
91     'COREDUMP_PID': int,
92     'COREDUMP_UID': int,
93     'COREDUMP_GID': int,
94     'COREDUMP_SESSION': int,
95     'COREDUMP_SIGNAL': int,
96     'COREDUMP_TIMESTAMP': _convert_timestamp,
97 }
98
99 class Reader(_Reader):
100     """Reader allows the access and filtering of systemd journal
101     entries. Note that in order to access the system journal, a
102     non-root user must be in the `systemd-journal` group.
103
104     Example usage to print out all informational or higher level
105     messages for systemd-udevd for this boot:
106
107     >>> j = journal.Reader()
108     >>> j.this_boot()
109     >>> j.log_level(journal.LOG_INFO)
110     >>> j.add_match(_SYSTEMD_UNIT="systemd-udevd.service")
111     >>> for entry in j:
112     ...    print(entry['MESSAGE'])
113
114     See systemd.journal-fields(7) for more info on typical fields
115     found in the journal.
116     """
117     def __init__(self, flags=0, path=None, converters=None):
118         """Create an instance of Reader, which allows filtering and
119         return of journal entries.
120
121         Argument `flags` sets open flags of the journal, which can be one
122         of, or ORed combination of constants: LOCAL_ONLY (default) opens
123         journal on local machine only; RUNTIME_ONLY opens only
124         volatile journal files; and SYSTEM_ONLY opens only
125         journal files of system services and the kernel.
126
127         Argument `path` is the directory of journal files. Note that
128         `flags` and `path` are exclusive.
129
130         Argument `converters` is a dictionary which updates the
131         DEFAULT_CONVERTERS to convert journal field values. Field
132         names are used as keys into this dictionary. The values must
133         be single argument functions, which take a `bytes` object and
134         return a converted value. When there's no entry for a field
135         name, then the default UTF-8 decoding will be attempted. If
136         the conversion fails with a ValueError, unconverted bytes
137         object will be returned. (Note that ValueEror is a superclass
138         of UnicodeDecodeError).
139
140         Reader implements the context manager protocol: the journal
141         will be closed when exiting the block.
142         """
143         super(Reader, self).__init__(flags, path)
144         if _sys.version_info >= (3,3):
145             self.converters = _ChainMap()
146             if converters is not None:
147                 self.converters.maps.append(converters)
148             self.converters.maps.append(DEFAULT_CONVERTERS)
149         else:
150             self.converters = DEFAULT_CONVERTERS.copy()
151             if converters is not None:
152                 self.converters.update(converters)
153
154     def _convert_field(self, key, value):
155         """Convert value using self.converters[key]
156
157         If `key` is not present in self.converters, a standard unicode
158         decoding will be attempted.  If the conversion (either
159         key-specific or the default one) fails with a ValueError, the
160         original bytes object will be returned.
161         """
162         convert = self.converters.get(key, bytes.decode)
163         try:
164             return convert(value)
165         except ValueError:
166             # Leave in default bytes
167             return value
168
169     def _convert_entry(self, entry):
170         """Convert entire journal entry utilising _covert_field"""
171         result = {}
172         for key, value in entry.items():
173             if isinstance(value, list):
174                 result[key] = [self._convert_field(key, val) for val in value]
175             else:
176                 result[key] = self._convert_field(key, value)
177         return result
178
179     def __iter__(self):
180         """Part of iterator protocol.
181         Returns self.
182         """
183         return self
184
185     if _sys.version_info >= (3,):
186         def __next__(self):
187             """Part of iterator protocol.
188             Returns self.get_next().
189             """
190             return self.get_next()
191     else:
192         def next(self):
193             """Part of iterator protocol.
194             Returns self.get_next().
195             """
196             return self.get_next()
197
198     def add_match(self, *args, **kwargs):
199         """Add one or more matches to the filter journal log entries.
200         All matches of different field are combined in a logical AND,
201         and matches of the same field are automatically combined in a
202         logical OR.
203         Matches can be passed as strings of form "FIELD=value", or
204         keyword arguments FIELD="value".
205         """
206         args = list(args)
207         args.extend(_make_line(key, val) for key, val in kwargs.items())
208         for arg in args:
209             super(Reader, self).add_match(arg)
210
211     def get_next(self, skip=1):
212         """Return the next log entry as a mapping type, currently
213         a standard dictionary of fields.
214
215         Optional skip value will return the `skip`\-th log entry.
216
217         Entries will be processed with converters specified during
218         Reader creation.
219         """
220         if super(Reader, self)._next(skip):
221             entry = super(Reader, self)._get_all()
222             if entry:
223                 entry['__REALTIME_TIMESTAMP'] =  self._get_realtime()
224                 entry['__MONOTONIC_TIMESTAMP']  = self._get_monotonic()
225                 entry['__CURSOR']  = self._get_cursor()
226                 return self._convert_entry(entry)
227         return dict()
228
229     def get_previous(self, skip=1):
230         """Return the previous log entry as a mapping type,
231         currently a standard dictionary of fields.
232
233         Optional skip value will return the -`skip`\-th log entry.
234
235         Entries will be processed with converters specified during
236         Reader creation.
237
238         Equivilent to get_next(-skip).
239         """
240         return self.get_next(-skip)
241
242     def query_unique(self, field):
243         """Return unique values appearing in the journal for given `field`.
244
245         Note this does not respect any journal matches.
246
247         Entries will be processed with converters specified during
248         Reader creation.
249         """
250         return set(self._convert_field(field, value)
251             for value in super(Reader, self).query_unique(field))
252
253     def wait(self, timeout=None):
254         """Wait for a change in the journal. `timeout` is the maximum
255         time in seconds to wait, or None, to wait forever.
256
257         Returns one of NOP (no change), APPEND (new entries have been
258         added to the end of the journal), or INVALIDATE (journal files
259         have been added or removed).
260         """
261         us = -1 if timeout is None else int(timeout * 1000000)
262         return super(Reader, self).wait(us)
263
264     def seek_realtime(self, realtime):
265         """Seek to a matching journal entry nearest to `realtime` time.
266
267         Argument `realtime` must be either an integer unix timestamp
268         or datetime.datetime instance.
269         """
270         if isinstance(realtime, _datetime.datetime):
271             realtime = float(realtime.strftime("%s.%f")) * 1000000
272         return super(Reader, self).seek_realtime(int(realtime))
273
274     def seek_monotonic(self, monotonic, bootid=None):
275         """Seek to a matching journal entry nearest to `monotonic` time.
276
277         Argument `monotonic` is a timestamp from boot in either
278         seconds or a datetime.timedelta instance. Argument `bootid`
279         is a string or UUID representing which boot the monotonic time
280         is reference to. Defaults to current bootid.
281         """
282         if isinstance(monotonic, _datetime.timedelta):
283             monotonic = monotonic.totalseconds()
284         monotonic = int(monotonic * 1000000)
285         if isinstance(bootid, _uuid.UUID):
286             bootid = bootid.get_hex()
287         return super(Reader, self).seek_monotonic(monotonic, bootid)
288
289     def log_level(self, level):
290         """Set maximum log `level` by setting matches for PRIORITY.
291         """
292         if 0 <= level <= 7:
293             for i in range(level+1):
294                 self.add_match(PRIORITY="%d" % i)
295         else:
296             raise ValueError("Log level must be 0 <= level <= 7")
297
298     def messageid_match(self, messageid):
299         """Add match for log entries with specified `messageid`.
300
301         `messageid` can be string of hexadicimal digits or a UUID
302         instance. Standard message IDs can be found in systemd.id128.
303
304         Equivalent to add_match(MESSAGE_ID=`messageid`).
305         """
306         if isinstance(messageid, _uuid.UUID):
307             messageid = messageid.get_hex()
308         self.add_match(MESSAGE_ID=messageid)
309
310     def this_boot(self, bootid=None):
311         """Add match for _BOOT_ID equal to current boot ID or the specified boot ID.
312
313         If specified, bootid should be either a UUID or a 32 digit hex number.
314
315         Equivalent to add_match(_BOOT_ID='bootid').
316         """
317         if bootid is None:
318             bootid = _id128.get_boot().hex
319         else:
320             bootid = getattr(bootid, 'hex', bootid)
321         self.add_match(_BOOT_ID=bootid)
322
323     def this_machine(self, machineid=None):
324         """Add match for _MACHINE_ID equal to the ID of this machine.
325
326         If specified, machineid should be either a UUID or a 32 digit hex number.
327
328         Equivalent to add_match(_MACHINE_ID='machineid').
329         """
330         if machineid is None:
331             machineid = _id128.get_machine().hex
332         else:
333             machineid = getattr(machineid, 'hex', machineid)
334         self.add_match(_MACHINE_ID=machineid)
335
336
337 def get_catalog(mid):
338     if isinstance(mid, _uuid.UUID):
339         mid = mid.get_hex()
340     return _get_catalog(mid)
341
342 def _make_line(field, value):
343         if isinstance(value, bytes):
344                 return field.encode('utf-8') + b'=' + value
345         else:
346                 return field + '=' + value
347
348 def send(MESSAGE, MESSAGE_ID=None,
349          CODE_FILE=None, CODE_LINE=None, CODE_FUNC=None,
350          **kwargs):
351         r"""Send a message to the journal.
352
353         >>> journal.send('Hello world')
354         >>> journal.send('Hello, again, world', FIELD2='Greetings!')
355         >>> journal.send('Binary message', BINARY=b'\xde\xad\xbe\xef')
356
357         Value of the MESSAGE argument will be used for the MESSAGE=
358         field. MESSAGE must be a string and will be sent as UTF-8 to
359         the journal.
360
361         MESSAGE_ID can be given to uniquely identify the type of
362         message. It must be a string or a uuid.UUID object.
363
364         CODE_LINE, CODE_FILE, and CODE_FUNC can be specified to
365         identify the caller. Unless at least on of the three is given,
366         values are extracted from the stack frame of the caller of
367         send(). CODE_FILE and CODE_FUNC must be strings, CODE_LINE
368         must be an integer.
369
370         Additional fields for the journal entry can only be specified
371         as keyword arguments. The payload can be either a string or
372         bytes. A string will be sent as UTF-8, and bytes will be sent
373         as-is to the journal.
374
375         Other useful fields include PRIORITY, SYSLOG_FACILITY,
376         SYSLOG_IDENTIFIER, SYSLOG_PID.
377         """
378
379         args = ['MESSAGE=' + MESSAGE]
380
381         if MESSAGE_ID is not None:
382                 id = getattr(MESSAGE_ID, 'hex', MESSAGE_ID)
383                 args.append('MESSAGE_ID=' + id)
384
385         if CODE_LINE == CODE_FILE == CODE_FUNC == None:
386                 CODE_FILE, CODE_LINE, CODE_FUNC = \
387                         _traceback.extract_stack(limit=2)[0][:3]
388         if CODE_FILE is not None:
389                 args.append('CODE_FILE=' + CODE_FILE)
390         if CODE_LINE is not None:
391                 args.append('CODE_LINE={:d}'.format(CODE_LINE))
392         if CODE_FUNC is not None:
393                 args.append('CODE_FUNC=' + CODE_FUNC)
394
395         args.extend(_make_line(key, val) for key, val in kwargs.items())
396         return sendv(*args)
397
398 def stream(identifier, priority=LOG_DEBUG, level_prefix=False):
399         r"""Return a file object wrapping a stream to journal.
400
401         Log messages written to this file as simple newline sepearted
402         text strings are written to the journal.
403
404         The file will be line buffered, so messages are actually sent
405         after a newline character is written.
406
407         >>> stream = journal.stream('myapp')
408         >>> stream
409         <open file '<fdopen>', mode 'w' at 0x...>
410         >>> stream.write('message...\n')
411
412         will produce the following message in the journal::
413
414           PRIORITY=7
415           SYSLOG_IDENTIFIER=myapp
416           MESSAGE=message...
417
418         Using the interface with print might be more convinient:
419
420         >>> from __future__ import print_function
421         >>> print('message...', file=stream)
422
423         priority is the syslog priority, one of `LOG_EMERG`,
424         `LOG_ALERT`, `LOG_CRIT`, `LOG_ERR`, `LOG_WARNING`,
425         `LOG_NOTICE`, `LOG_INFO`, `LOG_DEBUG`.
426
427         level_prefix is a boolean. If true, kernel-style log priority
428         level prefixes (such as '<1>') are interpreted. See
429         sd-daemon(3) for more information.
430         """
431
432         fd = stream_fd(identifier, priority, level_prefix)
433         return _os.fdopen(fd, 'w', 1)
434
435 class JournalHandler(_logging.Handler):
436         """Journal handler class for the Python logging framework.
437
438         Please see the Python logging module documentation for an
439         overview: http://docs.python.org/library/logging.html.
440
441         To create a custom logger whose messages go only to journal:
442
443         >>> log = logging.getLogger('custom_logger_name')
444         >>> log.propagate = False
445         >>> log.addHandler(journal.JournalHandler())
446         >>> log.warn("Some message: %s", detail)
447
448         Note that by default, message levels `INFO` and `DEBUG` are
449         ignored by the logging framework. To enable those log levels:
450
451         >>> log.setLevel(logging.DEBUG)
452
453         To attach journal MESSAGE_ID, an extra field is supported:
454
455         >>> import uuid
456         >>> mid = uuid.UUID('0123456789ABCDEF0123456789ABCDEF')
457         >>> log.warn("Message with ID", extra={'MESSAGE_ID': mid})
458
459         To redirect all logging messages to journal regardless of where
460         they come from, attach it to the root logger:
461
462         >>> logging.root.addHandler(journal.JournalHandler())
463
464         For more complex configurations when using `dictConfig` or
465         `fileConfig`, specify `systemd.journal.JournalHandler` as the
466         handler class.  Only standard handler configuration options
467         are supported: `level`, `formatter`, `filters`.
468
469         The following journal fields will be sent:
470         `MESSAGE`, `PRIORITY`, `THREAD_NAME`, `CODE_FILE`, `CODE_LINE`,
471         `CODE_FUNC`, `LOGGER` (name as supplied to getLogger call),
472         `MESSAGE_ID` (optional, see above).
473         """
474
475         def emit(self, record):
476                 """Write record as journal event.
477
478                 MESSAGE is taken from the message provided by the
479                 user, and PRIORITY, LOGGER, THREAD_NAME,
480                 CODE_{FILE,LINE,FUNC} fields are appended
481                 automatically. In addition, record.MESSAGE_ID will be
482                 used if present.
483                 """
484                 try:
485                         msg = self.format(record)
486                         pri = self.mapPriority(record.levelno)
487                         mid = getattr(record, 'MESSAGE_ID', None)
488                         send(msg,
489                              MESSAGE_ID=mid,
490                              PRIORITY=format(pri),
491                              LOGGER=record.name,
492                              THREAD_NAME=record.threadName,
493                              CODE_FILE=record.pathname,
494                              CODE_LINE=record.lineno,
495                              CODE_FUNC=record.funcName)
496                 except Exception:
497                         self.handleError(record)
498
499         @staticmethod
500         def mapPriority(levelno):
501                 """Map logging levels to journald priorities.
502
503                 Since Python log level numbers are "sparse", we have
504                 to map numbers in between the standard levels too.
505                 """
506                 if levelno <= _logging.DEBUG:
507                         return LOG_DEBUG
508                 elif levelno <= _logging.INFO:
509                         return LOG_INFO
510                 elif levelno <= _logging.WARNING:
511                         return LOG_WARNING
512                 elif levelno <= _logging.ERROR:
513                         return LOG_ERR
514                 elif levelno <= _logging.CRITICAL:
515                         return LOG_CRIT
516                 else:
517                         return LOG_ALERT