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