Log new database creation, bootstrap like a transaction.
This commit is contained in:
+7
-5
@@ -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
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user