Skip to main content

observability_core/
adapters.rs

1//! Adapters layer - Concrete implementations of ports for external integrations
2//!
3//! This module contains WASM-compatible implementations that connect our core domain
4//! to external systems like stdout, HTTP endpoints, and standard logging frameworks.
5
6use crate::domain::{LogEntry, ProcessorChain};
7use crate::error::{ObservabilityError, ObservabilityResult};
8use crate::ports::{FormatterPort, StandardLoggingPort, TransportPort};
9use crate::traits::LogLevel;
10use std::collections::HashMap;
11use std::sync::{Arc, Mutex};
12
13// ==================== FORMATTERS ====================
14
15/// JSON formatter for structured logging output
16pub struct JsonFormatter;
17
18impl FormatterPort for JsonFormatter {
19    fn format(&self, entry: &LogEntry) -> ObservabilityResult<String> {
20        serde_json::to_string(entry)
21            .map_err(|e| ObservabilityError::logging(format!("JSON formatting failed: {}", e)))
22    }
23}
24
25/// Compact JSON formatter (single line)
26pub struct CompactJsonFormatter;
27
28impl FormatterPort for CompactJsonFormatter {
29    fn format(&self, entry: &LogEntry) -> ObservabilityResult<String> {
30        let mut output = String::new();
31
32        // Add timestamp
33        output.push_str(&format!(
34            "{} ",
35            entry.timestamp.format("%Y-%m-%d %H:%M:%S%.3f")
36        ));
37
38        // Add level
39        output.push_str(&format!("[{}] ", entry.level.as_str().to_uppercase()));
40
41        // Add message
42        output.push_str(&entry.message);
43
44        // Add structured fields if any
45        if let serde_json::Value::Object(ref fields) = entry.fields {
46            if !fields.is_empty() {
47                output.push(' ');
48                output.push_str(&serde_json::to_string(fields).map_err(|e| {
49                    ObservabilityError::logging(format!("Field serialization failed: {}", e))
50                })?);
51            }
52        }
53
54        // Add trace context if available
55        if let Some(ref trace) = entry.trace_context {
56            output.push_str(&format!(
57                " trace_id={} span_id={}",
58                trace.trace_id, trace.span_id
59            ));
60        }
61
62        Ok(output)
63    }
64}
65
66/// Plain text formatter for human-readable output
67pub struct PlainTextFormatter;
68
69impl FormatterPort for PlainTextFormatter {
70    fn format(&self, entry: &LogEntry) -> ObservabilityResult<String> {
71        let mut output = String::new();
72
73        // Timestamp
74        output.push_str(&format!(
75            "{} ",
76            entry.timestamp.format("%Y-%m-%d %H:%M:%S%.3f")
77        ));
78
79        // Level with colors/styling (WASM-compatible)
80        let level_str = match entry.level {
81            LogLevel::Error => "ERROR",
82            LogLevel::Warn => "WARN ",
83            LogLevel::Info => "INFO ",
84            LogLevel::Debug => "DEBUG",
85            LogLevel::Trace => "TRACE",
86        };
87        output.push_str(&format!("[{}] ", level_str));
88
89        // Module path if available
90        if let Some(ref module) = entry.source.module {
91            output.push_str(&format!("{}: ", module));
92        }
93
94        // Message
95        output.push_str(&entry.message);
96
97        Ok(output)
98    }
99}
100
101// ==================== TRANSPORTS ====================
102
103/// WASM stdout transport adapter
104pub struct WasmStdoutAdapter {
105    formatter: Box<dyn FormatterPort>,
106}
107
108impl WasmStdoutAdapter {
109    pub fn new(formatter: Box<dyn FormatterPort>) -> Self {
110        Self { formatter }
111    }
112
113    pub fn with_json_formatter() -> Self {
114        Self::new(Box::new(JsonFormatter))
115    }
116
117    pub fn with_compact_formatter() -> Self {
118        Self::new(Box::new(CompactJsonFormatter))
119    }
120
121    pub fn with_plain_text_formatter() -> Self {
122        Self::new(Box::new(PlainTextFormatter))
123    }
124}
125
126impl TransportPort for WasmStdoutAdapter {
127    fn transport(&self, entry: &LogEntry) -> ObservabilityResult<()> {
128        let formatted = self.formatter.format(entry)?;
129
130        // WASM-compatible stdout output
131        println!("{}", formatted);
132
133        Ok(())
134    }
135}
136
137/// No-op transport for testing/disabled logging
138pub struct NoOpTransport;
139
140impl TransportPort for NoOpTransport {
141    fn transport(&self, _entry: &LogEntry) -> ObservabilityResult<()> {
142        // Do nothing
143        Ok(())
144    }
145}
146
147// ==================== LOG DIRECTIVES ====================
148
149/// Parsed `RUST_LOG`-style directives for per-target log filtering.
150///
151/// Supports: `"info"`, `"info,agent_sdk=debug,a2a_protocol_core=trace"`.
152/// Unknown tokens are silently ignored.
153#[derive(Debug, Clone)]
154pub struct LogDirectives {
155    global: LogLevel,
156    targets: Vec<(String, LogLevel)>,
157    max_level: LogLevel,
158}
159
160impl LogDirectives {
161    pub fn from_level(level: LogLevel) -> Self {
162        Self {
163            global: level,
164            targets: Vec::new(),
165            max_level: level,
166        }
167    }
168
169    /// Parse a `RUST_LOG`-style directive string.
170    ///
171    /// Examples: `"info"`, `"info,agent_sdk=debug"`, `"warn,a2a_protocol_core=trace,llm_client=debug"`.
172    pub fn parse(s: &str) -> Self {
173        let mut global = LogLevel::Info;
174        let mut targets = Vec::new();
175        let mut max = LogLevel::Info;
176
177        for part in s.split(',') {
178            let part = part.trim();
179            if part.is_empty() {
180                continue;
181            }
182            if let Some((target, level_str)) = part.split_once('=') {
183                if let Some(lvl) = Self::str_to_level(level_str.trim()) {
184                    targets.push((target.trim().to_string(), lvl));
185                    if lvl > max {
186                        max = lvl;
187                    }
188                }
189            } else if let Some(lvl) = Self::str_to_level(part) {
190                global = lvl;
191                if lvl > max {
192                    max = lvl;
193                }
194            }
195        }
196        if global > max {
197            max = global;
198        }
199        Self {
200            global,
201            targets,
202            max_level: max,
203        }
204    }
205
206    pub(crate) fn str_to_level(s: &str) -> Option<LogLevel> {
207        match s.to_ascii_lowercase().as_str() {
208            "error" => Some(LogLevel::Error),
209            "warn" => Some(LogLevel::Warn),
210            "info" => Some(LogLevel::Info),
211            "debug" => Some(LogLevel::Debug),
212            "trace" => Some(LogLevel::Trace),
213            _ => None,
214        }
215    }
216
217    pub fn global_level(&self) -> LogLevel {
218        self.global
219    }
220
221    pub fn max_level(&self) -> LogLevel {
222        self.max_level
223    }
224
225    /// Check whether a log record at `level` from `target` should be emitted.
226    pub fn enabled(&self, level: LogLevel, target: &str) -> bool {
227        for (prefix, directive_level) in &self.targets {
228            if target.starts_with(prefix.as_str()) {
229                return level <= *directive_level;
230            }
231        }
232        level <= self.global
233    }
234}
235
236// ==================== STANDARD LOGGING INTEGRATION ====================
237
238/// Standard logging adapter that hooks into Rust's log crate
239pub struct StandardLogAdapter {
240    processor_chain: ProcessorChain,
241    transport: Arc<dyn TransportPort>,
242    directives: LogDirectives,
243}
244
245impl StandardLogAdapter {
246    pub fn new(
247        processor_chain: ProcessorChain,
248        transport: Arc<dyn TransportPort>,
249        directives: LogDirectives,
250    ) -> Self {
251        Self {
252            processor_chain,
253            transport,
254            directives,
255        }
256    }
257
258    /// Convert log::Record to our LogEntry
259    fn record_to_log_entry(&self, record: &log::Record) -> LogEntry {
260        use crate::domain::LogSource;
261        use crate::domain::TraceCorrelation;
262
263        // Extract structured fields from log::kv
264        let kv_fields = crate::domain::LogKvExtractor::extract_kv_from_record(record);
265
266        // Best-effort trace correlation:
267        // If the SDK (A2A server/client) has set a thread-local trace context,
268        // attach it to the log entry so downstream processors can emit trace_id/span_id.
269        let trace_context = crate::context::get_current_context().map(|ctx| TraceCorrelation {
270            trace_id: ctx.trace_id,
271            span_id: ctx.span_id,
272            parent_span_id: ctx.parent_span_id,
273        });
274
275        LogEntry {
276            timestamp: chrono::Utc::now(),
277            level: self.convert_log_level(record.level()),
278            message: record.args().to_string(),
279            fields: kv_fields, // Use extracted kv fields instead of empty object
280            trace_context,
281            source: LogSource {
282                module: record.module_path().map(|s| s.to_string()),
283                file: record.file().map(|s| s.to_string()),
284                line: record.line(),
285                target: Some(record.target().to_string()),
286            },
287        }
288    }
289
290    /// Convert log::Level to our LogLevel
291    fn convert_log_level(&self, level: log::Level) -> LogLevel {
292        match level {
293            log::Level::Error => LogLevel::Error,
294            log::Level::Warn => LogLevel::Warn,
295            log::Level::Info => LogLevel::Info,
296            log::Level::Debug => LogLevel::Debug,
297            log::Level::Trace => LogLevel::Trace,
298        }
299    }
300
301    /// Convert our LogLevel to log::Level
302    fn convert_to_log_level(&self, level: &LogLevel) -> log::Level {
303        match level {
304            LogLevel::Error => log::Level::Error,
305            LogLevel::Warn => log::Level::Warn,
306            LogLevel::Info => log::Level::Info,
307            LogLevel::Debug => log::Level::Debug,
308            LogLevel::Trace => log::Level::Trace,
309        }
310    }
311}
312
313impl StandardLoggingPort for StandardLogAdapter {
314    fn initialize(&self) -> ObservabilityResult<()> {
315        log::set_max_level(
316            self.convert_to_log_level(&self.directives.max_level())
317                .to_level_filter(),
318        );
319
320        Ok(())
321    }
322
323    fn process_standard_log(&self, entry: LogEntry) -> ObservabilityResult<()> {
324        let processed_entry = self.processor_chain.process(entry)?;
325        self.transport.transport(&processed_entry)?;
326        Ok(())
327    }
328
329    fn enabled(&self, level: &LogLevel) -> bool {
330        *level <= self.directives.global_level()
331    }
332}
333
334impl log::Log for StandardLogAdapter {
335    fn enabled(&self, metadata: &log::Metadata) -> bool {
336        let level = self.convert_log_level(metadata.level());
337        self.directives.enabled(level, metadata.target())
338    }
339
340    fn log(&self, record: &log::Record) {
341        let level = self.convert_log_level(record.level());
342        if self.directives.enabled(level, record.target()) {
343            let log_entry = self.record_to_log_entry(record);
344            if let Err(e) = self.process_standard_log(log_entry) {
345                eprintln!("Logging error: {}", e);
346            }
347        }
348    }
349
350    fn flush(&self) {
351        // WASM stdout is immediately flushed
352    }
353}
354
355// ==================== CONTEXT ADAPTER ====================
356
357/// Thread-local context adapter (WASM-compatible)
358pub struct WasmContextAdapter {
359    // Using Arc<Mutex<>> since WASM doesn't have real threads
360    context: Arc<Mutex<HashMap<String, serde_json::Value>>>,
361}
362
363impl WasmContextAdapter {
364    pub fn new() -> Self {
365        Self {
366            context: Arc::new(Mutex::new(HashMap::new())),
367        }
368    }
369}
370
371impl crate::ports::ContextPort for WasmContextAdapter {
372    fn get_context(&self) -> HashMap<String, serde_json::Value> {
373        self.context.lock().unwrap().clone()
374    }
375
376    fn add_context(&self, key: String, value: serde_json::Value) {
377        self.context.lock().unwrap().insert(key, value);
378    }
379
380    fn remove_context(&self, key: &str) {
381        self.context.lock().unwrap().remove(key);
382    }
383
384    fn clear_context(&self) {
385        self.context.lock().unwrap().clear();
386    }
387}
388
389impl Default for WasmContextAdapter {
390    fn default() -> Self {
391        Self::new()
392    }
393}
394
395// ==================== BUILDER UTILITIES ====================
396
397/// Builder for creating a complete logging setup
398pub struct LoggingSetupBuilder {
399    processor_chain: Option<ProcessorChain>,
400    transport: Option<Arc<dyn TransportPort>>,
401    directives: LogDirectives,
402}
403
404impl LoggingSetupBuilder {
405    pub fn new() -> Self {
406        Self {
407            processor_chain: None,
408            transport: None,
409            directives: LogDirectives::from_level(LogLevel::Info),
410        }
411    }
412
413    pub fn with_processor_chain(mut self, chain: ProcessorChain) -> Self {
414        self.processor_chain = Some(chain);
415        self
416    }
417
418    pub fn with_transport(mut self, transport: Arc<dyn TransportPort>) -> Self {
419        self.transport = Some(transport);
420        self
421    }
422
423    pub fn with_level_filter(mut self, level: LogLevel) -> Self {
424        self.directives = LogDirectives::from_level(level);
425        self
426    }
427
428    pub fn with_directives(mut self, directives: LogDirectives) -> Self {
429        self.directives = directives;
430        self
431    }
432
433    pub fn build(self) -> ObservabilityResult<StandardLogAdapter> {
434        let processor_chain = self
435            .processor_chain
436            .unwrap_or_else(crate::domain::build_default_processor_chain);
437
438        let transport = self
439            .transport
440            .unwrap_or_else(|| Arc::new(WasmStdoutAdapter::with_compact_formatter()));
441
442        Ok(StandardLogAdapter::new(
443            processor_chain,
444            transport,
445            self.directives,
446        ))
447    }
448}
449
450impl Default for LoggingSetupBuilder {
451    fn default() -> Self {
452        Self::new()
453    }
454}
455
456// ==================== TRACING INTEGRATION ====================
457
458/// Tracing subscriber adapter for structured logging integration
459///
460/// This adapter implements tracing::Subscriber to capture tracing events
461/// and convert them to LogEntry for processing through the hexagonal architecture
462#[cfg(feature = "structured-logging")]
463pub struct TracingSubscriberAdapter {
464    processor_chain: ProcessorChain,
465    transport: Arc<dyn TransportPort>,
466    level_filter: LogLevel,
467}
468
469#[cfg(feature = "structured-logging")]
470impl TracingSubscriberAdapter {
471    pub fn new(
472        processor_chain: ProcessorChain,
473        transport: Arc<dyn TransportPort>,
474        level_filter: LogLevel,
475    ) -> Self {
476        Self {
477            processor_chain,
478            transport,
479            level_filter,
480        }
481    }
482
483    /// Convert tracing event to LogEntry
484    fn event_to_log_entry(&self, event: &tracing::Event<'_>) -> LogEntry {
485        use crate::domain::LogSource;
486
487        // Extract event metadata
488        let metadata = event.metadata();
489        let level = self.convert_tracing_level(*metadata.level());
490
491        // Create visitor to extract fields and message
492        let mut visitor = TracingFieldVisitor::new();
493        event.record(&mut visitor);
494
495        // Get current span context if available
496        let span_context = self.get_current_span_context();
497
498        LogEntry {
499            timestamp: chrono::Utc::now(),
500            level,
501            message: visitor
502                .message
503                .unwrap_or_else(|| "tracing event".to_string()),
504            fields: serde_json::Value::Object(visitor.fields),
505            trace_context: span_context,
506            source: LogSource {
507                module: metadata.module_path().map(|s| s.to_string()),
508                file: metadata.file().map(|s| s.to_string()),
509                line: metadata.line(),
510                target: Some(metadata.target().to_string()),
511            },
512        }
513    }
514
515    /// Convert tracing::Level to our LogLevel
516    fn convert_tracing_level(&self, level: tracing::Level) -> LogLevel {
517        match level {
518            tracing::Level::ERROR => LogLevel::Error,
519            tracing::Level::WARN => LogLevel::Warn,
520            tracing::Level::INFO => LogLevel::Info,
521            tracing::Level::DEBUG => LogLevel::Debug,
522            tracing::Level::TRACE => LogLevel::Trace,
523        }
524    }
525
526    /// Get current span context from tracing
527    fn get_current_span_context(&self) -> Option<crate::domain::TraceCorrelation> {
528        // Use tracing's span system to get current context
529        let current_span = tracing::Span::current();
530        if current_span.is_none() {
531            return None;
532        }
533
534        // Extract span ID and trace ID from current span
535        // This is a simplified implementation - in production you'd want
536        // to integrate with proper W3C trace context propagation
537        let span_id = format!("{:016x}", current_span.id()?.into_u64());
538
539        // For now, generate a trace ID from span hierarchy
540        // In a full implementation, this would come from W3C headers
541        let trace_id = self.generate_trace_id_from_span(&current_span);
542
543        Some(crate::domain::TraceCorrelation {
544            trace_id,
545            span_id,
546            parent_span_id: None, // TODO: Extract parent span ID
547        })
548    }
549
550    /// Generate trace ID from span (simplified implementation)
551    fn generate_trace_id_from_span(&self, span: &tracing::Span) -> String {
552        // This is a placeholder - in production you'd extract from W3C headers
553        format!(
554            "trace-{:032x}",
555            span.id().map(|id| id.into_u64()).unwrap_or(0)
556        )
557    }
558}
559
560#[cfg(feature = "structured-logging")]
561impl tracing::Subscriber for TracingSubscriberAdapter {
562    fn enabled(&self, metadata: &tracing::Metadata<'_>) -> bool {
563        let level = self.convert_tracing_level(*metadata.level());
564        level <= self.level_filter
565    }
566
567    fn new_span(&self, _span: &tracing::span::Attributes<'_>) -> tracing::span::Id {
568        // Create a new span ID
569        // This is a simplified implementation
570        let id = rand::random::<u64>();
571        tracing::span::Id::from_u64(id)
572    }
573
574    fn record(&self, _span: &tracing::span::Id, _values: &tracing::span::Record<'_>) {
575        // Record additional values to span
576        // For now, we don't store span state
577    }
578
579    fn record_follows_from(&self, _span: &tracing::span::Id, _follows: &tracing::span::Id) {
580        // Handle follows_from relationships
581    }
582
583    fn event(&self, event: &tracing::Event<'_>) {
584        let level = self.convert_tracing_level(*event.metadata().level());
585        if level <= self.level_filter {
586            let log_entry = self.event_to_log_entry(event);
587
588            // Process through processor chain and transport
589            match self.processor_chain.process(log_entry) {
590                Ok(processed_entry) => {
591                    if let Err(e) = self.transport.transport(&processed_entry) {
592                        eprintln!("Tracing transport error: {}", e);
593                    }
594                }
595                Err(e) => {
596                    eprintln!("Tracing processing error: {}", e);
597                }
598            }
599        }
600    }
601
602    fn enter(&self, _span: &tracing::span::Id) {
603        // Enter span - could set context here
604    }
605
606    fn exit(&self, _span: &tracing::span::Id) {
607        // Exit span - could clear context here
608    }
609}
610
611/// Visitor to extract fields from tracing events
612#[cfg(feature = "structured-logging")]
613struct TracingFieldVisitor {
614    fields: serde_json::Map<String, serde_json::Value>,
615    message: Option<String>,
616}
617
618#[cfg(feature = "structured-logging")]
619impl TracingFieldVisitor {
620    fn new() -> Self {
621        Self {
622            fields: serde_json::Map::new(),
623            message: None,
624        }
625    }
626}
627
628#[cfg(feature = "structured-logging")]
629impl tracing::field::Visit for TracingFieldVisitor {
630    fn record_f64(&mut self, field: &tracing::field::Field, value: f64) {
631        self.fields
632            .insert(field.name().to_string(), serde_json::json!(value));
633    }
634
635    fn record_i64(&mut self, field: &tracing::field::Field, value: i64) {
636        self.fields
637            .insert(field.name().to_string(), serde_json::json!(value));
638    }
639
640    fn record_u64(&mut self, field: &tracing::field::Field, value: u64) {
641        self.fields
642            .insert(field.name().to_string(), serde_json::json!(value));
643    }
644
645    fn record_bool(&mut self, field: &tracing::field::Field, value: bool) {
646        self.fields
647            .insert(field.name().to_string(), serde_json::json!(value));
648    }
649
650    fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
651        if field.name() == "message" {
652            self.message = Some(value.to_string());
653        } else {
654            self.fields
655                .insert(field.name().to_string(), serde_json::json!(value));
656        }
657    }
658
659    fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
660        self.fields.insert(
661            field.name().to_string(),
662            serde_json::json!(format!("{:?}", value)),
663        );
664    }
665}
666
667/// Builder for creating tracing-integrated logging setup
668#[cfg(feature = "structured-logging")]
669pub struct TracingIntegrationBuilder {
670    processor_chain: Option<ProcessorChain>,
671    transport: Option<Arc<dyn TransportPort>>,
672    level_filter: LogLevel,
673}
674
675#[cfg(feature = "structured-logging")]
676impl TracingIntegrationBuilder {
677    pub fn new() -> Self {
678        Self {
679            processor_chain: None,
680            transport: None,
681            level_filter: LogLevel::Info,
682        }
683    }
684
685    pub fn with_processor_chain(mut self, chain: ProcessorChain) -> Self {
686        self.processor_chain = Some(chain);
687        self
688    }
689
690    pub fn with_transport(mut self, transport: Arc<dyn TransportPort>) -> Self {
691        self.transport = Some(transport);
692        self
693    }
694
695    pub fn with_level_filter(mut self, level: LogLevel) -> Self {
696        self.level_filter = level;
697        self
698    }
699
700    pub fn build(self) -> ObservabilityResult<TracingSubscriberAdapter> {
701        let processor_chain = self
702            .processor_chain
703            .unwrap_or_else(crate::domain::build_default_processor_chain);
704
705        let transport = self
706            .transport
707            .unwrap_or_else(|| Arc::new(WasmStdoutAdapter::with_compact_formatter()));
708
709        Ok(TracingSubscriberAdapter::new(
710            processor_chain,
711            transport,
712            self.level_filter,
713        ))
714    }
715}
716
717#[cfg(feature = "structured-logging")]
718impl Default for TracingIntegrationBuilder {
719    fn default() -> Self {
720        Self::new()
721    }
722}
723
724/// Basic WASM stdout adapter for metrics (correlation-focused)
725pub struct WasmStdoutMetricsAdapter {
726    enabled: bool,
727}
728
729impl WasmStdoutMetricsAdapter {
730    pub fn new() -> Self {
731        Self { enabled: true }
732    }
733
734    pub fn disabled() -> Self {
735        Self { enabled: false }
736    }
737}
738
739impl Default for WasmStdoutMetricsAdapter {
740    fn default() -> Self {
741        Self::new()
742    }
743}
744
745impl crate::ports::MetricsPort for WasmStdoutMetricsAdapter {
746    fn emit_counter_simple(&self, name: &str, value: f64) -> ObservabilityResult<()> {
747        if !self.enabled {
748            return Ok(());
749        }
750
751        let timestamp = chrono::Utc::now().to_rfc3339();
752        println!(
753            "[METRIC] {} counter {} value={} timestamp={}",
754            timestamp, name, value, timestamp
755        );
756        Ok(())
757    }
758
759    fn emit_histogram_simple(&self, name: &str, value: f64) -> ObservabilityResult<()> {
760        if !self.enabled {
761            return Ok(());
762        }
763
764        let timestamp = chrono::Utc::now().to_rfc3339();
765        println!(
766            "[METRIC] {} histogram {} value={} timestamp={}",
767            timestamp, name, value, timestamp
768        );
769        Ok(())
770    }
771
772    fn emit_gauge_simple(&self, name: &str, value: f64) -> ObservabilityResult<()> {
773        if !self.enabled {
774            return Ok(());
775        }
776
777        let timestamp = chrono::Utc::now().to_rfc3339();
778        println!(
779            "[METRIC] {} gauge {} value={} timestamp={}",
780            timestamp, name, value, timestamp
781        );
782        Ok(())
783    }
784
785    fn is_enabled(&self) -> bool {
786        self.enabled
787    }
788}
789
790/// Extended WASM adapter that handles both logs and metrics with correlation
791pub struct UnifiedWasmStdoutAdapter {
792    log_adapter: WasmStdoutAdapter,
793    metrics_adapter: WasmStdoutMetricsAdapter,
794}
795
796impl UnifiedWasmStdoutAdapter {
797    pub fn new() -> Self {
798        Self {
799            log_adapter: WasmStdoutAdapter::with_json_formatter(),
800            metrics_adapter: WasmStdoutMetricsAdapter::new(),
801        }
802    }
803
804    /// Process metrics entry through same formatting as logs for correlation
805    pub fn transport_metric(&self, entry: &crate::domain::MetricsEntry) -> ObservabilityResult<()> {
806        let json_output = entry.to_json();
807        println!("[METRIC] {}", json_output);
808        Ok(())
809    }
810}
811
812impl Default for UnifiedWasmStdoutAdapter {
813    fn default() -> Self {
814        Self::new()
815    }
816}
817
818impl TransportPort for UnifiedWasmStdoutAdapter {
819    fn transport(&self, entry: &LogEntry) -> ObservabilityResult<()> {
820        self.log_adapter.transport(entry)
821    }
822
823    fn transport_batch(&self, entries: &[LogEntry]) -> ObservabilityResult<()> {
824        self.log_adapter.transport_batch(entries)
825    }
826}
827
828impl crate::ports::MetricsPort for UnifiedWasmStdoutAdapter {
829    fn emit_counter_simple(&self, name: &str, value: f64) -> ObservabilityResult<()> {
830        // Create a proper MetricsEntry for correlation
831        let entry = crate::domain::create_counter_metric(name, value);
832        self.transport_metric(&entry)
833    }
834
835    fn emit_histogram_simple(&self, name: &str, value: f64) -> ObservabilityResult<()> {
836        let entry = crate::domain::create_histogram_metric(name, value);
837        self.transport_metric(&entry)
838    }
839
840    fn emit_gauge_simple(&self, name: &str, value: f64) -> ObservabilityResult<()> {
841        let entry = crate::domain::create_gauge_metric(name, value);
842        self.transport_metric(&entry)
843    }
844
845    fn is_enabled(&self) -> bool {
846        self.metrics_adapter.is_enabled()
847    }
848}
849
850#[cfg(test)]
851mod tests {
852    use super::*;
853    use crate::domain::create_log_entry;
854
855    #[test]
856    fn test_json_formatter() {
857        let formatter = JsonFormatter;
858        let entry = create_log_entry(
859            LogLevel::Info,
860            "test message",
861            serde_json::json!({"key": "value"}),
862        );
863
864        let result = formatter.format(&entry).unwrap();
865        assert!(result.contains("test message"));
866        assert!(result.contains("Info"));
867    }
868
869    #[test]
870    fn test_compact_formatter() {
871        let formatter = CompactJsonFormatter;
872        let entry = create_log_entry(
873            LogLevel::Error,
874            "error occurred",
875            serde_json::json!({"error_code": 500}),
876        );
877
878        let result = formatter.format(&entry).unwrap();
879        assert!(result.contains("[ERROR]"));
880        assert!(result.contains("error occurred"));
881    }
882
883    #[test]
884    fn test_wasm_stdout_adapter() {
885        let adapter = WasmStdoutAdapter::with_json_formatter();
886        let entry = create_log_entry(LogLevel::Debug, "debug info", serde_json::json!({}));
887
888        // This would print to stdout in real usage
889        assert!(adapter.transport(&entry).is_ok());
890    }
891
892    #[test]
893    fn directives_simple_level() {
894        let d = LogDirectives::parse("debug");
895        assert_eq!(d.global_level(), LogLevel::Debug);
896        assert_eq!(d.max_level(), LogLevel::Debug);
897        assert!(d.enabled(LogLevel::Info, "anything"));
898        assert!(d.enabled(LogLevel::Debug, "anything"));
899        assert!(!d.enabled(LogLevel::Trace, "anything"));
900    }
901
902    #[test]
903    fn directives_per_crate() {
904        let d = LogDirectives::parse("info,agent_sdk=debug,a2a_protocol_core=trace");
905        assert_eq!(d.global_level(), LogLevel::Info);
906        assert_eq!(d.max_level(), LogLevel::Trace);
907
908        assert!(d.enabled(LogLevel::Info, "some_crate"));
909        assert!(!d.enabled(LogLevel::Debug, "some_crate"));
910
911        assert!(d.enabled(LogLevel::Debug, "agent_sdk"));
912        assert!(d.enabled(LogLevel::Debug, "agent_sdk::handler"));
913        assert!(!d.enabled(LogLevel::Trace, "agent_sdk"));
914
915        assert!(d.enabled(LogLevel::Trace, "a2a_protocol_core"));
916        assert!(d.enabled(LogLevel::Trace, "a2a_protocol_core::rpc"));
917    }
918
919    #[test]
920    fn directives_from_level() {
921        let d = LogDirectives::from_level(LogLevel::Warn);
922        assert!(d.enabled(LogLevel::Warn, "x"));
923        assert!(!d.enabled(LogLevel::Info, "x"));
924    }
925
926    #[test]
927    fn directives_empty_string() {
928        let d = LogDirectives::parse("");
929        assert_eq!(d.global_level(), LogLevel::Info);
930    }
931
932    #[test]
933    fn directives_unknown_tokens_ignored() {
934        let d = LogDirectives::parse("info,bad_token,agent_sdk=debug");
935        assert_eq!(d.global_level(), LogLevel::Info);
936        assert!(d.enabled(LogLevel::Debug, "agent_sdk"));
937    }
938}