1use serde_json::Value;
7use std::collections::HashMap;
8
9use turul_mcp_protocol::logging::{LoggingLevel, LoggingMessageNotification, SetLevelRequest};
11
12use crate::traits::{HasLogFormat, HasLogLevel, HasLogTransport, HasLoggingMetadata};
14
15pub struct LoggingBuilder {
20 level: LoggingLevel,
21 data: Value,
22 logger: Option<String>,
23 meta: Option<HashMap<String, Value>>,
24 batch_size: Option<usize>,
26}
27
28impl LoggingBuilder {
29 pub fn new(level: LoggingLevel, data: Value) -> Self {
31 Self {
32 level,
33 data,
34 logger: None,
35 meta: None,
36 batch_size: None,
37 }
38 }
39
40 pub fn logger(mut self, logger: impl Into<String>) -> Self {
42 self.logger = Some(logger.into());
43 self
44 }
45
46 pub fn meta(mut self, meta: HashMap<String, Value>) -> Self {
48 self.meta = Some(meta);
49 self
50 }
51
52 pub fn meta_value(mut self, key: impl Into<String>, value: Value) -> Self {
54 if self.meta.is_none() {
55 self.meta = Some(HashMap::new());
56 }
57 self.meta.as_mut().unwrap().insert(key.into(), value);
58 self
59 }
60
61 pub fn batch_size(mut self, size: usize) -> Self {
63 self.batch_size = Some(size);
64 self
65 }
66
67 pub fn build(self) -> LoggingMessageNotification {
69 let mut notification = LoggingMessageNotification::new(self.level, self.data);
70 if let Some(logger) = self.logger {
71 notification = notification.with_logger(logger);
72 }
73 if let Some(meta) = self.meta {
74 notification = notification.with_meta(meta);
75 }
76 notification
77 }
78
79 pub fn build_dynamic(self) -> DynamicLogger {
81 DynamicLogger {
82 level: self.level,
83 data: self.data,
84 logger: self.logger,
85 meta: self.meta,
86 batch_size: self.batch_size,
87 }
88 }
89
90 pub fn build_session_aware(self) -> SessionAwareLogger {
92 SessionAwareLogger {
93 level: self.level,
94 data: self.data,
95 logger: self.logger,
96 meta: self.meta,
97 batch_size: self.batch_size,
98 }
99 }
100}
101
102#[derive(Debug)]
104pub struct DynamicLogger {
105 level: LoggingLevel,
106 data: Value,
107 logger: Option<String>,
108 #[allow(dead_code)]
109 meta: Option<HashMap<String, Value>>,
110 batch_size: Option<usize>,
111}
112
113impl HasLoggingMetadata for DynamicLogger {
115 fn method(&self) -> &str {
116 "notifications/message"
117 }
118
119 fn logger_name(&self) -> Option<&str> {
120 self.logger.as_deref()
121 }
122}
123
124impl HasLogLevel for DynamicLogger {
125 fn level(&self) -> LoggingLevel {
126 self.level
127 }
128}
129
130impl HasLogFormat for DynamicLogger {
131 fn data(&self) -> &Value {
132 &self.data
133 }
134}
135
136impl HasLogTransport for DynamicLogger {
137 fn batch_size(&self) -> Option<usize> {
138 self.batch_size
139 }
140
141 fn should_deliver(&self, threshold_level: LoggingLevel) -> bool {
142 self.level.should_log(threshold_level)
143 }
144}
145
146#[derive(Debug)]
153pub struct SessionAwareLogger {
154 level: LoggingLevel,
155 data: Value,
156 logger: Option<String>,
157 #[allow(dead_code)]
158 meta: Option<HashMap<String, Value>>,
159 batch_size: Option<usize>,
160}
161
162pub trait LoggingTarget {
164 fn should_log(&self, level: LoggingLevel) -> bool;
166
167 fn notify_log(
169 &self,
170 level: LoggingLevel,
171 data: serde_json::Value,
172 logger: Option<String>,
173 meta: Option<std::collections::HashMap<String, serde_json::Value>>,
174 );
175}
176
177impl SessionAwareLogger {
178 pub fn send_to_target<T: LoggingTarget>(&self, target: &T) {
180 if target.should_log(self.level) {
181 let message = self.format_message();
182 target.notify_log(
183 self.level,
184 serde_json::json!(message),
185 self.logger.clone(),
186 self.meta.clone(),
187 );
188 }
189 }
190
191 pub fn send_to_targets<T: LoggingTarget>(&self, targets: &[&T]) {
193 for &target in targets {
194 self.send_to_target(target);
195 }
196 }
197
198 pub fn would_send_to_target<T: LoggingTarget>(&self, target: &T) -> bool {
200 target.should_log(self.level)
201 }
202
203 pub fn format_message(&self) -> String {
205 match &self.data {
206 Value::String(s) => s.clone(),
207 other => {
208 serde_json::to_string(other).unwrap_or_else(|_| "<invalid log data>".to_string())
209 }
210 }
211 }
212
213 pub fn level_to_string(&self) -> &'static str {
215 match self.level {
216 LoggingLevel::Debug => "debug",
217 LoggingLevel::Info => "info",
218 LoggingLevel::Notice => "notice",
219 LoggingLevel::Warning => "warning",
220 LoggingLevel::Error => "error",
221 LoggingLevel::Critical => "critical",
222 LoggingLevel::Alert => "alert",
223 LoggingLevel::Emergency => "emergency",
224 }
225 }
226}
227
228impl HasLoggingMetadata for SessionAwareLogger {
230 fn method(&self) -> &str {
231 "notifications/message"
232 }
233
234 fn logger_name(&self) -> Option<&str> {
235 self.logger.as_deref()
236 }
237}
238
239impl HasLogLevel for SessionAwareLogger {
240 fn level(&self) -> LoggingLevel {
241 self.level
242 }
243}
244
245impl HasLogFormat for SessionAwareLogger {
246 fn data(&self) -> &Value {
247 &self.data
248 }
249}
250
251impl HasLogTransport for SessionAwareLogger {
252 fn batch_size(&self) -> Option<usize> {
253 self.batch_size
254 }
255
256 fn should_deliver(&self, threshold_level: LoggingLevel) -> bool {
257 self.level.should_log(threshold_level)
258 }
259}
260
261pub struct SetLevelBuilder {
265 level: LoggingLevel,
266 meta: Option<HashMap<String, Value>>,
267}
268
269impl SetLevelBuilder {
270 pub fn new(level: LoggingLevel) -> Self {
271 Self { level, meta: None }
272 }
273
274 pub fn meta(mut self, meta: HashMap<String, Value>) -> Self {
276 self.meta = Some(meta);
277 self
278 }
279
280 pub fn meta_value(mut self, key: impl Into<String>, value: Value) -> Self {
282 if self.meta.is_none() {
283 self.meta = Some(HashMap::new());
284 }
285 self.meta.as_mut().unwrap().insert(key.into(), value);
286 self
287 }
288
289 pub fn build(self) -> SetLevelRequest {
291 let mut request = SetLevelRequest::new(self.level);
292 if let Some(meta) = self.meta {
293 request = request.with_meta(meta);
294 }
295 request
296 }
297}
298
299impl LoggingBuilder {
301 pub fn debug(data: Value) -> Self {
303 Self::new(LoggingLevel::Debug, data)
304 }
305
306 pub fn info(data: Value) -> Self {
308 Self::new(LoggingLevel::Info, data)
309 }
310
311 pub fn notice(data: Value) -> Self {
313 Self::new(LoggingLevel::Notice, data)
314 }
315
316 pub fn warning(data: Value) -> Self {
318 Self::new(LoggingLevel::Warning, data)
319 }
320
321 pub fn error(data: Value) -> Self {
323 Self::new(LoggingLevel::Error, data)
324 }
325
326 pub fn critical(data: Value) -> Self {
328 Self::new(LoggingLevel::Critical, data)
329 }
330
331 pub fn alert(data: Value) -> Self {
333 Self::new(LoggingLevel::Alert, data)
334 }
335
336 pub fn emergency(data: Value) -> Self {
338 Self::new(LoggingLevel::Emergency, data)
339 }
340
341 pub fn text(level: LoggingLevel, message: impl Into<String>) -> Self {
343 Self::new(level, Value::String(message.into()))
344 }
345
346 pub fn structured(level: LoggingLevel, fields: HashMap<String, Value>) -> Self {
348 Self::new(
349 level,
350 serde_json::to_value(fields).unwrap_or(Value::Object(serde_json::Map::new())),
351 )
352 }
353
354 pub fn with_context(
356 level: LoggingLevel,
357 message: impl Into<String>,
358 context: HashMap<String, Value>,
359 ) -> Self {
360 let mut data = context;
361 data.insert("message".to_string(), Value::String(message.into()));
362 Self::structured(level, data)
363 }
364
365 pub fn set_level(level: LoggingLevel) -> SetLevelBuilder {
367 SetLevelBuilder::new(level)
368 }
369}
370
371pub struct LogLevel;
373
374impl LogLevel {
375 pub fn parse(level: &str) -> Result<LoggingLevel, String> {
377 match level.to_lowercase().as_str() {
378 "debug" => Ok(LoggingLevel::Debug),
379 "info" => Ok(LoggingLevel::Info),
380 "notice" => Ok(LoggingLevel::Notice),
381 "warning" | "warn" => Ok(LoggingLevel::Warning),
382 "error" => Ok(LoggingLevel::Error),
383 "critical" => Ok(LoggingLevel::Critical),
384 "alert" => Ok(LoggingLevel::Alert),
385 "emergency" => Ok(LoggingLevel::Emergency),
386 _ => Err(format!("Invalid log level: {}", level)),
387 }
388 }
389
390 pub fn to_string(level: LoggingLevel) -> String {
392 match level {
393 LoggingLevel::Debug => "debug",
394 LoggingLevel::Info => "info",
395 LoggingLevel::Notice => "notice",
396 LoggingLevel::Warning => "warning",
397 LoggingLevel::Error => "error",
398 LoggingLevel::Critical => "critical",
399 LoggingLevel::Alert => "alert",
400 LoggingLevel::Emergency => "emergency",
401 }
402 .to_string()
403 }
404
405 pub fn all() -> Vec<LoggingLevel> {
407 vec![
408 LoggingLevel::Debug,
409 LoggingLevel::Info,
410 LoggingLevel::Notice,
411 LoggingLevel::Warning,
412 LoggingLevel::Error,
413 LoggingLevel::Critical,
414 LoggingLevel::Alert,
415 LoggingLevel::Emergency,
416 ]
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423 use serde_json::json;
424 use crate::traits::LoggerDefinition;
425
426 #[test]
427 fn test_logging_builder_basic() {
428 let data = json!({"message": "Test log message"});
429 let notification = LoggingBuilder::new(LoggingLevel::Info, data.clone())
430 .logger("test-logger")
431 .meta_value("request_id", json!("req-123"))
432 .build();
433
434 assert_eq!(notification.method, "notifications/message");
435 assert_eq!(notification.params.level, LoggingLevel::Info);
436 assert_eq!(notification.params.data, data);
437 assert_eq!(notification.params.logger, Some("test-logger".to_string()));
438
439 let meta = notification.params.meta.expect("Expected meta");
440 assert_eq!(meta.get("request_id"), Some(&json!("req-123")));
441 }
442
443 #[test]
444 fn test_logging_level_convenience_methods() {
445 let debug_log = LoggingBuilder::debug(json!({"debug": "info"})).build();
446 assert_eq!(debug_log.params.level, LoggingLevel::Debug);
447
448 let info_log = LoggingBuilder::info(json!({"info": "message"})).build();
449 assert_eq!(info_log.params.level, LoggingLevel::Info);
450
451 let warning_log = LoggingBuilder::warning(json!({"warning": "alert"})).build();
452 assert_eq!(warning_log.params.level, LoggingLevel::Warning);
453
454 let error_log = LoggingBuilder::error(json!({"error": "critical"})).build();
455 assert_eq!(error_log.params.level, LoggingLevel::Error);
456 }
457
458 #[test]
459 fn test_text_logging() {
460 let notification = LoggingBuilder::text(LoggingLevel::Info, "Simple text message")
461 .logger("text-logger")
462 .build();
463
464 assert_eq!(notification.params.level, LoggingLevel::Info);
465 assert_eq!(notification.params.data, json!("Simple text message"));
466 assert_eq!(notification.params.logger, Some("text-logger".to_string()));
467 }
468
469 #[test]
470 fn test_structured_logging() {
471 let mut fields = HashMap::new();
472 fields.insert("user".to_string(), json!("alice"));
473 fields.insert("action".to_string(), json!("login"));
474 fields.insert("success".to_string(), json!(true));
475
476 let notification = LoggingBuilder::structured(LoggingLevel::Notice, fields.clone())
477 .logger("auth-logger")
478 .build();
479
480 assert_eq!(notification.params.level, LoggingLevel::Notice);
481 let expected_data = serde_json::to_value(fields).unwrap();
483 assert_eq!(notification.params.data, expected_data);
484 }
485
486 #[test]
487 fn test_with_context_logging() {
488 let mut context = HashMap::new();
489 context.insert("session_id".to_string(), json!("sess-123"));
490 context.insert("ip_address".to_string(), json!("192.168.1.1"));
491
492 let notification = LoggingBuilder::with_context(
493 LoggingLevel::Info,
494 "User logged in successfully",
495 context.clone(),
496 )
497 .build();
498
499 assert_eq!(notification.params.level, LoggingLevel::Info);
500
501 if let Value::Object(data_obj) = ¬ification.params.data {
503 assert_eq!(
504 data_obj.get("message"),
505 Some(&json!("User logged in successfully"))
506 );
507 assert_eq!(data_obj.get("session_id"), Some(&json!("sess-123")));
508 assert_eq!(data_obj.get("ip_address"), Some(&json!("192.168.1.1")));
509 } else {
510 panic!("Expected object data");
511 }
512 }
513
514 #[test]
515 fn test_set_level_builder() {
516 let request = LoggingBuilder::set_level(LoggingLevel::Warning)
517 .meta_value("source", json!("admin_panel"))
518 .build();
519
520 assert_eq!(request.method, "logging/setLevel");
521 assert_eq!(request.params.level, LoggingLevel::Warning);
522
523 let meta = request.params.meta.expect("Expected meta");
524 assert_eq!(meta.get("source"), Some(&json!("admin_panel")));
525 }
526
527 #[test]
528 fn test_dynamic_logger_traits() {
529 let logger = LoggingBuilder::info(json!({"message": "Test"}))
530 .logger("test-logger")
531 .batch_size(10)
532 .build_dynamic();
533
534 assert_eq!(logger.method(), "notifications/message");
536 assert_eq!(logger.logger_name(), Some("test-logger"));
537
538 assert_eq!(logger.level(), LoggingLevel::Info);
540 assert!(!logger.should_log(LoggingLevel::Debug)); assert!(logger.should_log(LoggingLevel::Error)); assert_eq!(logger.data(), &json!({"message": "Test"}));
546 assert_eq!(logger.format_message(), "{\"message\":\"Test\"}");
547
548 assert_eq!(logger.batch_size(), Some(10));
550 assert!(logger.should_deliver(LoggingLevel::Debug));
551
552 let message_notification = logger.to_message_notification();
554 assert_eq!(message_notification.method, "notifications/message");
555 assert_eq!(message_notification.params.level, LoggingLevel::Info);
556
557 let set_level_request = logger.to_set_level_request();
558 assert_eq!(set_level_request.method, "logging/setLevel");
559 assert_eq!(set_level_request.params.level, LoggingLevel::Info);
560 }
561
562 #[test]
563 fn test_log_level_utilities() {
564 assert_eq!(LogLevel::parse("info").unwrap(), LoggingLevel::Info);
566 assert_eq!(LogLevel::parse("WARNING").unwrap(), LoggingLevel::Warning);
567 assert_eq!(LogLevel::parse("warn").unwrap(), LoggingLevel::Warning);
568 assert!(LogLevel::parse("invalid").is_err());
569
570 assert_eq!(LogLevel::to_string(LoggingLevel::Debug), "debug");
572 assert_eq!(LogLevel::to_string(LoggingLevel::Emergency), "emergency");
573
574 let all_levels = LogLevel::all();
576 assert_eq!(all_levels.len(), 8);
577 assert!(all_levels.contains(&LoggingLevel::Debug));
578 assert!(all_levels.contains(&LoggingLevel::Emergency));
579 }
580
581 #[test]
582 fn test_log_format_with_string_data() {
583 let logger =
584 LoggingBuilder::text(LoggingLevel::Info, "Simple string message").build_dynamic();
585
586 assert_eq!(logger.format_message(), "Simple string message");
587 }
588
589 #[test]
590 fn test_log_format_with_object_data() {
591 let data = json!({"key": "value", "number": 42});
592 let logger = LoggingBuilder::new(LoggingLevel::Info, data).build_dynamic();
593
594 let formatted = logger.format_message();
595 assert!(formatted.contains("key"));
596 assert!(formatted.contains("value"));
597 assert!(formatted.contains("42"));
598 }
599}