chiark / gitweb /
systemd-python: tidy up import names in 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     def __init__(self, converters=None, *args, **kwargs):
83         super(Journal, self).__init__(*args, **kwargs)
84         if sys.version_info >= (3,3):
85         if _sys.version_info >= (3,3):
86             self.converters = ChainMap()
87             if converters is not None:
88                 self.converters.maps.append(converters)
89             self.converters.maps.append(DEFAULT_CONVERTERS)
90         else:
91             # suitable fallback, e.g.
92             self.converters = DEFAULT_CONVERTERS.copy()
93             if converters is not None:
94                 self.converters.update(converters)
95
96     def _convert_field(self, key, value):
97         try:
98             result = self.converters[key](value)
99         except:
100             # Default conversion in unicode
101             try:
102                 result = _convert_unicode(value)
103             except:
104                 # Leave in default bytes
105                 result = value
106         return result
107
108     def _convert_entry(self, entry):
109         result = {}
110         for key, value in entry.items():
111             if isinstance(value, list):
112                 result[key] = [self._convert_field(key, val) for val in value]
113             else:
114                 result[key] = self._convert_field(key, value)
115         return result
116
117     def add_match(self, *args, **kwargs):
118         args = list(args)
119         args.extend(_make_line(key, val) for key, val in kwargs.items())
120         for arg in args:
121             super(Journal, self).add_match(arg)
122
123     def get_next(self, skip=1):
124         return self._convert_entry(
125             super(Journal, self).get_next(skip))
126
127     def query_unique(self, key):
128         return set(self._convert_field(key, value)
129             for value in super(Journal, self).query_unique(key))
130
131     def seek_realtime(self, timestamp):
132         if isinstance(realtime, _datetime.datetime):
133             timestamp = float(timestamp.strftime("%s.%f"))
134         return super(Journal, self).seek_realtime(timestamp)
135
136     def seek_monotonic(self, timestamp, bootid=None):
137         if isinstance(monotonic, _datetime.timedelta):
138             timestamp = timestamp.totalseconds()
139         return super(Journal, self).seek_monotonic(timestamp, bootid)
140
141     def log_level(self, level):
142         """Sets maximum log level by setting matches for PRIORITY."""
143         if 0 <= level <= 7:
144             for i in range(level+1):
145                 self.add_match(PRIORITY="%s" % i)
146         else:
147             raise ValueError("Log level must be 0 <= level <= 7")
148
149     def this_boot(self, bootid=None):
150         """Add match for _BOOT_ID equal to current boot ID or the specified boot ID.
151
152         bootid should be either a UUID or a 32 digit hex number.
153         """
154         if bootid is None:
155             bootid = _id128.get_boot().hex
156         else:
157             bootid = getattr(bootid, 'hex', bootid)
158         self.add_match(_BOOT_ID=bootid)
159
160     def this_machine(self, machineid=None):
161         """Add match for _MACHINE_ID equal to the ID of this machine.
162
163         bootid should be either a UUID or a 32 digit hex number.
164         """
165         if machineid is None:
166             machineid = _id128.get_machine().hex
167         else:
168             machineid = getattr(machineid, 'hex', machineid)
169         self.add_match(_MACHINE_ID=machineid)
170
171
172 def _make_line(field, value):
173         if isinstance(value, bytes):
174                 return field.encode('utf-8') + b'=' + value
175         else:
176                 return field + '=' + value
177
178 def send(MESSAGE, MESSAGE_ID=None,
179          CODE_FILE=None, CODE_LINE=None, CODE_FUNC=None,
180          **kwargs):
181         r"""Send a message to the journal.
182
183         >>> journal.send('Hello world')
184         >>> journal.send('Hello, again, world', FIELD2='Greetings!')
185         >>> journal.send('Binary message', BINARY=b'\xde\xad\xbe\xef')
186
187         Value of the MESSAGE argument will be used for the MESSAGE=
188         field. MESSAGE must be a string and will be sent as UTF-8 to
189         the journal.
190
191         MESSAGE_ID can be given to uniquely identify the type of
192         message. It must be a string or a uuid.UUID object.
193
194         CODE_LINE, CODE_FILE, and CODE_FUNC can be specified to
195         identify the caller. Unless at least on of the three is given,
196         values are extracted from the stack frame of the caller of
197         send(). CODE_FILE and CODE_FUNC must be strings, CODE_LINE
198         must be an integer.
199
200         Additional fields for the journal entry can only be specified
201         as keyword arguments. The payload can be either a string or
202         bytes. A string will be sent as UTF-8, and bytes will be sent
203         as-is to the journal.
204
205         Other useful fields include PRIORITY, SYSLOG_FACILITY,
206         SYSLOG_IDENTIFIER, SYSLOG_PID.
207         """
208
209         args = ['MESSAGE=' + MESSAGE]
210
211         if MESSAGE_ID is not None:
212                 id = getattr(MESSAGE_ID, 'hex', MESSAGE_ID)
213                 args.append('MESSAGE_ID=' + id)
214
215         if CODE_LINE == CODE_FILE == CODE_FUNC == None:
216                 CODE_FILE, CODE_LINE, CODE_FUNC = \
217                         _traceback.extract_stack(limit=2)[0][:3]
218         if CODE_FILE is not None:
219                 args.append('CODE_FILE=' + CODE_FILE)
220         if CODE_LINE is not None:
221                 args.append('CODE_LINE={:d}'.format(CODE_LINE))
222         if CODE_FUNC is not None:
223                 args.append('CODE_FUNC=' + CODE_FUNC)
224
225         args.extend(_make_line(key, val) for key, val in kwargs.items())
226         return sendv(*args)
227
228 def stream(identifier, priority=LOG_DEBUG, level_prefix=False):
229         r"""Return a file object wrapping a stream to journal.
230
231         Log messages written to this file as simple newline sepearted
232         text strings are written to the journal.
233
234         The file will be line buffered, so messages are actually sent
235         after a newline character is written.
236
237         >>> stream = journal.stream('myapp')
238         >>> stream
239         <open file '<fdopen>', mode 'w' at 0x...>
240         >>> stream.write('message...\n')
241
242         will produce the following message in the journal::
243
244           PRIORITY=7
245           SYSLOG_IDENTIFIER=myapp
246           MESSAGE=message...
247
248         Using the interface with print might be more convinient:
249
250         >>> from __future__ import print_function
251         >>> print('message...', file=stream)
252
253         priority is the syslog priority, one of `LOG_EMERG`,
254         `LOG_ALERT`, `LOG_CRIT`, `LOG_ERR`, `LOG_WARNING`,
255         `LOG_NOTICE`, `LOG_INFO`, `LOG_DEBUG`.
256
257         level_prefix is a boolean. If true, kernel-style log priority
258         level prefixes (such as '<1>') are interpreted. See
259         sd-daemon(3) for more information.
260         """
261
262         fd = stream_fd(identifier, priority, level_prefix)
263         return _os.fdopen(fd, 'w', 1)
264
265 class JournalHandler(_logging.Handler):
266         """Journal handler class for the Python logging framework.
267
268         Please see the Python logging module documentation for an
269         overview: http://docs.python.org/library/logging.html.
270
271         To create a custom logger whose messages go only to journal:
272
273         >>> log = logging.getLogger('custom_logger_name')
274         >>> log.propagate = False
275         >>> log.addHandler(journal.JournalHandler())
276         >>> log.warn("Some message: %s", detail)
277
278         Note that by default, message levels `INFO` and `DEBUG` are
279         ignored by the logging framework. To enable those log levels:
280
281         >>> log.setLevel(logging.DEBUG)
282
283         To attach journal MESSAGE_ID, an extra field is supported:
284
285         >>> import uuid
286         >>> mid = uuid.UUID('0123456789ABCDEF0123456789ABCDEF')
287         >>> log.warn("Message with ID", extra={'MESSAGE_ID': mid})
288
289         To redirect all logging messages to journal regardless of where
290         they come from, attach it to the root logger:
291
292         >>> logging.root.addHandler(journal.JournalHandler())
293
294         For more complex configurations when using `dictConfig` or
295         `fileConfig`, specify `systemd.journal.JournalHandler` as the
296         handler class.  Only standard handler configuration options
297         are supported: `level`, `formatter`, `filters`.
298
299         The following journal fields will be sent:
300         `MESSAGE`, `PRIORITY`, `THREAD_NAME`, `CODE_FILE`, `CODE_LINE`,
301         `CODE_FUNC`, `LOGGER` (name as supplied to getLogger call),
302         `MESSAGE_ID` (optional, see above).
303         """
304
305         def emit(self, record):
306                 """Write record as journal event.
307
308                 MESSAGE is taken from the message provided by the
309                 user, and PRIORITY, LOGGER, THREAD_NAME,
310                 CODE_{FILE,LINE,FUNC} fields are appended
311                 automatically. In addition, record.MESSAGE_ID will be
312                 used if present.
313                 """
314                 try:
315                         msg = self.format(record)
316                         pri = self.mapPriority(record.levelno)
317                         mid = getattr(record, 'MESSAGE_ID', None)
318                         send(msg,
319                              MESSAGE_ID=mid,
320                              PRIORITY=format(pri),
321                              LOGGER=record.name,
322                              THREAD_NAME=record.threadName,
323                              CODE_FILE=record.pathname,
324                              CODE_LINE=record.lineno,
325                              CODE_FUNC=record.funcName)
326                 except Exception:
327                         self.handleError(record)
328
329         @staticmethod
330         def mapPriority(levelno):
331                 """Map logging levels to journald priorities.
332
333                 Since Python log level numbers are "sparse", we have
334                 to map numbers in between the standard levels too.
335                 """
336                 if levelno <= _logging.DEBUG:
337                         return LOG_DEBUG
338                 elif levelno <= _logging.INFO:
339                         return LOG_INFO
340                 elif levelno <= _logging.WARNING:
341                         return LOG_WARNING
342                 elif levelno <= _logging.ERROR:
343                         return LOG_ERR
344                 elif levelno <= _logging.CRITICAL:
345                         return LOG_CRIT
346                 else:
347                         return LOG_ALERT