All remaining events through logformat, cleaner migration message, prettier demo.

This commit is contained in:
2026-08-07 17:12:29 +00:00
parent 08f3c44f1f
commit f489216c2a
6 changed files with 44 additions and 31 deletions
+3 -5
View File
@@ -57,7 +57,7 @@ def resolve_user(value: str, path: str, previous: DictPre) -> str | None:
async def main() -> None:
filename.unlink(missing_ok=True)
print("# Database creation with v0 schema and basic ops, pretty logs")
print("Database creation with v0 schema and basic transactions:\n")
# Open and close automatically; you can also `await kanta.open()` instead
async with kanta_v0 as kanta:
with kanta.transaction(action="create", user="userid001") as data:
@@ -73,9 +73,7 @@ async def main() -> None:
) as data:
data.counter = 2
print(
"\n# A later version of our application with new data model, migrations and logfmt"
)
print("\nA new data model, migrations and logfmt pretty names:\n")
async with kanta_v1 as kanta:
with kanta.transaction(
action="update", user="userid002", extra=filename.name
@@ -87,7 +85,7 @@ async def main() -> None:
data.total = 99
raise ValueError("simulated failure")
except ValueError:
print(f"# Reading does not need transaction: {data.total=}", flush=True)
print(f"\nReset rolled back: {data.total=} (we can always read data without tx)\n")
with kanta.transaction(action="delete", user="userid002") as data:
del data.users["userid001"]
+7 -5
View File
@@ -227,13 +227,15 @@ def resolve_user_key(value: str) -> str | None:
#### Log Emitters
- Every change-related message Kanta emits (transaction/bootstrap/migration
changes, `Created <file>`, migration summaries, aborted transactions) is
described by a `kanta.logging.LogEvent` and dispatched through
changes, file created/opened lines, migration summaries, aborted
transactions) is described by a `kanta.logging.LogEvent` and dispatched
through
`kanta.logging.emit_event`. Kanta's own output goes through the same
mechanism: when no `logemit` callback handles an event,
`kanta.logging.default_emit` renders it with the built-in formatting.
- A `LogEvent` carries the event `kind` (`"change"`, `"created"`,
`"migrated"`, `"aborted"`), the preferred `logger` and `level`, the
`"opened"`, `"migrated"`, `"aborted"`), the preferred `logger` and `level`,
the
`kanta` instance, and all relevant state: `action`, `user`, `extra`,
`error` (for aborted transactions), `diff`, `previous`/`current` state
dicts, the built `logfmt` chain, and version info for migration events.
@@ -245,8 +247,8 @@ def resolve_user_key(value: str) -> str | None:
- `event.header` — a lazy property producing the default one-line header
for any kind: `<action>[ <extra>][ by <user>]` for changes,
`<action>[ <extra>][ by <user>] transaction aborted: <error>` for aborts,
and the
plain `Created`/`Migrated` summaries. It is settable: assign
and the `🛢️ <file> created|opened|migrated ...` summaries. It is
settable: assign
`event.header = ...` and return truthy to restyle the header while
keeping the default diff routing.
- `event.diff_lines` — a lazy property producing the pretty diff body for
+16 -6
View File
@@ -242,9 +242,18 @@ class KantaImpl(PersistenceMixin, Generic[T]):
)
self.version = rr.version
self.mtime = rr.m
if log is not False:
if log is not False and not migrations_ran:
logger = log if isinstance(log, logging.Logger) else bootstrap_logger
logger.debug("Using %s", self.filename.resolve())
emit_event(
LogEvent(
kind="opened",
logger=logger,
level=logging.DEBUG,
kanta=self._kanta,
filename=str(self.filename.resolve()),
),
self.callback_registry.logemit_handlers,
)
normalized = struct_to_dict(self.data, serializer=self.serializer)
if self.readonly:
self.statedict = copy.deepcopy(normalized)
@@ -257,6 +266,11 @@ class KantaImpl(PersistenceMixin, Generic[T]):
f"migrate:v{self.version}" if migrations_ran else "migrate:msgspec"
)
record = self.queue_change(action, normalized, mtime=False)
# The migration summary introduces the diff, so log it first.
if migrations_ran and migration_result is not None:
await self._handle_migration_log(
migration_result, previous_version, log
)
if (
record is not None
and log is not False
@@ -277,10 +291,6 @@ class KantaImpl(PersistenceMixin, Generic[T]):
),
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()
+11 -8
View File
@@ -40,10 +40,11 @@ class LogEvent(msgspec.Struct, kw_only=True):
"""All state describing one loggable event, passed to logemit callbacks.
``kind`` is ``"change"`` (transaction, bootstrap, or migration diff),
``"created"`` (database file created), ``"migrated"`` (migration
summary), or ``"aborted"`` (transaction rolled back). ``logger`` and
``level`` are Kanta's preferred destination; a callback may use them,
log elsewhere, or not log at all.
``"created"`` (database file created), ``"opened"`` (database file
opened), ``"migrated"`` (migration summary), or ``"aborted"``
(transaction rolled back). ``logger`` and ``level`` are Kanta's
preferred destination; a callback may use them, log elsewhere, or not
log at all.
The event is mutable: a callback may modify it before returning a truthy
value to pass it on, affecting later callbacks and the built-in fallback.
@@ -75,8 +76,8 @@ class LogEvent(msgspec.Struct, kw_only=True):
Covers every event kind: ``"<action>[ <extra>][ by <user>]"`` for
changes, ``"<action>[ <extra>][ by <user>] transaction aborted:
<error>"`` for aborts, and the plain ``Created``/``Migrated``
summaries.
<error>"`` for aborts, and the ``🛢️ <filename> <verb>`` file
summaries (created / opened / migrated).
"""
if self._header is None:
self._header = self._build_header()
@@ -93,11 +94,13 @@ class LogEvent(msgspec.Struct, kw_only=True):
def _build_header(self) -> str:
if self.kind == "created":
return f"Created {self.filename}"
return f"🛢️ {self.filename} created"
if self.kind == "opened":
return f"🛢️ {self.filename} opened"
if self.kind == "migrated":
migrations = ", ".join(self.migrations)
return (
f"Migrated {self.filename} "
f"🛢️ {self.filename} migrated "
f"v{self.from_version} -> v{self.to_version}: {migrations}"
)
if self.kind == "change":
+3 -3
View File
@@ -634,7 +634,7 @@ async def test_open_log_true_logs_bootstrap(tmp_path, format_config, caplog):
info_messages = [r.message for r in caplog.records if r.levelno == logging.INFO]
assert len(info_messages) >= 2
assert "Created" in info_messages[0]
assert "created" in info_messages[0]
assert "bootstrap" in info_messages[1]
@@ -665,7 +665,7 @@ async def test_open_log_custom_logger_logs_bootstrap(tmp_path, format_config, ca
info_messages = [r.message for r in caplog.records if r.levelno == logging.INFO]
assert len(info_messages) >= 2
assert "Created" in info_messages[0]
assert "created" in info_messages[0]
assert "bootstrap" in info_messages[1]
@@ -685,7 +685,7 @@ async def test_open_existing_database_logs_using_on_debug(
await kanta2.close()
debug_messages = [r.message for r in caplog.records if r.levelno == logging.DEBUG]
assert any("Using" in m and str(path.resolve()) in m for m in debug_messages)
assert any("opened" in m and str(path.resolve()) in m for m in debug_messages)
@pytest.mark.asyncio
+4 -4
View File
@@ -123,8 +123,8 @@ def test_default_emit_created_and_migrated(capsys):
)
)
err = capsys.readouterr().err
assert "Created x.kantadb" in err
assert "Migrated x.kantadb v0 -> v1: migrate_v1 (rename)" in err
assert "🛢️ x.kantadb created" in err
assert "🛢️ x.kantadb migrated v0 -> v1: migrate_v1 (rename)" in err
@pytest.mark.asyncio
@@ -295,7 +295,7 @@ async def test_aborted_transaction_includes_resolved_user(
def test_event_header_covers_all_kinds():
created = LogEvent(kind="created", logger=transaction_logger, filename="x.db")
assert created.header == "Created x.db"
assert created.header == "🛢️ x.db created"
migrated = LogEvent(
kind="migrated",
@@ -305,7 +305,7 @@ def test_event_header_covers_all_kinds():
to_version=1,
migrations=["migrate_v1 (rename)"],
)
assert migrated.header == "Migrated x.db v0 -> v1: migrate_v1 (rename)"
assert migrated.header == "🛢️ x.db migrated v0 -> v1: migrate_v1 (rename)"
aborted = LogEvent(
kind="aborted",