Skip to content

Commit f285929

Browse files
authored
Merge pull request #100 from praekeltfoundation/log-config-problems
Log all config validation errors as warnings, including (ignored) extra keys
2 parents e7a6247 + b111d52 commit f285929

3 files changed

Lines changed: 72 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
- Message cache for storing inbound messages
66
- Link last inbound message to outbound messages for USSD flows
77
- Retry messages if Turn rate limits us
8+
- Warning logs for config errors, including (ignored) extra keys
89

9-
## [0.3.2] - 2024-01-22
10+
## [0.3.2] - 2025-01-22
1011
### Added
1112
- Add Turn Channels application
1213

src/vumi2/config.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22
from argparse import Namespace
33
from collections.abc import Callable, Iterable
4+
from logging import getLogger
45
from pathlib import Path
56
from typing import (
67
Any,
@@ -13,7 +14,9 @@
1314
import yaml
1415
from attrs import Attribute, AttrsInstance, Factory, define, fields
1516
from attrs import has as is_attrs
16-
from cattrs import Converter
17+
from cattrs import Converter, transform_error
18+
19+
logger = getLogger(__name__)
1720

1821
_conv = Converter(prefer_attrib_converters=True)
1922

@@ -29,6 +32,11 @@ def get_config_class(cls: Configurable[CT] | type[Configurable[CT]]) -> type[CT]
2932

3033

3134
def structure(config: dict, cls: type[CT]) -> CT:
35+
try:
36+
_conv.copy(forbid_extra_keys=True).structure(config, cls)
37+
except Exception as e:
38+
for err in transform_error(e, cls.__name__):
39+
logger.warning(f"Config error: {err}")
3240
return _conv.structure(config, cls)
3341

3442

tests/test_config.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import logging
12
import os
23
from argparse import Namespace
34
from pathlib import Path
45

56
import pytest
7+
from cattrs import ClassValidationError
68

79
from vumi2.config import (
810
BaseConfig,
@@ -18,6 +20,9 @@ def deserialise(config: dict) -> BaseConfig:
1820

1921

2022
def test_default_base_config():
23+
"""
24+
All fields in BaseConfig have default values.
25+
"""
2126
config = deserialise({})
2227
assert config.amqp.hostname == "127.0.0.1"
2328
assert config.amqp.port == 5672
@@ -29,6 +34,9 @@ def test_default_base_config():
2934

3035

3136
def test_specified_base_config():
37+
"""
38+
Specified values override all defaults.
39+
"""
3240
config = deserialise(
3341
{
3442
"amqp": {
@@ -52,6 +60,9 @@ def test_specified_base_config():
5260

5361

5462
def test_load_config_from_environment():
63+
"""
64+
Config values can be provided in environment variables.
65+
"""
5566
environment = {
5667
"VUMI_AMQP_HOSTNAME": "localhost",
5768
"VUMI_AMQP_PORT": "1234",
@@ -79,6 +90,9 @@ def test_load_config_from_environment():
7990

8091

8192
def test_load_config_from_cli():
93+
"""
94+
Config values can be provided as cli args.
95+
"""
8296
# Make sure we don't have a stray config file set in the environment.
8397
assert "VUMI_CONFIG_FILE" not in os.environ
8498

@@ -179,3 +193,50 @@ def test_missing_config_file(monkeypatch, tmp_path):
179193

180194
with pytest.raises(FileNotFoundError):
181195
load_config(cli=Namespace())
196+
197+
198+
def test_extra_field_warnings_logged(caplog):
199+
"""
200+
Extra fields are logged as warnings, but don't fail validation.
201+
"""
202+
config = deserialise(
203+
{
204+
"amqp": {
205+
"vhost": "/vumi",
206+
"not_amqp": "blah",
207+
},
208+
"worker_concurrency": 10,
209+
"extra": "value",
210+
}
211+
)
212+
assert config.amqp.vhost == "/vumi"
213+
assert config.worker_concurrency == 10
214+
215+
logs = [log.message for log in caplog.records if log.levelno >= logging.WARNING]
216+
assert logs == [
217+
"Config error: extra fields found (not_amqp) @ BaseConfig.amqp",
218+
"Config error: extra fields found (extra) @ BaseConfig",
219+
]
220+
221+
222+
def test_config_validation_warnings_logged(caplog):
223+
"""
224+
All validation failures are logged as warnings.
225+
"""
226+
with pytest.raises(ClassValidationError):
227+
deserialise(
228+
{
229+
"amqp": {
230+
"vhost": "/vumi",
231+
"port": "eleventy one",
232+
},
233+
"worker_concurrency": 10,
234+
"extra": "value",
235+
}
236+
)
237+
238+
logs = [log.message for log in caplog.records if log.levelno >= logging.WARNING]
239+
assert logs == [
240+
"Config error: invalid value for type, expected int @ BaseConfig.amqp.port",
241+
"Config error: extra fields found (extra) @ BaseConfig",
242+
]

0 commit comments

Comments
 (0)