use std::fmt::Debug;
use std::sync::Mutex;
use chrono::Utc;
use once_cell::sync::Lazy;
use serde::Serialize;
use serde_json::json;
use crate::console::client_info::ClientInfo;
use crate::console::uri::normalize_uri;
use crate::domain::LogType;
use crate::infrastructure::WsConnection;
static INSTANCE: Lazy<Mutex<WebConsole>> = Lazy::new(|| Mutex::new(WebConsole::new()));
pub(crate) struct WebConsole {
uri: String,
is_enable: bool,
ws: Option<WsConnection>,
current_group: Vec<String>,
client_info: ClientInfo,
log_listener: Option<Box<dyn Fn(&str, LogType) + Send>>,
}
impl WebConsole {
fn new() -> Self {
WebConsole {
uri: normalize_uri(None),
is_enable: true,
ws: None,
current_group: Vec::new(),
client_info: ClientInfo::new(),
log_listener: None,
}
}
pub(crate) fn instance() -> std::sync::MutexGuard<'static, WebConsole> {
INSTANCE.lock().expect("WebConsole mutex poisoned")
}
pub(crate) fn set_uri(&mut self, uri: &str) {
self.uri = normalize_uri(Some(uri));
if let Some(ref mut ws) = self.ws {
ws.close();
}
self.ws = None;
}
pub(crate) fn set_enable(&mut self, enable: bool) {
self.is_enable = enable;
}
pub(crate) fn is_enable(&self) -> bool {
self.is_enable
}
pub(crate) fn uri(&self) -> &str {
&self.uri
}
pub(crate) fn set_client_info(&mut self, info: ClientInfo) {
self.client_info = info;
}
pub(crate) fn set_log_listener<F>(&mut self, listener: F)
where
F: Fn(&str, LogType) + Send + 'static,
{
self.log_listener = Some(Box::new(listener));
}
pub(crate) fn clear_log_listener(&mut self) {
self.log_listener = None;
}
pub(crate) fn push_group(&mut self, label: &str) {
self.current_group.push(label.to_string());
}
pub(crate) fn pop_group(&mut self) -> bool {
self.current_group.pop().is_some()
}
pub(crate) fn send_log<T: Debug + Serialize>(&mut self, log_type: LogType, args: &[T]) {
let values: Vec<serde_json::Value> = args
.iter()
.map(|a| serde_json::to_value(a).unwrap_or(serde_json::Value::Null))
.collect();
self.send_log_values(log_type, values);
}
pub(crate) fn send_log_values(&mut self, log_type: LogType, args: Vec<serde_json::Value>) {
if !self.is_enable {
return;
}
let payload_data = json!({
"clientInfo": self.client_info,
"data": args,
});
let payload_data_str = payload_data.to_string();
if let Some(ref listener) = self.log_listener {
listener(&payload_data_str, log_type);
}
let request = json!({
"timestamp": Utc::now().timestamp_millis(),
"logType": log_type.as_str(),
"language": "rust",
"secure": false,
"payload": {
"data": payload_data_str,
},
});
let message = request.to_string();
if self.ws.is_none() {
self.connect();
}
if let Some(ref mut ws) = self.ws {
if let Err(e) = ws.send_text(&message) {
eprintln!("[nconsole] Failed to send log: {}", e);
self.ws = None;
}
}
}
fn connect(&mut self) {
match WsConnection::connect(&self.uri) {
Ok(ws) => self.ws = Some(ws),
Err(e) => {
eprintln!("[nconsole] WebSocket connection failed: {}", e);
self.ws = None;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_web_console_default_state() {
let console = WebConsole::new();
assert!(console.is_enable);
assert_eq!(console.uri, "ws://localhost:9090");
assert!(console.ws.is_none());
assert!(console.current_group.is_empty());
}
#[test]
fn test_set_uri() {
let mut console = WebConsole::new();
console.set_uri("10.10.30.40");
assert_eq!(console.uri, "ws://10.10.30.40:9090");
}
#[test]
fn test_set_enable() {
let mut console = WebConsole::new();
assert!(console.is_enable());
console.set_enable(false);
assert!(!console.is_enable());
console.set_enable(true);
assert!(console.is_enable());
}
#[test]
fn test_group_stack() {
let mut console = WebConsole::new();
console.push_group("Group A");
console.push_group("Group B");
assert_eq!(console.current_group.len(), 2);
assert!(console.pop_group());
assert_eq!(console.current_group.len(), 1);
assert_eq!(console.current_group[0], "Group A");
assert!(console.pop_group());
assert!(console.current_group.is_empty());
assert!(!console.pop_group());
}
#[test]
fn test_send_log_when_disabled() {
let mut console = WebConsole::new();
console.set_enable(false);
console.send_log(LogType::Log, &["test message"]);
assert!(console.ws.is_none());
}
#[test]
fn test_set_client_info() {
let mut console = WebConsole::new();
let custom_info = ClientInfo {
id: "custom-id".to_string(),
name: "Custom Client".to_string(),
platform: "custom".to_string(),
version: "0.0.1".to_string(),
os: "test-os".to_string(),
os_version: "1.0".to_string(),
language: "vi-VN".to_string(),
time_zone: "+07:00".to_string(),
user_agent: "test-agent".to_string(),
debug: true,
};
console.set_client_info(custom_info.clone());
assert_eq!(console.client_info.id, "custom-id");
assert_eq!(console.client_info.name, "Custom Client");
}
#[test]
fn test_log_listener() {
use std::sync::{Arc, Mutex as StdMutex};
let received = Arc::new(StdMutex::new(Vec::new()));
let received_clone = received.clone();
let mut console = WebConsole::new();
console.set_enable(true);
console.set_log_listener(move |data, log_type| {
received_clone
.lock()
.unwrap()
.push((data.to_string(), log_type));
});
console.send_log(LogType::Info, &["test data"]);
let messages = received.lock().unwrap();
assert_eq!(messages.len(), 1);
assert_eq!(messages[0].1, LogType::Info);
assert!(messages[0].0.contains("test data"));
}
}