commit 028fb8f50c54fe717e8bb24bbb56a53e9ff4c9e3
parent 7f2afb5b635dea2ed63e697a37ddc3674a3a00e7
Author: triesap <triesap@radroots.dev>
Date: Mon, 19 Jan 2026 23:46:12 +0000
app: flush buffered logs after init
- add log buffer flush helper with retention pruning
- flush buffered entries after backends initialize
- emit debug logs for init stage transitions
- add unit test coverage for buffer flush behavior
Diffstat:
3 files changed, 82 insertions(+), 10 deletions(-)
diff --git a/app/src/app.rs b/app/src/app.rs
@@ -14,6 +14,8 @@ use crate::{
app_init_stage_set,
app_init_total_add,
app_init_total_unknown,
+ app_log_buffer_flush,
+ app_log_debug_emit,
app_log_error_emit,
app_log_error_store,
app_config_default,
@@ -58,6 +60,10 @@ fn active_key_label(value: Option<String>) -> String {
format!("{head}...{tail}")
}
+fn log_init_stage(stage: AppInitStage) {
+ let _ = app_log_debug_emit("log.app.init.stage", stage.as_str(), None);
+}
+
fn spawn_health_checks(
config: AppConfig,
health_report: RwSignal<AppHealthReport, LocalStorage>,
@@ -108,7 +114,9 @@ fn HomePage() -> impl IntoView {
provide_context(init_state);
Effect::new(move || {
spawn_local(async move {
- init_state.update(|state| app_init_stage_set(state, AppInitStage::Storage));
+ let stage = AppInitStage::Storage;
+ init_state.update(|state| app_init_stage_set(state, stage));
+ log_init_stage(stage);
let config = app_config_default();
if !app_init_has_completed() {
init_state.update(|state| {
@@ -117,7 +125,10 @@ fn HomePage() -> impl IntoView {
});
let assets_result = app_init_assets(
&config,
- |stage| init_state.update(|state| app_init_stage_set(state, stage)),
+ |stage| {
+ init_state.update(|state| app_init_stage_set(state, stage));
+ log_init_stage(stage);
+ },
|loaded, total| {
init_state.update(|state| {
app_init_progress_add(state, loaded);
@@ -133,21 +144,30 @@ fn HomePage() -> impl IntoView {
let init_err = AppInitError::Assets(err);
let _ = app_log_error_emit(&init_err);
init_error.set(Some(init_err));
- init_state.update(|state| app_init_stage_set(state, AppInitStage::Error));
+ let stage = AppInitStage::Error;
+ init_state.update(|state| app_init_stage_set(state, stage));
+ log_init_stage(stage);
return;
}
- init_state.update(|state| app_init_stage_set(state, AppInitStage::Storage));
+ let stage = AppInitStage::Storage;
+ init_state.update(|state| app_init_stage_set(state, stage));
+ log_init_stage(stage);
}
match app_init_backends(config).await {
Ok(value) => {
+ let _ = app_log_buffer_flush(&value.datastore, &value.config.datastore.key_maps).await;
backends.set(Some(value));
app_init_mark_completed();
- init_state.update(|state| app_init_stage_set(state, AppInitStage::Ready));
+ let stage = AppInitStage::Ready;
+ init_state.update(|state| app_init_stage_set(state, stage));
+ log_init_stage(stage);
}
Err(err) => {
let _ = app_log_error_emit(&err);
init_error.set(Some(err));
- init_state.update(|state| app_init_stage_set(state, AppInitStage::Error));
+ let stage = AppInitStage::Error;
+ init_state.update(|state| app_init_stage_set(state, stage));
+ log_init_stage(stage);
}
}
})
diff --git a/app/src/lib.rs b/app/src/lib.rs
@@ -54,6 +54,7 @@ pub use logging::{
app_log_entry_record,
app_log_entry_store,
app_log_buffer_drain,
+ app_log_buffer_flush,
app_log_buffer_push,
app_log_entries_dump,
app_log_entries_load,
diff --git a/app/src/logging.rs b/app/src/logging.rs
@@ -329,6 +329,27 @@ pub fn app_log_buffer_drain() -> Vec<AppLogEntry> {
entries.drain(..).collect()
}
+pub async fn app_log_buffer_flush<T: RadrootsClientDatastore>(
+ datastore: &T,
+ key_maps: &AppKeyMapConfig,
+) -> AppLogResult<usize> {
+ let entries = app_log_buffer_drain();
+ let mut stored = 0;
+ let mut iter = entries.into_iter();
+ while let Some(entry) = iter.next() {
+ if let Err(err) = app_log_entry_store(datastore, key_maps, &entry).await {
+ app_log_buffer_push(entry);
+ for remaining in iter {
+ app_log_buffer_push(remaining);
+ }
+ return Err(err);
+ }
+ stored += 1;
+ }
+ let _ = app_log_entries_prune(datastore, key_maps, APP_LOG_MAX_ENTRIES).await?;
+ Ok(stored)
+}
+
pub fn app_log_entry_prefix(key_maps: &AppKeyMapConfig) -> AppLogResult<String> {
let param = app_datastore_param_key(key_maps, "log_entry")?;
Ok(param(""))
@@ -450,6 +471,7 @@ mod tests {
app_log_entry_key,
app_log_entry_prefix,
app_log_buffer_drain,
+ app_log_buffer_flush,
app_log_buffer_push,
app_log_metadata,
app_log_timestamp_ms,
@@ -475,6 +497,8 @@ mod tests {
use serde::{de::DeserializeOwned, Serialize};
use std::sync::Mutex;
+ static LOG_TEST_LOCK: Mutex<()> = Mutex::new(());
+
struct TestDatastore {
entries: Mutex<Vec<RadrootsClientDatastoreEntry>>,
}
@@ -515,13 +539,21 @@ mod tests {
async fn set_obj<T>(
&self,
- _key: &str,
- _value: &T,
+ key: &str,
+ value: &T,
) -> RadrootsClientDatastoreResult<T>
where
T: Serialize + DeserializeOwned + Clone,
{
- Err(RadrootsClientDatastoreError::IdbUndefined)
+ let encoded = serde_json::to_string(value)
+ .map_err(|_| RadrootsClientDatastoreError::NoResult)?;
+ let mut entries = self.entries.lock().unwrap_or_else(|err| err.into_inner());
+ entries.retain(|entry| entry.key != key);
+ entries.push(RadrootsClientDatastoreEntry::new(
+ key.to_string(),
+ Some(encoded),
+ ));
+ Ok(value.clone())
}
async fn update_obj<T>(
@@ -653,6 +685,7 @@ mod tests {
#[test]
fn log_buffer_drains_entries() {
+ let _guard = LOG_TEST_LOCK.lock().unwrap_or_else(|err| err.into_inner());
let _ = app_log_buffer_drain();
let entry = app_log_entry_new(AppLogLevel::Debug, "log.code.test", "buf", None);
app_log_buffer_push(entry.clone());
@@ -737,8 +770,26 @@ mod tests {
let datastore = TestDatastore::new(stored);
let removed =
futures::executor::block_on(app_log_entries_prune(&datastore, &key_maps, 2))
- .expect("prune");
+ .expect("prune");
assert_eq!(removed, 1);
assert_eq!(datastore.len(), 2);
}
+
+ #[test]
+ fn log_buffer_flush_stores_entries() {
+ let _guard = LOG_TEST_LOCK.lock().unwrap_or_else(|err| err.into_inner());
+ let _ = app_log_buffer_drain();
+ let key_maps = app_key_maps_default();
+ let datastore = TestDatastore::new(Vec::new());
+ app_log_buffer_push(app_log_entry_new(
+ AppLogLevel::Info,
+ "log.code.flush",
+ "flush",
+ None,
+ ));
+ let stored = futures::executor::block_on(app_log_buffer_flush(&datastore, &key_maps))
+ .expect("flush");
+ assert_eq!(stored, 1);
+ assert_eq!(datastore.len(), 1);
+ }
}