Log new database creation, bootstrap like a transaction.

This commit is contained in:
2026-06-20 18:15:08 +00:00
parent 753b7eba86
commit 55fa475a13
4 changed files with 81 additions and 7 deletions
+7 -5
View File
@@ -163,11 +163,13 @@ class Kanta(Generic[T]):
readonly: If True, open the database read-only. No lock is acquired, readonly: If True, open the database read-only. No lock is acquired,
no background flush task is started, and transactions are no background flush task is started, and transactions are
rejected. The file is not created if missing. rejected. The file is not created if missing.
log: Controls migration logging. ``True`` (default) uses the log: Controls bootstrap and migration logging. ``True`` (default)
``kanta.migrations`` logger. ``False`` suppresses the default uses the ``kanta.changes`` logger for bootstrap records and the
migration log. A :class:`~logging.Logger` instance writes ``kanta.migrations`` logger for migration output. ``False``
default migration output to that logger instead. Custom suppresses the default bootstrap and migration logs. A
``@kanta.logmigr`` callbacks run regardless of this setting. :class:`~logging.Logger` instance writes default output to that
logger instead. Custom ``@kanta.logmigr`` callbacks run
regardless of this setting.
Calling ``open`` more than once on the same instance is not allowed. Calling ``open`` more than once on the same instance is not allowed.
+26 -2
View File
@@ -12,7 +12,7 @@ from typing import Any, Generic, TypeVar
from kanta.callbacks import CallbackRegistry, InjectionContext from kanta.callbacks import CallbackRegistry, InjectionContext
from kanta.exceptions import DatabaseError, DataIntegrityError, ReplayError from kanta.exceptions import DatabaseError, DataIntegrityError, ReplayError
from kanta.logging import log_change, migration_logger from kanta.logging import _USER_PATH, changes_logger, log_change, migration_logger
from kanta.migrations import MigrationResult, Migrations from kanta.migrations import MigrationResult, Migrations
from kanta.persistence import PersistenceMixin from kanta.persistence import PersistenceMixin
from kanta.serialization import restore_data_in_place, struct_to_dict from kanta.serialization import restore_data_in_place, struct_to_dict
@@ -277,13 +277,37 @@ class KantaImpl(PersistenceMixin, Generic[T]):
self.statedict = {} self.statedict = {}
current = struct_to_dict(self.data, serializer=self.serializer) current = struct_to_dict(self.data, serializer=self.serializer)
self.queue_change( record = self.queue_change(
self.bootstrap_action, self.bootstrap_action,
current, current,
user=self.bootstrap_user, user=self.bootstrap_user,
mtime=self.bootstrap_mtime, mtime=self.bootstrap_mtime,
force=True, force=True,
) )
if record is not None and log is not False:
logger = log if isinstance(log, logging.Logger) else changes_logger
logger.info("Created %s", self.filename.resolve())
logfmt = self.callback_registry.build_logfmt(
InjectionContext(
previous_state={},
current_state=current,
kanta=self._kanta,
)
)
formatted_user = self.bootstrap_user
if formatted_user is not None and logfmt is not None:
resolved = logfmt(formatted_user, _USER_PATH)
if resolved is not None:
formatted_user = resolved
log_change(
self.bootstrap_action,
record.diff,
formatted_user,
previous={},
logfmt=logfmt,
logger=logger,
)
except Exception: except Exception:
self.opened = False self.opened = False
self.file.close() self.file.close()
+2
View File
@@ -177,6 +177,8 @@ async def test_logfmt_class_injection(tmp_path, format_config, caplog):
@kanta.logfmt @kanta.logfmt
class UserLogFmt(LogFmt): class UserLogFmt(LogFmt):
def resolve(self, value: str, path: str) -> str | None: def resolve(self, value: str, path: str) -> str | None:
if not isinstance(value, str):
return None
return self.current_state.get("users", {}).get(value, {}).get("name") return self.current_state.get("users", {}).get(value, {}).get("name")
await kanta.open() await kanta.open()
+46
View File
@@ -623,6 +623,52 @@ async def test_open_log_false_suppresses_migration_log(tmp_path, format_config,
assert not info_messages assert not info_messages
@pytest.mark.asyncio
async def test_open_log_true_logs_bootstrap(tmp_path, format_config, caplog):
path = tmp_path / "test.db"
kanta = make_kanta(path, Data, format_config)
with caplog.at_level(logging.INFO, logger="kanta.changes"):
await kanta.open()
await kanta.close()
info_messages = [r.message for r in caplog.records if r.levelno == logging.INFO]
assert len(info_messages) >= 2
assert "Created" in info_messages[0]
assert "bootstrap" in info_messages[1]
@pytest.mark.asyncio
async def test_open_log_false_suppresses_bootstrap_log(tmp_path, format_config, caplog):
path = tmp_path / "test.db"
kanta = make_kanta(path, Data, format_config)
with caplog.at_level(logging.INFO, logger="kanta.changes"):
await kanta.open(log=False)
await kanta.close()
info_messages = [r for r in caplog.records if r.levelno == logging.INFO]
assert not info_messages
@pytest.mark.asyncio
async def test_open_log_custom_logger_logs_bootstrap(tmp_path, format_config, caplog):
path = tmp_path / "test.db"
kanta = make_kanta(path, Data, format_config)
custom_logger = logging.getLogger("custom.bootstrap")
custom_logger.setLevel(logging.INFO)
with caplog.at_level(logging.INFO, logger="custom.bootstrap"):
await kanta.open(log=custom_logger)
await kanta.close()
info_messages = [r.message for r in caplog.records if r.levelno == logging.INFO]
assert len(info_messages) >= 2
assert "Created" in info_messages[0]
assert "bootstrap" in info_messages[1]
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_logmigr_callback_replaces_default_logging( async def test_logmigr_callback_replaces_default_logging(
tmp_path, format_config, caplog tmp_path, format_config, caplog