diff --git a/docs/database.md b/docs/database.md index 35fe7b3..60ffa39 100644 --- a/docs/database.md +++ b/docs/database.md @@ -231,16 +231,18 @@ def resolve_user_key(value: str) -> str | None: 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`, 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. + `"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. - The built-in formatting is assembled from standard blocks that custom emitters can reuse as-is or replace piecemeal: - `event.header` — a lazy property producing the default one-line header for any kind: `[ ][ by ]` for changes, `[ by ] transaction aborted: ` for aborts, and the - plain `Created`/`Migrated` summaries. + plain `Created`/`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 change events (built only if accessed). - `default_emit` itself is just `header` plus the `diff_lines` routing. diff --git a/kanta/kantaimpl.py b/kanta/kantaimpl.py index a598119..7888ba5 100644 --- a/kanta/kantaimpl.py +++ b/kanta/kantaimpl.py @@ -131,6 +131,7 @@ class KantaImpl(PersistenceMixin, Generic[T]): kind="change", logger=migration_log, level=logging.DEBUG, + kanta=self._kanta, action=info.name, diff=info.diff, previous=info.before, @@ -143,6 +144,7 @@ class KantaImpl(PersistenceMixin, Generic[T]): LogEvent( kind="migrated", logger=migration_log, + kanta=self._kanta, filename=str(self.filename), from_version=previous_version, to_version=migration_result.version, @@ -324,6 +326,7 @@ class KantaImpl(PersistenceMixin, Generic[T]): LogEvent( kind="created", logger=logger, + kanta=self._kanta, filename=str(self.filename.resolve()), ), self.callback_registry.logemit_handlers, @@ -344,6 +347,7 @@ class KantaImpl(PersistenceMixin, Generic[T]): LogEvent( kind="change", logger=logger, + kanta=self._kanta, action=self.bootstrap_action, user=formatted_user, diff=record.diff, diff --git a/kanta/logging.py b/kanta/logging.py index 293484f..18c4a08 100644 --- a/kanta/logging.py +++ b/kanta/logging.py @@ -52,6 +52,7 @@ class LogEvent(msgspec.Struct, kw_only=True): kind: str logger: logging.Logger level: int = logging.INFO + kanta: Any = None action: str | None = None user: str | None = None extra: str | None = None @@ -80,6 +81,15 @@ class LogEvent(msgspec.Struct, kw_only=True): self._header = self._build_header() return self._header + @header.setter + def header(self, value: str) -> None: + """Override the header, keeping the default diff routing. + + A logemit callback can restyle the header and return a truthy value: + :func:`default_emit` then logs this header instead of building one. + """ + self._header = value + def _build_header(self) -> str: if self.kind == "created": return f"Created {self.filename}" diff --git a/kanta/transaction.py b/kanta/transaction.py index 5dd8509..f947bed 100644 --- a/kanta/transaction.py +++ b/kanta/transaction.py @@ -99,6 +99,7 @@ def transaction( LogEvent( kind="change", logger=logger, + kanta=impl._kanta, action=action, user=_resolve_user(logfmt, user), extra=extra, @@ -120,6 +121,7 @@ def transaction( kind="aborted", logger=transaction_logger, level=logging.WARNING, + kanta=impl._kanta, action=action, user=resolved_user, error=exc, diff --git a/tests/test_logemit.py b/tests/test_logemit.py index 311f39a..a4218e7 100644 --- a/tests/test_logemit.py +++ b/tests/test_logemit.py @@ -315,3 +315,32 @@ def test_event_header_covers_all_kinds(): ) assert "transaction aborted: boom" in aborted.header assert "alice" in aborted.header + + +@pytest.mark.asyncio +async def test_event_carries_kanta_instance(tmp_path, format_config): + path = tmp_path / "test.db" + kanta = make_kanta(path, Data, format_config) + events = [] + kanta.logemit(lambda ev: events.append(ev) or True) + await kanta.open() + with kanta.transaction(action="inc") as data: + data.counter = 1 + await kanta.close() + + assert events + assert all(ev.kanta is kanta for ev in events) + + +def test_header_is_settable_and_used_by_default_emit(capsys): + logging.getLogger("kanta").handlers.clear() + configure_logging() + + def restyle(ev): + ev.header = f"CUSTOM {ev.action}" + return True + + emit_event(_change_event(diff={"counter": 1}, previous={}), [restyle]) + err = capsys.readouterr().err + assert "CUSTOM update" in err + assert "counter" in err # default diff routing still applies