]>
Commit | Line | Data |
---|---|---|
927e9632 | 1 | # -*- Mode: python; coding:utf-8; indent-tabs-mode: nil -*- */ |
6b91ae13 LP |
2 | # |
3 | # This file is part of systemd. | |
4 | # | |
927e9632 ZJS |
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> | |
6b91ae13 LP |
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 | ||
6a6633a1 ZJS |
22 | from __future__ import division |
23 | ||
b8359823 SH |
24 | import sys as _sys |
25 | import datetime as _datetime | |
b8359823 | 26 | import uuid as _uuid |
8d7e170a LP |
27 | import traceback as _traceback |
28 | import os as _os | |
73c0495f | 29 | import logging as _logging |
b5edbff0 | 30 | if _sys.version_info >= (3,3): |
5c083fa8 | 31 | from collections import ChainMap as _ChainMap |
8d7e170a LP |
32 | from syslog import (LOG_EMERG, LOG_ALERT, LOG_CRIT, LOG_ERR, |
33 | LOG_WARNING, LOG_NOTICE, LOG_INFO, LOG_DEBUG) | |
5afbe712 | 34 | from ._journal import __version__, sendv, stream_fd |
7f41820b | 35 | from ._reader import (_Reader, NOP, APPEND, INVALIDATE, |
affba8e9 ZJS |
36 | LOCAL_ONLY, RUNTIME_ONLY, |
37 | SYSTEM, SYSTEM_ONLY, CURRENT_USER, | |
811de196 | 38 | _get_catalog) |
6a71de70 | 39 | from . import id128 as _id128 |
8d7e170a | 40 | |
86e3d32a ZJS |
41 | if _sys.version_info >= (3,): |
42 | from ._reader import Monotonic | |
43 | else: | |
44 | Monotonic = tuple | |
45 | ||
aaf08061 ZJS |
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): | |
806bc1cb | 54 | return _datetime.datetime.fromtimestamp(t / 1000000) |
aaf08061 ZJS |
55 | |
56 | def _convert_timestamp(s): | |
806bc1cb | 57 | return _datetime.datetime.fromtimestamp(int(s) / 1000000) |
aaf08061 | 58 | |
0a0c35d1 ZJS |
59 | def _convert_trivial(x): |
60 | return x | |
61 | ||
aaf08061 ZJS |
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 | ||
3aa8f086 | 68 | DEFAULT_CONVERTERS = { |
aaf08061 ZJS |
69 | 'MESSAGE_ID': _convert_uuid, |
70 | '_MACHINE_ID': _convert_uuid, | |
71 | '_BOOT_ID': _convert_uuid, | |
3aa8f086 SH |
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, | |
aaf08061 ZJS |
90 | '_SOURCE_REALTIME_TIMESTAMP': _convert_timestamp, |
91 | '__REALTIME_TIMESTAMP': _convert_realtime, | |
92 | '_SOURCE_MONOTONIC_TIMESTAMP': _convert_source_monotonic, | |
93 | '__MONOTONIC_TIMESTAMP': _convert_monotonic, | |
0a0c35d1 | 94 | '__CURSOR': _convert_trivial, |
3aa8f086 SH |
95 | 'COREDUMP': bytes, |
96 | 'COREDUMP_PID': int, | |
97 | 'COREDUMP_UID': int, | |
98 | 'COREDUMP_GID': int, | |
99 | 'COREDUMP_SESSION': int, | |
100 | 'COREDUMP_SIGNAL': int, | |
aaf08061 | 101 | 'COREDUMP_TIMESTAMP': _convert_timestamp, |
3aa8f086 SH |
102 | } |
103 | ||
b5edbff0 ZJS |
104 | _IDENT_LETTER = set('ABCDEFGHIJKLMNOPQRTSUVWXYZ_') |
105 | ||
106 | def _valid_field_name(s): | |
107 | return not (set(s) - _IDENT_LETTER) | |
108 | ||
7f41820b ZJS |
109 | class Reader(_Reader): |
110 | """Reader allows the access and filtering of systemd journal | |
cac40fbe | 111 | entries. Note that in order to access the system journal, a |
614e5fbb | 112 | non-root user must be in the `systemd-journal` group. |
cac40fbe | 113 | |
aaf08061 ZJS |
114 | Example usage to print out all informational or higher level |
115 | messages for systemd-udevd for this boot: | |
cac40fbe | 116 | |
aaf08061 ZJS |
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: | |
cac40fbe SH |
122 | ... print(entry['MESSAGE']) |
123 | ||
2c076467 ZJS |
124 | See systemd.journal-fields(7) for more info on typical fields |
125 | found in the journal. | |
cac40fbe | 126 | """ |
affba8e9 | 127 | def __init__(self, flags=0, path=None, files=None, converters=None): |
7f41820b | 128 | """Create an instance of Reader, which allows filtering and |
c71f26eb | 129 | return of journal entries. |
aaf08061 | 130 | |
c71f26eb SH |
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. | |
aaf08061 | 136 | |
c71f26eb | 137 | Argument `path` is the directory of journal files. Note that |
0eff0f3b | 138 | `flags` and `path` are exclusive. |
aaf08061 ZJS |
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). | |
811de196 ZJS |
149 | |
150 | Reader implements the context manager protocol: the journal | |
151 | will be closed when exiting the block. | |
c71f26eb | 152 | """ |
affba8e9 | 153 | super(Reader, self).__init__(flags, path, files) |
b8359823 | 154 | if _sys.version_info >= (3,3): |
5c083fa8 | 155 | self.converters = _ChainMap() |
3aa8f086 SH |
156 | if converters is not None: |
157 | self.converters.maps.append(converters) | |
158 | self.converters.maps.append(DEFAULT_CONVERTERS) | |
2d0603bd | 159 | else: |
3aa8f086 SH |
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): | |
aaf08061 ZJS |
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) | |
3aa8f086 | 173 | try: |
aaf08061 ZJS |
174 | return convert(value) |
175 | except ValueError: | |
176 | # Leave in default bytes | |
177 | return value | |
3aa8f086 SH |
178 | |
179 | def _convert_entry(self, entry): | |
2c076467 | 180 | """Convert entire journal entry utilising _covert_field""" |
3aa8f086 | 181 | result = {} |
bf1ced55 | 182 | for key, value in entry.items(): |
3aa8f086 SH |
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 | ||
6a58bf41 SH |
189 | def __iter__(self): |
190 | """Part of iterator protocol. | |
191 | Returns self. | |
192 | """ | |
193 | return self | |
194 | ||
1070f974 ZJS |
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__ | |
6a58bf41 | 207 | |
bf1ced55 | 208 | def add_match(self, *args, **kwargs): |
c71f26eb SH |
209 | """Add one or more matches to the filter journal log entries. |
210 | All matches of different field are combined in a logical AND, | |
2c076467 | 211 | and matches of the same field are automatically combined in a |
c71f26eb | 212 | logical OR. |
2c076467 ZJS |
213 | Matches can be passed as strings of form "FIELD=value", or |
214 | keyword arguments FIELD="value". | |
215 | """ | |
bf1ced55 SH |
216 | args = list(args) |
217 | args.extend(_make_line(key, val) for key, val in kwargs.items()) | |
5bb2b8d5 | 218 | for arg in args: |
7f41820b | 219 | super(Reader, self).add_match(arg) |
bf1ced55 | 220 | |
71766afa | 221 | def get_next(self, skip=1): |
6a58bf41 SH |
222 | """Return the next log entry as a mapping type, currently |
223 | a standard dictionary of fields. | |
2c076467 ZJS |
224 | |
225 | Optional skip value will return the `skip`\-th log entry. | |
226 | ||
227 | Entries will be processed with converters specified during | |
7f41820b | 228 | Reader creation. |
2c076467 | 229 | """ |
6a58bf41 SH |
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 | ||
d2bbca68 | 248 | Equivalent to get_next(-skip). |
6a58bf41 SH |
249 | """ |
250 | return self.get_next(-skip) | |
3aa8f086 | 251 | |
c71f26eb | 252 | def query_unique(self, field): |
7f41820b | 253 | """Return unique values appearing in the journal for given `field`. |
2c076467 ZJS |
254 | |
255 | Note this does not respect any journal matches. | |
256 | ||
257 | Entries will be processed with converters specified during | |
7f41820b | 258 | Reader creation. |
2c076467 | 259 | """ |
c71f26eb | 260 | return set(self._convert_field(field, value) |
7f41820b | 261 | for value in super(Reader, self).query_unique(field)) |
c71f26eb | 262 | |
806bc1cb ZJS |
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) | |
7c962c86 | 272 | return super(Reader, self).wait(us) |
806bc1cb | 273 | |
c71f26eb | 274 | def seek_realtime(self, realtime): |
2c076467 ZJS |
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 | """ | |
b8359823 | 280 | if isinstance(realtime, _datetime.datetime): |
806bc1cb ZJS |
281 | realtime = float(realtime.strftime("%s.%f")) * 1000000 |
282 | return super(Reader, self).seek_realtime(int(realtime)) | |
c71f26eb SH |
283 | |
284 | def seek_monotonic(self, monotonic, bootid=None): | |
2c076467 ZJS |
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 | """ | |
b8359823 | 292 | if isinstance(monotonic, _datetime.timedelta): |
c71f26eb | 293 | monotonic = monotonic.totalseconds() |
806bc1cb | 294 | monotonic = int(monotonic * 1000000) |
c71f26eb SH |
295 | if isinstance(bootid, _uuid.UUID): |
296 | bootid = bootid.get_hex() | |
7f41820b | 297 | return super(Reader, self).seek_monotonic(monotonic, bootid) |
7a1b9cd5 | 298 | |
25523db4 | 299 | def log_level(self, level): |
2c076467 ZJS |
300 | """Set maximum log `level` by setting matches for PRIORITY. |
301 | """ | |
25523db4 SH |
302 | if 0 <= level <= 7: |
303 | for i in range(level+1): | |
aaf08061 | 304 | self.add_match(PRIORITY="%d" % i) |
25523db4 SH |
305 | else: |
306 | raise ValueError("Log level must be 0 <= level <= 7") | |
307 | ||
89d9a233 | 308 | def messageid_match(self, messageid): |
2c076467 ZJS |
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 | """ | |
89d9a233 SH |
316 | if isinstance(messageid, _uuid.UUID): |
317 | messageid = messageid.get_hex() | |
318 | self.add_match(MESSAGE_ID=messageid) | |
319 | ||
301ae164 SH |
320 | def this_boot(self, bootid=None): |
321 | """Add match for _BOOT_ID equal to current boot ID or the specified boot ID. | |
322 | ||
c71f26eb SH |
323 | If specified, bootid should be either a UUID or a 32 digit hex number. |
324 | ||
325 | Equivalent to add_match(_BOOT_ID='bootid'). | |
301ae164 SH |
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 | ||
c71f26eb SH |
336 | If specified, machineid should be either a UUID or a 32 digit hex number. |
337 | ||
338 | Equivalent to add_match(_MACHINE_ID='machineid'). | |
301ae164 SH |
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) | |
6a71de70 | 345 | |
6a71de70 | 346 | |
811de196 ZJS |
347 | def get_catalog(mid): |
348 | if isinstance(mid, _uuid.UUID): | |
349 | mid = mid.get_hex() | |
350 | return _get_catalog(mid) | |
351 | ||
8d7e170a | 352 | def _make_line(field, value): |
0aee68ad LP |
353 | if isinstance(value, bytes): |
354 | return field.encode('utf-8') + b'=' + value | |
8ff8ee83 RM |
355 | elif isinstance(value, int): |
356 | return field + '=' + str(value) | |
0aee68ad LP |
357 | else: |
358 | return field + '=' + value | |
8d7e170a LP |
359 | |
360 | def send(MESSAGE, MESSAGE_ID=None, | |
361 | CODE_FILE=None, CODE_LINE=None, CODE_FUNC=None, | |
362 | **kwargs): | |
afcd68c1 | 363 | r"""Send a message to the journal. |
8d7e170a | 364 | |
0aee68ad LP |
365 | >>> journal.send('Hello world') |
366 | >>> journal.send('Hello, again, world', FIELD2='Greetings!') | |
367 | >>> journal.send('Binary message', BINARY=b'\xde\xad\xbe\xef') | |
8d7e170a | 368 | |
0aee68ad | 369 | Value of the MESSAGE argument will be used for the MESSAGE= |
afcd68c1 ZJS |
370 | field. MESSAGE must be a string and will be sent as UTF-8 to |
371 | the journal. | |
8d7e170a | 372 | |
0aee68ad | 373 | MESSAGE_ID can be given to uniquely identify the type of |
afcd68c1 | 374 | message. It must be a string or a uuid.UUID object. |
8d7e170a | 375 | |
0aee68ad LP |
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. | |
8d7e170a | 381 | |
afcd68c1 ZJS |
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 | ||
0aee68ad LP |
387 | Other useful fields include PRIORITY, SYSLOG_FACILITY, |
388 | SYSLOG_IDENTIFIER, SYSLOG_PID. | |
389 | """ | |
8d7e170a | 390 | |
0aee68ad | 391 | args = ['MESSAGE=' + MESSAGE] |
8d7e170a | 392 | |
0aee68ad | 393 | if MESSAGE_ID is not None: |
afcd68c1 ZJS |
394 | id = getattr(MESSAGE_ID, 'hex', MESSAGE_ID) |
395 | args.append('MESSAGE_ID=' + id) | |
8d7e170a | 396 | |
0aee68ad LP |
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) | |
8d7e170a | 406 | |
0aee68ad LP |
407 | args.extend(_make_line(key, val) for key, val in kwargs.items()) |
408 | return sendv(*args) | |
8d7e170a LP |
409 | |
410 | def stream(identifier, priority=LOG_DEBUG, level_prefix=False): | |
0aee68ad | 411 | r"""Return a file object wrapping a stream to journal. |
8d7e170a | 412 | |
0aee68ad LP |
413 | Log messages written to this file as simple newline sepearted |
414 | text strings are written to the journal. | |
8d7e170a | 415 | |
0aee68ad LP |
416 | The file will be line buffered, so messages are actually sent |
417 | after a newline character is written. | |
8d7e170a | 418 | |
0aee68ad LP |
419 | >>> stream = journal.stream('myapp') |
420 | >>> stream | |
421 | <open file '<fdopen>', mode 'w' at 0x...> | |
422 | >>> stream.write('message...\n') | |
8d7e170a | 423 | |
9015fa64 | 424 | will produce the following message in the journal:: |
8d7e170a | 425 | |
9015fa64 ZJS |
426 | PRIORITY=7 |
427 | SYSLOG_IDENTIFIER=myapp | |
428 | MESSAGE=message... | |
8d7e170a | 429 | |
0aee68ad | 430 | Using the interface with print might be more convinient: |
8d7e170a | 431 | |
0aee68ad LP |
432 | >>> from __future__ import print_function |
433 | >>> print('message...', file=stream) | |
8d7e170a | 434 | |
9015fa64 ZJS |
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`. | |
8d7e170a | 438 | |
0aee68ad LP |
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 | """ | |
8d7e170a | 443 | |
0aee68ad LP |
444 | fd = stream_fd(identifier, priority, level_prefix) |
445 | return _os.fdopen(fd, 'w', 1) | |
73c0495f MR |
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 | |
927e9632 | 451 | overview: http://docs.python.org/library/logging.html. |
73c0495f MR |
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 | ||
9015fa64 ZJS |
460 | Note that by default, message levels `INFO` and `DEBUG` are |
461 | ignored by the logging framework. To enable those log levels: | |
73c0495f MR |
462 | |
463 | >>> log.setLevel(logging.DEBUG) | |
464 | ||
73c0495f MR |
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 | ||
9015fa64 ZJS |
470 | For more complex configurations when using `dictConfig` or |
471 | `fileConfig`, specify `systemd.journal.JournalHandler` as the | |
73c0495f | 472 | handler class. Only standard handler configuration options |
9015fa64 | 473 | are supported: `level`, `formatter`, `filters`. |
73c0495f | 474 | |
b5edbff0 ZJS |
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 | ||
73c0495f | 488 | The following journal fields will be sent: |
9015fa64 ZJS |
489 | `MESSAGE`, `PRIORITY`, `THREAD_NAME`, `CODE_FILE`, `CODE_LINE`, |
490 | `CODE_FUNC`, `LOGGER` (name as supplied to getLogger call), | |
b5edbff0 ZJS |
491 | `MESSAGE_ID` (optional, see above), `SYSLOG_IDENTIFIER` (defaults |
492 | to sys.argv[0]). | |
73c0495f MR |
493 | """ |
494 | ||
b5edbff0 ZJS |
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 | ||
73c0495f MR |
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, | |
b5edbff0 ZJS |
525 | CODE_FUNC=record.funcName, |
526 | **self._extra) | |
73c0495f MR |
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 |