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