1use 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
13pub 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
25pub struct CompactJsonFormatter;
27
28impl FormatterPort for CompactJsonFormatter {
29 fn format(&self, entry: &LogEntry) -> ObservabilityResult<String> {
30 let mut output = String::new();
31
32 output.push_str(&format!(
34 "{} ",
35 entry.timestamp.format("%Y-%m-%d %H:%M:%S%.3f")
36 ));
37
38 output.push_str(&format!("[{}] ", entry.level.as_str().to_uppercase()));
40
41 output.push_str(&entry.message);
43
44 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 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
66pub struct PlainTextFormatter;
68
69impl FormatterPort for PlainTextFormatter {
70 fn format(&self, entry: &LogEntry) -> ObservabilityResult<String> {
71 let mut output = String::new();
72
73 output.push_str(&format!(
75 "{} ",
76 entry.timestamp.format("%Y-%m-%d %H:%M:%S%.3f")
77 ));
78
79 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 if let Some(ref module) = entry.source.module {
91 output.push_str(&format!("{}: ", module));
92 }
93
94 output.push_str(&entry.message);
96
97 Ok(output)
98 }
99}
100
101pub 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 println!("{}", formatted);
132
133 Ok(())
134 }
135}
136
137pub struct NoOpTransport;
139
140impl TransportPort for NoOpTransport {
141 fn transport(&self, _entry: &LogEntry) -> ObservabilityResult<()> {
142 Ok(())
144 }
145}
146
147#[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 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 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
236pub 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 fn record_to_log_entry(&self, record: &log::Record) -> LogEntry {
260 use crate::domain::LogSource;
261 use crate::domain::TraceCorrelation;
262
263 let kv_fields = crate::domain::LogKvExtractor::extract_kv_from_record(record);
265
266 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, 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 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 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 }
353}
354
355pub struct WasmContextAdapter {
359 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
395pub 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#[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 fn event_to_log_entry(&self, event: &tracing::Event<'_>) -> LogEntry {
485 use crate::domain::LogSource;
486
487 let metadata = event.metadata();
489 let level = self.convert_tracing_level(*metadata.level());
490
491 let mut visitor = TracingFieldVisitor::new();
493 event.record(&mut visitor);
494
495 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 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 fn get_current_span_context(&self) -> Option<crate::domain::TraceCorrelation> {
528 let current_span = tracing::Span::current();
530 if current_span.is_none() {
531 return None;
532 }
533
534 let span_id = format!("{:016x}", current_span.id()?.into_u64());
538
539 let trace_id = self.generate_trace_id_from_span(¤t_span);
542
543 Some(crate::domain::TraceCorrelation {
544 trace_id,
545 span_id,
546 parent_span_id: None, })
548 }
549
550 fn generate_trace_id_from_span(&self, span: &tracing::Span) -> String {
552 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 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 }
578
579 fn record_follows_from(&self, _span: &tracing::span::Id, _follows: &tracing::span::Id) {
580 }
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 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 }
605
606 fn exit(&self, _span: &tracing::span::Id) {
607 }
609}
610
611#[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#[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
724pub 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
790pub 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 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 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 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}