Skip to main content

observability_core/
extension.rs

1//! Core observability components for structured logging
2//!
3//! This module provides core observability functionality including configuration,
4//! global logger singleton, and structured logging integration without external dependencies.
5
6use crate::adapters::{LogDirectives, LoggingSetupBuilder, StandardLogAdapter, WasmStdoutAdapter};
7use crate::domain::{
8    EnhancedContextEnricher, LogKvExtractor, ProcessorChain, StructuredFieldsProcessor,
9    TimestampProcessor,
10};
11use crate::error::{ObservabilityError, ObservabilityResult};
12use crate::ports::{StandardLoggingPort, TransportPort};
13use crate::traits::LogLevel;
14use serde::{Deserialize, Serialize};
15use std::sync::{Arc, OnceLock};
16
17struct ForwardingLogger;
18
19static FORWARDING_LOGGER: ForwardingLogger = ForwardingLogger;
20static FORWARDING_ADAPTER: OnceLock<Arc<StandardLogAdapter>> = OnceLock::new();
21static LOGGER_REGISTRATION: OnceLock<LoggerRegistration> = OnceLock::new();
22
23enum LoggerRegistration {
24    InstalledByProxy,
25    AlreadySet(String),
26}
27
28impl log::Log for ForwardingLogger {
29    fn enabled(&self, metadata: &log::Metadata) -> bool {
30        FORWARDING_ADAPTER
31            .get()
32            .is_some_and(|adapter| log::Log::enabled(adapter.as_ref(), metadata))
33    }
34
35    fn log(&self, record: &log::Record) {
36        if let Some(adapter) = FORWARDING_ADAPTER.get() {
37            log::Log::log(adapter.as_ref(), record);
38        }
39    }
40
41    fn flush(&self) {
42        if let Some(adapter) = FORWARDING_ADAPTER.get() {
43            log::Log::flush(adapter.as_ref());
44        }
45    }
46}
47
48fn install_forwarding_logger() -> &'static LoggerRegistration {
49    LOGGER_REGISTRATION.get_or_init(|| match log::set_logger(&FORWARDING_LOGGER) {
50        Ok(()) => LoggerRegistration::InstalledByProxy,
51        Err(error) => LoggerRegistration::AlreadySet(error.to_string()),
52    })
53}
54
55/// Singleton global logger that initializes once and is shared across all extension instances
56pub struct GlobalLoggerSingleton {
57    adapter: Arc<StandardLogAdapter>,
58    config: ObservabilityConfig,
59}
60
61impl GlobalLoggerSingleton {
62    /// Get or create the singleton instance
63    ///
64    /// This is thread-safe and will initialize exactly once
65    pub fn get_or_init(
66        config: ObservabilityConfig,
67    ) -> ObservabilityResult<&'static GlobalLoggerSingleton> {
68        static INSTANCE: OnceLock<Result<GlobalLoggerSingleton, String>> = OnceLock::new();
69
70        match INSTANCE.get_or_init(|| Self::create_instance(config).map_err(|e| e.to_string())) {
71            Ok(instance) => Ok(instance),
72            Err(error) => Err(ObservabilityError::logging(format!(
73                "Singleton initialization failed: {}",
74                error
75            ))),
76        }
77    }
78
79    /// Create the singleton instance (called exactly once)
80    fn create_instance(config: ObservabilityConfig) -> ObservabilityResult<GlobalLoggerSingleton> {
81        let directives = config.parse_directives();
82        let transport = config.create_transport();
83        let processor_chain = if config.structured {
84            let mut enricher = EnhancedContextEnricher::new();
85            if config.context_enrichment && !config.default_context.is_empty() {
86                for (k, v) in &config.default_context {
87                    enricher = enricher.with_field(k.clone(), v.clone());
88                }
89            }
90
91            ProcessorChain::new()
92                .add_processor(Box::new(TimestampProcessor))
93                .add_processor(Box::new(LogKvExtractor::new()))
94                .add_processor(Box::new(enricher))
95                .add_processor(Box::new(StructuredFieldsProcessor))
96        } else {
97            ProcessorChain::new()
98        };
99
100        let adapter = LoggingSetupBuilder::new()
101            .with_processor_chain(processor_chain)
102            .with_transport(transport)
103            .with_directives(directives)
104            .build()?;
105
106        let adapter_arc = Arc::new(adapter);
107
108        match install_forwarding_logger() {
109            LoggerRegistration::InstalledByProxy => {
110                let _ = FORWARDING_ADAPTER.set(adapter_arc.clone());
111            }
112            LoggerRegistration::AlreadySet(error) => {
113                eprintln!("Global Rust logger already initialized: {}", error);
114            }
115        }
116
117        // Initialize the adapter
118        adapter_arc.initialize()?;
119
120        log::info!(
121            "🔍 Global logger singleton initialized: Standard Rust logging is now structured"
122        );
123
124        Ok(GlobalLoggerSingleton {
125            adapter: adapter_arc,
126            config,
127        })
128    }
129
130    /// Get the logger adapter
131    pub fn adapter(&self) -> &Arc<StandardLogAdapter> {
132        &self.adapter
133    }
134
135    /// Get the configuration used
136    pub fn config(&self) -> &ObservabilityConfig {
137        &self.config
138    }
139}
140
141/// Configuration for observability
142#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
143pub struct ObservabilityConfig {
144    /// Minimum log level to process
145    pub level: String, // "error", "warn", "info", "debug", "trace"
146
147    /// Output format: "json", "compact", "plain"
148    pub format: String,
149
150    /// Enable structured logging features
151    pub structured: bool,
152
153    /// Enable context enrichment
154    pub context_enrichment: bool,
155
156    /// Additional context fields to always include
157    #[serde(default)]
158    pub default_context: std::collections::HashMap<String, serde_json::Value>,
159}
160
161impl Default for ObservabilityConfig {
162    fn default() -> Self {
163        Self {
164            level: "info".to_string(),
165            format: "compact".to_string(),
166            structured: true,
167            context_enrichment: true,
168            default_context: std::collections::HashMap::new(),
169        }
170    }
171}
172
173impl ObservabilityConfig {
174    /// Parse log level from string (global level only, ignores per-crate directives).
175    pub fn parse_level(&self) -> ObservabilityResult<LogLevel> {
176        let global = self.parse_directives().global_level();
177        Ok(global)
178    }
179
180    /// Parse `RUST_LOG`-style directives from the level string.
181    ///
182    /// Accepts both simple levels (`"info"`) and per-crate directives
183    /// (`"info,agent_sdk=debug,a2a_protocol_core=trace"`).
184    pub fn parse_directives(&self) -> LogDirectives {
185        LogDirectives::parse(&self.level)
186    }
187
188    /// Create transport based on format configuration
189    pub fn create_transport(&self) -> Arc<dyn TransportPort> {
190        match self.format.as_str() {
191            "json" => Arc::new(WasmStdoutAdapter::with_json_formatter()),
192            "plain" => Arc::new(WasmStdoutAdapter::with_plain_text_formatter()),
193            _ => Arc::new(WasmStdoutAdapter::with_compact_formatter()),
194        }
195    }
196
197    /// Validate configuration
198    pub fn validate(&self) -> ObservabilityResult<()> {
199        // Validate level string: must contain at least one valid level token
200        let directives = self.parse_directives();
201        let has_any_valid = !self.level.is_empty()
202            && self.level.split(',').any(|p| {
203                let p = p.trim();
204                if p.contains('=') {
205                    let (_, lvl) = p.split_once('=').unwrap();
206                    LogDirectives::str_to_level(lvl.trim()).is_some()
207                } else {
208                    LogDirectives::str_to_level(p).is_some()
209                }
210            });
211        if !has_any_valid {
212            return Err(ObservabilityError::configuration(format!(
213                "Invalid log level: '{}'. Expected e.g. 'info' or 'info,crate_name=debug'",
214                self.level
215            )));
216        }
217        let _ = directives;
218
219        // Validate format
220        match self.format.as_str() {
221            "json" | "compact" | "plain" => {}
222            _ => {
223                return Err(ObservabilityError::configuration(format!(
224                    "Invalid format: {}. Must be 'json', 'compact', or 'plain'",
225                    self.format
226                )));
227            }
228        }
229
230        Ok(())
231    }
232}
233
234/// Core observability manager
235///
236/// This provides structured logging integration with standard Rust logging macros
237/// (log::info!, log::debug!, etc.) without external extension system dependencies
238pub struct ObservabilityManager {
239    config: ObservabilityConfig,
240    singleton_ref: &'static GlobalLoggerSingleton,
241}
242
243impl Default for ObservabilityManager {
244    fn default() -> Self {
245        let config = ObservabilityConfig::default();
246        Self::new(config).expect("Failed to create default observability manager")
247    }
248}
249
250impl ObservabilityManager {
251    /// Create a new observability manager with configuration
252    pub fn new(config: ObservabilityConfig) -> ObservabilityResult<Self> {
253        // Validate configuration first
254        config.validate()?;
255
256        // Get or initialize the singleton
257        let singleton_ref = GlobalLoggerSingleton::get_or_init(config.clone())?;
258
259        Ok(Self {
260            config,
261            singleton_ref,
262        })
263    }
264
265    /// Initialize global logging (convenience method)
266    pub fn initialize(&mut self) -> ObservabilityResult<()> {
267        // Initialization already happened in the singleton
268        log::info!("🔍 Observability manager initialized: Using singleton global logger");
269        Ok(())
270    }
271
272    /// Get current configuration
273    pub fn config(&self) -> &ObservabilityConfig {
274        &self.config
275    }
276
277    /// Check if logging is enabled for a level
278    pub fn is_enabled(&self, level: LogLevel) -> bool {
279        StandardLoggingPort::enabled(self.singleton_ref.adapter().as_ref(), &level)
280    }
281
282    /// Get the global logger instance (always available with singleton pattern)
283    pub fn global_logger() -> Option<Arc<StandardLogAdapter>> {
284        // With singleton pattern, try to get the instance with default config
285        GlobalLoggerSingleton::get_or_init(ObservabilityConfig::default())
286            .ok()
287            .map(|singleton| singleton.adapter().clone())
288    }
289
290    /// Get capabilities as strings
291    pub fn capabilities(&self) -> Vec<String> {
292        let mut caps = vec![
293            "structured_logging".to_string(),
294            "standard_rust_logging".to_string(),
295            "context_enrichment".to_string(),
296        ];
297
298        if self.config.structured {
299            caps.push("json_output".to_string());
300        }
301
302        caps.push(format!("log_level_{}", self.config.level));
303        caps.push(format!("format_{}", self.config.format));
304
305        caps
306    }
307}
308
309/// Factory function for creating observability manager from JSON configuration
310pub fn create_observability_manager(
311    config: Option<serde_json::Value>,
312) -> ObservabilityResult<ObservabilityManager> {
313    let obs_config = match config {
314        Some(value) => serde_json::from_value(value).map_err(|e| {
315            ObservabilityError::configuration(format!("Invalid observability config: {}", e))
316        })?,
317        None => ObservabilityConfig::default(),
318    };
319
320    ObservabilityManager::new(obs_config)
321}
322
323/// Convenience functions for common logging patterns
324///
325/// These are optional - users can still use standard log::info! etc.
326/// But these provide some additional structured logging capabilities
327pub mod convenience {
328    use super::*;
329
330    /// Log with additional structured fields
331    pub fn log_with_fields(
332        level: LogLevel,
333        message: &str,
334        fields: serde_json::Value,
335    ) -> ObservabilityResult<()> {
336        if let Some(logger) = ObservabilityManager::global_logger() {
337            if StandardLoggingPort::enabled(logger.as_ref(), &level) {
338                let entry = crate::domain::create_log_entry(level, message, fields);
339                logger.process_standard_log(entry)?;
340            }
341        }
342        Ok(())
343    }
344
345    /// Add context that will be included in all subsequent log entries
346    /// (This would integrate with a context adapter)
347    pub fn add_log_context(key: &str, value: serde_json::Value) {
348        // TODO: Integrate with ContextPort implementation
349        // For now, this is a placeholder for future context integration
350        log::debug!("Adding log context: {} = {}", key, value);
351    }
352
353    /// Log structured info with fields
354    pub fn info_with_fields(message: &str, fields: serde_json::Value) -> ObservabilityResult<()> {
355        log_with_fields(LogLevel::Info, message, fields)
356    }
357
358    /// Log structured error with fields
359    pub fn error_with_fields(message: &str, fields: serde_json::Value) -> ObservabilityResult<()> {
360        log_with_fields(LogLevel::Error, message, fields)
361    }
362
363    /// Log structured debug with fields
364    pub fn debug_with_fields(message: &str, fields: serde_json::Value) -> ObservabilityResult<()> {
365        log_with_fields(LogLevel::Debug, message, fields)
366    }
367}
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372
373    #[test]
374    fn test_observability_config_default() {
375        let config = ObservabilityConfig::default();
376        assert_eq!(config.level, "info");
377        assert_eq!(config.format, "compact");
378        assert!(config.structured);
379    }
380
381    #[test]
382    fn test_config_parse_level() {
383        let config = ObservabilityConfig {
384            level: "debug".to_string(),
385            ..Default::default()
386        };
387
388        assert!(matches!(config.parse_level().unwrap(), LogLevel::Debug));
389    }
390
391    #[test]
392    fn test_manager_creation() {
393        let config = ObservabilityConfig::default();
394        let manager = ObservabilityManager::new(config);
395        assert!(manager.is_ok());
396    }
397
398    #[test]
399    fn test_manager_capabilities() {
400        let config = ObservabilityConfig::default();
401        let manager = ObservabilityManager::new(config).unwrap();
402        let caps = manager.capabilities();
403
404        assert!(caps.contains(&"structured_logging".to_string()));
405        assert!(caps.contains(&"standard_rust_logging".to_string()));
406        assert!(caps.contains(&"log_level_info".to_string()));
407    }
408
409    #[test]
410    fn test_manager_default() {
411        let manager = ObservabilityManager::default();
412        assert_eq!(manager.config.level, "info");
413    }
414
415    #[test]
416    fn test_config_validation() {
417        let config = ObservabilityConfig {
418            level: "invalid".to_string(),
419            ..Default::default()
420        };
421        assert!(config.validate().is_err());
422
423        let config = ObservabilityConfig {
424            format: "invalid".to_string(),
425            ..Default::default()
426        };
427        assert!(config.validate().is_err());
428    }
429}