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