Skip to content

Commit a2f4d6a

Browse files
committed
Convert runtime annotations to plugin and additional testing for actions plugin
1 parent 8de95a8 commit a2f4d6a

File tree

9 files changed

+75
-33
lines changed

9 files changed

+75
-33
lines changed

annotated_logger/__init__.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from makefun import wraps
2525

2626
from annotated_logger.filter import AnnotatedFilter
27-
from annotated_logger.plugins import BasePlugin
27+
from annotated_logger.plugins import BasePlugin, RuntimeAnnotationsPlugin
2828

2929
if TYPE_CHECKING: # pragma: no cover
3030
from collections.abc import MutableMapping
@@ -255,8 +255,8 @@ class AnnotatedLogger:
255255
Args:
256256
----
257257
annotations: Dictionary of annotations to be added to every log message
258-
runtime_annotations: dictionary of method references to be called when
259-
a log message is emitted
258+
runtime_annotations: [Deprecated] Dictionary of method references to be called
259+
when a log message is emitted. Use the `RuntimeAnnotationsPlugin` instead.
260260
plugins: list of plugins to use
261261
262262
Methods:
@@ -313,9 +313,11 @@ def __init__( # noqa: PLR0913
313313
self.logger_base = logging.getLogger(self.logger_root_name)
314314
self.logger_base.setLevel(self.log_level)
315315
self.annotations = annotations or {}
316-
self.runtime_annotations = runtime_annotations or {}
317316
self.plugins = [BasePlugin()]
318317
self.plugins.extend(plugins)
318+
# Preserve the `runtime_annotations` param for backwards compat
319+
if runtime_annotations:
320+
self.plugins.append(RuntimeAnnotationsPlugin(runtime_annotations))
319321
if formatter and config:
320322
msg = "Cannot pass both formatter and config."
321323
raise ValueError(msg)
@@ -385,7 +387,6 @@ def generate_filter(
385387
return AnnotatedFilter(
386388
annotations=annotations,
387389
class_annotations=class_annotations,
388-
runtime_annotations=self.runtime_annotations,
389390
plugins=self.plugins,
390391
)
391392

annotated_logger/filter.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import logging
4+
from copy import copy
45
from typing import Any
56

67
import annotated_logger
@@ -14,29 +15,23 @@ class AnnotatedFilter(logging.Filter):
1415
def __init__(
1516
self,
1617
annotations: Annotations | None = None,
17-
runtime_annotations: Annotations | None = None,
1818
class_annotations: Annotations | None = None,
1919
plugins: list[annotated_logger.BasePlugin] | None = None,
2020
) -> None:
2121
"""Store the annotations, attributes and plugins."""
2222
self.annotations = annotations or {}
2323
self.class_annotations = class_annotations or {}
24-
self.runtime_annotations = runtime_annotations or {}
2524
self.plugins = plugins or [annotated_logger.BasePlugin()]
2625

2726
# This allows plugins to determine what fields were added by the user
2827
# vs the ones native to the log record
2928
# TODO(crimsonknave): Make a test for this # noqa: TD003, FIX002
3029
self.base_attributes = logging.makeLogRecord({}).__dict__ # pragma: no mutate
3130

32-
def _all_annotations(self, record: logging.LogRecord) -> Annotations:
31+
def _all_annotations(self) -> Annotations:
3332
annotations = {}
34-
# Using copy might be better, but, we don't want to add
35-
# the runtime annotations to the stored annotations
36-
annotations.update(self.class_annotations)
37-
annotations.update(self.annotations)
38-
for key, function in self.runtime_annotations.items():
39-
annotations[key] = function(record)
33+
annotations.update(copy(self.class_annotations))
34+
annotations.update(copy(self.annotations))
4035
annotations["annotated"] = True
4136
return annotations
4237

@@ -48,7 +43,7 @@ def filter(self, record: logging.LogRecord) -> bool:
4843
sees it. Returning False from the filter method will stop the evaluation and
4944
the log record won't be emitted.
5045
"""
51-
record.__dict__.update(self._all_annotations(record))
46+
record.__dict__.update(self._all_annotations())
5247
for plugin in self.plugins:
5348
try:
5449
result = plugin.filter(record)

annotated_logger/plugins.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import contextlib
44
import logging
5-
from typing import TYPE_CHECKING
5+
from typing import TYPE_CHECKING, Any, Callable
66

77
from requests.exceptions import HTTPError
88

@@ -23,11 +23,29 @@ def uncaught_exception(
2323
self, exception: Exception, logger: AnnotatedAdapter
2424
) -> AnnotatedAdapter:
2525
"""Handle an uncaught excaption."""
26-
logger.annotate(success=False)
27-
logger.annotate(exception_title=str(exception))
26+
if "success" not in logger.filter.annotations:
27+
logger.annotate(success=False)
28+
if "exception_title" not in logger.filter.annotations:
29+
logger.annotate(exception_title=str(exception))
2830
return logger
2931

3032

33+
class RuntimeAnnotationsPlugin(BasePlugin):
34+
"""Plugin that sets annotations dynamically."""
35+
36+
def __init__(
37+
self, runtime_annotations: dict[str, Callable[[logging.LogRecord], Any]]
38+
) -> None:
39+
"""Store the runtime annotations."""
40+
self.runtime_annotations = runtime_annotations
41+
42+
def filter(self, record: logging.LogRecord) -> bool:
43+
"""Add any configured runtime annotations."""
44+
for key, function in self.runtime_annotations.items():
45+
record.__dict__[key] = function(record)
46+
return True
47+
48+
3149
class RequestsPlugin(BasePlugin):
3250
"""Plugin for the requests library."""
3351

@@ -182,7 +200,7 @@ def logging_config(self) -> dict[str, dict[str, object]]:
182200
"level": "DEBUG",
183201
"handlers": [
184202
# This is from the default logging config
185-
"annotated_handler",
203+
# "annotated_handler",
186204
"actions_handler",
187205
],
188206
},

example/actions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,8 @@ class ActionsExample:
4141
def first_step(self, annotated_logger: AnnotatedAdapter) -> None:
4242
"""First step of your action."""
4343
annotated_logger.info("Step 1 running!")
44+
45+
@annotate_logs(_typing_requested=True)
46+
def second_step(self, annotated_logger: AnnotatedAdapter) -> None:
47+
"""Second step of your action."""
48+
annotated_logger.debug("Step 2 running!")

example/api.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from requests.models import Response
88

99
from annotated_logger import AnnotatedAdapter, AnnotatedLogger
10-
from annotated_logger.plugins import RequestsPlugin
10+
from annotated_logger.plugins import RequestsPlugin, RuntimeAnnotationsPlugin
1111

1212

1313
def runtime(_record: logging.LogRecord) -> str:
@@ -17,8 +17,10 @@ def runtime(_record: logging.LogRecord) -> str:
1717

1818
annotated_logger = AnnotatedLogger(
1919
annotations={"extra": "new data"},
20-
runtime_annotations={"runtime": runtime},
21-
plugins=[RequestsPlugin()],
20+
plugins=[
21+
RequestsPlugin(),
22+
RuntimeAnnotationsPlugin({"runtime": runtime}),
23+
],
2224
log_level=logging.DEBUG,
2325
name="annotated_logger.api",
2426
)

example/calculator.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
NameAdjusterPlugin,
88
NestedRemoverPlugin,
99
RemoverPlugin,
10+
RuntimeAnnotationsPlugin,
1011
)
1112

1213

@@ -24,13 +25,13 @@ def runtime(_record: logging.LogRecord) -> str:
2425
"extra": "new data",
2526
"nested_extra": {"nested_key": {"double_nested_key": "value"}},
2627
},
27-
runtime_annotations={"runtime": runtime},
2828
log_level=logging.DEBUG,
2929
plugins=[
3030
NameAdjusterPlugin(names=["joke"], prefix="cheezy_"),
3131
NameAdjusterPlugin(names=["power"], postfix="_overwhelming"),
3232
RemoverPlugin("taskName"),
3333
NestedRemoverPlugin(["double_nested_key"]),
34+
RuntimeAnnotationsPlugin({"runtime": runtime}),
3435
],
3536
name="annotated_logger.calculator",
3637
)

example/logging_config.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import logging.config
33

44
from annotated_logger import AnnotatedAdapter, AnnotatedFilter, AnnotatedLogger
5-
from annotated_logger.plugins import BasePlugin, RenamerPlugin
5+
from annotated_logger.plugins import BasePlugin, RenamerPlugin, RuntimeAnnotationsPlugin
66

77
# This logging config creates 4 loggers:
88
# * A logger for "annotated_logger.logging_config", which logs all messages as json and
@@ -48,8 +48,10 @@
4848
"()": AnnotatedFilter,
4949
"annotations": {"decorated": False, "class_based_filter": True},
5050
"class_annotations": {},
51-
"runtime_annotations": {"custom_runtime": lambda _record: True},
52-
"plugins": [BasePlugin()],
51+
"plugins": [
52+
BasePlugin(),
53+
RuntimeAnnotationsPlugin({"custom_runtime": lambda _record: True}),
54+
],
5355
},
5456
},
5557
"handlers": {
@@ -153,6 +155,10 @@ def runtime(_record: logging.LogRecord) -> str:
153155

154156
annotated_logger = AnnotatedLogger(
155157
annotations={"hostname": "my-host"},
158+
# This is deprecated, use the RuntimeAnnotationsPlugin instead.
159+
# This param is kept for backwards compatibility and creates a
160+
# RuntimeAnnotationsPlugin instead.
161+
# This is left as an example and to provide test coverage.
156162
runtime_annotations={"runtime": runtime},
157163
plugins=[RenamerPlugin(time="created", lvl="levelname")],
158164
log_level=logging.DEBUG,

test/test_decorator.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import example.default
1212
import test.demo
1313
from annotated_logger import AnnotatedLogger
14+
from annotated_logger.plugins import RuntimeAnnotationsPlugin
1415

1516
if TYPE_CHECKING:
1617
from annotated_logger.mocks import AnnotatedLogMock
@@ -217,10 +218,14 @@ def test_debug(self, annotated_logger_mock):
217218
def test_runtime_not_cached(self, annotated_logger_mock, mocker):
218219
runtime_mock = mocker.Mock(name="runtime_not_cached")
219220
runtime_mock.side_effect = ["first", "second", "third", "fourth"]
220-
runtime_annotations = example.calculator.annotated_logger.runtime_annotations
221-
example.calculator.annotated_logger.runtime_annotations = {
222-
"runtime": runtime_mock
223-
}
221+
plugin = next(
222+
plugin
223+
for plugin in example.calculator.annotated_logger.plugins
224+
if isinstance(plugin, RuntimeAnnotationsPlugin)
225+
)
226+
227+
runtime_annotations = plugin.runtime_annotations
228+
plugin.runtime_annotations = {"runtime": runtime_mock}
224229
# Need to use full path as that's what's reloaded. The other tests don't need
225230
# to as they're not mocking the runtime annotations
226231
calc = example.calculator.Calculator(12, 13)
@@ -243,7 +248,7 @@ def test_runtime_not_cached(self, annotated_logger_mock, mocker):
243248
"runtime": "second",
244249
},
245250
)
246-
example.calculator.annotated_logger.runtime_annotations = runtime_annotations
251+
plugin.runtime_annotations = runtime_annotations
247252

248253
def test_raises_type_error_with_too_few_args(self):
249254
calc = example.calculator.Calculator(12, 13)

test/test_plugins.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,12 +236,21 @@ def test_exclude_nested_fields(self, annotated_logger_mock):
236236
)
237237

238238

239+
@pytest.mark.usefixtures("_reload_actions")
239240
class TestGitHubActionsPlugin:
240241
def test_logs_normally(self, annotated_logger_mock):
241242
action = ActionsExample()
242243
action.first_step()
243244

244245
annotated_logger_mock.assert_logged("info", "Step 1 running!")
245246

246-
def test_logs_actions_annotations(self, annotated_logger_mock, caplog):
247-
pass
247+
@pytest.mark.parametrize(
248+
"annotated_logger_object", [logging.getLogger("annotated_logger.actions")]
249+
)
250+
def test_logs_actions_annotations(self, annotated_logger_mock):
251+
action = ActionsExample()
252+
action.first_step()
253+
action.second_step()
254+
255+
assert "notice:: Step 1 running!" in annotated_logger_mock.messages[0]
256+
annotated_logger_mock.assert_logged("DEBUG", count=0)

0 commit comments

Comments
 (0)