Unify default emitter around universal LogEvent.header
All event kinds now share one shape: a one-line header plus an optional diff body for changes. LogEvent.header is a lazy property covering every kind (change, aborted, created, migrated), and default_emit reduces to logging the header plus routing diff_lines to the .diff child logger. Custom emitters can tap the same blocks - header, diff_lines, Line, format_diff - instead of reimplementing formatting per message type.
This commit is contained in:
+7
-7
@@ -73,13 +73,6 @@ async def main() -> None:
|
|||||||
) as data:
|
) as data:
|
||||||
data.counter = 2
|
data.counter = 2
|
||||||
|
|
||||||
try:
|
|
||||||
with kanta.transaction(action="reset", user="foo") as data:
|
|
||||||
data.counter = 99
|
|
||||||
raise ValueError("simulated failure")
|
|
||||||
except ValueError:
|
|
||||||
print(f"# Reading does not need transaction: {data.counter=}", flush=True)
|
|
||||||
|
|
||||||
print(
|
print(
|
||||||
"\n# A later version of our application with new data model, migrations and logfmt"
|
"\n# A later version of our application with new data model, migrations and logfmt"
|
||||||
)
|
)
|
||||||
@@ -89,6 +82,13 @@ async def main() -> None:
|
|||||||
) as data:
|
) as data:
|
||||||
data.total += 1
|
data.total += 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
with kanta.transaction(action="reset", user="userid001") as data:
|
||||||
|
data.total = 99
|
||||||
|
raise ValueError("simulated failure")
|
||||||
|
except ValueError:
|
||||||
|
print(f"# Reading does not need transaction: {data.total=}", flush=True)
|
||||||
|
|
||||||
with kanta.transaction(action="delete", user="userid002") as data:
|
with kanta.transaction(action="delete", user="userid002") as data:
|
||||||
del data.users["userid001"]
|
del data.users["userid001"]
|
||||||
|
|
||||||
|
|||||||
+10
-4
@@ -234,10 +234,16 @@ def resolve_user_key(value: str) -> str | None:
|
|||||||
`"migrated"`, `"aborted"`), the preferred `logger` and `level`, and all
|
`"migrated"`, `"aborted"`), the preferred `logger` and `level`, and all
|
||||||
relevant state: `action`, `user`, `extra`, `error` (for aborted
|
relevant state: `action`, `user`, `extra`, `error` (for aborted
|
||||||
transactions), `diff`, `previous`/`current` state dicts, the built `logfmt`
|
transactions), `diff`, `previous`/`current` state dicts, the built `logfmt`
|
||||||
chain, and version info for migration events. The default `"aborted"`
|
chain, and version info for migration events.
|
||||||
rendering is `<action>[ by <user>] transaction aborted: <error>` with the
|
- The built-in formatting is assembled from standard blocks that custom
|
||||||
action and user colored and the user resolved via `logfmt`. Pretty
|
emitters can reuse as-is or replace piecemeal:
|
||||||
`header` and `diff_lines` are lazy properties, built only if accessed.
|
- `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.
|
||||||
|
- `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.
|
||||||
- `@kanta.logemit` registers a callback receiving the event. The callback
|
- `@kanta.logemit` registers a callback receiving the event. The callback
|
||||||
decides what is logged and where: it may log one or more messages on
|
decides what is logged and where: it may log one or more messages on
|
||||||
`event.logger`, log somewhere else, or nothing at all. A falsy return
|
`event.logger`, log somewhere else, or nothing at all. A falsy return
|
||||||
|
|||||||
+29
-27
@@ -70,13 +70,33 @@ class LogEvent(msgspec.Struct, kw_only=True):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def header(self) -> str:
|
def header(self) -> str:
|
||||||
"""The default header line (colored), built on first access."""
|
"""The default one-line header for this event, built on first access.
|
||||||
|
|
||||||
|
Covers every event kind: ``"<action>[ <extra>][ by <user>]"`` for
|
||||||
|
changes, ``"<action>[ by <user>] transaction aborted: <error>"`` for
|
||||||
|
aborts, and the plain ``Created``/``Migrated`` summaries.
|
||||||
|
"""
|
||||||
if self._header is None:
|
if self._header is None:
|
||||||
self._header = format_action_header(
|
self._header = self._build_header()
|
||||||
self.action or "", self.user, self.extra
|
|
||||||
)
|
|
||||||
return self._header
|
return self._header
|
||||||
|
|
||||||
|
def _build_header(self) -> str:
|
||||||
|
if self.kind == "created":
|
||||||
|
return f"Created {self.filename}"
|
||||||
|
if self.kind == "migrated":
|
||||||
|
migrations = ", ".join(self.migrations)
|
||||||
|
return (
|
||||||
|
f"Migrated {self.filename} "
|
||||||
|
f"v{self.from_version} -> v{self.to_version}: {migrations}"
|
||||||
|
)
|
||||||
|
if self.kind == "change":
|
||||||
|
return format_action_header(self.action or "", self.user, self.extra)
|
||||||
|
line = Line().action(self.action or "")
|
||||||
|
if self.user:
|
||||||
|
line(" by ").user(self.user)
|
||||||
|
line(f" transaction aborted: {self.error}")
|
||||||
|
return str(line)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def diff_lines(self) -> list[str]:
|
def diff_lines(self) -> list[str]:
|
||||||
"""Pretty-printed diff lines, built on first access and cached."""
|
"""Pretty-printed diff lines, built on first access and cached."""
|
||||||
@@ -118,34 +138,16 @@ def emit_event(
|
|||||||
def default_emit(ev: LogEvent) -> None:
|
def default_emit(ev: LogEvent) -> None:
|
||||||
"""Emit *ev* with Kanta's built-in formatting.
|
"""Emit *ev* with Kanta's built-in formatting.
|
||||||
|
|
||||||
|
Logs :attr:`LogEvent.header`; for change events the
|
||||||
|
:attr:`LogEvent.diff_lines` body follows on the ``<logger>.diff`` child
|
||||||
|
logger so it can be silenced or routed separately from the headers.
|
||||||
This is what runs when no logemit callback handles the event; custom
|
This is what runs when no logemit callback handles the event; custom
|
||||||
callbacks may call it to delegate events they do not care about.
|
callbacks may call it to delegate events they do not care about.
|
||||||
"""
|
"""
|
||||||
if ev.kind == "created":
|
if ev.kind != "change":
|
||||||
ev.logger.log(ev.level, "Created %s", ev.filename)
|
ev.logger.log(ev.level, ev.header)
|
||||||
return
|
return
|
||||||
|
|
||||||
if ev.kind == "migrated":
|
|
||||||
ev.logger.log(
|
|
||||||
ev.level,
|
|
||||||
"Migrated %s v%s -> v%s: %s",
|
|
||||||
ev.filename,
|
|
||||||
ev.from_version,
|
|
||||||
ev.to_version,
|
|
||||||
", ".join(ev.migrations),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
if ev.kind == "aborted":
|
|
||||||
line = Line().action(ev.action or "")
|
|
||||||
if ev.user:
|
|
||||||
line(" by ").user(ev.user)
|
|
||||||
line(f" transaction aborted: {ev.error}")
|
|
||||||
ev.logger.log(ev.level, str(line))
|
|
||||||
return
|
|
||||||
|
|
||||||
# kind == "change": diff lines go to the <logger>.diff child logger so
|
|
||||||
# they can be silenced or routed separately from the headers.
|
|
||||||
diff_logger = logging.getLogger(f"{ev.logger.name}.diff")
|
diff_logger = logging.getLogger(f"{ev.logger.name}.diff")
|
||||||
lines = ev.diff_lines if ev.show_diff and diff_logger.isEnabledFor(ev.level) else []
|
lines = ev.diff_lines if ev.show_diff and diff_logger.isEnabledFor(ev.level) else []
|
||||||
|
|
||||||
|
|||||||
@@ -290,3 +290,28 @@ async def test_aborted_transaction_includes_resolved_user(
|
|||||||
messages = [r.getMessage() for r in caplog.records if r.levelno == logging.WARNING]
|
messages = [r.getMessage() for r in caplog.records if r.levelno == logging.WARNING]
|
||||||
assert any(" by " in m and "Alice" in m for m in messages)
|
assert any(" by " in m and "Alice" in m for m in messages)
|
||||||
assert any(" transaction aborted: boom" in m for m in messages)
|
assert any(" transaction aborted: boom" in m for m in messages)
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_header_covers_all_kinds():
|
||||||
|
created = LogEvent(kind="created", logger=transaction_logger, filename="x.db")
|
||||||
|
assert created.header == "Created x.db"
|
||||||
|
|
||||||
|
migrated = LogEvent(
|
||||||
|
kind="migrated",
|
||||||
|
logger=transaction_logger,
|
||||||
|
filename="x.db",
|
||||||
|
from_version=0,
|
||||||
|
to_version=1,
|
||||||
|
migrations=["migrate_v1 (rename)"],
|
||||||
|
)
|
||||||
|
assert migrated.header == "Migrated x.db v0 -> v1: migrate_v1 (rename)"
|
||||||
|
|
||||||
|
aborted = LogEvent(
|
||||||
|
kind="aborted",
|
||||||
|
logger=transaction_logger,
|
||||||
|
action="reset",
|
||||||
|
user="alice",
|
||||||
|
error=ValueError("boom"),
|
||||||
|
)
|
||||||
|
assert "transaction aborted: boom" in aborted.header
|
||||||
|
assert "alice" in aborted.header
|
||||||
|
|||||||
Reference in New Issue
Block a user