chiark / gitweb /
systemd-python: Journal log_level moved to python
[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 datetime
23 import functools
24 import sys
25 import uuid
26 import traceback as _traceback
27 import os as _os
28 import logging as _logging
29 from syslog import (LOG_EMERG, LOG_ALERT, LOG_CRIT, LOG_ERR,
30                     LOG_WARNING, LOG_NOTICE, LOG_INFO, LOG_DEBUG)
31 from ._journal import sendv, stream_fd
32 from ._reader import (_Journal, NOP, APPEND, INVALIDATE,
33                       LOCAL_ONLY, RUNTIME_ONLY, SYSTEM_ONLY)
34
35 _MONOTONIC_CONVERTER = lambda x: datetime.timedelta(microseconds=float(x))
36 _REALTIME_CONVERTER = lambda x: datetime.datetime.fromtimestamp(float(x)/1E6)
37 DEFAULT_CONVERTERS = {
38     'MESSAGE_ID': uuid.UUID,
39     'PRIORITY': int,
40     'LEADER': int,
41     'SESSION_ID': int,
42     'USERSPACE_USEC': int,
43     'INITRD_USEC': int,
44     'KERNEL_USEC': int,
45     '_UID': int,
46     '_GID': int,
47     '_PID': int,
48     'SYSLOG_FACILITY': int,
49     'SYSLOG_PID': int,
50     '_AUDIT_SESSION': int,
51     '_AUDIT_LOGINUID': int,
52     '_SYSTEMD_SESSION': int,
53     '_SYSTEMD_OWNER_UID': int,
54     'CODE_LINE': int,
55     'ERRNO': int,
56     'EXIT_STATUS': int,
57     '_SOURCE_REALTIME_TIMESTAMP': _REALTIME_CONVERTER,
58     '__REALTIME_TIMESTAMP': _REALTIME_CONVERTER,
59     '_SOURCE_MONOTONIC_TIMESTAMP': _MONOTONIC_CONVERTER,
60     '__MONOTONIC_TIMESTAMP': _MONOTONIC_CONVERTER,
61     'COREDUMP': bytes,
62     'COREDUMP_PID': int,
63     'COREDUMP_UID': int,
64     'COREDUMP_GID': int,
65     'COREDUMP_SESSION': int,
66     'COREDUMP_SIGNAL': int,
67     'COREDUMP_TIMESTAMP': _REALTIME_CONVERTER,
68 }
69
70 if sys.version_info >= (3,):
71     _convert_unicode = functools.partial(str, encoding='utf-8')
72 else:
73     _convert_unicode = functools.partial(unicode, encoding='utf-8')
74
75 class Journal(_Journal):
76     def __init__(self, converters=None, *args, **kwargs):
77         super(Journal, self).__init__(*args, **kwargs)
78         if sys.version_info >= (3,3):
79             self.converters = ChainMap()
80             if converters is not None:
81                 self.converters.maps.append(converters)
82             self.converters.maps.append(DEFAULT_CONVERTERS)
83         else:
84             # suitable fallback, e.g.
85             self.converters = DEFAULT_CONVERTERS.copy()
86             if converters is not None:
87                 self.converters.update(converters)
88
89     def _convert_field(self, key, value):
90         try:
91             result = self.converters[key](value)
92         except KeyError:
93             # Default conversion in unicode
94             try:
95                 result = _convert_unicode(value)
96             except:
97                 # Leave in default bytes
98                 result = value
99         return result
100
101     def _convert_entry(self, entry):
102         result = {}
103         for key, value in entry.iteritems():
104             if isinstance(value, list):
105                 result[key] = [self._convert_field(key, val) for val in value]
106             else:
107                 result[key] = self._convert_field(key, value)
108         return result
109
110     def get_next(self, *args, **kwargs):
111         return self._convert_entry(
112             super(Journal, self).get_next(*args, **kwargs))
113
114     def query_unique(self, key, *args, **kwargs):
115         return set(self._convert_field(key, value)
116             for value in super(Journal, self).query_unique(key, *args, **kwargs))
117
118     def log_level(self, level):
119         """Sets maximum log level by setting matches for PRIORITY."""
120         if 0 <= level <= 7:
121             for i in range(level+1):
122                 self.add_match(PRIORITY="%s" % i)
123         else:
124             raise ValueError("Log level must be 0 <= level <= 7")
125
126 def _make_line(field, value):
127         if isinstance(value, bytes):
128                 return field.encode('utf-8') + b'=' + value
129         else:
130                 return field + '=' + value
131
132 def send(MESSAGE, MESSAGE_ID=None,
133          CODE_FILE=None, CODE_LINE=None, CODE_FUNC=None,
134          **kwargs):
135         r"""Send a message to the journal.
136
137         >>> journal.send('Hello world')
138         >>> journal.send('Hello, again, world', FIELD2='Greetings!')
139         >>> journal.send('Binary message', BINARY=b'\xde\xad\xbe\xef')
140
141         Value of the MESSAGE argument will be used for the MESSAGE=
142         field. MESSAGE must be a string and will be sent as UTF-8 to
143         the journal.
144
145         MESSAGE_ID can be given to uniquely identify the type of
146         message. It must be a string or a uuid.UUID object.
147
148         CODE_LINE, CODE_FILE, and CODE_FUNC can be specified to
149         identify the caller. Unless at least on of the three is given,
150         values are extracted from the stack frame of the caller of
151         send(). CODE_FILE and CODE_FUNC must be strings, CODE_LINE
152         must be an integer.
153
154         Additional fields for the journal entry can only be specified
155         as keyword arguments. The payload can be either a string or
156         bytes. A string will be sent as UTF-8, and bytes will be sent
157         as-is to the journal.
158
159         Other useful fields include PRIORITY, SYSLOG_FACILITY,
160         SYSLOG_IDENTIFIER, SYSLOG_PID.
161         """
162
163         args = ['MESSAGE=' + MESSAGE]
164
165         if MESSAGE_ID is not None:
166                 id = getattr(MESSAGE_ID, 'hex', MESSAGE_ID)
167                 args.append('MESSAGE_ID=' + id)
168
169         if CODE_LINE == CODE_FILE == CODE_FUNC == None:
170                 CODE_FILE, CODE_LINE, CODE_FUNC = \
171                         _traceback.extract_stack(limit=2)[0][:3]
172         if CODE_FILE is not None:
173                 args.append('CODE_FILE=' + CODE_FILE)
174         if CODE_LINE is not None:
175                 args.append('CODE_LINE={:d}'.format(CODE_LINE))
176         if CODE_FUNC is not None:
177                 args.append('CODE_FUNC=' + CODE_FUNC)
178
179         args.extend(_make_line(key, val) for key, val in kwargs.items())
180         return sendv(*args)
181
182 def stream(identifier, priority=LOG_DEBUG, level_prefix=False):
183         r"""Return a file object wrapping a stream to journal.
184
185         Log messages written to this file as simple newline sepearted
186         text strings are written to the journal.
187
188         The file will be line buffered, so messages are actually sent
189         after a newline character is written.
190
191         >>> stream = journal.stream('myapp')
192         >>> stream
193         <open file '<fdopen>', mode 'w' at 0x...>
194         >>> stream.write('message...\n')
195
196         will produce the following message in the journal::
197
198           PRIORITY=7
199           SYSLOG_IDENTIFIER=myapp
200           MESSAGE=message...
201
202         Using the interface with print might be more convinient:
203
204         >>> from __future__ import print_function
205         >>> print('message...', file=stream)
206
207         priority is the syslog priority, one of `LOG_EMERG`,
208         `LOG_ALERT`, `LOG_CRIT`, `LOG_ERR`, `LOG_WARNING`,
209         `LOG_NOTICE`, `LOG_INFO`, `LOG_DEBUG`.
210
211         level_prefix is a boolean. If true, kernel-style log priority
212         level prefixes (such as '<1>') are interpreted. See
213         sd-daemon(3) for more information.
214         """
215
216         fd = stream_fd(identifier, priority, level_prefix)
217         return _os.fdopen(fd, 'w', 1)
218
219 class JournalHandler(_logging.Handler):
220         """Journal handler class for the Python logging framework.
221
222         Please see the Python logging module documentation for an
223         overview: http://docs.python.org/library/logging.html.
224
225         To create a custom logger whose messages go only to journal:
226
227         >>> log = logging.getLogger('custom_logger_name')
228         >>> log.propagate = False
229         >>> log.addHandler(journal.JournalHandler())
230         >>> log.warn("Some message: %s", detail)
231
232         Note that by default, message levels `INFO` and `DEBUG` are
233         ignored by the logging framework. To enable those log levels:
234
235         >>> log.setLevel(logging.DEBUG)
236
237         To attach journal MESSAGE_ID, an extra field is supported:
238
239         >>> import uuid
240         >>> mid = uuid.UUID('0123456789ABCDEF0123456789ABCDEF')
241         >>> log.warn("Message with ID", extra={'MESSAGE_ID': mid})
242
243         To redirect all logging messages to journal regardless of where
244         they come from, attach it to the root logger:
245
246         >>> logging.root.addHandler(journal.JournalHandler())
247
248         For more complex configurations when using `dictConfig` or
249         `fileConfig`, specify `systemd.journal.JournalHandler` as the
250         handler class.  Only standard handler configuration options
251         are supported: `level`, `formatter`, `filters`.
252
253         The following journal fields will be sent:
254         `MESSAGE`, `PRIORITY`, `THREAD_NAME`, `CODE_FILE`, `CODE_LINE`,
255         `CODE_FUNC`, `LOGGER` (name as supplied to getLogger call),
256         `MESSAGE_ID` (optional, see above).
257         """
258
259         def emit(self, record):
260                 """Write record as journal event.
261
262                 MESSAGE is taken from the message provided by the
263                 user, and PRIORITY, LOGGER, THREAD_NAME,
264                 CODE_{FILE,LINE,FUNC} fields are appended
265                 automatically. In addition, record.MESSAGE_ID will be
266                 used if present.
267                 """
268                 try:
269                         msg = self.format(record)
270                         pri = self.mapPriority(record.levelno)
271                         mid = getattr(record, 'MESSAGE_ID', None)
272                         send(msg,
273                              MESSAGE_ID=mid,
274                              PRIORITY=format(pri),
275                              LOGGER=record.name,
276                              THREAD_NAME=record.threadName,
277                              CODE_FILE=record.pathname,
278                              CODE_LINE=record.lineno,
279                              CODE_FUNC=record.funcName)
280                 except Exception:
281                         self.handleError(record)
282
283         @staticmethod
284         def mapPriority(levelno):
285                 """Map logging levels to journald priorities.
286
287                 Since Python log level numbers are "sparse", we have
288                 to map numbers in between the standard levels too.
289                 """
290                 if levelno <= _logging.DEBUG:
291                         return LOG_DEBUG
292                 elif levelno <= _logging.INFO:
293                         return LOG_INFO
294                 elif levelno <= _logging.WARNING:
295                         return LOG_WARNING
296                 elif levelno <= _logging.ERROR:
297                         return LOG_ERR
298                 elif levelno <= _logging.CRITICAL:
299                         return LOG_CRIT
300                 else:
301                         return LOG_ALERT