Implement richer, fully customizable logging; customizable timestamps #1

Merged
LeoVasanko merged 24 commits from rich-logging into main 2026-08-07 16:08:28 +00:00
5 changed files with 52 additions and 5 deletions
Showing only changes of commit 799b2438be - Show all commits
+7 -5
View File
@@ -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.
+4
View File
@@ -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,
+10
View File
@@ -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}"
+2
View File
@@ -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,
+29
View File
@@ -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