Projet

Général

Profil

journal.py.diff

Benjamin Dauvergne, 04 décembre 2018 20:37

Télécharger (28,1 ko)

Voir les différences:


  

/tmp/journal.234.py 2018-12-04 20:32:58.561589785 +0100
5 5
#  Copyright 2012 Zbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>
6 6
#  Copyright 2012 Marti Raudsepp <marti@juffo.org>
7 7
#
8
#  systemd is free software; you can redistribute it and/or modify it
8
#  python-systemd is free software; you can redistribute it and/or modify it
9 9
#  under the terms of the GNU Lesser General Public License as published by
10 10
#  the Free Software Foundation; either version 2.1 of the License, or
11 11
#  (at your option) any later version.
12 12
#
13
#  systemd is distributed in the hope that it will be useful, but
13
#  python-systemd is distributed in the hope that it will be useful, but
14 14
#  WITHOUT ANY WARRANTY; without even the implied warranty of
15 15
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 16
#  Lesser General Public License for more details.
17 17
#
18 18
#  You should have received a copy of the GNU Lesser General Public License
19
#  along with systemd; If not, see <http://www.gnu.org/licenses/>.
19
#  along with python-systemd; If not, see <http://www.gnu.org/licenses/>.
20 20

  
21 21
from __future__ import division
22 22

  
......
26 26
import traceback as _traceback
27 27
import os as _os
28 28
import logging as _logging
29
if _sys.version_info >= (3,3):
30
    from collections import ChainMap as _ChainMap
31 29
from syslog import (LOG_EMERG, LOG_ALERT, LOG_CRIT, LOG_ERR,
32 30
                    LOG_WARNING, LOG_NOTICE, LOG_INFO, LOG_DEBUG)
31
if _sys.version_info >= (3,3):
32
    from collections import ChainMap as _ChainMap
33

  
33 34
from ._journal import __version__, sendv, stream_fd
34 35
from ._reader import (_Reader, NOP, APPEND, INVALIDATE,
35 36
                      LOCAL_ONLY, RUNTIME_ONLY,
36 37
                      SYSTEM, SYSTEM_ONLY, CURRENT_USER,
38
                      OS_ROOT,
37 39
                      _get_catalog)
38 40
from . import id128 as _id128
39 41

  
......
42 44
else:
43 45
    Monotonic = tuple
44 46

  
47

  
45 48
def _convert_monotonic(m):
46 49
    return Monotonic((_datetime.timedelta(microseconds=m[0]),
47 50
                      _uuid.UUID(bytes=m[1])))
48 51

  
52

  
49 53
def _convert_source_monotonic(s):
50 54
    return _datetime.timedelta(microseconds=int(s))
51 55

  
56

  
52 57
def _convert_realtime(t):
53 58
    return _datetime.datetime.fromtimestamp(t / 1000000)
54 59

  
60

  
55 61
def _convert_timestamp(s):
56 62
    return _datetime.datetime.fromtimestamp(int(s) / 1000000)
57 63

  
64

  
58 65
def _convert_trivial(x):
59 66
    return x
60 67

  
......
100 107
    'COREDUMP_TIMESTAMP': _convert_timestamp,
101 108
}
102 109

  
103
_IDENT_LETTER = set('ABCDEFGHIJKLMNOPQRTSUVWXYZ_')
110
_IDENT_CHARACTER = set('ABCDEFGHIJKLMNOPQRTSUVWXYZ_0123456789')
111

  
104 112

  
105 113
def _valid_field_name(s):
106
    return not (set(s) - _IDENT_LETTER)
114
    return not (set(s) - _IDENT_CHARACTER)
115

  
107 116

  
108 117
class Reader(_Reader):
109
    """Reader allows the access and filtering of systemd journal
110
    entries. Note that in order to access the system journal, a
111
    non-root user must be in the `systemd-journal` group.
118
    """Access systemd journal entries.
112 119

  
113
    Example usage to print out all informational or higher level
114
    messages for systemd-udevd for this boot:
120
    Entries are subject to filtering and limits, see `add_match`, `this_boot`,
121
    `this_machine` functions and the `data_treshold` attribute.
115 122

  
123
    Note that in order to access the system journal, a non-root user must have
124
    the necessary privileges, see journalctl(1) for details.  Unprivileged users
125
    can access only their own journal.
126

  
127
    Example usage to print out all informational or higher level messages for
128
    systemd-udevd for this boot:
129

  
130
    >>> from systemd import journal
116 131
    >>> j = journal.Reader()
117 132
    >>> j.this_boot()
118 133
    >>> j.log_level(journal.LOG_INFO)
119 134
    >>> j.add_match(_SYSTEMD_UNIT="systemd-udevd.service")
120
    >>> for entry in j:
135
    >>> for entry in j:                                 # doctest: +SKIP
121 136
    ...    print(entry['MESSAGE'])
137
    starting version ...
138

  
139
    See systemd.journal-fields(7) for more info on typical fields found in the
140
    journal.
122 141

  
123
    See systemd.journal-fields(7) for more info on typical fields
124
    found in the journal.
125 142
    """
126
    def __init__(self, flags=0, path=None, files=None, converters=None):
127
        """Create an instance of Reader, which allows filtering and
128
        return of journal entries.
129

  
130
        Argument `flags` sets open flags of the journal, which can be one
131
        of, or ORed combination of constants: LOCAL_ONLY (default) opens
132
        journal on local machine only; RUNTIME_ONLY opens only
133
        volatile journal files; and SYSTEM_ONLY opens only
134
        journal files of system services and the kernel.
143
    def __init__(self, flags=None, path=None, files=None, converters=None):
144
        """Create a new Reader.
135 145

  
136
        Argument `path` is the directory of journal files. Note that
137
        `flags` and `path` are exclusive.
146
        Argument `flags` defines the open flags of the journal, which can be one
147
        of, or ORed combination of constants: LOCAL_ONLY (default) opens journal
148
        on local machine only; RUNTIME_ONLY opens only volatile journal files;
149
        and SYSTEM_ONLY opens only journal files of system services and the kernel.
150

  
151
        Argument `path` is the directory of journal files, either a file system
152
        path or a file descriptor. Note that `flags`, `path`, and `files` are
153
        exclusive.
138 154

  
139 155
        Argument `converters` is a dictionary which updates the
140
        DEFAULT_CONVERTERS to convert journal field values. Field
141
        names are used as keys into this dictionary. The values must
142
        be single argument functions, which take a `bytes` object and
143
        return a converted value. When there's no entry for a field
144
        name, then the default UTF-8 decoding will be attempted. If
145
        the conversion fails with a ValueError, unconverted bytes
146
        object will be returned. (Note that ValueEror is a superclass
147
        of UnicodeDecodeError).
156
        DEFAULT_CONVERTERS to convert journal field values. Field names are used
157
        as keys into this dictionary. The values must be single argument
158
        functions, which take a `bytes` object and return a converted
159
        value. When there's no entry for a field name, then the default UTF-8
160
        decoding will be attempted. If the conversion fails with a ValueError,
161
        unconverted bytes object will be returned. (Note that ValueEror is a
162
        superclass of UnicodeDecodeError).
163

  
164
        Reader implements the context manager protocol: the journal will be
165
        closed when exiting the block.
166
        """
167
        if flags is None:
168
            if path is None and files is None:
169
                # This mimics journalctl behaviour of default to local journal only
170
                flags = LOCAL_ONLY
171
            else:
172
                flags = 0
148 173

  
149
        Reader implements the context manager protocol: the journal
150
        will be closed when exiting the block.
151
        """
152 174
        super(Reader, self).__init__(flags, path, files)
153
        if _sys.version_info >= (3,3):
175
        if _sys.version_info >= (3, 3):
154 176
            self.converters = _ChainMap()
155 177
            if converters is not None:
156 178
                self.converters.maps.append(converters)
......
161 183
                self.converters.update(converters)
162 184

  
163 185
    def _convert_field(self, key, value):
164
        """Convert value using self.converters[key]
186
        """Convert value using self.converters[key].
165 187

  
166
        If `key` is not present in self.converters, a standard unicode
167
        decoding will be attempted.  If the conversion (either
168
        key-specific or the default one) fails with a ValueError, the
169
        original bytes object will be returned.
188
        If `key` is not present in self.converters, a standard unicode decoding
189
        will be attempted.  If the conversion (either key-specific or the
190
        default one) fails with a ValueError, the original bytes object will be
191
        returned.
170 192
        """
171 193
        convert = self.converters.get(key, bytes.decode)
172 194
        try:
......
176 198
            return value
177 199

  
178 200
    def _convert_entry(self, entry):
179
        """Convert entire journal entry utilising _covert_field"""
201
        """Convert entire journal entry utilising _convert_field."""
180 202
        result = {}
181 203
        for key, value in entry.items():
182 204
            if isinstance(value, list):
......
186 208
        return result
187 209

  
188 210
    def __iter__(self):
189
        """Part of iterator protocol.
190
        Returns self.
211
        """Return self.
212

  
213
        Part of the iterator protocol.
191 214
        """
192 215
        return self
193 216

  
194 217
    def __next__(self):
195
        """Part of iterator protocol.
218
        """Return the next entry in the journal.
219

  
196 220
        Returns self.get_next() or raises StopIteration.
221

  
222
        Part of the iterator protocol.
197 223
        """
198 224
        ans = self.get_next()
199 225
        if ans:
......
206 232

  
207 233
    def add_match(self, *args, **kwargs):
208 234
        """Add one or more matches to the filter journal log entries.
209
        All matches of different field are combined in a logical AND,
210
        and matches of the same field are automatically combined in a
211
        logical OR.
212
        Matches can be passed as strings of form "FIELD=value", or
213
        keyword arguments FIELD="value".
235

  
236
        All matches of different field are combined with logical AND, and
237
        matches of the same field are automatically combined with logical OR.
238
        Matches can be passed as strings of form "FIELD=value", or keyword
239
        arguments FIELD="value".
214 240
        """
215 241
        args = list(args)
216 242
        args.extend(_make_line(key, val) for key, val in kwargs.items())
......
218 244
            super(Reader, self).add_match(arg)
219 245

  
220 246
    def get_next(self, skip=1):
221
        """Return the next log entry as a mapping type, currently
222
        a standard dictionary of fields.
247
        r"""Return the next log entry as a dictionary.
223 248

  
224
        Optional skip value will return the `skip`\-th log entry.
249
        Entries will be processed with converters specified during Reader
250
        creation.
225 251

  
226
        Entries will be processed with converters specified during
227
        Reader creation.
252
        Optional `skip` value will return the `skip`-th log entry.
253

  
254
        Currently a standard dictionary of fields is returned, but in the
255
        future this might be changed to a different mapping type, so the
256
        calling code should not make assumptions about a specific type.
228 257
        """
229 258
        if super(Reader, self)._next(skip):
230 259
            entry = super(Reader, self)._get_all()
......
236 265
        return dict()
237 266

  
238 267
    def get_previous(self, skip=1):
239
        """Return the previous log entry as a mapping type,
240
        currently a standard dictionary of fields.
268
        r"""Return the previous log entry.
241 269

  
242
        Optional skip value will return the -`skip`\-th log entry.
270
        Equivalent to get_next(-skip).
243 271

  
244
        Entries will be processed with converters specified during
245
        Reader creation.
272
        Optional `skip` value will return the -`skip`-th log entry.
246 273

  
247
        Equivalent to get_next(-skip).
274
        Entries will be processed with converters specified during Reader
275
        creation.
276

  
277
        Currently a standard dictionary of fields is returned, but in the
278
        future this might be changed to a different mapping type, so the
279
        calling code should not make assumptions about a specific type.
248 280
        """
249 281
        return self.get_next(-skip)
250 282

  
251 283
    def query_unique(self, field):
252
        """Return unique values appearing in the journal for given `field`.
284
        """Return a list of unique values appearing in the journal for the given
285
        `field`.
253 286

  
254 287
        Note this does not respect any journal matches.
255 288

  
......
260 293
            for value in super(Reader, self).query_unique(field))
261 294

  
262 295
    def wait(self, timeout=None):
263
        """Wait for a change in the journal. `timeout` is the maximum
264
        time in seconds to wait, or None, to wait forever.
296
        """Wait for a change in the journal.
297

  
298
        `timeout` is the maximum time in seconds to wait, or None which
299
        means to wait forever.
265 300

  
266
        Returns one of NOP (no change), APPEND (new entries have been
267
        added to the end of the journal), or INVALIDATE (journal files
268
        have been added or removed).
301
        Returns one of NOP (no change), APPEND (new entries have been added to
302
        the end of the journal), or INVALIDATE (journal files have been added or
303
        removed).
269 304
        """
270 305
        us = -1 if timeout is None else int(timeout * 1000000)
271 306
        return super(Reader, self).wait(us)
272 307

  
273 308
    def seek_realtime(self, realtime):
274
        """Seek to a matching journal entry nearest to `realtime` time.
309
        """Seek to a matching journal entry nearest to `timestamp` time.
310

  
311
        Argument `realtime` must be either an integer UNIX timestamp (in
312
        microseconds since the beginning of the UNIX epoch), or an float UNIX
313
        timestamp (in seconds since the beginning of the UNIX epoch), or a
314
        datetime.datetime instance. The integer form is deprecated.
315

  
316
        >>> import time
317
        >>> from systemd import journal
275 318

  
276
        Argument `realtime` must be either an integer unix timestamp
277
        or datetime.datetime instance.
319
        >>> yesterday = time.time() - 24 * 60**2
320
        >>> j = journal.Reader()
321
        >>> j.seek_realtime(yesterday)
278 322
        """
279 323
        if isinstance(realtime, _datetime.datetime):
280
            realtime = float(realtime.strftime("%s.%f")) * 1000000
281
        return super(Reader, self).seek_realtime(int(realtime))
324
            realtime = int(float(realtime.strftime("%s.%f")) * 1000000)
325
        elif not isinstance(realtime, int):
326
            realtime = int(realtime * 1000000)
327
        return super(Reader, self).seek_realtime(realtime)
282 328

  
283 329
    def seek_monotonic(self, monotonic, bootid=None):
284 330
        """Seek to a matching journal entry nearest to `monotonic` time.
285 331

  
286
        Argument `monotonic` is a timestamp from boot in either
287
        seconds or a datetime.timedelta instance. Argument `bootid`
288
        is a string or UUID representing which boot the monotonic time
289
        is reference to. Defaults to current bootid.
332
        Argument `monotonic` is a timestamp from boot in either seconds or a
333
        datetime.timedelta instance. Argument `bootid` is a string or UUID
334
        representing which boot the monotonic time is reference to. Defaults to
335
        current bootid.
290 336
        """
291 337
        if isinstance(monotonic, _datetime.timedelta):
292
            monotonic = monotonic.totalseconds()
338
            monotonic = monotonic.total_seconds()
293 339
        monotonic = int(monotonic * 1000000)
294 340
        if isinstance(bootid, _uuid.UUID):
295 341
            bootid = bootid.hex
......
317 363
        self.add_match(MESSAGE_ID=messageid)
318 364

  
319 365
    def this_boot(self, bootid=None):
320
        """Add match for _BOOT_ID equal to current boot ID or the specified boot ID.
366
        """Add match for _BOOT_ID for current boot or the specified boot ID.
321 367

  
322 368
        If specified, bootid should be either a UUID or a 32 digit hex number.
323 369

  
......
332 378
    def this_machine(self, machineid=None):
333 379
        """Add match for _MACHINE_ID equal to the ID of this machine.
334 380

  
335
        If specified, machineid should be either a UUID or a 32 digit hex number.
381
        If specified, machineid should be either a UUID or a 32 digit hex
382
        number.
336 383

  
337 384
        Equivalent to add_match(_MACHINE_ID='machineid').
338 385
        """
......
344 391

  
345 392

  
346 393
def get_catalog(mid):
394
    """Return catalog entry for the specified ID.
395

  
396
    `mid` should be either a UUID or a 32 digit hex number.
397
    """
347 398
    if isinstance(mid, _uuid.UUID):
348 399
        mid = mid.hex
349 400
    return _get_catalog(mid)
350 401

  
402

  
351 403
def _make_line(field, value):
352 404
        if isinstance(value, bytes):
353 405
                return field.encode('utf-8') + b'=' + value
354
        elif isinstance(value, int):
355
                return field + '=' + str(value)
356
        else:
406
    elif isinstance(value, str):
357 407
                return field + '=' + value
408
    else:
409
        return field + '=' + str(value)
410

  
358 411

  
359 412
def send(MESSAGE, MESSAGE_ID=None,
360 413
         CODE_FILE=None, CODE_LINE=None, CODE_FUNC=None,
361 414
         **kwargs):
362 415
        r"""Send a message to the journal.
363 416

  
417
    >>> from systemd import journal
364 418
        >>> journal.send('Hello world')
365 419
        >>> journal.send('Hello, again, world', FIELD2='Greetings!')
366 420
        >>> journal.send('Binary message', BINARY=b'\xde\xad\xbe\xef')
367 421

  
368
        Value of the MESSAGE argument will be used for the MESSAGE=
369
        field. MESSAGE must be a string and will be sent as UTF-8 to
370
        the journal.
371

  
372
        MESSAGE_ID can be given to uniquely identify the type of
373
        message. It must be a string or a uuid.UUID object.
374

  
375
        CODE_LINE, CODE_FILE, and CODE_FUNC can be specified to
376
        identify the caller. Unless at least on of the three is given,
377
        values are extracted from the stack frame of the caller of
378
        send(). CODE_FILE and CODE_FUNC must be strings, CODE_LINE
379
        must be an integer.
380

  
381
        Additional fields for the journal entry can only be specified
382
        as keyword arguments. The payload can be either a string or
383
        bytes. A string will be sent as UTF-8, and bytes will be sent
384
        as-is to the journal.
422
    Value of the MESSAGE argument will be used for the MESSAGE= field. MESSAGE
423
    must be a string and will be sent as UTF-8 to the journal.
424

  
425
    MESSAGE_ID can be given to uniquely identify the type of message. It must be
426
    a string or a uuid.UUID object.
427

  
428
    CODE_LINE, CODE_FILE, and CODE_FUNC can be specified to identify the caller.
429
    Unless at least on of the three is given, values are extracted from the
430
    stack frame of the caller of send(). CODE_FILE and CODE_FUNC must be
431
    strings, CODE_LINE must be an integer.
432

  
433
    Additional fields for the journal entry can only be specified as keyword
434
    arguments. The payload can be either a string or bytes. A string will be
435
    sent as UTF-8, and bytes will be sent as-is to the journal.
385 436

  
386
        Other useful fields include PRIORITY, SYSLOG_FACILITY,
387
        SYSLOG_IDENTIFIER, SYSLOG_PID.
437
    Other useful fields include PRIORITY, SYSLOG_FACILITY, SYSLOG_IDENTIFIER,
438
    SYSLOG_PID.
388 439
        """
389 440

  
390 441
        args = ['MESSAGE=' + MESSAGE]
......
393 444
                id = getattr(MESSAGE_ID, 'hex', MESSAGE_ID)
394 445
                args.append('MESSAGE_ID=' + id)
395 446

  
396
        if CODE_LINE == CODE_FILE == CODE_FUNC == None:
397
                CODE_FILE, CODE_LINE, CODE_FUNC = \
398
                        _traceback.extract_stack(limit=2)[0][:3]
447
    if CODE_LINE is CODE_FILE is CODE_FUNC is None:
448
        CODE_FILE, CODE_LINE, CODE_FUNC = _traceback.extract_stack(limit=2)[0][:3]
399 449
        if CODE_FILE is not None:
400 450
                args.append('CODE_FILE=' + CODE_FILE)
401 451
        if CODE_LINE is not None:
......
406 456
        args.extend(_make_line(key, val) for key, val in kwargs.items())
407 457
        return sendv(*args)
408 458

  
409
def stream(identifier, priority=LOG_DEBUG, level_prefix=False):
459

  
460
def stream(identifier=None, priority=LOG_INFO, level_prefix=False):
410 461
        r"""Return a file object wrapping a stream to journal.
411 462

  
412
        Log messages written to this file as simple newline sepearted
413
        text strings are written to the journal.
463
    Log messages written to this file as simple newline sepearted text strings
464
    are written to the journal.
414 465

  
415
        The file will be line buffered, so messages are actually sent
416
        after a newline character is written.
466
    The file will be line buffered, so messages are actually sent after a
467
    newline character is written.
417 468

  
418
        >>> stream = journal.stream('myapp')
419
        >>> stream
420
        <open file '<fdopen>', mode 'w' at 0x...>
421
        >>> stream.write('message...\n')
469
    >>> from systemd import journal
470
    >>> stream = journal.stream('myapp')                       # doctest: +SKIP
471
    >>> res = stream.write('message...\n')                     # doctest: +SKIP
422 472

  
423 473
        will produce the following message in the journal::
424 474

  
......
426 476
          SYSLOG_IDENTIFIER=myapp
427 477
          MESSAGE=message...
428 478

  
429
        Using the interface with print might be more convinient:
479
    If identifier is None, a suitable default based on sys.argv[0] will be used.
480

  
481
    This interface can be used conveniently with the print function:
430 482

  
431 483
        >>> from __future__ import print_function
432
        >>> print('message...', file=stream)
484
    >>> stream = journal.stream()                              # doctest: +SKIP
485
    >>> print('message...', file=stream)                       # doctest: +SKIP
433 486

  
434
        priority is the syslog priority, one of `LOG_EMERG`,
435
        `LOG_ALERT`, `LOG_CRIT`, `LOG_ERR`, `LOG_WARNING`,
436
        `LOG_NOTICE`, `LOG_INFO`, `LOG_DEBUG`.
437

  
438
        level_prefix is a boolean. If true, kernel-style log priority
439
        level prefixes (such as '<1>') are interpreted. See
440
        sd-daemon(3) for more information.
487
    priority is the syslog priority, one of `LOG_EMERG`, `LOG_ALERT`,
488
    `LOG_CRIT`, `LOG_ERR`, `LOG_WARNING`, `LOG_NOTICE`, `LOG_INFO`, `LOG_DEBUG`.
489

  
490
    level_prefix is a boolean. If true, kernel-style log priority level prefixes
491
    (such as '<1>') are interpreted. See sd-daemon(3) for more information.
441 492
        """
442 493

  
494
    if identifier is None:
495
        if not _sys.argv or not _sys.argv[0] or _sys.argv[0] == '-c':
496
            identifier = 'python'
497
        else:
498
            identifier = _sys.argv[0]
499

  
443 500
        fd = stream_fd(identifier, priority, level_prefix)
444 501
        return _os.fdopen(fd, 'w', 1)
445 502

  
503

  
446 504
class JournalHandler(_logging.Handler):
447 505
        """Journal handler class for the Python logging framework.
448 506

  
449
        Please see the Python logging module documentation for an
450
        overview: http://docs.python.org/library/logging.html.
507
    Please see the Python logging module documentation for an overview:
508
    http://docs.python.org/library/logging.html.
451 509

  
452 510
        To create a custom logger whose messages go only to journal:
453 511

  
512
    >>> import logging
454 513
        >>> log = logging.getLogger('custom_logger_name')
455 514
        >>> log.propagate = False
456
        >>> log.addHandler(journal.JournalHandler())
457
        >>> log.warn("Some message: %s", detail)
515
    >>> log.addHandler(JournalHandler())
516
    >>> log.warning("Some message: %s", 'detail')
458 517

  
459
        Note that by default, message levels `INFO` and `DEBUG` are
460
        ignored by the logging framework. To enable those log levels:
518
    Note that by default, message levels `INFO` and `DEBUG` are ignored by the
519
    logging framework. To enable those log levels:
461 520

  
462 521
        >>> log.setLevel(logging.DEBUG)
463 522

  
464
        To redirect all logging messages to journal regardless of where
465
        they come from, attach it to the root logger:
523
    To redirect all logging messages to journal regardless of where they come
524
    from, attach it to the root logger:
466 525

  
467
        >>> logging.root.addHandler(journal.JournalHandler())
526
    >>> logging.root.addHandler(JournalHandler())
468 527

  
469
        For more complex configurations when using `dictConfig` or
470
        `fileConfig`, specify `systemd.journal.JournalHandler` as the
471
        handler class.  Only standard handler configuration options
472
        are supported: `level`, `formatter`, `filters`.
528
    For more complex configurations when using `dictConfig` or `fileConfig`,
529
    specify `systemd.journal.JournalHandler` as the handler class.  Only
530
    standard handler configuration options are supported: `level`, `formatter`,
531
    `filters`.
473 532

  
474 533
        To attach journal MESSAGE_ID, an extra field is supported:
475 534

  
476 535
        >>> import uuid
477 536
        >>> mid = uuid.UUID('0123456789ABCDEF0123456789ABCDEF')
478
        >>> log.warn("Message with ID", extra={'MESSAGE_ID': mid})
537
    >>> log.warning("Message with ID", extra={'MESSAGE_ID': mid})
538

  
539
    Fields to be attached to all messages sent through this handler can be
540
    specified as keyword arguments. This probably makes sense only for
541
    SYSLOG_IDENTIFIER and similar fields which are constant for the whole
542
    program:
479 543

  
480
        Fields to be attached to all messages sent through this
481
        handler can be specified as keyword arguments. This probably
482
        makes sense only for SYSLOG_IDENTIFIER and similar fields
483
        which are constant for the whole program:
544
    >>> JournalHandler(SYSLOG_IDENTIFIER='my-cool-app')
545
    <...JournalHandler ...>
484 546

  
485
        >>> journal.JournalHandler(SYSLOG_IDENTIFIER='my-cool-app')
547
    The following journal fields will be sent: `MESSAGE`, `PRIORITY`,
548
    `THREAD_NAME`, `CODE_FILE`, `CODE_LINE`, `CODE_FUNC`, `LOGGER` (name as
549
    supplied to getLogger call), `MESSAGE_ID` (optional, see above),
550
    `SYSLOG_IDENTIFIER` (defaults to sys.argv[0]).
486 551

  
487
        The following journal fields will be sent:
488
        `MESSAGE`, `PRIORITY`, `THREAD_NAME`, `CODE_FILE`, `CODE_LINE`,
489
        `CODE_FUNC`, `LOGGER` (name as supplied to getLogger call),
490
        `MESSAGE_ID` (optional, see above), `SYSLOG_IDENTIFIER` (defaults
491
        to sys.argv[0]).
552
    The function used to actually send messages can be overridden using
553
    the `sender_function` parameter.
492 554
        """
493 555

  
494
        def __init__(self, level=_logging.NOTSET, **kwargs):
556
    def __init__(self, level=_logging.NOTSET, sender_function=send, **kwargs):
495 557
                super(JournalHandler, self).__init__(level)
496 558

  
497 559
                for name in kwargs:
......
499 561
                                raise ValueError('Invalid field name: ' + name)
500 562
                if 'SYSLOG_IDENTIFIER' not in kwargs:
501 563
                        kwargs['SYSLOG_IDENTIFIER'] = _sys.argv[0]
564

  
565
        self.send = sender_function
502 566
                self._extra = kwargs
503 567

  
504 568
        def emit(self, record):
505
                """Write record as journal event.
569
        """Write `record` as a journal event.
506 570

  
507
                MESSAGE is taken from the message provided by the
508
                user, and PRIORITY, LOGGER, THREAD_NAME,
509
                CODE_{FILE,LINE,FUNC} fields are appended
510
                automatically. In addition, record.MESSAGE_ID will be
511
                used if present.
571
        MESSAGE is taken from the message provided by the user, and PRIORITY,
572
        LOGGER, THREAD_NAME, CODE_{FILE,LINE,FUNC} fields are appended
573
        automatically. In addition, record.MESSAGE_ID will be used if present.
512 574
                """
513 575
                try:
514 576
                        msg = self.format(record)
515
                        pri = self.mapPriority(record.levelno)
516
                        mid = getattr(record, 'MESSAGE_ID', None)
517
                        send(msg,
518
                             MESSAGE_ID=mid,
577
            pri = self.map_priority(record.levelno)
578
            # defaults
579
            extras = self._extra.copy()
580

  
581
            # higher priority
582
            if record.exc_text:
583
                extras['EXCEPTION_TEXT'] = record.exc_text
584

  
585
            if record.exc_info:
586
                extras['EXCEPTION_INFO'] = record.exc_info
587

  
588
            if record.args:
589
                extras['CODE_ARGS'] = str(record.args)
590

  
591
            # explicit arguments — highest priority
592
            extras.update(record.__dict__)
593

  
594
            self.send(msg,
519 595
                             PRIORITY=format(pri),
520 596
                             LOGGER=record.name,
521 597
                             THREAD_NAME=record.threadName,
598
                      PROCESS_NAME=record.processName,
522 599
                             CODE_FILE=record.pathname,
523 600
                             CODE_LINE=record.lineno,
524 601
                             CODE_FUNC=record.funcName,
525
                             **self._extra)
602
                      **extras)
526 603
                except Exception:
527 604
                        self.handleError(record)
528 605

  
529 606
        @staticmethod
530
        def mapPriority(levelno):
607
    def map_priority(levelno):
531 608
                """Map logging levels to journald priorities.
532 609

  
533
                Since Python log level numbers are "sparse", we have
534
                to map numbers in between the standard levels too.
610
        Since Python log level numbers are "sparse", we have to map numbers in
611
        between the standard levels too.
535 612
                """
536 613
                if levelno <= _logging.DEBUG:
537 614
                        return LOG_DEBUG
......
545 622
                        return LOG_CRIT
546 623
                else:
547 624
                        return LOG_ALERT
625

  
626
    mapPriority = map_priority