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