1 # -*- Mode: python; coding:utf-8; indent-tabs-mode: nil -*- */
3 # This file is part of systemd.
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>
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.
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.
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/>.
23 import datetime as _datetime
24 import functools as _functools
26 import traceback as _traceback
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
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,
48 'USERSPACE_USEC': int,
54 'SYSLOG_FACILITY': int,
56 '_AUDIT_SESSION': int,
57 '_AUDIT_LOGINUID': int,
58 '_SYSTEMD_SESSION': int,
59 '_SYSTEMD_OWNER_UID': int,
63 '_SOURCE_REALTIME_TIMESTAMP': _REALTIME_CONVERTER,
64 '__REALTIME_TIMESTAMP': _REALTIME_CONVERTER,
65 '_SOURCE_MONOTONIC_TIMESTAMP': _MONOTONIC_CONVERTER,
66 '__MONOTONIC_TIMESTAMP': _MONOTONIC_CONVERTER,
71 'COREDUMP_SESSION': int,
72 'COREDUMP_SIGNAL': int,
73 'COREDUMP_TIMESTAMP': _REALTIME_CONVERTER,
76 if _sys.version_info >= (3,):
77 _convert_unicode = _functools.partial(str, encoding='utf-8')
79 _convert_unicode = _functools.partial(unicode, encoding='utf-8')
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)
91 # suitable fallback, e.g.
92 self.converters = DEFAULT_CONVERTERS.copy()
93 if converters is not None:
94 self.converters.update(converters)
96 def _convert_field(self, key, value):
98 result = self.converters[key](value)
100 # Default conversion in unicode
102 result = _convert_unicode(value)
104 # Leave in default bytes
108 def _convert_entry(self, entry):
110 for key, value in entry.items():
111 if isinstance(value, list):
112 result[key] = [self._convert_field(key, val) for val in value]
114 result[key] = self._convert_field(key, value)
117 def add_match(self, *args, **kwargs):
119 args.extend(_make_line(key, val) for key, val in kwargs.items())
121 super(Journal, self).add_match(arg)
123 def get_next(self, skip=1):
124 return self._convert_entry(
125 super(Journal, self).get_next(skip))
127 def query_unique(self, key):
128 return set(self._convert_field(key, value)
129 for value in super(Journal, self).query_unique(key))
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)
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)
141 def log_level(self, level):
142 """Sets maximum log level by setting matches for PRIORITY."""
144 for i in range(level+1):
145 self.add_match(PRIORITY="%s" % i)
147 raise ValueError("Log level must be 0 <= level <= 7")
149 def this_boot(self, bootid=None):
150 """Add match for _BOOT_ID equal to current boot ID or the specified boot ID.
152 bootid should be either a UUID or a 32 digit hex number.
155 bootid = _id128.get_boot().hex
157 bootid = getattr(bootid, 'hex', bootid)
158 self.add_match(_BOOT_ID=bootid)
160 def this_machine(self, machineid=None):
161 """Add match for _MACHINE_ID equal to the ID of this machine.
163 bootid should be either a UUID or a 32 digit hex number.
165 if machineid is None:
166 machineid = _id128.get_machine().hex
168 machineid = getattr(machineid, 'hex', machineid)
169 self.add_match(_MACHINE_ID=machineid)
172 def _make_line(field, value):
173 if isinstance(value, bytes):
174 return field.encode('utf-8') + b'=' + value
176 return field + '=' + value
178 def send(MESSAGE, MESSAGE_ID=None,
179 CODE_FILE=None, CODE_LINE=None, CODE_FUNC=None,
181 r"""Send a message to the journal.
183 >>> journal.send('Hello world')
184 >>> journal.send('Hello, again, world', FIELD2='Greetings!')
185 >>> journal.send('Binary message', BINARY=b'\xde\xad\xbe\xef')
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
191 MESSAGE_ID can be given to uniquely identify the type of
192 message. It must be a string or a uuid.UUID object.
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
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.
205 Other useful fields include PRIORITY, SYSLOG_FACILITY,
206 SYSLOG_IDENTIFIER, SYSLOG_PID.
209 args = ['MESSAGE=' + MESSAGE]
211 if MESSAGE_ID is not None:
212 id = getattr(MESSAGE_ID, 'hex', MESSAGE_ID)
213 args.append('MESSAGE_ID=' + id)
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)
225 args.extend(_make_line(key, val) for key, val in kwargs.items())
228 def stream(identifier, priority=LOG_DEBUG, level_prefix=False):
229 r"""Return a file object wrapping a stream to journal.
231 Log messages written to this file as simple newline sepearted
232 text strings are written to the journal.
234 The file will be line buffered, so messages are actually sent
235 after a newline character is written.
237 >>> stream = journal.stream('myapp')
239 <open file '<fdopen>', mode 'w' at 0x...>
240 >>> stream.write('message...\n')
242 will produce the following message in the journal::
245 SYSLOG_IDENTIFIER=myapp
248 Using the interface with print might be more convinient:
250 >>> from __future__ import print_function
251 >>> print('message...', file=stream)
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`.
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.
262 fd = stream_fd(identifier, priority, level_prefix)
263 return _os.fdopen(fd, 'w', 1)
265 class JournalHandler(_logging.Handler):
266 """Journal handler class for the Python logging framework.
268 Please see the Python logging module documentation for an
269 overview: http://docs.python.org/library/logging.html.
271 To create a custom logger whose messages go only to journal:
273 >>> log = logging.getLogger('custom_logger_name')
274 >>> log.propagate = False
275 >>> log.addHandler(journal.JournalHandler())
276 >>> log.warn("Some message: %s", detail)
278 Note that by default, message levels `INFO` and `DEBUG` are
279 ignored by the logging framework. To enable those log levels:
281 >>> log.setLevel(logging.DEBUG)
283 To attach journal MESSAGE_ID, an extra field is supported:
286 >>> mid = uuid.UUID('0123456789ABCDEF0123456789ABCDEF')
287 >>> log.warn("Message with ID", extra={'MESSAGE_ID': mid})
289 To redirect all logging messages to journal regardless of where
290 they come from, attach it to the root logger:
292 >>> logging.root.addHandler(journal.JournalHandler())
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`.
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).
305 def emit(self, record):
306 """Write record as journal event.
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
315 msg = self.format(record)
316 pri = self.mapPriority(record.levelno)
317 mid = getattr(record, 'MESSAGE_ID', None)
320 PRIORITY=format(pri),
322 THREAD_NAME=record.threadName,
323 CODE_FILE=record.pathname,
324 CODE_LINE=record.lineno,
325 CODE_FUNC=record.funcName)
327 self.handleError(record)
330 def mapPriority(levelno):
331 """Map logging levels to journald priorities.
333 Since Python log level numbers are "sparse", we have
334 to map numbers in between the standard levels too.
336 if levelno <= _logging.DEBUG:
338 elif levelno <= _logging.INFO:
340 elif levelno <= _logging.WARNING:
342 elif levelno <= _logging.ERROR:
344 elif levelno <= _logging.CRITICAL: