From 08f3c44f1fe07b749e075d0829901258dcfbfc2b Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Fri, 7 Aug 2026 16:20:56 +0000 Subject: [PATCH] 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 --- demo/demo.py | 2 +- docs/database.md | 5 ++-- kanta/kantaimpl.py | 65 +++++++++++++++++++++++----------------------- kanta/logging.py | 6 ++++- 4 files changed, 41 insertions(+), 37 deletions(-) diff --git a/demo/demo.py b/demo/demo.py index 1bdb436..cf2a793 100644 --- a/demo/demo.py +++ b/demo/demo.py @@ -106,5 +106,5 @@ def fake_clock() -> datetime: if __name__ == "__main__": - configure_logging() + configure_logging(debug=True) asyncio.run(main()) diff --git a/docs/database.md b/docs/database.md index 5b2cb58..a7ad4b2 100644 --- a/docs/database.md +++ b/docs/database.md @@ -76,8 +76,9 @@ history. - In-memory data is defined by an application `msgspec.Struct` type. - Kanta round-trips through plain builtins for persistence and diffing. - Dict keys are serialized as strings (`str_keys=True`) for stable JSON form. -- Normalization changes introduced by struct decode/encode are logged as - `migrate:msgspec` when they produce a diff. +- Normalization changes introduced by struct decode/encode are logged together + with migrations as `migrate:vN`, or as `migrate:msgspec` when no migration + ran but normalization still produces a diff. ## Transaction Semantics diff --git a/kanta/kantaimpl.py b/kanta/kantaimpl.py index 7888ba5..a38e943 100644 --- a/kanta/kantaimpl.py +++ b/kanta/kantaimpl.py @@ -124,21 +124,6 @@ class KantaImpl(PersistenceMixin, Generic[T]): if not changed: 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] emit_event( LogEvent( @@ -237,10 +222,6 @@ class KantaImpl(PersistenceMixin, Generic[T]): rr.version = migration_result.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 = ( datetime.fromtimestamp(rr.last_snapshot_mtime, UTC) @@ -268,16 +249,39 @@ class KantaImpl(PersistenceMixin, Generic[T]): if self.readonly: self.statedict = copy.deepcopy(normalized) else: - if migrations_ran and migration_state_changed: - self.queue_change( - f"migrate:v{self.version}", - rr.state, - mtime=False, - ) - msgspec_record = self.queue_change( - "migrate:msgspec", normalized, mtime=False + # One record per open: migration changes and normalization are + # grouped into migrate:vN, or migrate:msgspec when only the + # serialization drifted. + previous = self.statedict + action = ( + f"migrate:v{self.version}" if migrations_ran else "migrate:msgspec" ) - 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() await self.flush() self.snapshot.maybe_write( @@ -287,11 +291,6 @@ class KantaImpl(PersistenceMixin, Generic[T]): m=self.mtime, 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: self.opened = False self.file.close() diff --git a/kanta/logging.py b/kanta/logging.py index 910ea26..fe213a9 100644 --- a/kanta/logging.py +++ b/kanta/logging.py @@ -480,6 +480,7 @@ def configure_logging( migration: bool = True, transaction: bool = True, diff: bool = True, + debug: bool = False, ) -> None: """Configure Kanta's default logging output. @@ -497,6 +498,9 @@ def configure_logging( only transaction headers are printed and diff formatting is skipped. Per transaction this is controlled by the ``logdiff`` 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 default output can call it, but most applications will configure logging @@ -521,4 +525,4 @@ def configure_logging( handler = logging.StreamHandler(sys.stderr) handler.setFormatter(logging.Formatter("%(message)s")) target.addHandler(handler) - target.setLevel(logging.INFO) + target.setLevel(logging.DEBUG if debug else logging.INFO)