use crate::adapters::{LogDirectives, LoggingSetupBuilder, StandardLogAdapter, WasmStdoutAdapter};
use crate::domain::{
EnhancedContextEnricher, LogKvExtractor, ProcessorChain, StructuredFieldsProcessor,
TimestampProcessor,
};
use crate::error::{ObservabilityError, ObservabilityResult};
use crate::ports::{StandardLoggingPort, TransportPort};
use crate::traits::LogLevel;
use serde::{Deserialize, Serialize};
use std::sync::{Arc, OnceLock};
struct ForwardingLogger;
static FORWARDING_LOGGER: ForwardingLogger = ForwardingLogger;
static FORWARDING_ADAPTER: OnceLock<Arc<StandardLogAdapter>> = OnceLock::new();
static LOGGER_REGISTRATION: OnceLock<LoggerRegistration> = OnceLock::new();
enum LoggerRegistration {
InstalledByProxy,
AlreadySet(String),
}
impl log::Log for ForwardingLogger {
fn enabled(&self, metadata: &log::Metadata) -> bool {
FORWARDING_ADAPTER
.get()
.is_some_and(|adapter| log::Log::enabled(adapter.as_ref(), metadata))
}
fn log(&self, record: &log::Record) {
if let Some(adapter) = FORWARDING_ADAPTER.get() {
log::Log::log(adapter.as_ref(), record);
}
}
fn flush(&self) {
if let Some(adapter) = FORWARDING_ADAPTER.get() {
log::Log::flush(adapter.as_ref());
}
}
}
fn install_forwarding_logger() -> &'static LoggerRegistration {
LOGGER_REGISTRATION.get_or_init(|| match log::set_logger(&FORWARDING_LOGGER) {
Ok(()) => LoggerRegistration::InstalledByProxy,
Err(error) => LoggerRegistration::AlreadySet(error.to_string()),
})
}
pub struct GlobalLoggerSingleton {
adapter: Arc<StandardLogAdapter>,
config: ObservabilityConfig,
}
impl GlobalLoggerSingleton {
pub fn get_or_init(
config: ObservabilityConfig,
) -> ObservabilityResult<&'static GlobalLoggerSingleton> {
static INSTANCE: OnceLock<Result<GlobalLoggerSingleton, String>> = OnceLock::new();
match INSTANCE.get_or_init(|| Self::create_instance(config).map_err(|e| e.to_string())) {
Ok(instance) => Ok(instance),
Err(error) => Err(ObservabilityError::logging(format!(
"Singleton initialization failed: {}",
error
))),
}
}
fn create_instance(config: ObservabilityConfig) -> ObservabilityResult<GlobalLoggerSingleton> {
let directives = config.parse_directives();
let transport = config.create_transport();
let processor_chain = if config.structured {
let mut enricher = EnhancedContextEnricher::new();
if config.context_enrichment && !config.default_context.is_empty() {
for (k, v) in &config.default_context {
enricher = enricher.with_field(k.clone(), v.clone());
}
}
ProcessorChain::new()
.add_processor(Box::new(TimestampProcessor))
.add_processor(Box::new(LogKvExtractor::new()))
.add_processor(Box::new(enricher))
.add_processor(Box::new(StructuredFieldsProcessor))
} else {
ProcessorChain::new()
};
let adapter = LoggingSetupBuilder::new()
.with_processor_chain(processor_chain)
.with_transport(transport)
.with_directives(directives)
.build()?;
let adapter_arc = Arc::new(adapter);
match install_forwarding_logger() {
LoggerRegistration::InstalledByProxy => {
let _ = FORWARDING_ADAPTER.set(adapter_arc.clone());
}
LoggerRegistration::AlreadySet(error) => {
eprintln!("Global Rust logger already initialized: {}", error);
}
}
adapter_arc.initialize()?;
log::info!(
"🔍 Global logger singleton initialized: Standard Rust logging is now structured"
);
Ok(GlobalLoggerSingleton {
adapter: adapter_arc,
config,
})
}
pub fn adapter(&self) -> &Arc<StandardLogAdapter> {
&self.adapter
}
pub fn config(&self) -> &ObservabilityConfig {
&self.config
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ObservabilityConfig {
pub level: String,
pub format: String,
pub structured: bool,
pub context_enrichment: bool,
#[serde(default)]
pub default_context: std::collections::HashMap<String, serde_json::Value>,
}
impl Default for ObservabilityConfig {
fn default() -> Self {
Self {
level: "info".to_string(),
format: "compact".to_string(),
structured: true,
context_enrichment: true,
default_context: std::collections::HashMap::new(),
}
}
}
impl ObservabilityConfig {
pub fn parse_level(&self) -> ObservabilityResult<LogLevel> {
let global = self.parse_directives().global_level();
Ok(global)
}
pub fn parse_directives(&self) -> LogDirectives {
LogDirectives::parse(&self.level)
}
pub fn create_transport(&self) -> Arc<dyn TransportPort> {
match self.format.as_str() {
"json" => Arc::new(WasmStdoutAdapter::with_json_formatter()),
"plain" => Arc::new(WasmStdoutAdapter::with_plain_text_formatter()),
_ => Arc::new(WasmStdoutAdapter::with_compact_formatter()),
}
}
pub fn validate(&self) -> ObservabilityResult<()> {
let directives = self.parse_directives();
let has_any_valid = !self.level.is_empty()
&& self.level.split(',').any(|p| {
let p = p.trim();
if p.contains('=') {
let (_, lvl) = p.split_once('=').unwrap();
LogDirectives::str_to_level(lvl.trim()).is_some()
} else {
LogDirectives::str_to_level(p).is_some()
}
});
if !has_any_valid {
return Err(ObservabilityError::configuration(format!(
"Invalid log level: '{}'. Expected e.g. 'info' or 'info,crate_name=debug'",
self.level
)));
}
let _ = directives;
match self.format.as_str() {
"json" | "compact" | "plain" => {}
_ => {
return Err(ObservabilityError::configuration(format!(
"Invalid format: {}. Must be 'json', 'compact', or 'plain'",
self.format
)));
}
}
Ok(())
}
}
pub struct ObservabilityManager {
config: ObservabilityConfig,
singleton_ref: &'static GlobalLoggerSingleton,
}
impl Default for ObservabilityManager {
fn default() -> Self {
let config = ObservabilityConfig::default();
Self::new(config).expect("Failed to create default observability manager")
}
}
impl ObservabilityManager {
pub fn new(config: ObservabilityConfig) -> ObservabilityResult<Self> {
config.validate()?;
let singleton_ref = GlobalLoggerSingleton::get_or_init(config.clone())?;
Ok(Self {
config,
singleton_ref,
})
}
pub fn initialize(&mut self) -> ObservabilityResult<()> {
log::info!("🔍 Observability manager initialized: Using singleton global logger");
Ok(())
}
pub fn config(&self) -> &ObservabilityConfig {
&self.config
}
pub fn is_enabled(&self, level: LogLevel) -> bool {
StandardLoggingPort::enabled(self.singleton_ref.adapter().as_ref(), &level)
}
pub fn global_logger() -> Option<Arc<StandardLogAdapter>> {
GlobalLoggerSingleton::get_or_init(ObservabilityConfig::default())
.ok()
.map(|singleton| singleton.adapter().clone())
}
pub fn capabilities(&self) -> Vec<String> {
let mut caps = vec![
"structured_logging".to_string(),
"standard_rust_logging".to_string(),
"context_enrichment".to_string(),
];
if self.config.structured {
caps.push("json_output".to_string());
}
caps.push(format!("log_level_{}", self.config.level));
caps.push(format!("format_{}", self.config.format));
caps
}
}
pub fn create_observability_manager(
config: Option<serde_json::Value>,
) -> ObservabilityResult<ObservabilityManager> {
let obs_config = match config {
Some(value) => serde_json::from_value(value).map_err(|e| {
ObservabilityError::configuration(format!("Invalid observability config: {}", e))
})?,
None => ObservabilityConfig::default(),
};
ObservabilityManager::new(obs_config)
}
pub mod convenience {
use super::*;
pub fn log_with_fields(
level: LogLevel,
message: &str,
fields: serde_json::Value,
) -> ObservabilityResult<()> {
if let Some(logger) = ObservabilityManager::global_logger() {
if StandardLoggingPort::enabled(logger.as_ref(), &level) {
let entry = crate::domain::create_log_entry(level, message, fields);
logger.process_standard_log(entry)?;
}
}
Ok(())
}
pub fn add_log_context(key: &str, value: serde_json::Value) {
log::debug!("Adding log context: {} = {}", key, value);
}
pub fn info_with_fields(message: &str, fields: serde_json::Value) -> ObservabilityResult<()> {
log_with_fields(LogLevel::Info, message, fields)
}
pub fn error_with_fields(message: &str, fields: serde_json::Value) -> ObservabilityResult<()> {
log_with_fields(LogLevel::Error, message, fields)
}
pub fn debug_with_fields(message: &str, fields: serde_json::Value) -> ObservabilityResult<()> {
log_with_fields(LogLevel::Debug, message, fields)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_observability_config_default() {
let config = ObservabilityConfig::default();
assert_eq!(config.level, "info");
assert_eq!(config.format, "compact");
assert!(config.structured);
}
#[test]
fn test_config_parse_level() {
let config = ObservabilityConfig {
level: "debug".to_string(),
..Default::default()
};
assert!(matches!(config.parse_level().unwrap(), LogLevel::Debug));
}
#[test]
fn test_manager_creation() {
let config = ObservabilityConfig::default();
let manager = ObservabilityManager::new(config);
assert!(manager.is_ok());
}
#[test]
fn test_manager_capabilities() {
let config = ObservabilityConfig::default();
let manager = ObservabilityManager::new(config).unwrap();
let caps = manager.capabilities();
assert!(caps.contains(&"structured_logging".to_string()));
assert!(caps.contains(&"standard_rust_logging".to_string()));
assert!(caps.contains(&"log_level_info".to_string()));
}
#[test]
fn test_manager_default() {
let manager = ObservabilityManager::default();
assert_eq!(manager.config.level, "info");
}
#[test]
fn test_config_validation() {
let config = ObservabilityConfig {
level: "invalid".to_string(),
..Default::default()
};
assert!(config.validate().is_err());
let config = ObservabilityConfig {
format: "invalid".to_string(),
..Default::default()
};
assert!(config.validate().is_err());
}
}