Group all migration events into one row migrate:vN (if version changed) OR migrate:msgspec

- Log as a single event
- Logging config has debug parameter to lower level to DEBUG, showing migration diffs
This commit is contained in:
2026-08-07 16:20:56 +00:00
parent e0046ae9d9
commit 08f3c44f1f
4 changed files with 41 additions and 37 deletions
+1 -1
View File
@@ -106,5 +106,5 @@ def fake_clock() -> datetime:
if __name__ == "__main__": if __name__ == "__main__":
configure_logging() configure_logging(debug=True)
asyncio.run(main()) asyncio.run(main())
+3 -2
View File
@@ -76,8 +76,9 @@ history.
- In-memory data is defined by an application `msgspec.Struct` type. - In-memory data is defined by an application `msgspec.Struct` type.
- Kanta round-trips through plain builtins for persistence and diffing. - Kanta round-trips through plain builtins for persistence and diffing.
- Dict keys are serialized as strings (`str_keys=True`) for stable JSON form. - Dict keys are serialized as strings (`str_keys=True`) for stable JSON form.
- Normalization changes introduced by struct decode/encode are logged as - Normalization changes introduced by struct decode/encode are logged together
`migrate:msgspec` when they produce a diff. with migrations as `migrate:vN`, or as `migrate:msgspec` when no migration
ran but normalization still produces a diff.
## Transaction Semantics ## Transaction Semantics
+32 -33
View File
@@ -124,21 +124,6 @@ class KantaImpl(PersistenceMixin, Generic[T]):
if not changed: if not changed:
return return
for info in changed:
if info.diff:
emit_event(
LogEvent(
kind="change",
logger=migration_log,
level=logging.DEBUG,
kanta=self._kanta,
action=info.name,
diff=info.diff,
previous=info.before,
),
self.callback_registry.logemit_handlers,
)
descriptions = [f"{m.name} ({m.description})" for m in changed] descriptions = [f"{m.name} ({m.description})" for m in changed]
emit_event( emit_event(
LogEvent( LogEvent(
@@ -237,10 +222,6 @@ class KantaImpl(PersistenceMixin, Generic[T]):
rr.version = migration_result.version rr.version = migration_result.version
migrations_ran = rr.version != previous_version migrations_ran = rr.version != previous_version
migration_state_changed = (
state_before_migrations is not None
and state_before_migrations != rr.state
)
self.snapshot.ts = ( self.snapshot.ts = (
datetime.fromtimestamp(rr.last_snapshot_mtime, UTC) datetime.fromtimestamp(rr.last_snapshot_mtime, UTC)
@@ -268,16 +249,39 @@ class KantaImpl(PersistenceMixin, Generic[T]):
if self.readonly: if self.readonly:
self.statedict = copy.deepcopy(normalized) self.statedict = copy.deepcopy(normalized)
else: else:
if migrations_ran and migration_state_changed: # One record per open: migration changes and normalization are
self.queue_change( # grouped into migrate:vN, or migrate:msgspec when only the
f"migrate:v{self.version}", # serialization drifted.
rr.state, previous = self.statedict
mtime=False, action = (
) f"migrate:v{self.version}" if migrations_ran else "migrate:msgspec"
msgspec_record = self.queue_change(
"migrate:msgspec", normalized, mtime=False
) )
if migrations_ran or msgspec_record is not None: record = self.queue_change(action, normalized, mtime=False)
if (
record is not None
and log is not False
and not (migrations_ran and self.callback_registry.has("logmigr"))
):
logger = (
log if isinstance(log, logging.Logger) else migration_logger
)
emit_event(
LogEvent(
kind="change",
logger=logger,
level=logging.DEBUG,
kanta=self._kanta,
action=action,
diff=record.diff,
previous=previous,
),
self.callback_registry.logemit_handlers,
)
if migrations_ran and migration_result is not None:
await self._handle_migration_log(
migration_result, previous_version, log
)
if migrations_ran or record is not None:
self.snapshot.request_force() self.snapshot.request_force()
await self.flush() await self.flush()
self.snapshot.maybe_write( self.snapshot.maybe_write(
@@ -287,11 +291,6 @@ class KantaImpl(PersistenceMixin, Generic[T]):
m=self.mtime, m=self.mtime,
now=self.now, now=self.now,
) )
if migrations_ran and migration_result is not None:
await self._handle_migration_log(
migration_result, previous_version, log
)
elif self.readonly: elif self.readonly:
self.opened = False self.opened = False
self.file.close() self.file.close()
+5 -1
View File
@@ -480,6 +480,7 @@ def configure_logging(
migration: bool = True, migration: bool = True,
transaction: bool = True, transaction: bool = True,
diff: bool = True, diff: bool = True,
debug: bool = False,
) -> None: ) -> None:
"""Configure Kanta's default logging output. """Configure Kanta's default logging output.
@@ -497,6 +498,9 @@ def configure_logging(
only transaction headers are printed and diff formatting is only transaction headers are printed and diff formatting is
skipped. Per transaction this is controlled by the ``logdiff`` skipped. Per transaction this is controlled by the ``logdiff``
argument of :meth:`Kanta.transaction`. argument of :meth:`Kanta.transaction`.
debug: Whether to set the ``kanta`` logger level to ``DEBUG`` instead
of ``INFO``. This reveals debug-level output such as migration
diffs, which are hidden by default.
This helper is not called automatically; applications that want Kanta's This helper is not called automatically; applications that want Kanta's
default output can call it, but most applications will configure logging default output can call it, but most applications will configure logging
@@ -521,4 +525,4 @@ def configure_logging(
handler = logging.StreamHandler(sys.stderr) handler = logging.StreamHandler(sys.stderr)
handler.setFormatter(logging.Formatter("%(message)s")) handler.setFormatter(logging.Formatter("%(message)s"))
target.addHandler(handler) target.addHandler(handler)
target.setLevel(logging.INFO) target.setLevel(logging.DEBUG if debug else logging.INFO)