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