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