Skip to content

Commit 5e6fc1e

Browse files
committed
Small logging fixes
1 parent f044219 commit 5e6fc1e

File tree

5 files changed

+122
-8
lines changed

5 files changed

+122
-8
lines changed

key-value/key-value-aio/src/key_value/aio/adapters/pydantic/base.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ def _validate_model(self, value: dict[str, Any]) -> T | None:
6868
extra={
6969
"model_type": self._get_model_type_name(),
7070
"error": "missing 'items' wrapper",
71-
"value_preview": str(value)[:200],
7271
},
7372
exc_info=False,
7473
)
@@ -91,7 +90,6 @@ def _validate_model(self, value: dict[str, Any]) -> T | None:
9190
"model_type": self._get_model_type_name(),
9291
"error_count": len(error_details),
9392
"errors": error_details,
94-
"value_preview": str(value)[:200],
9593
},
9694
exc_info=True,
9795
)

key-value/key-value-aio/tests/adapters/test_pydantic.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from datetime import datetime, timezone
2+
from logging import LogRecord
23

34
import pytest
45
from inline_snapshot import snapshot
@@ -47,6 +48,20 @@ class Order(BaseModel):
4748
TEST_KEY_2: str = "test_key_2"
4849

4950

51+
def model_type_from_log_record(record: LogRecord) -> str:
52+
if not hasattr(record, "model_type"):
53+
msg = "Log record does not have a model_type attribute"
54+
raise ValueError(msg)
55+
return record.model_type # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType, reportAttributeAccessIssue]
56+
57+
58+
def errors_from_log_record(record: LogRecord) -> list[str]:
59+
if not hasattr(record, "errors"):
60+
msg = "Log record does not have an errors attribute"
61+
raise ValueError(msg)
62+
return record.errors # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType, reportAttributeAccessIssue]
63+
64+
5065
class TestPydanticAdapter:
5166
@pytest.fixture
5267
async def store(self) -> MemoryStore:
@@ -166,9 +181,11 @@ async def test_validation_error_logging(
166181
record = caplog.records[0]
167182
assert record.levelname == "ERROR"
168183
assert "Validation failed" in record.message
169-
assert record.model_type == "Pydantic model"
170-
assert record.error_count == 1
171-
assert "is_admin" in str(record.errors)
184+
assert model_type_from_log_record(record) == "Pydantic model"
185+
186+
errors = errors_from_log_record(record)
187+
assert len(errors) == 1
188+
assert "is_admin" in errors[0]
172189

173190
async def test_list_validation_error_logging(
174191
self, product_list_adapter: PydanticAdapter[list[Product]], store: MemoryStore, caplog: pytest.LogCaptureFixture
@@ -190,5 +207,7 @@ async def test_list_validation_error_logging(
190207
record = caplog.records[0]
191208
assert record.levelname == "ERROR"
192209
assert "Missing 'items' wrapper" in record.message
193-
assert record.model_type == "Pydantic model"
194-
assert "missing 'items' wrapper" in record.error
210+
assert model_type_from_log_record(record) == "Pydantic model"
211+
errors = errors_from_log_record(record)
212+
assert len(errors) == 1
213+
assert "missing 'items' wrapper" in errors[0]

key-value/key-value-sync/src/key_value/sync/code_gen/adapters/pydantic/base.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# WARNING: this file is auto-generated by 'build_sync_library.py'
22
# from the original file 'base.py'
33
# DO NOT CHANGE! Change the original file instead.
4+
import logging
45
from abc import ABC, abstractmethod
56
from collections.abc import Sequence
67
from typing import Any, Generic, SupportsFloat, TypeVar, overload
@@ -12,6 +13,8 @@
1213

1314
from key_value.sync.code_gen.protocols.key_value import KeyValue
1415

16+
logger = logging.getLogger(__name__)
17+
1518
T = TypeVar("T")
1619

1720

@@ -60,6 +63,14 @@ def _validate_model(self, value: dict[str, Any]) -> T | None:
6063
if self._raise_on_validation_error:
6164
msg = f"Invalid {self._get_model_type_name()} payload: missing 'items' wrapper"
6265
raise DeserializationError(msg)
66+
67+
# Log the missing 'items' wrapper when not raising
68+
logger.error(
69+
"Missing 'items' wrapper for list %s",
70+
self._get_model_type_name(),
71+
extra={"model_type": self._get_model_type_name(), "error": "missing 'items' wrapper"},
72+
exc_info=False,
73+
)
6374
return None
6475
return self._type_adapter.validate_python(value["items"])
6576

@@ -69,6 +80,15 @@ def _validate_model(self, value: dict[str, Any]) -> T | None:
6980
details = e.errors(include_input=False)
7081
msg = f"Invalid {self._get_model_type_name()}: {details}"
7182
raise DeserializationError(msg) from e
83+
84+
# Log the validation error when not raising
85+
error_details = e.errors(include_input=False)
86+
logger.error(
87+
"Validation failed for %s",
88+
self._get_model_type_name(),
89+
extra={"model_type": self._get_model_type_name(), "error_count": len(error_details), "errors": error_details},
90+
exc_info=True,
91+
)
7292
return None
7393

7494
def _serialize_model(self, value: T) -> dict[str, Any]:

key-value/key-value-sync/src/key_value/sync/code_gen/stores/elasticsearch/store.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# WARNING: this file is auto-generated by 'build_sync_library.py'
22
# from the original file 'store.py'
33
# DO NOT CHANGE! Change the original file instead.
4+
import logging
45
from collections.abc import Sequence
56
from datetime import datetime
67
from typing import Any, overload
@@ -37,6 +38,8 @@
3738
msg = "ElasticsearchStore requires py-key-value-aio[elasticsearch]"
3839
raise ImportError(msg) from e
3940

41+
logger = logging.getLogger(__name__)
42+
4043
DEFAULT_INDEX_PREFIX = "kv_store"
4144

4245
# You might think the `string` field should be a text/keyword field
@@ -249,7 +252,15 @@ def _get_managed_entries(self, *, collection: str, keys: Sequence[str]) -> list[
249252
entries_by_id[doc_id] = None
250253
continue
251254

252-
entries_by_id[doc_id] = source_to_managed_entry(source=source)
255+
try:
256+
entries_by_id[doc_id] = source_to_managed_entry(source=source)
257+
except DeserializationError as e:
258+
logger.error(
259+
"Failed to deserialize Elasticsearch document in batch operation",
260+
extra={"collection": collection, "document_id": doc_id, "error": str(e)},
261+
exc_info=True,
262+
)
263+
entries_by_id[doc_id] = None
253264

254265
# Return entries in the same order as input keys
255266
return [entries_by_id.get(document_id) for document_id in document_ids]

key-value/key-value-sync/tests/code_gen/adapters/test_pydantic.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# from the original file 'test_pydantic.py'
33
# DO NOT CHANGE! Change the original file instead.
44
from datetime import datetime, timezone
5+
from logging import LogRecord
56

67
import pytest
78
from inline_snapshot import snapshot
@@ -50,6 +51,20 @@ class Order(BaseModel):
5051
TEST_KEY_2: str = "test_key_2"
5152

5253

54+
def model_type_from_log_record(record: LogRecord) -> str:
55+
if not hasattr(record, "model_type"):
56+
msg = "Log record does not have a model_type attribute"
57+
raise ValueError(msg)
58+
return record.model_type # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType, reportAttributeAccessIssue]
59+
60+
61+
def errors_from_log_record(record: LogRecord) -> list[str]:
62+
if not hasattr(record, "errors"):
63+
msg = "Log record does not have an errors attribute"
64+
raise ValueError(msg)
65+
return record.errors # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType, reportAttributeAccessIssue]
66+
67+
5368
class TestPydanticAdapter:
5469
@pytest.fixture
5570
def store(self) -> MemoryStore:
@@ -148,3 +163,54 @@ def test_complex_adapter_with_list(self, product_list_adapter: PydanticAdapter[l
148163

149164
assert product_list_adapter.delete(collection=TEST_COLLECTION, key=TEST_KEY)
150165
assert product_list_adapter.get(collection=TEST_COLLECTION, key=TEST_KEY) is None
166+
167+
def test_validation_error_logging(
168+
self, user_adapter: PydanticAdapter[User], updated_user_adapter: PydanticAdapter[UpdatedUser], caplog: pytest.LogCaptureFixture
169+
):
170+
"""Test that validation errors are logged when raise_on_validation_error=False."""
171+
import logging
172+
173+
# Store a User, then try to retrieve as UpdatedUser (missing is_admin field)
174+
user_adapter.put(collection=TEST_COLLECTION, key=TEST_KEY, value=SAMPLE_USER)
175+
176+
with caplog.at_level(logging.ERROR):
177+
updated_user = updated_user_adapter.get(collection=TEST_COLLECTION, key=TEST_KEY)
178+
179+
# Should return None due to validation failure
180+
assert updated_user is None
181+
182+
# Check that an error was logged
183+
assert len(caplog.records) == 1
184+
record = caplog.records[0]
185+
assert record.levelname == "ERROR"
186+
assert "Validation failed" in record.message
187+
assert model_type_from_log_record(record) == "Pydantic model"
188+
189+
errors = errors_from_log_record(record)
190+
assert len(errors) == 1
191+
assert "is_admin" in errors[0]
192+
193+
def test_list_validation_error_logging(
194+
self, product_list_adapter: PydanticAdapter[list[Product]], store: MemoryStore, caplog: pytest.LogCaptureFixture
195+
):
196+
"""Test that missing 'items' wrapper is logged for list models."""
197+
import logging
198+
199+
# Manually store invalid data (missing 'items' wrapper)
200+
store.put(collection=TEST_COLLECTION, key=TEST_KEY, value={"invalid": "data"})
201+
202+
with caplog.at_level(logging.ERROR):
203+
result = product_list_adapter.get(collection=TEST_COLLECTION, key=TEST_KEY)
204+
205+
# Should return None due to missing 'items' wrapper
206+
assert result is None
207+
208+
# Check that an error was logged
209+
assert len(caplog.records) == 1
210+
record = caplog.records[0]
211+
assert record.levelname == "ERROR"
212+
assert "Missing 'items' wrapper" in record.message
213+
assert model_type_from_log_record(record) == "Pydantic model"
214+
errors = errors_from_log_record(record)
215+
assert len(errors) == 1
216+
assert "missing 'items' wrapper" in errors[0]

0 commit comments

Comments
 (0)