From 27a66c180cd99ad6b36e4d49bbbc2230413888c3 Mon Sep 17 00:00:00 2001 From: Rob Galanakis Date: Wed, 11 Dec 2019 22:44:10 -0800 Subject: [PATCH 1/3] Add capture_logs context manager See /~https://github.com/hynek/structlog/issues/14#issuecomment-563321043 for more context. It needed to go into its own file, rather than `dev`, because it relies on `_config` and `_config` relies on `dev`. --- CHANGELOG.rst | 3 ++- docs/loggers.rst | 12 ++++++++++- src/structlog/__init__.py | 2 ++ src/structlog/_capture.py | 41 ++++++++++++++++++++++++++++++++++++++ tests/test_capture.py | 42 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 src/structlog/_capture.py create mode 100644 tests/test_capture.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d9d4ccf9..bd06531d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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 `_ ---- diff --git a/docs/loggers.rst b/docs/loggers.rst index 6b5375bb..0b8adda0 100644 --- a/docs/loggers.rst +++ b/docs/loggers.rst @@ -126,7 +126,17 @@ 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.capture_logs`` context manager: + +.. doctest:: + + >>> from structlog import get_logger, 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:: diff --git a/src/structlog/__init__.py b/src/structlog/__init__.py index e8024f86..7d038231 100644 --- a/src/structlog/__init__.py +++ b/src/structlog/__init__.py @@ -10,6 +10,7 @@ from structlog import dev, processors, stdlib, threadlocal from structlog._base import BoundLoggerBase +from structlog._capture import capture_logs from structlog._config import ( configure, configure_once, @@ -57,6 +58,7 @@ "PrintLoggerFactory", "ReturnLogger", "ReturnLoggerFactory", + "capture_logs", "configure", "configure_once", "dev", diff --git a/src/structlog/_capture.py b/src/structlog/_capture.py new file mode 100644 index 00000000..5185b12d --- /dev/null +++ b/src/structlog/_capture.py @@ -0,0 +1,41 @@ +# 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): + 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) diff --git a/tests/test_capture.py b/tests/test_capture.py new file mode 100644 index 00000000..67bc7787 --- /dev/null +++ b/tests/test_capture.py @@ -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 _capture, _config + + +class TestCaptureLogs(object): + @classmethod + def teardown_class(cls): + _config.reset_defaults() + + def test_captures_logs(self): + with _capture.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 _capture.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 _capture.capture_logs(): + raise NotImplementedError("from test") + assert orig_procs is self.get_active_procs() From 3e2376f505af8f011f3d2d3ff0ab737d9178098b Mon Sep 17 00:00:00 2001 From: Rob Galanakis Date: Thu, 12 Dec 2019 09:47:44 -0800 Subject: [PATCH 2/3] Rename _capture.py to testing.py --- docs/loggers.rst | 5 +++-- src/structlog/__init__.py | 5 ++--- src/structlog/{_capture.py => testing.py} | 0 tests/test_capture.py | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) rename src/structlog/{_capture.py => testing.py} (100%) diff --git a/docs/loggers.rst b/docs/loggers.rst index 0b8adda0..60891008 100644 --- a/docs/loggers.rst +++ b/docs/loggers.rst @@ -126,11 +126,12 @@ To save you the hassle and slowdown of using standard library's ``logging`` for >>> PrintLogger().info("hello world!") hello world! -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.capture_logs`` context manager: +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, capture_logs + >>> from structlog import get_logger + >>> from structlog.testing import capture_logs >>> with capture_logs() as logs: ... get_logger().bind(x="y").info("hello") >>> logs diff --git a/src/structlog/__init__.py b/src/structlog/__init__.py index 7d038231..6e6b9340 100644 --- a/src/structlog/__init__.py +++ b/src/structlog/__init__.py @@ -8,9 +8,8 @@ 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._capture import capture_logs from structlog._config import ( configure, configure_once, @@ -58,7 +57,6 @@ "PrintLoggerFactory", "ReturnLogger", "ReturnLoggerFactory", - "capture_logs", "configure", "configure_once", "dev", @@ -69,6 +67,7 @@ "processors", "reset_defaults", "stdlib", + "testing", "threadlocal", "twisted", "wrap_logger", diff --git a/src/structlog/_capture.py b/src/structlog/testing.py similarity index 100% rename from src/structlog/_capture.py rename to src/structlog/testing.py diff --git a/tests/test_capture.py b/tests/test_capture.py index 67bc7787..1437b57d 100644 --- a/tests/test_capture.py +++ b/tests/test_capture.py @@ -8,7 +8,7 @@ import pytest -from structlog import _capture, _config +from structlog import _config, testing class TestCaptureLogs(object): @@ -17,7 +17,7 @@ def teardown_class(cls): _config.reset_defaults() def test_captures_logs(self): - with _capture.capture_logs() as logs: + with testing.capture_logs() as logs: _config.get_logger().bind(x="y").info("hello") _config.get_logger().bind(a="b").info("goodbye") assert [ @@ -30,13 +30,13 @@ def get_active_procs(self): def test_restores_processors_on_success(self): orig_procs = self.get_active_procs() - with _capture.capture_logs(): + 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 _capture.capture_logs(): + with testing.capture_logs(): raise NotImplementedError("from test") assert orig_procs is self.get_active_procs() From c98cc298dd2c204fdc7ef08338392918c6db2bb3 Mon Sep 17 00:00:00 2001 From: Rob Galanakis Date: Thu, 12 Dec 2019 09:51:42 -0800 Subject: [PATCH 3/3] Add docstring to testing.LogCapture --- src/structlog/testing.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/structlog/testing.py b/src/structlog/testing.py index 5185b12d..590ff1f5 100644 --- a/src/structlog/testing.py +++ b/src/structlog/testing.py @@ -15,6 +15,30 @@ 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 = [] @@ -39,3 +63,6 @@ def capture_logs(): yield cap.entries finally: configure(processors=old_processors) + + +__all__ = ["LogCapture", "capture_logs"]