Skip to content
18 changes: 16 additions & 2 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,25 @@

## Upgrading

- The `LoggingConfigUpdater` was renamed to `LoggingConfigUpdatingActor` to follow the actor naming convention.
- `frequenz.sdk.config`

* `LoggingConfigUpdater`

+ Renamed to `LoggingConfigUpdatingActor` to follow the actor naming convention.
+ Make all arguments to the constructor keyword-only.

* `LoggingConfig`

+ The `load()` method was removed. Please use `frequenz.sdk.config.load_config()` instead.
+ The class is now a standard `dataclass` instead of a `marshmallow_dataclass`.

* `LoggerConfig` is not a standard `dataclass` instead of a `marshmallow_dataclass`.

## New Features

<!-- Here goes the main new features and examples or instructions on how to use them -->
- `LoggingConfigUpdatingActor`

* Added a new `name` argument to the constructor to be able to override the actor's name.

## Bug Fixes

Expand Down
2 changes: 1 addition & 1 deletion src/frequenz/sdk/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@

__all__ = [
"ConfigManagingActor",
"LoggingConfig",
"LoggerConfig",
"LoggingConfig",
"LoggingConfigUpdatingActor",
"load_config",
]
42 changes: 14 additions & 28 deletions src/frequenz/sdk/config/_logging_actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@

import logging
from collections.abc import Mapping
from dataclasses import field
from typing import Annotated, Any, Self, cast
from dataclasses import dataclass, field
from typing import Annotated, Any

import marshmallow
import marshmallow.validate
from frequenz.channels import Receiver
from marshmallow import RAISE
from marshmallow_dataclass import class_schema, dataclass

from frequenz.sdk.actor import Actor
from ..actor import Actor
from ._util import load_config

_logger = logging.getLogger(__name__)

Expand All @@ -24,6 +23,7 @@
validate=marshmallow.validate.OneOf(choices=logging.getLevelNamesMapping())
),
]
"""A marshmallow field for validating log levels."""


@dataclass
Expand All @@ -36,7 +36,6 @@ class LoggerConfig:
"metadata": {
"description": "Log level for the logger. Uses standard logging levels."
},
"required": False,
},
)
"""The log level for the logger."""
Expand All @@ -52,7 +51,6 @@ class LoggingConfig:
"metadata": {
"description": "Default default configuration for all loggers.",
},
"required": False,
},
)
"""The default log level."""
Expand All @@ -63,27 +61,10 @@ class LoggingConfig:
"metadata": {
"description": "Configuration for a logger (the key is the logger name)."
},
"required": False,
},
)
"""The list of loggers configurations."""

@classmethod
def load(cls, configs: Mapping[str, Any]) -> Self: # noqa: DOC502
"""Load and validate configs from a dictionary.

Args:
configs: The configuration to validate.

Returns:
The configuration if they are valid.

Raises:
ValidationError: if the configuration are invalid.
"""
schema = class_schema(cls)()
return cast(Self, schema.load(configs, unknown=RAISE))


class LoggingConfigUpdatingActor(Actor):
"""Actor that listens for logging configuration changes and sets them.
Expand Down Expand Up @@ -133,31 +114,36 @@ async def run() -> None:

def __init__(
self,
*,
config_recv: Receiver[Mapping[str, Any]],
log_format: str = "%(asctime)s %(levelname)-8s %(name)s:%(lineno)s: %(message)s",
log_datefmt: str = "%Y-%m-%dT%H:%M:%S%z",
log_format: str = "%(asctime)s %(levelname)-8s %(name)s:%(lineno)s: %(message)s",
name: str | None = None,
):
"""Initialize this instance.

Args:
config_recv: The receiver to listen for configuration changes.
log_format: Use the specified format string in logs.
log_datefmt: Use the specified date/time format in logs.
log_format: Use the specified format string in logs.
name: The name of this actor. If `None`, `str(id(self))` will be used. This
is used mostly for debugging purposes.

Note:
The `log_format` and `log_datefmt` parameters are used in a call to
`logging.basicConfig()`. If logging has already been configured elsewhere
in the application (through a previous `basicConfig()` call), then the format
settings specified here will be ignored.
"""
super().__init__()
self._config_recv = config_recv

# Setup default configuration.
# This ensures logging is configured even if actor fails to start or
# if the configuration cannot be loaded.
self._current_config: LoggingConfig = LoggingConfig()

super().__init__(name=name)

logging.basicConfig(
format=log_format,
datefmt=log_datefmt,
Expand All @@ -168,7 +154,7 @@ async def _run(self) -> None:
"""Listen for configuration changes and update logging."""
async for message in self._config_recv:
try:
new_config = LoggingConfig.load(message)
new_config = load_config(LoggingConfig, message)
except marshmallow.ValidationError:
_logger.exception(
"Invalid logging configuration received. Skipping config update"
Expand Down
15 changes: 10 additions & 5 deletions tests/config/test_logging_actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
from marshmallow import ValidationError
from pytest_mock import MockerFixture

from frequenz.sdk.config import LoggerConfig, LoggingConfig, LoggingConfigUpdatingActor
from frequenz.sdk.config import (
LoggerConfig,
LoggingConfig,
LoggingConfigUpdatingActor,
load_config,
)


def test_logging_config() -> None:
Expand All @@ -33,19 +38,19 @@ def test_logging_config() -> None:
},
)

assert LoggingConfig.load(config_raw) == config
assert load_config(LoggingConfig, config_raw) == config

config_raw = {}
config = LoggingConfig()
assert LoggingConfig.load(config_raw) == config
assert load_config(LoggingConfig, config_raw) == config

config_raw = {"root_logger": {"level": "UNKNOWN"}}
with pytest.raises(ValidationError):
LoggingConfig.load(config_raw)
load_config(LoggingConfig, config_raw)

config_raw = {"unknown": {"frequenz.sdk.actor": {"level": "DEBUG"}}}
with pytest.raises(ValidationError):
LoggingConfig.load(config_raw)
load_config(LoggingConfig, config_raw)


@pytest.fixture
Expand Down
Loading