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

Added structlog support #109

Merged
merged 13 commits into from
Feb 17, 2024
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
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]
etianen marked this conversation as resolved.
Show resolved Hide resolved
select = ["E", "F", "W", "I", "UP"]

[build-system]
Expand Down
Loading