Implement richer, fully customizable logging; customizable timestamps #1
+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