chiark / gitweb /
bus: use GREEDY_REALLOC() when allocating message queues
[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 uuid as _uuid
27 import traceback as _traceback
28 import os as _os
29 import logging as _logging
30 if _sys.version_info >= (3,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 __version__, sendv, stream_fd
35 from ._reader import (_Reader, NOP, APPEND, INVALIDATE,
36                       LOCAL_ONLY, RUNTIME_ONLY,
37                       SYSTEM, SYSTEM_ONLY, CURRENT_USER,
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 / 1000000)
55
56 def _convert_timestamp(s):
57     return _datetime.datetime.fromtimestamp(int(s) / 1000000)
58
59 def _convert_trivial(x):
60     return x
61
62 if _sys.version_info >= (3,):
63     def _convert_uuid(s):
64         return _uuid.UUID(s.decode())
65 else:
66     _convert_uuid = _uuid.UUID
67
68 DEFAULT_CONVERTERS = {
69     'MESSAGE_ID': _convert_uuid,
70     '_MACHINE_ID': _convert_uuid,
71     '_BOOT_ID': _convert_uuid,
72     'PRIORITY': int,
73     'LEADER': int,
74     'SESSION_ID': int,
75     'USERSPACE_USEC': int,
76     'INITRD_USEC': int,
77     'KERNEL_USEC': int,
78     '_UID': int,
79     '_GID': int,
80     '_PID': int,
81     'SYSLOG_FACILITY': int,
82     'SYSLOG_PID': int,
83     '_AUDIT_SESSION': int,
84     '_AUDIT_LOGINUID': int,
85     '_SYSTEMD_SESSION': int,
86     '_SYSTEMD_OWNER_UID': int,
87     'CODE_LINE': int,
88     'ERRNO': int,
89     'EXIT_STATUS': int,
90     '_SOURCE_REALTIME_TIMESTAMP': _convert_timestamp,
91     '__REALTIME_TIMESTAMP': _convert_realtime,
92     '_SOURCE_MONOTONIC_TIMESTAMP': _convert_source_monotonic,
93     '__MONOTONIC_TIMESTAMP': _convert_monotonic,
94     '__CURSOR': _convert_trivial,
95     'COREDUMP': bytes,
96     'COREDUMP_PID': int,
97     'COREDUMP_UID': int,
98     'COREDUMP_GID': int,
99     'COREDUMP_SESSION': int,
100     'COREDUMP_SIGNAL': int,
101     'COREDUMP_TIMESTAMP': _convert_timestamp,
102 }
103
104 _IDENT_LETTER = set('ABCDEFGHIJKLMNOPQRTSUVWXYZ_')
105
106 def _valid_field_name(s):
107     return not (set(s) - _IDENT_LETTER)
108
109 class Reader(_Reader):
110     """Reader allows the access and filtering of systemd journal
111     entries. Note that in order to access the system journal, a
112     non-root user must be in the `systemd-journal` group.
113
114     Example usage to print out all informational or higher level
115     messages for systemd-udevd for this boot:
116
117     >>> j = journal.Reader()
118     >>> j.this_boot()
119     >>> j.log_level(journal.LOG_INFO)
120     >>> j.add_match(_SYSTEMD_UNIT="systemd-udevd.service")
121     >>> for entry in j:
122     ...    print(entry['MESSAGE'])
123
124     See systemd.journal-fields(7) for more info on typical fields
125     found in the journal.
126     """
127     def __init__(self, flags=0, path=None, files=None, converters=None):
128         """Create an instance of Reader, which allows filtering and
129         return of journal entries.
130
131         Argument `flags` sets open flags of the journal, which can be one
132         of, or ORed combination of constants: LOCAL_ONLY (default) opens
133         journal on local machine only; RUNTIME_ONLY opens only
134         volatile journal files; and SYSTEM_ONLY opens only
135         journal files of system services and the kernel.
136
137         Argument `path` is the directory of journal files. Note that
138         `flags` and `path` are exclusive.
139
140         Argument `converters` is a dictionary which updates the
141         DEFAULT_CONVERTERS to convert journal field values. Field
142         names are used as keys into this dictionary. The values must
143         be single argument functions, which take a `bytes` object and
144         return a converted value. When there's no entry for a field
145         name, then the default UTF-8 decoding will be attempted. If
146         the conversion fails with a ValueError, unconverted bytes
147         object will be returned. (Note that ValueEror is a superclass
148         of UnicodeDecodeError).
149
150         Reader implements the context manager protocol: the journal
151         will be closed when exiting the block.
152         """
153         super(Reader, self).__init__(flags, path, files)
154         if _sys.version_info >= (3,3):
155             self.converters = _ChainMap()
156             if converters is not None:
157                 self.converters.maps.append(converters)
158             self.converters.maps.append(DEFAULT_CONVERTERS)
159         else:
160             self.converters = DEFAULT_CONVERTERS.copy()
161             if converters is not None:
162                 self.converters.update(converters)
163
164     def _convert_field(self, key, value):
165         """Convert value using self.converters[key]
166
167         If `key` is not present in self.converters, a standard unicode
168         decoding will be attempted.  If the conversion (either
169         key-specific or the default one) fails with a ValueError, the
170         original bytes object will be returned.
171         """
172         convert = self.converters.get(key, bytes.decode)
173         try:
174             return convert(value)
175         except ValueError:
176             # Leave in default bytes
177             return value
178
179     def _convert_entry(self, entry):
180         """Convert entire journal entry utilising _covert_field"""
181         result = {}
182         for key, value in entry.items():
183             if isinstance(value, list):
184                 result[key] = [self._convert_field(key, val) for val in value]
185             else:
186                 result[key] = self._convert_field(key, value)
187         return result
188
189     def __iter__(self):
190         """Part of iterator protocol.
191         Returns self.
192         """
193         return self
194
195     def __next__(self):
196         """Part of iterator protocol.
197         Returns self.get_next() or raises StopIteration.
198         """
199         ans = self.get_next()
200         if ans:
201             return ans
202         else:
203             raise StopIteration()
204
205     if _sys.version_info < (3,):
206         next = __next__
207
208     def add_match(self, *args, **kwargs):
209         """Add one or more matches to the filter journal log entries.
210         All matches of different field are combined in a logical AND,
211         and matches of the same field are automatically combined in a
212         logical OR.
213         Matches can be passed as strings of form "FIELD=value", or
214         keyword arguments FIELD="value".
215         """
216         args = list(args)
217         args.extend(_make_line(key, val) for key, val in kwargs.items())
218         for arg in args:
219             super(Reader, self).add_match(arg)
220
221     def get_next(self, skip=1):
222         """Return the next log entry as a mapping type, currently
223         a standard dictionary of fields.
224
225         Optional skip value will return the `skip`\-th log entry.
226
227         Entries will be processed with converters specified during
228         Reader creation.
229         """
230         if super(Reader, self)._next(skip):
231             entry = super(Reader, self)._get_all()
232             if entry:
233                 entry['__REALTIME_TIMESTAMP'] =  self._get_realtime()
234                 entry['__MONOTONIC_TIMESTAMP']  = self._get_monotonic()
235                 entry['__CURSOR']  = self._get_cursor()
236                 return self._convert_entry(entry)
237         return dict()
238
239     def get_previous(self, skip=1):
240         """Return the previous log entry as a mapping type,
241         currently a standard dictionary of fields.
242
243         Optional skip value will return the -`skip`\-th log entry.
244
245         Entries will be processed with converters specified during
246         Reader creation.
247
248         Equivalent to get_next(-skip).
249         """
250         return self.get_next(-skip)
251
252     def query_unique(self, field):
253         """Return unique values appearing in the journal for given `field`.
254
255         Note this does not respect any journal matches.
256
257         Entries will be processed with converters specified during
258         Reader creation.
259         """
260         return set(self._convert_field(field, value)
261             for value in super(Reader, self).query_unique(field))
262
263     def wait(self, timeout=None):
264         """Wait for a change in the journal. `timeout` is the maximum
265         time in seconds to wait, or None, to wait forever.
266
267         Returns one of NOP (no change), APPEND (new entries have been
268         added to the end of the journal), or INVALIDATE (journal files
269         have been added or removed).
270         """
271         us = -1 if timeout is None else int(timeout * 1000000)
272         return super(Reader, self).wait(us)
273
274     def seek_realtime(self, realtime):
275         """Seek to a matching journal entry nearest to `realtime` time.
276
277         Argument `realtime` must be either an integer unix timestamp
278         or datetime.datetime instance.
279         """
280         if isinstance(realtime, _datetime.datetime):
281             realtime = float(realtime.strftime("%s.%f")) * 1000000
282         return super(Reader, self).seek_realtime(int(realtime))
283
284     def seek_monotonic(self, monotonic, bootid=None):
285         """Seek to a matching journal entry nearest to `monotonic` time.
286
287         Argument `monotonic` is a timestamp from boot in either
288         seconds or a datetime.timedelta instance. Argument `bootid`
289         is a string or UUID representing which boot the monotonic time
290         is reference to. Defaults to current bootid.
291         """
292         if isinstance(monotonic, _datetime.timedelta):
293             monotonic = monotonic.totalseconds()
294         monotonic = int(monotonic * 1000000)
295         if isinstance(bootid, _uuid.UUID):
296             bootid = bootid.get_hex()
297         return super(Reader, self).seek_monotonic(monotonic, bootid)
298
299     def log_level(self, level):
300         """Set maximum log `level` by setting matches for PRIORITY.
301         """
302         if 0 <= level <= 7:
303             for i in range(level+1):
304                 self.add_match(PRIORITY="%d" % i)
305         else:
306             raise ValueError("Log level must be 0 <= level <= 7")
307
308     def messageid_match(self, messageid):
309         """Add match for log entries with specified `messageid`.
310
311         `messageid` can be string of hexadicimal digits or a UUID
312         instance. Standard message IDs can be found in systemd.id128.
313
314         Equivalent to add_match(MESSAGE_ID=`messageid`).
315         """
316         if isinstance(messageid, _uuid.UUID):
317             messageid = messageid.get_hex()
318         self.add_match(MESSAGE_ID=messageid)
319
320     def this_boot(self, bootid=None):
321         """Add match for _BOOT_ID equal to current boot ID or the specified boot ID.
322
323         If specified, bootid should be either a UUID or a 32 digit hex number.
324
325         Equivalent to add_match(_BOOT_ID='bootid').
326         """
327         if bootid is None:
328             bootid = _id128.get_boot().hex
329         else:
330             bootid = getattr(bootid, 'hex', bootid)
331         self.add_match(_BOOT_ID=bootid)
332
333     def this_machine(self, machineid=None):
334         """Add match for _MACHINE_ID equal to the ID of this machine.
335
336         If specified, machineid should be either a UUID or a 32 digit hex number.
337
338         Equivalent to add_match(_MACHINE_ID='machineid').
339         """
340         if machineid is None:
341             machineid = _id128.get_machine().hex
342         else:
343             machineid = getattr(machineid, 'hex', machineid)
344         self.add_match(_MACHINE_ID=machineid)
345
346
347 def get_catalog(mid):
348     if isinstance(mid, _uuid.UUID):
349         mid = mid.get_hex()
350     return _get_catalog(mid)
351
352 def _make_line(field, value):
353         if isinstance(value, bytes):
354                 return field.encode('utf-8') + b'=' + value
355         elif isinstance(value, int):
356                 return field + '=' + str(value)
357         else:
358                 return field + '=' + value
359
360 def send(MESSAGE, MESSAGE_ID=None,
361          CODE_FILE=None, CODE_LINE=None, CODE_FUNC=None,
362          **kwargs):
363         r"""Send a message to the journal.
364
365         >>> journal.send('Hello world')
366         >>> journal.send('Hello, again, world', FIELD2='Greetings!')
367         >>> journal.send('Binary message', BINARY=b'\xde\xad\xbe\xef')
368
369         Value of the MESSAGE argument will be used for the MESSAGE=
370         field. MESSAGE must be a string and will be sent as UTF-8 to
371         the journal.
372
373         MESSAGE_ID can be given to uniquely identify the type of
374         message. It must be a string or a uuid.UUID object.
375
376         CODE_LINE, CODE_FILE, and CODE_FUNC can be specified to
377         identify the caller. Unless at least on of the three is given,
378         values are extracted from the stack frame of the caller of
379         send(). CODE_FILE and CODE_FUNC must be strings, CODE_LINE
380         must be an integer.
381
382         Additional fields for the journal entry can only be specified
383         as keyword arguments. The payload can be either a string or
384         bytes. A string will be sent as UTF-8, and bytes will be sent
385         as-is to the journal.
386
387         Other useful fields include PRIORITY, SYSLOG_FACILITY,
388         SYSLOG_IDENTIFIER, SYSLOG_PID.
389         """
390
391         args = ['MESSAGE=' + MESSAGE]
392
393         if MESSAGE_ID is not None:
394                 id = getattr(MESSAGE_ID, 'hex', MESSAGE_ID)
395                 args.append('MESSAGE_ID=' + id)
396
397         if CODE_LINE == CODE_FILE == CODE_FUNC == None:
398                 CODE_FILE, CODE_LINE, CODE_FUNC = \
399                         _traceback.extract_stack(limit=2)[0][:3]
400         if CODE_FILE is not None:
401                 args.append('CODE_FILE=' + CODE_FILE)
402         if CODE_LINE is not None:
403                 args.append('CODE_LINE={:d}'.format(CODE_LINE))
404         if CODE_FUNC is not None:
405                 args.append('CODE_FUNC=' + CODE_FUNC)
406
407         args.extend(_make_line(key, val) for key, val in kwargs.items())
408         return sendv(*args)
409
410 def stream(identifier, priority=LOG_DEBUG, level_prefix=False):
411         r"""Return a file object wrapping a stream to journal.
412
413         Log messages written to this file as simple newline sepearted
414         text strings are written to the journal.
415
416         The file will be line buffered, so messages are actually sent
417         after a newline character is written.
418
419         >>> stream = journal.stream('myapp')
420         >>> stream
421         <open file '<fdopen>', mode 'w' at 0x...>
422         >>> stream.write('message...\n')
423
424         will produce the following message in the journal::
425
426           PRIORITY=7
427           SYSLOG_IDENTIFIER=myapp
428           MESSAGE=message...
429
430         Using the interface with print might be more convinient:
431
432         >>> from __future__ import print_function
433         >>> print('message...', file=stream)
434
435         priority is the syslog priority, one of `LOG_EMERG`,
436         `LOG_ALERT`, `LOG_CRIT`, `LOG_ERR`, `LOG_WARNING`,
437         `LOG_NOTICE`, `LOG_INFO`, `LOG_DEBUG`.
438
439         level_prefix is a boolean. If true, kernel-style log priority
440         level prefixes (such as '<1>') are interpreted. See
441         sd-daemon(3) for more information.
442         """
443
444         fd = stream_fd(identifier, priority, level_prefix)
445         return _os.fdopen(fd, 'w', 1)
446
447 class JournalHandler(_logging.Handler):
448         """Journal handler class for the Python logging framework.
449
450         Please see the Python logging module documentation for an
451         overview: http://docs.python.org/library/logging.html.
452
453         To create a custom logger whose messages go only to journal:
454
455         >>> log = logging.getLogger('custom_logger_name')
456         >>> log.propagate = False
457         >>> log.addHandler(journal.JournalHandler())
458         >>> log.warn("Some message: %s", detail)
459
460         Note that by default, message levels `INFO` and `DEBUG` are
461         ignored by the logging framework. To enable those log levels:
462
463         >>> log.setLevel(logging.DEBUG)
464
465         To redirect all logging messages to journal regardless of where
466         they come from, attach it to the root logger:
467
468         >>> logging.root.addHandler(journal.JournalHandler())
469
470         For more complex configurations when using `dictConfig` or
471         `fileConfig`, specify `systemd.journal.JournalHandler` as the
472         handler class.  Only standard handler configuration options
473         are supported: `level`, `formatter`, `filters`.
474
475         To attach journal MESSAGE_ID, an extra field is supported:
476
477         >>> import uuid
478         >>> mid = uuid.UUID('0123456789ABCDEF0123456789ABCDEF')
479         >>> log.warn("Message with ID", extra={'MESSAGE_ID': mid})
480
481         Fields to be attached to all messages sent through this
482         handler can be specified as keyword arguments. This probably
483         makes sense only for SYSLOG_IDENTIFIER and similar fields
484         which are constant for the whole program:
485
486         >>> journal.JournalHandler(SYSLOG_IDENTIFIER='my-cool-app')
487
488         The following journal fields will be sent:
489         `MESSAGE`, `PRIORITY`, `THREAD_NAME`, `CODE_FILE`, `CODE_LINE`,
490         `CODE_FUNC`, `LOGGER` (name as supplied to getLogger call),
491         `MESSAGE_ID` (optional, see above), `SYSLOG_IDENTIFIER` (defaults
492         to sys.argv[0]).
493         """
494
495         def __init__(self, level=_logging.NOTSET, **kwargs):
496                 super(JournalHandler, self).__init__(level)
497
498                 for name in kwargs:
499                         if not _valid_field_name(name):
500                                 raise ValueError('Invalid field name: ' + name)
501                 if 'SYSLOG_IDENTIFIER' not in kwargs:
502                         kwargs['SYSLOG_IDENTIFIER'] = _sys.argv[0]
503                 self._extra = kwargs
504
505         def emit(self, record):
506                 """Write record as journal event.
507
508                 MESSAGE is taken from the message provided by the
509                 user, and PRIORITY, LOGGER, THREAD_NAME,
510                 CODE_{FILE,LINE,FUNC} fields are appended
511                 automatically. In addition, record.MESSAGE_ID will be
512                 used if present.
513                 """
514                 try:
515                         msg = self.format(record)
516                         pri = self.mapPriority(record.levelno)
517                         mid = getattr(record, 'MESSAGE_ID', None)
518                         send(msg,
519                              MESSAGE_ID=mid,
520                              PRIORITY=format(pri),
521                              LOGGER=record.name,
522                              THREAD_NAME=record.threadName,
523                              CODE_FILE=record.pathname,
524                              CODE_LINE=record.lineno,
525                              CODE_FUNC=record.funcName,
526                              **self._extra)
527                 except Exception:
528                         self.handleError(record)
529
530         @staticmethod
531         def mapPriority(levelno):
532                 """Map logging levels to journald priorities.
533
534                 Since Python log level numbers are "sparse", we have
535                 to map numbers in between the standard levels too.
536                 """
537                 if levelno <= _logging.DEBUG:
538                         return LOG_DEBUG
539                 elif levelno <= _logging.INFO:
540                         return LOG_INFO
541                 elif levelno <= _logging.WARNING:
542                         return LOG_WARNING
543                 elif levelno <= _logging.ERROR:
544                         return LOG_ERR
545                 elif levelno <= _logging.CRITICAL:
546                         return LOG_CRIT
547                 else:
548                         return LOG_ALERT