Add kanta instance to LogEvent and make header settable
Every emitted event now carries the originating Kanta instance so logemit callbacks can reach application state attached to it. The header property gains a setter, formalizing restyle-then-delegate: assign ev.header and return truthy to keep the default diff routing with a custom header.
This commit is contained in:
+7
-5
@@ -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: `<action>[ <extra>][ by <user>]` for changes,
|
||||
`<action>[ by <user>] transaction aborted: <error>` 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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user