milestone: enforce required services and add mongo logger PoC path
This commit is contained in:
182
src/brokers/logger_store.rs
Normal file
182
src/brokers/logger_store.rs
Normal 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)
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user