milestone: enforce required services and add mongo logger PoC path

This commit is contained in:
2026-04-06 18:16:48 -07:00
parent dd04fb5168
commit 836a968806
17 changed files with 1081 additions and 112 deletions

182
src/brokers/logger_store.rs Normal file
View File

@@ -0,0 +1,182 @@
use std::collections::HashMap;
use std::sync::OnceLock;
use futures_lite::StreamExt;
use mongodb::Client;
use mongodb::bson::{self, Document, doc};
use serde_json::Value;
use crate::config::RecNodeConfig;
const LOGGER_COLLECTION_NAME: &str = "msLogs";
struct LoggerMongoStore {
client: Client,
db_name: String,
}
static LOGGER_STORE: OnceLock<LoggerMongoStore> = OnceLock::new();
pub fn is_logger_template(template: &str) -> bool {
matches!(
template.to_ascii_lowercase().as_str(),
"logger" | "log" | "mst_logger" | "mst_logger_rec"
)
}
pub async fn init_from_rec_services(
rec_services: &HashMap<String, RecNodeConfig>,
env_name: &str,
) -> Result<(), String> {
if LOGGER_STORE.get().is_some() {
return Ok(());
}
let rec_node = rec_services
.get("admin")
.or_else(|| rec_services.get("app_server"))
.or_else(|| rec_services.values().next())
.ok_or_else(|| "rec_services is empty; cannot initialize logger store".to_string())?;
let is_dev = matches!(env_name, "development" | "dev");
let auth_uri = format!(
"mongodb://{}:{}@{}:{}/?authSource={}",
rec_node.user,
rec_node.pass,
rec_node.host,
rec_node.port,
rec_node.database,
);
let unauth_uri = format!("mongodb://{}:{}/", rec_node.host, rec_node.port);
let client = if rec_node.user.trim().is_empty() {
connect_and_ping(&unauth_uri, &rec_node.database).await?
} else {
match connect_and_ping(&auth_uri, &rec_node.database).await {
Ok(client) => client,
Err(auth_err) if is_dev => {
tracing::warn!(
"Mongo authenticated init failed in dev, retrying unauthenticated: {}",
auth_err
);
connect_and_ping(&unauth_uri, &rec_node.database).await?
}
Err(auth_err) => return Err(auth_err),
}
};
// In development, bootstrap the target DB if it does not exist yet.
if matches!(env_name, "development" | "dev") {
let db_names = client
.list_database_names()
.await
.map_err(|e| format!("Mongo list databases failed: {}", e))?;
if !db_names.iter().any(|name| name == &rec_node.database) {
let bootstrap_doc = doc! {
"_beds_bootstrap": true,
"created": epoch_secs(),
};
client
.database(&rec_node.database)
.collection::<Document>(LOGGER_COLLECTION_NAME)
.insert_one(bootstrap_doc)
.await
.map_err(|e| format!("Mongo dev DB bootstrap insert failed: {}", e))?;
client
.database(&rec_node.database)
.collection::<Document>(LOGGER_COLLECTION_NAME)
.delete_one(doc! { "_beds_bootstrap": true })
.await
.map_err(|e| format!("Mongo dev DB bootstrap cleanup failed: {}", e))?;
}
}
let _ = LOGGER_STORE.set(LoggerMongoStore {
client,
db_name: rec_node.database.clone(),
});
Ok(())
}
pub async fn append_log(mut data: HashMap<String, Value>) -> Result<String, String> {
let store = LOGGER_STORE
.get()
.ok_or_else(|| "logger store is not initialized".to_string())?;
let token = data
.get("db_token")
.and_then(|v| v.as_str())
.map(ToString::to_string)
.unwrap_or_else(|| format!("log-{}", epoch_secs()));
data.entry("db_token".to_string())
.or_insert_with(|| Value::String(token.clone()));
data.entry("created".to_string())
.or_insert_with(|| Value::from(epoch_secs() as i64));
let doc = bson::to_document(&data)
.map_err(|e| format!("logger write encode failed: {}", e))?;
store
.client
.database(&store.db_name)
.collection::<Document>(LOGGER_COLLECTION_NAME)
.insert_one(doc)
.await
.map_err(|e| format!("logger write failed: {}", e))?;
Ok(token)
}
pub async fn fetch_recent(limit: usize) -> Result<Vec<Value>, String> {
let store = LOGGER_STORE
.get()
.ok_or_else(|| "logger store is not initialized".to_string())?;
let mut cursor = store
.client
.database(&store.db_name)
.collection::<Document>(LOGGER_COLLECTION_NAME)
.find(doc! {})
.await
.map_err(|e| format!("logger fetch failed: {}", e))?;
let mut out = Vec::new();
while let Some(next) = cursor.next().await {
let doc = next.map_err(|e| format!("logger fetch cursor error: {}", e))?;
let json = serde_json::to_value(&doc)
.map_err(|e| format!("logger fetch decode failed: {}", e))?;
out.push(json);
}
out.reverse();
out.truncate(limit);
Ok(out)
}
fn epoch_secs() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64
}
async fn connect_and_ping(uri: &str, database: &str) -> Result<Client, String> {
let client = Client::with_uri_str(uri)
.await
.map_err(|e| format!("Mongo logger client init failed: {}", e))?;
client
.database(database)
.run_command(doc! { "ping": 1 })
.await
.map_err(|e| format!("Mongo logger ping failed: {}", e))?;
Ok(client)
}

View File

@@ -21,6 +21,7 @@
//! * `2026-04-05` - mks - original coding
pub mod error;
pub mod logger_store;
pub mod payload;
pub mod r_broker;
pub mod w_broker;

View File

@@ -44,6 +44,8 @@ use lapin::{
};
use crate::services::amqp::EXCHANGE_NAME;
use crate::brokers::logger_store;
use crate::brokers::payload::BrokerPayload;
use super::error::BrokerError;
/// Routing key this broker binds to.
@@ -167,7 +169,7 @@ async fn run(
tracing::info!("rBroker[{}] shutdown event received — exiting", instance_id);
break;
}
"fetch" => handle_fetch(&delivery.data, instance_id),
"fetch" => handle_fetch(&delivery.data, instance_id).await,
unknown => {
tracing::warn!("rBroker[{}] unknown event type '{}'", instance_id, unknown);
None
@@ -229,12 +231,56 @@ fn handle_ping(instance_id: u32) -> Option<Vec<u8>> {
/// # History
///
/// * `2026-04-05` - mks - stub
fn handle_fetch(data: &[u8], instance_id: u32) -> Option<Vec<u8>> {
tracing::warn!(
"rBroker[{}] fetch event received ({} bytes) — factory dispatch not yet implemented",
instance_id,
data.len()
);
let response = r#"{"status":"error","code":"NOT_IMPLEMENTED","message":"factory dispatch not yet implemented"}"#;
Some(response.as_bytes().to_vec())
async fn handle_fetch(data: &[u8], instance_id: u32) -> Option<Vec<u8>> {
let payload: BrokerPayload = match serde_json::from_slice(data) {
Ok(p) => p,
Err(e) => {
let response = serde_json::json!({
"status": "error",
"code": "INVALID_PAYLOAD",
"message": format!("invalid JSON payload: {}", e),
});
return Some(response.to_string().into_bytes());
}
};
if !logger_store::is_logger_template(&payload.template) {
tracing::warn!(
"rBroker[{}] fetch template '{}' not implemented yet",
instance_id,
payload.template
);
let response = serde_json::json!({
"status": "error",
"code": "NOT_IMPLEMENTED",
"message": "only logger fetch is implemented in PoC step 1",
});
return Some(response.to_string().into_bytes());
}
let limit = payload
.data
.get("limit")
.and_then(|v| v.as_u64())
.map(|n| n as usize)
.unwrap_or(50);
let logs = match logger_store::fetch_recent(limit).await {
Ok(items) => items,
Err(e) => {
let response = serde_json::json!({
"status": "error",
"code": "LOGGER_STORE_UNAVAILABLE",
"message": e,
});
return Some(response.to_string().into_bytes());
}
};
let response = serde_json::json!({
"status": "ok",
"code": "LOGGER_FETCH",
"count": logs.len(),
"logs": logs,
});
Some(response.to_string().into_bytes())
}

View File

@@ -44,6 +44,8 @@ use lapin::{
};
use crate::services::amqp::EXCHANGE_NAME;
use crate::brokers::logger_store;
use crate::brokers::payload::BrokerPayload;
use super::error::BrokerError;
/// Routing key this broker binds to.
@@ -156,7 +158,7 @@ async fn run(
tracing::info!("wBroker[{}] shutdown event received — exiting", instance_id);
break;
}
"write" => handle_write(&delivery.data, instance_id),
"write" => handle_write(&delivery.data, instance_id).await,
"update" => handle_update(&delivery.data, instance_id),
"delete" => handle_delete(&delivery.data, instance_id),
unknown => {
@@ -218,12 +220,46 @@ fn handle_ping(instance_id: u32) -> Option<Vec<u8>> {
/// # History
///
/// * `2026-04-05` - mks - stub
fn handle_write(data: &[u8], instance_id: u32) -> Option<Vec<u8>> {
tracing::warn!(
"wBroker[{}] write event ({} bytes) — factory dispatch not yet implemented",
instance_id, data.len()
);
not_implemented_response()
async fn handle_write(data: &[u8], instance_id: u32) -> Option<Vec<u8>> {
let payload: BrokerPayload = match serde_json::from_slice(data) {
Ok(p) => p,
Err(e) => {
let response = serde_json::json!({
"status": "error",
"code": "INVALID_PAYLOAD",
"message": format!("invalid JSON payload: {}", e),
});
return Some(response.to_string().into_bytes());
}
};
if !logger_store::is_logger_template(&payload.template) {
tracing::warn!(
"wBroker[{}] write template '{}' not implemented yet",
instance_id,
payload.template
);
return not_implemented_response();
}
let token = match logger_store::append_log(payload.data).await {
Ok(token) => token,
Err(e) => {
let response = serde_json::json!({
"status": "error",
"code": "LOGGER_STORE_UNAVAILABLE",
"message": e,
});
return Some(response.to_string().into_bytes());
}
};
let response = serde_json::json!({
"status": "ok",
"code": "LOGGER_WRITE",
"token": token,
});
Some(response.to_string().into_bytes())
}
/// Stub handler for `update` events.