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