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