Skip to content

Commit

Permalink
Added structlog support (#109)
Browse files Browse the repository at this point in the history
- Adds `StructlogCapturer` and related docs. 

Follows `loguru` extension structure, borrowing inspiration from the
structlog testing tools. I've matched the suggested version range in
hynek/structlog#596.

Closes #108
  • Loading branch information
will-ockmore authored Feb 17, 2024
1 parent 4f2e4f0 commit e7349c6
Show file tree
Hide file tree
Showing 15 changed files with 304 additions and 16 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ jobs:
- python-version: "3.10"
- python-version: "3.11"
- python-version: "3.12"
- lib-versions: "structlog~=23.3.0"
- lib-versions: "structlog~=24.1.0"
- lib-versions: "loguru~=0.6.0"
- lib-versions: "loguru~=0.7.0"
- lib-versions: "pytest~=7.0"
Expand Down
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,28 @@

`logot` makes it easy to test whether your code is logging correctly:

``` python
```python
from logot import Logot, logged

def test_something(logot: Logot) -> None:
do_something()
logot.assert_logged(logged.info("Something was done"))
```

`logot` integrates with popular testing (e.g. [`pytest`](https://logot.readthedocs.io/latest/using-pytest.html), [`unittest`](https://logot.readthedocs.io/latest/using-unittest.html)), asynchronous (e.g. [`asyncio`](https://logot.readthedocs.io/latest/index.html#index-testing-threaded), [`trio`](https://logot.readthedocs.io/latest/integrations/trio.html)) and logging frameworks (e.g. [`logging`](https://logot.readthedocs.io/latest/log-capturing.html), [`loguru`](https://logot.readthedocs.io/latest/integrations/loguru.html)). It can be extended to support many others. 💪

`logot` integrates with popular testing (e.g. [`pytest`](https://logot.readthedocs.io/latest/using-pytest.html), [`unittest`](https://logot.readthedocs.io/latest/using-unittest.html)), asynchronous (e.g. [`asyncio`](https://logot.readthedocs.io/latest/index.html#index-testing-threaded), [`trio`](https://logot.readthedocs.io/latest/integrations/trio.html)) and logging frameworks (e.g. [`logging`](https://logot.readthedocs.io/latest/log-capturing.html), [`loguru`](https://logot.readthedocs.io/latest/integrations/loguru.html), [`structlog`](https://logot.readthedocs.io/latest/integrations/structlog.html)). It can be extended to support many others. 💪

## Documentation 📖

Full documentation is published on [Read the Docs](https://logot.readthedocs.io).


## Bugs / feedback 🐛

Issue tracking is hosted on [GitHub](https://github.com/etianen/logot/issues).


## Changelog 🏗️

Release notes are published on [GitHub](https://github.com/etianen/logot/releases).


## License ⚖️

`logot` is published as open-source software under the [MIT license](https://github.com/etianen/logot/blob/main/LICENSE).
10 changes: 10 additions & 0 deletions docs/api/logot.structlog.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
:mod:`logot.structlog`
=======================

.. automodule:: logot.structlog


API reference
-------------

.. autoclass:: StructlogCapturer
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
intersphinx_mapping = {
"python": ("https://docs.python.org/3/", None),
"loguru": ("https://loguru.readthedocs.io/en/latest/", None),
"structlog": ("https://www.structlog.org/en/stable/", None),
"pytest": ("https://docs.pytest.org/en/latest/", None),
"trio": ("https://trio.readthedocs.io/en/latest/", None),
}
Expand Down
3 changes: 2 additions & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ Log-based testing 🪵
:mod:`logot` integrates with popular testing (e.g. :doc:`pytest </using-pytest>`,
:doc:`unittest </using-unittest>`), asynchronous (e.g. :ref:`asyncio <index-testing-threaded>`,
:doc:`trio </integrations/trio>`) and logging frameworks (e.g. :doc:`logging </log-capturing>`,
:doc:`loguru </integrations/loguru>`). It can be extended to support many others. 💪
:doc:`loguru </integrations/loguru>`, :doc:`structlog </integrations/structlog>`). It can be extended
to support many others. 💪


Why test logging? 🤔
Expand Down
1 change: 1 addition & 0 deletions docs/integrations/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Supported frameworks:
:maxdepth: 1

loguru
structlog

.. seealso::

Expand Down
95 changes: 95 additions & 0 deletions docs/integrations/structlog.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
Using with :mod:`structlog`
============================

.. currentmodule:: logot

:mod:`logot` makes it easy to capture logs from :mod:`structlog`:

.. code:: python
from logot.structlog import StructlogCapturer
with Logot(capturer=StructlogCapturer).capturing() as logot:
do_something()
logot.assert_logged(logged.info("App started"))
:mod:`logot` preserves the preconfigured :mod:`structlog` processor chain. Events are captured at the end of the chain,
but before the final processor, as it is responsible for emitting the log event to the underlying logging system. For
more information, see the
`structlog documentation <https://www.structlog.org/en/stable/processors.html#adapting-and-rendering>`_.


Installing
----------

Ensure :mod:`logot` is installed alongside a compatible :mod:`structlog` version by adding the ``structlog`` extra:

.. code:: bash
pip install 'logot[structlog]'
.. seealso::

See :ref:`installing-extras` usage guide.


Enabling for :mod:`pytest`
--------------------------

Enable :mod:`structlog` support in your :external+pytest:doc:`pytest configuration <reference/customize>`:

.. code:: ini
# pytest.ini or .pytest.ini
[pytest]
logot_capturer = logot.structlog.StructlogCapturer
.. code:: toml
# pyproject.toml
[tool.pytest.ini_options]
logot_capturer = "logot.structlog.StructlogCapturer"
.. seealso::

See :doc:`/using-pytest` usage guide.


Enabling for :mod:`unittest`
----------------------------

Enable :mod:`structlog` support in your :class:`logot.unittest.LogotTestCase`:

.. code:: python
from logot.structlog import StructlogCapturer
class MyAppTest(LogotTestCase):
logot_capturer = StructlogCapturer
.. seealso::

See :doc:`/using-unittest` usage guide.


Enabling manually
-----------------

Enable :mod:`structlog` support for your :class:`Logot` instance:

.. code:: python
from logot.structlog import StructlogCapturer
logot = Logot(capturer=StructlogCapturer)
Enable :mod:`structlog` support for a single :meth:`Logot.capturing` call:

.. code:: python
with Logot().capturing(capturer=StructlogCapturer) as logot:
do_something()
.. seealso::

See :class:`Logot` and :meth:`Logot.capturing` API reference.
2 changes: 1 addition & 1 deletion docs/log-capturing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Log capturing
.. seealso::

See :ref:`integrations-logging` for other supported logging frameworks (e.g. :doc:`loguru </integrations/loguru>`).
See :ref:`integrations-logging` for other supported logging frameworks (e.g. :doc:`loguru </integrations/loguru>`, :doc:`structlog </integrations/structlog>`).


Test framework integrations
Expand Down
3 changes: 2 additions & 1 deletion docs/using-pytest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ using |caplog|_ as:
- Support for :doc:`log message matching </log-message-matching>` using ``%``-style placeholders.
- Support for :doc:`log pattern matching </log-pattern-matching>` using *log pattern operators*.
- Support for testing :ref:`threaded <index-testing-threaded>` and :ref:`async <index-testing-async>` code.
- Support for :ref:`3rd-party logging frameworks <integrations-logging>` (e.g. :doc:`loguru </integrations/loguru>`).
- Support for :ref:`3rd-party logging frameworks <integrations-logging>` (e.g. :doc:`loguru </integrations/loguru>`,
:doc:`structlog </integrations/structlog>`).
- A cleaner, clearer syntax.


Expand Down
3 changes: 2 additions & 1 deletion docs/using-unittest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ testing. The above example can be rewritten using :meth:`assertLogs() <unittest.
- Support for :doc:`log message matching </log-message-matching>` using ``%``-style placeholders.
- Support for :doc:`log pattern matching </log-pattern-matching>` using *log pattern operators*.
- Support for testing :ref:`threaded <index-testing-threaded>` and :ref:`async <index-testing-async>` code.
- Support for :ref:`3rd-party logging frameworks <integrations-logging>` (e.g. :doc:`loguru </integrations/loguru>`).
- Support for :ref:`3rd-party logging frameworks <integrations-logging>` (e.g. :doc:`loguru </integrations/loguru>`,
:doc:`structlog </integrations/structlog>`).
- A cleaner, clearer syntax.


Expand Down
53 changes: 53 additions & 0 deletions logot/_structlog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from __future__ import annotations

from functools import partial

import structlog
from structlog.processors import NAME_TO_LEVEL
from structlog.types import EventDict, WrappedLogger

from logot._capture import Captured
from logot._logot import Capturer, Logot
from logot._typing import Level, Name


class StructlogCapturer(Capturer):
"""
A :class:`logot.Capturer` implementation for :mod:`structlog`.
"""

__slots__ = ("_old_processors",)

def start_capturing(self, logot: Logot, /, *, level: Level, name: Name) -> None:
config = structlog.get_config()
processors = config["processors"]
self._old_processors = processors

if isinstance(level, str):
levelno = NAME_TO_LEVEL[level.lower()]
else:
levelno = level

# We need to insert our processor before the last processor, as this is the processor that transforms the
# `event_dict` into the final log message. As this depends on the wrapped logger's formatting requirements,
# it can interfere with our capturing.
# See https://www.structlog.org/en/stable/processors.html#adapting-and-rendering
structlog.configure(
processors=[*processors[:-1], partial(_processor, logot=logot, name=name, levelno=levelno), processors[-1]]
)

def stop_capturing(self) -> None:
structlog.configure(processors=self._old_processors)


def _processor(
logger: WrappedLogger, method_name: str, event_dict: EventDict, *, logot: Logot, name: Name, levelno: int
) -> EventDict:
msg = event_dict["event"]
level = method_name.upper()
event_levelno = NAME_TO_LEVEL[method_name]

if getattr(logger, "name", None) == name and event_levelno >= levelno:
logot.capture(Captured(level, msg, levelno=event_levelno))

return event_dict
10 changes: 10 additions & 0 deletions logot/structlog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""
Integration API for :mod:`structlog`.
.. seealso::
See :doc:`/integrations/structlog` usage guide.
"""
from __future__ import annotations

from logot._structlog import StructlogCapturer as StructlogCapturer
30 changes: 24 additions & 6 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ packages = [{ include = "logot" }]
[tool.poetry.dependencies]
python = "^3.8"
loguru = { version = ">=0.6,<0.8", optional = true }
structlog = { version = ">=23.3,<25", optional = true }
pytest = { version = ">=7,<9", optional = true }
trio = { version = ">=0.22,<0.25", optional = true }
typing-extensions = { version = ">=4.9", python = "<3.10" }

[tool.poetry.extras]
loguru = ["loguru"]
structlog = ["structlog"]
pytest = ["pytest"]
trio = ["trio"]

Expand Down Expand Up @@ -75,6 +77,8 @@ addopts = "--tb=native --import-mode=importlib"
[tool.ruff]
include = ["docs/**/*.py", "logot/**/*.py", "tests/**/*.py"]
line-length = 120

[tool.ruff.lint]
select = ["E", "F", "W", "I", "UP"]

[build-system]
Expand Down
Loading

0 comments on commit e7349c6

Please sign in to comment.