chiark / gitweb /
systemd-python: fix wait bug
[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 add_match(self, *args, **kwargs):
180         """Add one or more matches to the filter journal log entries.
181         All matches of different field are combined in a logical AND,
182         and matches of the same field are automatically combined in a
183         logical OR.
184         Matches can be passed as strings of form "FIELD=value", or
185         keyword arguments FIELD="value".
186         """
187         args = list(args)
188         args.extend(_make_line(key, val) for key, val in kwargs.items())
189         for arg in args:
190             super(Reader, self).add_match(arg)
191
192     def get_next(self, skip=1):
193         """Return the next log entry as a dictionary of fields.
194
195         Optional skip value will return the `skip`\-th log entry.
196
197         Entries will be processed with converters specified during
198         Reader creation.
199         """
200         return self._convert_entry(
201             super(Reader, self).get_next(skip))
202
203     def query_unique(self, field):
204         """Return unique values appearing in the journal for given `field`.
205
206         Note this does not respect any journal matches.
207
208         Entries will be processed with converters specified during
209         Reader creation.
210         """
211         return set(self._convert_field(field, value)
212             for value in super(Reader, self).query_unique(field))
213
214     def wait(self, timeout=None):
215         """Wait for a change in the journal. `timeout` is the maximum
216         time in seconds to wait, or None, to wait forever.
217
218         Returns one of NOP (no change), APPEND (new entries have been
219         added to the end of the journal), or INVALIDATE (journal files
220         have been added or removed).
221         """
222         us = -1 if timeout is None else int(timeout * 1000000)
223         return super(Reader, self).wait(us)
224
225     def seek_realtime(self, realtime):
226         """Seek to a matching journal entry nearest to `realtime` time.
227
228         Argument `realtime` must be either an integer unix timestamp
229         or datetime.datetime instance.
230         """
231         if isinstance(realtime, _datetime.datetime):
232             realtime = float(realtime.strftime("%s.%f")) * 1000000
233         return super(Reader, self).seek_realtime(int(realtime))
234
235     def seek_monotonic(self, monotonic, bootid=None):
236         """Seek to a matching journal entry nearest to `monotonic` time.
237
238         Argument `monotonic` is a timestamp from boot in either
239         seconds or a datetime.timedelta instance. Argument `bootid`
240         is a string or UUID representing which boot the monotonic time
241         is reference to. Defaults to current bootid.
242         """
243         if isinstance(monotonic, _datetime.timedelta):
244             monotonic = monotonic.totalseconds()
245         monotonic = int(monotonic * 1000000)
246         if isinstance(bootid, _uuid.UUID):
247             bootid = bootid.get_hex()
248         return super(Reader, self).seek_monotonic(monotonic, bootid)
249
250     def log_level(self, level):
251         """Set maximum log `level` by setting matches for PRIORITY.
252         """
253         if 0 <= level <= 7:
254             for i in range(level+1):
255                 self.add_match(PRIORITY="%d" % i)
256         else:
257             raise ValueError("Log level must be 0 <= level <= 7")
258
259     def messageid_match(self, messageid):
260         """Add match for log entries with specified `messageid`.
261
262         `messageid` can be string of hexadicimal digits or a UUID
263         instance. Standard message IDs can be found in systemd.id128.
264
265         Equivalent to add_match(MESSAGE_ID=`messageid`).
266         """
267         if isinstance(messageid, _uuid.UUID):
268             messageid = messageid.get_hex()
269         self.add_match(MESSAGE_ID=messageid)
270
271     def this_boot(self, bootid=None):
272         """Add match for _BOOT_ID equal to current boot ID or the specified boot ID.
273
274         If specified, bootid should be either a UUID or a 32 digit hex number.
275
276         Equivalent to add_match(_BOOT_ID='bootid').
277         """
278         if bootid is None:
279             bootid = _id128.get_boot().hex
280         else:
281             bootid = getattr(bootid, 'hex', bootid)
282         self.add_match(_BOOT_ID=bootid)
283
284     def this_machine(self, machineid=None):
285         """Add match for _MACHINE_ID equal to the ID of this machine.
286
287         If specified, machineid should be either a UUID or a 32 digit hex number.
288
289         Equivalent to add_match(_MACHINE_ID='machineid').
290         """
291         if machineid is None:
292             machineid = _id128.get_machine().hex
293         else:
294             machineid = getattr(machineid, 'hex', machineid)
295         self.add_match(_MACHINE_ID=machineid)
296
297
298 def get_catalog(mid):
299     if isinstance(mid, _uuid.UUID):
300         mid = mid.get_hex()
301     return _get_catalog(mid)
302
303 def _make_line(field, value):
304         if isinstance(value, bytes):
305                 return field.encode('utf-8') + b'=' + value
306         else:
307                 return field + '=' + value
308
309 def send(MESSAGE, MESSAGE_ID=None,
310          CODE_FILE=None, CODE_LINE=None, CODE_FUNC=None,
311          **kwargs):
312         r"""Send a message to the journal.
313
314         >>> journal.send('Hello world')
315         >>> journal.send('Hello, again, world', FIELD2='Greetings!')
316         >>> journal.send('Binary message', BINARY=b'\xde\xad\xbe\xef')
317
318         Value of the MESSAGE argument will be used for the MESSAGE=
319         field. MESSAGE must be a string and will be sent as UTF-8 to
320         the journal.
321
322         MESSAGE_ID can be given to uniquely identify the type of
323         message. It must be a string or a uuid.UUID object.
324
325         CODE_LINE, CODE_FILE, and CODE_FUNC can be specified to
326         identify the caller. Unless at least on of the three is given,
327         values are extracted from the stack frame of the caller of
328         send(). CODE_FILE and CODE_FUNC must be strings, CODE_LINE
329         must be an integer.
330
331         Additional fields for the journal entry can only be specified
332         as keyword arguments. The payload can be either a string or
333         bytes. A string will be sent as UTF-8, and bytes will be sent
334         as-is to the journal.
335
336         Other useful fields include PRIORITY, SYSLOG_FACILITY,
337         SYSLOG_IDENTIFIER, SYSLOG_PID.
338         """
339
340         args = ['MESSAGE=' + MESSAGE]
341
342         if MESSAGE_ID is not None:
343                 id = getattr(MESSAGE_ID, 'hex', MESSAGE_ID)
344                 args.append('MESSAGE_ID=' + id)
345
346         if CODE_LINE == CODE_FILE == CODE_FUNC == None:
347                 CODE_FILE, CODE_LINE, CODE_FUNC = \
348                         _traceback.extract_stack(limit=2)[0][:3]
349         if CODE_FILE is not None:
350                 args.append('CODE_FILE=' + CODE_FILE)
351         if CODE_LINE is not None:
352                 args.append('CODE_LINE={:d}'.format(CODE_LINE))
353         if CODE_FUNC is not None:
354                 args.append('CODE_FUNC=' + CODE_FUNC)
355
356         args.extend(_make_line(key, val) for key, val in kwargs.items())
357         return sendv(*args)
358
359 def stream(identifier, priority=LOG_DEBUG, level_prefix=False):
360         r"""Return a file object wrapping a stream to journal.
361
362         Log messages written to this file as simple newline sepearted
363         text strings are written to the journal.
364
365         The file will be line buffered, so messages are actually sent
366         after a newline character is written.
367
368         >>> stream = journal.stream('myapp')
369         >>> stream
370         <open file '<fdopen>', mode 'w' at 0x...>
371         >>> stream.write('message...\n')
372
373         will produce the following message in the journal::
374
375           PRIORITY=7
376           SYSLOG_IDENTIFIER=myapp
377           MESSAGE=message...
378
379         Using the interface with print might be more convinient:
380
381         >>> from __future__ import print_function
382         >>> print('message...', file=stream)
383
384         priority is the syslog priority, one of `LOG_EMERG`,
385         `LOG_ALERT`, `LOG_CRIT`, `LOG_ERR`, `LOG_WARNING`,
386         `LOG_NOTICE`, `LOG_INFO`, `LOG_DEBUG`.
387
388         level_prefix is a boolean. If true, kernel-style log priority
389         level prefixes (such as '<1>') are interpreted. See
390         sd-daemon(3) for more information.
391         """
392
393         fd = stream_fd(identifier, priority, level_prefix)
394         return _os.fdopen(fd, 'w', 1)
395
396 class JournalHandler(_logging.Handler):
397         """Journal handler class for the Python logging framework.
398
399         Please see the Python logging module documentation for an
400         overview: http://docs.python.org/library/logging.html.
401
402         To create a custom logger whose messages go only to journal:
403
404         >>> log = logging.getLogger('custom_logger_name')
405         >>> log.propagate = False
406         >>> log.addHandler(journal.JournalHandler())
407         >>> log.warn("Some message: %s", detail)
408
409         Note that by default, message levels `INFO` and `DEBUG` are
410         ignored by the logging framework. To enable those log levels:
411
412         >>> log.setLevel(logging.DEBUG)
413
414         To attach journal MESSAGE_ID, an extra field is supported:
415
416         >>> import uuid
417         >>> mid = uuid.UUID('0123456789ABCDEF0123456789ABCDEF')
418         >>> log.warn("Message with ID", extra={'MESSAGE_ID': mid})
419
420         To redirect all logging messages to journal regardless of where
421         they come from, attach it to the root logger:
422
423         >>> logging.root.addHandler(journal.JournalHandler())
424
425         For more complex configurations when using `dictConfig` or
426         `fileConfig`, specify `systemd.journal.JournalHandler` as the
427         handler class.  Only standard handler configuration options
428         are supported: `level`, `formatter`, `filters`.
429
430         The following journal fields will be sent:
431         `MESSAGE`, `PRIORITY`, `THREAD_NAME`, `CODE_FILE`, `CODE_LINE`,
432         `CODE_FUNC`, `LOGGER` (name as supplied to getLogger call),
433         `MESSAGE_ID` (optional, see above).
434         """
435
436         def emit(self, record):
437                 """Write record as journal event.
438
439                 MESSAGE is taken from the message provided by the
440                 user, and PRIORITY, LOGGER, THREAD_NAME,
441                 CODE_{FILE,LINE,FUNC} fields are appended
442                 automatically. In addition, record.MESSAGE_ID will be
443                 used if present.
444                 """
445                 try:
446                         msg = self.format(record)
447                         pri = self.mapPriority(record.levelno)
448                         mid = getattr(record, 'MESSAGE_ID', None)
449                         send(msg,
450                              MESSAGE_ID=mid,
451                              PRIORITY=format(pri),
452                              LOGGER=record.name,
453                              THREAD_NAME=record.threadName,
454                              CODE_FILE=record.pathname,
455                              CODE_LINE=record.lineno,
456                              CODE_FUNC=record.funcName)
457                 except Exception:
458                         self.handleError(record)
459
460         @staticmethod
461         def mapPriority(levelno):
462                 """Map logging levels to journald priorities.
463
464                 Since Python log level numbers are "sparse", we have
465                 to map numbers in between the standard levels too.
466                 """
467                 if levelno <= _logging.DEBUG:
468                         return LOG_DEBUG
469                 elif levelno <= _logging.INFO:
470                         return LOG_INFO
471                 elif levelno <= _logging.WARNING:
472                         return LOG_WARNING
473                 elif levelno <= _logging.ERROR:
474                         return LOG_ERR
475                 elif levelno <= _logging.CRITICAL:
476                         return LOG_CRIT
477                 else:
478                         return LOG_ALERT