Skip to content

Commit da2c32a

Browse files
committed
Make the LoggingConfigUpdatingActor use the ConfigManager
The `LoggingConfigUpdatingActor` can now also receive `None` as configuration (for example if the configuration is removed), in which case it will go back to the default configuration. Signed-off-by: Leandro Lucarella <[email protected]>
1 parent c931692 commit da2c32a

File tree

3 files changed

+71
-83
lines changed

3 files changed

+71
-83
lines changed

RELEASE_NOTES.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
* `LoggingConfigUpdater`
1212

1313
+ Renamed to `LoggingConfigUpdatingActor` to follow the actor naming convention.
14-
+ Make all arguments to the constructor keyword-only.
14+
+ The actor must now be constructed using a `ConfigManager` instead of a receiver.
15+
+ Make all arguments to the constructor keyword-only, except for the `config_manager` argument.
16+
+ If the configuration is removed, the actor will now load back the default configuration.
1517

1618
* `LoggingConfig`
1719

src/frequenz/sdk/config/_logging_actor.py

Lines changed: 30 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,14 @@
44
"""Read and update logging severity from config."""
55

66
import logging
7-
from collections.abc import Mapping
87
from dataclasses import dataclass, field
9-
from typing import Annotated, Any
8+
from typing import Annotated, Sequence, assert_never
109

1110
import marshmallow
1211
import marshmallow.validate
13-
from frequenz.channels import Receiver
1412

1513
from ..actor import Actor
16-
from ._util import load_config
14+
from ._manager import ConfigManager
1715

1816
_logger = logging.getLogger(__name__)
1917

@@ -84,26 +82,13 @@ class LoggingConfigUpdatingActor(Actor):
8482
8583
```python
8684
import asyncio
87-
from collections.abc import Mapping
88-
from typing import Any
8985
90-
from frequenz.channels import Broadcast
91-
from frequenz.sdk.config import LoggingConfigUpdatingActor, ConfigManagingActor
86+
from frequenz.sdk.config import LoggingConfigUpdatingActor
9287
from frequenz.sdk.actor import run as run_actors
9388
9489
async def run() -> None:
95-
config_channel = Broadcast[Mapping[str, Any]](name="config", resend_latest=True)
96-
actors = [
97-
ConfigManagingActor(
98-
config_paths=["config.toml"], output=config_channel.new_sender()
99-
),
100-
LoggingConfigUpdatingActor(
101-
config_recv=config_channel.new_receiver(limit=1)).map(
102-
lambda app_config: app_config.get("logging", {}
103-
)
104-
),
105-
]
106-
await run_actors(*actors)
90+
config_manager: ConfigManager = ...
91+
await run_actors(LoggingConfigUpdatingActor(config_manager))
10792
10893
asyncio.run(run())
10994
```
@@ -112,18 +97,25 @@ async def run() -> None:
11297
will be updated as well.
11398
"""
11499

100+
# pylint: disable-next=too-many-arguments
115101
def __init__(
116102
self,
103+
config_manager: ConfigManager,
104+
/,
117105
*,
118-
config_recv: Receiver[Mapping[str, Any]],
106+
config_key: str | Sequence[str] = "logging",
119107
log_datefmt: str = "%Y-%m-%dT%H:%M:%S%z",
120108
log_format: str = "%(asctime)s %(levelname)-8s %(name)s:%(lineno)s: %(message)s",
121109
name: str | None = None,
122110
):
123111
"""Initialize this instance.
124112
125113
Args:
126-
config_recv: The receiver to listen for configuration changes.
114+
config_manager: The configuration manager to use. If `None`, the [global
115+
configuration manager][frequenz.sdk.config.get_config_manager] will be
116+
used.
117+
config_key: The key to use to retrieve the configuration from the
118+
configuration manager. If `None`, the whole configuration will be used.
127119
log_datefmt: Use the specified date/time format in logs.
128120
log_format: Use the specified format string in logs.
129121
name: The name of this actor. If `None`, `str(id(self))` will be used. This
@@ -135,7 +127,9 @@ def __init__(
135127
in the application (through a previous `basicConfig()` call), then the format
136128
settings specified here will be ignored.
137129
"""
138-
self._config_recv = config_recv
130+
self._config_receiver = config_manager.new_receiver(
131+
config_key, LoggingConfig, base_schema=None
132+
)
139133

140134
# Setup default configuration.
141135
# This ensures logging is configured even if actor fails to start or
@@ -153,17 +147,19 @@ def __init__(
153147

154148
async def _run(self) -> None:
155149
"""Listen for configuration changes and update logging."""
156-
async for message in self._config_recv:
157-
try:
158-
new_config = load_config(LoggingConfig, message)
159-
except marshmallow.ValidationError:
160-
_logger.exception(
161-
"Invalid logging configuration received. Skipping config update"
162-
)
163-
continue
164-
165-
if new_config != self._current_config:
166-
self._update_logging(new_config)
150+
async for new_config in self._config_receiver:
151+
match new_config:
152+
case None:
153+
# When we receive None, we want to reset the logging configuration
154+
# to the default
155+
self._update_logging(LoggingConfig())
156+
case LoggingConfig():
157+
self._update_logging(new_config)
158+
case Exception():
159+
# We ignore errors and just keep the old configuration
160+
pass
161+
case unexpected:
162+
assert_never(unexpected)
167163

168164
def _update_logging(self, config: LoggingConfig) -> None:
169165
"""Configure the logging level."""

tests/config/test_logging_actor.py

Lines changed: 38 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
import asyncio
77
import logging
8-
from collections.abc import Mapping
98
from typing import Any
109

1110
import pytest
@@ -77,78 +76,69 @@ async def test_logging_config_updating_actor(
7776
# is not working anyway - python ignores it.
7877
mocker.patch("frequenz.sdk.config._logging_actor.logging.basicConfig")
7978

80-
config_channel = Broadcast[Mapping[str, Any]](name="config")
81-
config_sender = config_channel.new_sender()
82-
async with LoggingConfigUpdatingActor(
83-
config_recv=config_channel.new_receiver().map(
84-
lambda app_config: app_config.get("logging", {})
85-
)
86-
) as actor:
79+
# Mock ConfigManager
80+
mock_config_manager = mocker.Mock()
81+
mock_config_manager.config_channel = Broadcast[LoggingConfig | Exception | None](
82+
name="config"
83+
)
84+
mock_config_manager.new_receiver = mocker.Mock(
85+
return_value=mock_config_manager.config_channel.new_receiver()
86+
)
87+
88+
async with LoggingConfigUpdatingActor(mock_config_manager) as actor:
8789
assert logging.getLogger("frequenz.sdk.actor").level == logging.NOTSET
8890
assert logging.getLogger("frequenz.sdk.timeseries").level == logging.NOTSET
8991

9092
update_logging_spy = mocker.spy(actor, "_update_logging")
9193

9294
# Send first config
93-
await config_sender.send(
94-
{
95-
"logging": {
96-
"root_logger": {"level": "ERROR"},
97-
"loggers": {
98-
"frequenz.sdk.actor": {"level": "DEBUG"},
99-
"frequenz.sdk.timeseries": {"level": "ERROR"},
100-
},
101-
}
102-
}
95+
expected_config = LoggingConfig(
96+
root_logger=LoggerConfig(level="ERROR"),
97+
loggers={
98+
"frequenz.sdk.actor": LoggerConfig(level="DEBUG"),
99+
"frequenz.sdk.timeseries": LoggerConfig(level="ERROR"),
100+
},
103101
)
102+
await mock_config_manager.config_channel.new_sender().send(expected_config)
104103
await asyncio.sleep(0.01)
105-
update_logging_spy.assert_called_once_with(
106-
LoggingConfig(
107-
root_logger=LoggerConfig(level="ERROR"),
108-
loggers={
109-
"frequenz.sdk.actor": LoggerConfig(level="DEBUG"),
110-
"frequenz.sdk.timeseries": LoggerConfig(level="ERROR"),
111-
},
112-
)
113-
)
104+
update_logging_spy.assert_called_once_with(expected_config)
114105
assert logging.getLogger("frequenz.sdk.actor").level == logging.DEBUG
115106
assert logging.getLogger("frequenz.sdk.timeseries").level == logging.ERROR
116107
update_logging_spy.reset_mock()
117108

118-
# Update config
119-
await config_sender.send(
120-
{
121-
"logging": {
122-
"root_logger": {"level": "WARNING"},
123-
"loggers": {
124-
"frequenz.sdk.actor": {"level": "INFO"},
125-
},
126-
}
127-
}
109+
# Send an exception and verify the previous config is maintained
110+
await mock_config_manager.config_channel.new_sender().send(
111+
ValueError("Test error")
128112
)
129113
await asyncio.sleep(0.01)
114+
update_logging_spy.assert_not_called() # Should not try to update logging
115+
# Previous config should be maintained
116+
assert logging.getLogger("frequenz.sdk.actor").level == logging.DEBUG
117+
assert logging.getLogger("frequenz.sdk.timeseries").level == logging.ERROR
118+
assert (
119+
actor._current_config == expected_config # pylint: disable=protected-access
120+
) # pylint: disable=protected-access
121+
update_logging_spy.reset_mock()
122+
123+
# Update config
130124
expected_config = LoggingConfig(
131125
root_logger=LoggerConfig(level="WARNING"),
132126
loggers={
133127
"frequenz.sdk.actor": LoggerConfig(level="INFO"),
134128
},
135129
)
130+
await mock_config_manager.config_channel.new_sender().send(expected_config)
131+
await asyncio.sleep(0.01)
136132
update_logging_spy.assert_called_once_with(expected_config)
137133
assert logging.getLogger("frequenz.sdk.actor").level == logging.INFO
138134
assert logging.getLogger("frequenz.sdk.timeseries").level == logging.NOTSET
139135
update_logging_spy.reset_mock()
140136

141-
# Send invalid config to make sure actor doesn't crash and doesn't setup invalid config.
142-
await config_sender.send({"logging": {"root_logger": {"level": "UNKNOWN"}}})
143-
await asyncio.sleep(0.01)
144-
update_logging_spy.assert_not_called()
145-
assert actor._current_config == expected_config
146-
update_logging_spy.reset_mock()
147-
148-
# Send empty config to reset logging to default
149-
await config_sender.send({"other": {"var1": 1}})
137+
# Send a None config to make sure actor doesn't crash and configures a default logging
138+
await mock_config_manager.config_channel.new_sender().send(None)
150139
await asyncio.sleep(0.01)
151140
update_logging_spy.assert_called_once_with(LoggingConfig())
152-
assert logging.getLogger("frequenz.sdk.actor").level == logging.NOTSET
153-
assert logging.getLogger("frequenz.sdk.timeseries").level == logging.NOTSET
141+
assert (
142+
actor._current_config == LoggingConfig() # pylint: disable=protected-access
143+
)
154144
update_logging_spy.reset_mock()

0 commit comments

Comments
 (0)