Logging capture

Within a context manager, capture log messages from third party packages

Capture logging messages from package foo, INFO level and higher, into a list.

Can optionally set formatting. Has sane default, normally leave out the logging message format string.

import logging

from logging_strict.tech_niques import captureLogs
from logging_strict.constants import LOG_FORMAT

msg0 = 'first msg'
msg1 = 'second msg'

with captureLogs('foo', level='INFO', format_=LOG_FORMAT) as cm:
    logging.getLogger('foo').info(msg0)
    logging.getLogger('foo.bar').error(msg1)

out = cm.output
line0 = out[0]
line1 = out[1]
assert "INFO" in line0
assert msg0 in line0
assert "ERROR" in line1
assert msg1 in line1

Can be chained with other context managers. Such as capturing the streams as well

import logging
import sys

from logging_strict.tech_niques import CaptureOutput, captureLogs

msg0 = 'first msg'
msg1 = 'second msg'
msg_err = "StdMoooooo!"
msg_out = "StdMagpie"
with (
    captureLogs('foo', level='INFO') as cm,
    CaptureOutput() as cow,
):
    logging.getLogger('foo').info(msg0)
    logging.getLogger('foo.bar').error(msg1)
    sys.stdout.write(msg_out)
    sys.stderr.write(msg_err)

assert cow.stderr == msg_err
assert cow.stdout == msg_out

out = cm.output
line0 = out[0]
line1 = out[1]
assert "INFO" in line0
assert msg0 in line0
assert "ERROR" in line1
assert msg1 in line1

with block using parentheses style is useful when there is more than one context manager. Even with only one context manager, with block using parentheses is the preferred style.

When when chaining context managers together, it’s a one liner

In addition to the two context managers above, in unittests, unittest.mock.patch() can alter modules behavior and results without changes to the modules source code.

sync and async logging

unittest assertLogs/assertNoLogs

tl;dr;

unittest.TestCase.assertLogs() assertion makes it ill-suited for general use, besides within unittests

The details

unittest.TestCase.assertLogs() does log capturing

Tests that at least one message is logged on the logger or one of its children, with at least the given level.

captureLogs ‣ does no assertion

assertLogs ‣ does assertion

So captureLogs is suitable for general usage

Having made that strong disclaimer, lets see how assertLogs works

Code snippet

  • Source unittest docs

  • Confirmed in unittest #12, tests/utils/test_logging_capture

class DocumentAssertLogs(unittest.TestCase):
    def test_assert_logging_output(self):
        with self.assertLogs('foo', level='INFO') as cm:
            logging.getLogger('foo').info('first message')
            logging.getLogger('foo.bar').error('second message')
        self.assertEqual(cm.output, [
            'INFO:foo:first message',
            'ERROR:foo.bar:second message',
        ])

See also

assertLogs [docs] [source]

assertNoLogs (py310+) [docs]

Into the rabbit hole

More in-depth low level implementation notes

Module private variables

logging_strict.tech_niques.logging_capture.__all__: tuple[str, str] = ("captureLogs", "captureLogsMany")

Exported objects from this module

Module objects

class logging_strict.tech_niques.logging_capture._LoggingWatcher(records: MutableSequence[LogRecord] = NOTHING, output: MutableSequence[str] = NOTHING)

Replaces collections.namedtuple

getHandlerByName(name)

Get a handler with the specified name, or None if there isn’t one with that name.

Parameters:

name (str) – handler function name

Returns:

A logging handler func

Return type:

type[logging.Handler]

getHandlerNames()

Return all known handler names as an immutable set

Returns:

Handler function names

Return type:

frozenset[str]

getLevelNo(level_name)

Get Logging level number, given a logging level name

Parameters:

level_name (str) – logging level name

Returns:

Logging level integer

Return type:

int | None

class logging_strict.tech_niques.logging_capture._CapturingHandler

A logging handler capturing all (raw and formatted) logging output.

emit(record)

Save record. Format/Save message

Parameters:

record (logging.LogRecord) – logging record. Save as record and as str message

flush()

Flush records

logging_strict.tech_niques.logging_capture.captureLogs(logger=None, level=None, format_='%(levelname)s %(module)s %(funcName)s: %(lineno)d: %(message)s')

A context manager to capture logging a loggers logging output

Example:

import logging
from logging_strict.tech_niques import captureLogs

with captureLogs('foo', level='INFO') as cm:
    logging.getLogger('foo').info('first message')
    logging.getLogger('foo.bar').error('second message')
print(cm.output)

The watcher ( logging_strict.tech_niques.logging_capture._LoggingWatcher ) has attributes:

  • output

  • records

    unformatted records

Parameters:
  • logger (str | logging.Logger | None) – Default None. logger or logger name

  • level (str | int | None) – Default None. Logging level

  • format_ (str | None) – Default None. Can override logging format spec

Returns:

Context manager yields one logging_strict.tech_niques.logging_capture._LoggingWatcher. Which stores the log records/messages

Return type:

Iterator[logging_strict.tech_niques.logging_capture._LoggingWatcher]

See also

Context manager howto, PEP 343

logging_strict.tech_niques.logging_capture.captureLogsMany(loggers=(), levels=(), format_='%(levelname)s %(module)s %(funcName)s: %(lineno)d: %(message)s')

Behave exactly like captureLogs() except intended for multiple loggers rather than one

Parameters:
  • loggers (Sequence[str | logging.Logger]) – Sequence of loggers

  • levels (Sequence[str | int | None]) – Sequence of levels corresponding to each loggers in order

  • format_ (str | None) – Default None. Can override logging format spec

Returns:

Context manager yields all logging_strict.tech_niques.logging_capture._LoggingWatcher. in a tuple. Order maintained

Return type:

Iterator[tuple[logging_strict.tech_niques.logging_capture._LoggingWatcher]]

Raises: