Log new database creation, bootstrap like a transaction.

This commit is contained in:
Leo Vasanko
2026-06-20 18:15:08 +00:00
parent 652d60b74b
commit 3e1a86aee3
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,
no background flush task is started, and transactions are
rejected. The file is not created if missing.
log: Controls migration logging. ``True`` (default) uses the
``kanta.migrations`` logger. ``False`` suppresses the default
migration log. A :class:`~logging.Logger` instance writes
default migration output to that logger instead. Custom
``@kanta.logmigr`` callbacks run regardless of this setting.
log: Controls bootstrap and migration logging. ``True`` (default)
uses the ``kanta.changes`` logger for bootstrap records and the
``kanta.migrations`` logger for migration output. ``False``
suppresses the default bootstrap and migration logs. A
: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.
+26 -2
View File
@@ -12,7 +12,7 @@ from typing import Any, Generic, TypeVar
from kanta.callbacks import CallbackRegistry, InjectionContext
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.persistence import PersistenceMixin
from kanta.serialization import restore_data_in_place, struct_to_dict
@@ -277,13 +277,37 @@ class KantaImpl(PersistenceMixin, Generic[T]):
self.statedict = {}
current = struct_to_dict(self.data, serializer=self.serializer)
self.queue_change(
record = self.queue_change(
self.bootstrap_action,
current,
user=self.bootstrap_user,
mtime=self.bootstrap_mtime,
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:
self.opened = False
self.file.close()
+2
View File
@@ -177,6 +177,8 @@ async def test_logfmt_class_injection(tmp_path, format_config, caplog):
@kanta.logfmt
class UserLogFmt(LogFmt):
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")
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
@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
async def test_logmigr_callback_replaces_default_logging(
tmp_path, format_config, caplog