Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add capture_logs context manager #234

Merged
merged 4 commits into from
Jan 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ Deprecations:
Changes:
^^^^^^^^

*none*
- Added a context manager for capturing logs, mostly to assist users who want to assert some logging happened during unit tests.
`#14 </~https://github.com/hynek/structlog/issues/14>`_


----
Expand Down
13 changes: 12 additions & 1 deletion docs/loggers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,18 @@ To save you the hassle and slowdown of using standard library's ``logging`` for
>>> PrintLogger().info("hello world!")
hello world!

Additionally -- mostly for unit testing -- ``structlog`` also ships with a logger that just returns whatever it gets passed into it: :class:`~structlog.ReturnLogger`.
If you need functionality similar to ``unittest.TestCase.assertLogs``, or you want to capture all logs for some other reason, you can use the ``structlog.testing.capture_logs`` context manager:

.. doctest::

>>> from structlog import get_logger
>>> from structlog.testing import capture_logs
>>> with capture_logs() as logs:
... get_logger().bind(x="y").info("hello")
>>> logs
[{'x': 'y', 'event': 'hello', 'log_level': 'info'}]

Additionally -- mostly for unit testing within ``structlog`` itself -- ``structlog`` also ships with a logger that just returns whatever it gets passed into it: :class:`~structlog.ReturnLogger`.

.. doctest::

Expand Down
3 changes: 2 additions & 1 deletion src/structlog/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from __future__ import absolute_import, division, print_function

from structlog import dev, processors, stdlib, threadlocal
from structlog import dev, processors, stdlib, testing, threadlocal
from structlog._base import BoundLoggerBase
from structlog._config import (
configure,
Expand Down Expand Up @@ -67,6 +67,7 @@
"processors",
"reset_defaults",
"stdlib",
"testing",
"threadlocal",
"twisted",
"wrap_logger",
Expand Down
68 changes: 68 additions & 0 deletions src/structlog/testing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the MIT License. See the LICENSE file in the root of this
# repository for complete details.

"""
Thin module holding capture_logs.
Ended up here since there were circular references
in other likely places (``dev`` module for example).
"""

from contextlib import contextmanager

from ._config import configure, get_config
from .exceptions import DropEvent


class LogCapture(object):
"""
Class for capturing log messages in its entries list.
Generally you should use :func:`structlog.testing.capture_logs`,
but you can use this class if you want to capture logs with other patterns.
For example, using ``pytest`` fixtures::

@pytest.fixture(scope='function')
def log_output():
return LogCapture()


@pytest.fixture(scope='function', autouse=True)
def configure_structlog(log_output):
structlog.configure(
processors=[log_output]
)

def test_my_stuff(log_output):
do_something()
assert log_output.entries == [...]

.. versionadded:: 19.3.0
"""

def __init__(self):
self.entries = []

def __call__(self, _, method_name, event_dict):
event_dict["log_level"] = method_name
self.entries.append(event_dict)
raise DropEvent


@contextmanager
def capture_logs():
"""
Context manager that appends all logging statements to its yielded list
while it is active.

.. versionadded:: 19.3.0
"""
cap = LogCapture()
old_processors = get_config()["processors"]
try:
configure(processors=[cap])
yield cap.entries
finally:
configure(processors=old_processors)


__all__ = ["LogCapture", "capture_logs"]
42 changes: 42 additions & 0 deletions tests/test_capture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-

# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the MIT License. See the LICENSE file in the root of this
# repository for complete details.

from __future__ import absolute_import, division, print_function

import pytest

from structlog import _config, testing


class TestCaptureLogs(object):
@classmethod
def teardown_class(cls):
_config.reset_defaults()

def test_captures_logs(self):
with testing.capture_logs() as logs:
_config.get_logger().bind(x="y").info("hello")
_config.get_logger().bind(a="b").info("goodbye")
assert [
{"event": "hello", "log_level": "info", "x": "y"},
{"a": "b", "event": "goodbye", "log_level": "info"},
] == logs

def get_active_procs(self):
return _config.get_config()["processors"]

def test_restores_processors_on_success(self):
orig_procs = self.get_active_procs()
with testing.capture_logs():
assert orig_procs is not self.get_active_procs()
assert orig_procs is self.get_active_procs()

def test_restores_processors_on_error(self):
orig_procs = self.get_active_procs()
with pytest.raises(NotImplementedError):
with testing.capture_logs():
raise NotImplementedError("from test")
assert orig_procs is self.get_active_procs()