1use crate::crypto::{Aes256GcmCrypto, EncryptedData, KeyUtils};
15use crate::secrets::SecretStore;
16use crate::types::AgentId;
17use chrono::{DateTime, Utc};
18use futures;
19use serde::{Deserialize, Serialize};
20use std::collections::HashMap;
21use std::sync::Arc;
22use std::time::{Duration, Instant};
23use thiserror::Error;
24use tracing as log;
25use uuid::Uuid;
26
27#[derive(Debug, Error)]
29pub enum LoggingError {
30 #[error("Encryption failed: {message}")]
31 EncryptionFailed { message: String },
32
33 #[error("Key management error: {message}")]
34 KeyManagementError { message: String },
35
36 #[error("Serialization error: {source}")]
37 SerializationError {
38 #[from]
39 source: serde_json::Error,
40 },
41
42 #[error("I/O error: {source}")]
43 IoError {
44 #[from]
45 source: std::io::Error,
46 },
47
48 #[error("Configuration error: {message}")]
49 ConfigurationError { message: String },
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct LoggingConfig {
55 pub enabled: bool,
57 pub log_file_path: String,
59 pub encryption_key_name: String,
61 pub encryption_key_env: Option<String>,
63 pub max_entry_size: usize,
65 pub retention_days: u32,
67 pub enable_pii_masking: bool,
69 pub batch_size: usize,
71}
72
73impl Default for LoggingConfig {
74 fn default() -> Self {
75 Self {
76 enabled: true,
77 log_file_path: "logs/model_io.encrypted.log".to_string(),
78 encryption_key_name: "symbiont/logging/encryption_key".to_string(),
79 encryption_key_env: Some("SYMBIONT_LOGGING_KEY".to_string()),
80 max_entry_size: 1024 * 1024, retention_days: 90,
82 enable_pii_masking: true,
83 batch_size: 100,
84 }
85 }
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
90pub enum ModelInteractionType {
91 Completion,
93 ToolCall,
95 RagQuery,
97 AgentExecution,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct ModelLogEntry {
104 pub id: String,
106 pub agent_id: AgentId,
108 pub interaction_type: ModelInteractionType,
110 pub timestamp: DateTime<Utc>,
112 pub latency_ms: u64,
114 pub model_identifier: String,
116 pub request_data: EncryptedData,
118 pub response_data: Option<EncryptedData>,
120 pub metadata: HashMap<String, String>,
122 pub error: Option<String>,
124 pub token_usage: Option<TokenUsage>,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct RequestData {
131 pub prompt: String,
133 pub tool_name: Option<String>,
135 pub tool_arguments: Option<serde_json::Value>,
137 pub parameters: HashMap<String, serde_json::Value>,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct ResponseData {
144 pub content: String,
146 pub tool_result: Option<serde_json::Value>,
148 pub confidence: Option<f64>,
150 pub metadata: HashMap<String, serde_json::Value>,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct TokenUsage {
157 pub input_tokens: u32,
159 pub output_tokens: u32,
161 pub total_tokens: u32,
163}
164
165pub struct ModelLogger {
167 config: LoggingConfig,
168 #[allow(dead_code)]
169 crypto: Aes256GcmCrypto,
170 #[allow(dead_code)]
171 secret_store: Option<Arc<dyn SecretStore>>,
172 encryption_key: String,
173}
174
175impl ModelLogger {
176 pub fn new(
178 config: LoggingConfig,
179 secret_store: Option<Arc<dyn SecretStore>>,
180 ) -> Result<Self, LoggingError> {
181 let crypto = Aes256GcmCrypto::new();
182
183 let encryption_key = Self::get_encryption_key(&config, &secret_store)?;
185
186 Ok(Self {
187 config,
188 crypto,
189 secret_store,
190 encryption_key,
191 })
192 }
193
194 pub fn with_defaults() -> Result<Self, LoggingError> {
196 Self::new(LoggingConfig::default(), None)
197 }
198
199 fn get_encryption_key(
201 config: &LoggingConfig,
202 secret_store: &Option<Arc<dyn SecretStore>>,
203 ) -> Result<String, LoggingError> {
204 if let Some(store) = secret_store {
206 if let Ok(secret) =
207 futures::executor::block_on(store.get_secret(&config.encryption_key_name))
208 {
209 log::debug!("Retrieved logging encryption key from SecretStore");
210 return Ok(secret.value().to_string());
211 } else {
212 log::warn!("Failed to retrieve logging encryption key from SecretStore, falling back to environment variable");
213 }
214 }
215
216 if let Some(env_var) = &config.encryption_key_env {
218 if let Ok(key) = KeyUtils::get_key_from_env(env_var) {
219 log::debug!("Retrieved logging encryption key from environment variable");
220 return Ok(key);
221 }
222 }
223
224 let key_utils = KeyUtils::new();
226 key_utils
227 .get_or_create_key()
228 .map_err(|e| LoggingError::KeyManagementError {
229 message: format!("Failed to get encryption key: {}", e),
230 })
231 }
232
233 pub async fn log_request(
235 &self,
236 agent_id: AgentId,
237 interaction_type: ModelInteractionType,
238 model_identifier: &str,
239 request_data: RequestData,
240 metadata: HashMap<String, String>,
241 ) -> Result<String, LoggingError> {
242 if !self.config.enabled {
243 return Ok(String::new());
244 }
245
246 let entry_id = Uuid::new_v4().to_string();
247 let timestamp = Utc::now();
248
249 let sanitized_request = if self.config.enable_pii_masking {
251 self.mask_pii_in_request(request_data)?
252 } else {
253 request_data
254 };
255
256 let encrypted_request = self.encrypt_request_data(&sanitized_request)?;
258
259 let log_entry = ModelLogEntry {
261 id: entry_id.clone(),
262 agent_id,
263 interaction_type,
264 timestamp,
265 latency_ms: 0, model_identifier: model_identifier.to_string(),
267 request_data: encrypted_request,
268 response_data: None,
269 metadata,
270 error: None,
271 token_usage: None,
272 };
273
274 self.write_log_entry(&log_entry).await?;
275
276 log::debug!("Logged model request {} for agent {}", entry_id, agent_id);
277 Ok(entry_id)
278 }
279
280 pub async fn log_response(
282 &self,
283 entry_id: &str,
284 response_data: ResponseData,
285 latency: Duration,
286 token_usage: Option<TokenUsage>,
287 error: Option<String>,
288 ) -> Result<(), LoggingError> {
289 if !self.config.enabled {
290 return Ok(());
291 }
292
293 let sanitized_response = if self.config.enable_pii_masking {
295 self.mask_pii_in_response(response_data)?
296 } else {
297 response_data
298 };
299
300 let encrypted_response = self.encrypt_response_data(&sanitized_response)?;
302
303 let update_entry = serde_json::json!({
305 "id": entry_id,
306 "response_data": encrypted_response,
307 "latency_ms": latency.as_millis() as u64,
308 "token_usage": token_usage,
309 "error": error,
310 "updated_at": Utc::now()
311 });
312
313 self.write_log_update(&update_entry).await?;
314
315 log::debug!("Logged model response for entry {}", entry_id);
316 Ok(())
317 }
318
319 #[allow(clippy::too_many_arguments)]
321 pub async fn log_interaction(
322 &self,
323 agent_id: AgentId,
324 interaction_type: ModelInteractionType,
325 model_identifier: &str,
326 request_data: RequestData,
327 response_data: ResponseData,
328 latency: Duration,
329 metadata: HashMap<String, String>,
330 token_usage: Option<TokenUsage>,
331 error: Option<String>,
332 ) -> Result<(), LoggingError> {
333 if !self.config.enabled {
334 return Ok(());
335 }
336
337 let entry_id = Uuid::new_v4().to_string();
338 let timestamp = Utc::now();
339
340 let sanitized_request = if self.config.enable_pii_masking {
342 self.mask_pii_in_request(request_data)?
343 } else {
344 request_data
345 };
346
347 let sanitized_response = if self.config.enable_pii_masking {
348 self.mask_pii_in_response(response_data)?
349 } else {
350 response_data
351 };
352
353 let encrypted_request = self.encrypt_request_data(&sanitized_request)?;
355 let encrypted_response = self.encrypt_response_data(&sanitized_response)?;
356
357 let log_entry = ModelLogEntry {
359 id: entry_id,
360 agent_id,
361 interaction_type,
362 timestamp,
363 latency_ms: latency.as_millis() as u64,
364 model_identifier: model_identifier.to_string(),
365 request_data: encrypted_request,
366 response_data: Some(encrypted_response),
367 metadata,
368 error,
369 token_usage,
370 };
371
372 self.write_log_entry(&log_entry).await?;
373
374 log::debug!("Logged complete model interaction for agent {}", agent_id);
375 Ok(())
376 }
377
378 fn encrypt_request_data(&self, data: &RequestData) -> Result<EncryptedData, LoggingError> {
380 let json_data = serde_json::to_string(data)?;
381 let encrypted =
382 Aes256GcmCrypto::encrypt_with_password(json_data.as_bytes(), &self.encryption_key)
383 .map_err(|e| LoggingError::EncryptionFailed {
384 message: format!("Failed to encrypt request data: {}", e),
385 })?;
386
387 Ok(encrypted)
388 }
389
390 fn encrypt_response_data(&self, data: &ResponseData) -> Result<EncryptedData, LoggingError> {
392 let json_data = serde_json::to_string(data)?;
393 let encrypted =
394 Aes256GcmCrypto::encrypt_with_password(json_data.as_bytes(), &self.encryption_key)
395 .map_err(|e| LoggingError::EncryptionFailed {
396 message: format!("Failed to encrypt response data: {}", e),
397 })?;
398
399 Ok(encrypted)
400 }
401
402 fn mask_pii_in_request(&self, mut data: RequestData) -> Result<RequestData, LoggingError> {
404 data.prompt = self.mask_sensitive_patterns(&data.prompt);
406
407 if let Some(ref mut args) = data.tool_arguments {
409 *args = self.mask_json_values(args.clone());
410 }
411
412 for (key, value) in data.parameters.iter_mut() {
414 if self.is_sensitive_key(key) {
415 *value = serde_json::Value::String("***".to_string());
416 } else {
417 *value = self.mask_json_values(value.clone());
418 }
419 }
420
421 Ok(data)
422 }
423
424 fn mask_pii_in_response(&self, mut data: ResponseData) -> Result<ResponseData, LoggingError> {
426 data.content = self.mask_sensitive_patterns(&data.content);
427
428 if let Some(ref mut result) = data.tool_result {
430 *result = self.mask_json_values(result.clone());
431 }
432
433 for (key, value) in data.metadata.iter_mut() {
435 if self.is_sensitive_key(key) {
436 *value = serde_json::Value::String("***".to_string());
437 } else {
438 *value = self.mask_json_values(value.clone());
439 }
440 }
441
442 Ok(data)
443 }
444
445 fn mask_sensitive_patterns(&self, text: &str) -> String {
447 use regex::Regex;
448
449 let patterns = [
451 (r"\b\d{3}-\d{2}-\d{4}\b", "***-**-****"), (
453 r"\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b",
454 "****-****-****-****",
455 ), (
457 r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b",
458 "***@***.***",
459 ), (r"\b\d{3}[\s-]?\d{3}[\s-]?\d{4}\b", "***-***-****"), (r"\bAPI[_\s]*KEY[\s:=]*[A-Za-z0-9+/]{20,}\b", "API_KEY=***"), (r"\bTOKEN[\s:=]*[A-Za-z0-9+/]{20,}\b", "TOKEN=***"), ];
464
465 let mut masked_text = text.to_string();
466 for (pattern, replacement) in patterns {
467 if let Ok(re) = Regex::new(pattern) {
468 masked_text = re.replace_all(&masked_text, replacement).to_string();
469 }
470 }
471
472 masked_text
473 }
474
475 fn mask_json_values(&self, value: serde_json::Value) -> serde_json::Value {
477 match value {
478 serde_json::Value::String(s) => {
479 serde_json::Value::String(self.mask_sensitive_patterns(&s))
480 }
481 serde_json::Value::Object(mut map) => {
482 for (key, val) in map.iter_mut() {
483 if self.is_sensitive_key(key) {
485 *val = serde_json::Value::String("***".to_string());
486 } else {
487 *val = self.mask_json_values(val.clone());
488 }
489 }
490 serde_json::Value::Object(map)
491 }
492 serde_json::Value::Array(arr) => serde_json::Value::Array(
493 arr.into_iter().map(|v| self.mask_json_values(v)).collect(),
494 ),
495 _ => value,
496 }
497 }
498
499 fn is_sensitive_key(&self, key: &str) -> bool {
501 let sensitive_keys = [
502 "password",
503 "token",
504 "key",
505 "secret",
506 "credential",
507 "api_key",
508 "auth",
509 "authorization",
510 "ssn",
511 "social_security",
512 "credit_card",
513 "card_number",
514 "cvv",
515 "pin",
516 ];
517
518 let key_lower = key.to_lowercase();
519 sensitive_keys
520 .iter()
521 .any(|&sensitive| key_lower.contains(sensitive))
522 }
523
524 async fn write_log_entry(&self, entry: &ModelLogEntry) -> Result<(), LoggingError> {
526 if let Some(parent) = std::path::Path::new(&self.config.log_file_path).parent() {
528 tokio::fs::create_dir_all(parent).await?;
529 }
530
531 let json_line = serde_json::to_string(entry)?;
533 let log_line = format!("{}\n", json_line);
534
535 tokio::fs::write(&self.config.log_file_path, log_line.as_bytes()).await?;
536
537 Ok(())
538 }
539
540 async fn write_log_update(&self, update: &serde_json::Value) -> Result<(), LoggingError> {
542 let update_line = format!("UPDATE: {}\n", serde_json::to_string(update)?);
545
546 use tokio::io::AsyncWriteExt;
547 let mut file = tokio::fs::OpenOptions::new()
548 .create(true)
549 .append(true)
550 .open(&self.config.log_file_path)
551 .await?;
552
553 file.write_all(update_line.as_bytes()).await?;
554 file.flush().await?;
555
556 Ok(())
557 }
558
559 pub async fn decrypt_log_entry(
561 &self,
562 encrypted_entry: &ModelLogEntry,
563 ) -> Result<(RequestData, Option<ResponseData>), LoggingError> {
564 let request_json = Aes256GcmCrypto::decrypt_with_password(
566 &encrypted_entry.request_data,
567 &self.encryption_key,
568 )
569 .map_err(|e| LoggingError::EncryptionFailed {
570 message: format!("Failed to decrypt request data: {}", e),
571 })?;
572
573 let request_data: RequestData = serde_json::from_slice(&request_json)?;
574
575 let response_data = if let Some(ref encrypted_response) = encrypted_entry.response_data {
577 let response_json =
578 Aes256GcmCrypto::decrypt_with_password(encrypted_response, &self.encryption_key)
579 .map_err(|e| LoggingError::EncryptionFailed {
580 message: format!("Failed to decrypt response data: {}", e),
581 })?;
582
583 Some(serde_json::from_slice(&response_json)?)
584 } else {
585 None
586 };
587
588 Ok((request_data, response_data))
589 }
590}
591
592pub trait TimedOperation {
594 #[allow(async_fn_in_trait)]
596 async fn timed<F, R, E>(&self, operation: F) -> (Result<R, E>, Duration)
597 where
598 F: std::future::Future<Output = Result<R, E>>;
599}
600
601impl TimedOperation for ModelLogger {
602 async fn timed<F, R, E>(&self, operation: F) -> (Result<R, E>, Duration)
603 where
604 F: std::future::Future<Output = Result<R, E>>,
605 {
606 let start = Instant::now();
607 let result = operation.await;
608 let duration = start.elapsed();
609 (result, duration)
610 }
611}
612
613#[cfg(test)]
614mod tests {
615 use super::*;
616 use crate::types::AgentId;
617 use std::collections::HashMap;
618 use std::sync::Arc;
619 use tempfile::tempdir;
620
621 #[derive(Debug, Clone)]
623 struct MockSecretStore {
624 secrets: HashMap<String, String>,
625 should_fail: bool,
626 }
627
628 impl MockSecretStore {
629 fn new() -> Self {
630 let mut secrets = HashMap::new();
631 secrets.insert(
632 "symbiont/logging/encryption_key".to_string(),
633 "test_key_123".to_string(),
634 );
635 Self {
636 secrets,
637 should_fail: false,
638 }
639 }
640
641 fn new_failing() -> Self {
642 Self {
643 secrets: HashMap::new(),
644 should_fail: true,
645 }
646 }
647 }
648
649 #[async_trait::async_trait]
650 impl crate::secrets::SecretStore for MockSecretStore {
651 async fn get_secret(
652 &self,
653 key: &str,
654 ) -> Result<crate::secrets::Secret, crate::secrets::SecretError> {
655 if self.should_fail {
656 return Err(crate::secrets::SecretError::NotFound {
657 key: key.to_string(),
658 });
659 }
660
661 if let Some(value) = self.secrets.get(key) {
662 Ok(crate::secrets::Secret::new(key.to_string(), value.clone()))
663 } else {
664 Err(crate::secrets::SecretError::NotFound {
665 key: key.to_string(),
666 })
667 }
668 }
669
670 async fn list_secrets(&self) -> Result<Vec<String>, crate::secrets::SecretError> {
671 Ok(self.secrets.keys().cloned().collect())
672 }
673 }
674
675 #[tokio::test]
676 async fn test_logger_creation_with_secret_store() {
677 let config = LoggingConfig {
678 log_file_path: "/tmp/test_model_logs.json".to_string(),
679 ..Default::default()
680 };
681
682 let secret_store: Arc<dyn crate::secrets::SecretStore> = Arc::new(MockSecretStore::new());
683 let logger = ModelLogger::new(config, Some(secret_store));
684 assert!(logger.is_ok());
685 }
686
687 #[tokio::test]
688 async fn test_logger_creation_without_secret_store() {
689 let config = LoggingConfig {
690 log_file_path: "/tmp/test_model_logs.json".to_string(),
691 encryption_key_env: Some("TEST_LOGGING_KEY".to_string()),
692 ..Default::default()
693 };
694
695 std::env::set_var("TEST_LOGGING_KEY", "fallback_key_456");
697
698 let logger = ModelLogger::new(config, None);
699 assert!(logger.is_ok());
700
701 std::env::remove_var("TEST_LOGGING_KEY");
702 }
703
704 #[tokio::test]
705 async fn test_logger_creation_with_defaults() {
706 let logger = ModelLogger::with_defaults();
707 assert!(logger.is_ok());
708 }
709
710 #[tokio::test]
711 async fn test_encryption_key_retrieval_priority() {
712 let config = LoggingConfig {
714 encryption_key_name: "test/key".to_string(),
715 encryption_key_env: Some("TEST_ENV_KEY".to_string()),
716 ..Default::default()
717 };
718
719 let secret_store: Arc<dyn crate::secrets::SecretStore> = Arc::new(MockSecretStore::new());
720 std::env::set_var("TEST_ENV_KEY", "env_key_value");
721
722 let key = ModelLogger::get_encryption_key(&config, &Some(secret_store));
723 assert!(key.is_ok());
725
726 std::env::remove_var("TEST_ENV_KEY");
727 }
728
729 #[tokio::test]
730 async fn test_encryption_key_fallback_to_env() {
731 let config = LoggingConfig {
732 encryption_key_name: "nonexistent/key".to_string(),
733 encryption_key_env: Some("TEST_FALLBACK_KEY".to_string()),
734 ..Default::default()
735 };
736
737 let secret_store: Arc<dyn crate::secrets::SecretStore> =
738 Arc::new(MockSecretStore::new_failing());
739 std::env::set_var("TEST_FALLBACK_KEY", "fallback_env_key");
740
741 let key = ModelLogger::get_encryption_key(&config, &Some(secret_store));
742 assert!(key.is_ok());
743
744 std::env::remove_var("TEST_FALLBACK_KEY");
745 }
746
747 #[tokio::test]
748 async fn test_encryption_decryption_roundtrip() {
749 let logger = ModelLogger::with_defaults().unwrap();
750
751 let request_data = RequestData {
752 prompt: "Test prompt".to_string(),
753 tool_name: Some("test_tool".to_string()),
754 tool_arguments: Some(serde_json::json!({"arg1": "value1"})),
755 parameters: {
756 let mut params = HashMap::new();
757 params.insert("param1".to_string(), serde_json::json!("value1"));
758 params
759 },
760 };
761
762 let response_data = ResponseData {
763 content: "Test response".to_string(),
764 tool_result: Some(serde_json::json!({"result": "success"})),
765 confidence: Some(0.95),
766 metadata: {
767 let mut meta = HashMap::new();
768 meta.insert("meta1".to_string(), serde_json::json!("value1"));
769 meta
770 },
771 };
772
773 let encrypted_request = logger.encrypt_request_data(&request_data).unwrap();
775 let encrypted_response = logger.encrypt_response_data(&response_data).unwrap();
776
777 let log_entry = ModelLogEntry {
779 id: "test_id".to_string(),
780 agent_id: AgentId::new(),
781 interaction_type: ModelInteractionType::Completion,
782 timestamp: chrono::Utc::now(),
783 latency_ms: 100,
784 model_identifier: "test_model".to_string(),
785 request_data: encrypted_request,
786 response_data: Some(encrypted_response),
787 metadata: HashMap::new(),
788 error: None,
789 token_usage: None,
790 };
791
792 let (decrypted_request, decrypted_response) =
793 logger.decrypt_log_entry(&log_entry).await.unwrap();
794
795 assert_eq!(decrypted_request.prompt, request_data.prompt);
796 assert_eq!(decrypted_request.tool_name, request_data.tool_name);
797
798 let decrypted_resp = decrypted_response.unwrap();
799 assert_eq!(decrypted_resp.content, response_data.content);
800 assert_eq!(decrypted_resp.confidence, response_data.confidence);
801 }
802
803 #[tokio::test]
804 async fn test_pii_masking_comprehensive() {
805 let logger = ModelLogger::with_defaults().unwrap();
806
807 let test_cases = vec![
809 ("SSN: 123-45-6789", "***-**-****"),
810 ("Credit card: 4532-1234-5678-9012", "****-****-****-****"),
811 ("Email: user@example.com", "***@***.***"),
812 ("Phone: 555-123-4567", "***-***-****"),
813 ("API_KEY: abc123def456ghi789abcdef", "API_KEY=***"),
814 ("TOKEN: xyz789uvw456rst123abcdef", "TOKEN=***"),
815 ];
816
817 for (input, expected_pattern) in test_cases {
818 let masked = logger.mask_sensitive_patterns(input);
819 assert!(
820 masked.contains(expected_pattern),
821 "Failed to mask '{}', got '{}'",
822 input,
823 masked
824 );
825 }
826 }
827
828 #[tokio::test]
829 async fn test_pii_masking_json_values() {
830 let logger = ModelLogger::with_defaults().unwrap();
831
832 let json_data = serde_json::json!({
833 "password": "secret123",
834 "api_key": "abc123def456",
835 "username": "john_doe",
836 "data": "safe_content",
837 "nested": {
838 "token": "xyz789",
839 "info": "public_info"
840 }
841 });
842
843 let masked_json = logger.mask_json_values(json_data);
844
845 assert_eq!(masked_json["password"], "***");
847 assert_eq!(masked_json["api_key"], "***");
848 assert_eq!(masked_json["nested"]["token"], "***");
849
850 assert_eq!(masked_json["username"], "john_doe");
852 assert_eq!(masked_json["data"], "safe_content");
853 assert_eq!(masked_json["nested"]["info"], "public_info");
854 }
855
856 #[tokio::test]
857 async fn test_sensitive_key_detection() {
858 let logger = ModelLogger::with_defaults().unwrap();
859
860 let sensitive_keys = vec![
862 "password",
863 "PASSWORD",
864 "Password",
865 "token",
866 "TOKEN",
867 "auth_token",
868 "key",
869 "api_key",
870 "API_KEY",
871 "secret",
872 "SECRET",
873 "client_secret",
874 "credential",
875 "credentials",
876 "ssn",
877 "social_security",
878 "credit_card",
879 "card_number",
880 "cvv",
881 "pin",
882 ];
883
884 for key in sensitive_keys {
885 assert!(
886 logger.is_sensitive_key(key),
887 "Should detect '{}' as sensitive",
888 key
889 );
890 }
891
892 let safe_keys = vec![
894 "username",
895 "user_id",
896 "name",
897 "data",
898 "content",
899 "message",
900 "timestamp",
901 "id",
902 "status",
903 ];
904
905 for key in safe_keys {
906 assert!(
907 !logger.is_sensitive_key(key),
908 "Should not detect '{}' as sensitive",
909 key
910 );
911 }
912 }
913
914 #[tokio::test]
915 async fn test_log_request_and_response() {
916 let temp_dir = tempdir().unwrap();
917 let log_path = temp_dir.path().join("test_request_response.json");
918
919 let config = LoggingConfig {
920 log_file_path: log_path.to_string_lossy().to_string(),
921 ..Default::default()
922 };
923
924 let logger = ModelLogger::new(config, None).unwrap();
925 let agent_id = AgentId::new();
926
927 let request_data = RequestData {
928 prompt: "What is the weather?".to_string(),
929 tool_name: None,
930 tool_arguments: None,
931 parameters: HashMap::new(),
932 };
933
934 let entry_id = logger
936 .log_request(
937 agent_id,
938 ModelInteractionType::Completion,
939 "test-model",
940 request_data,
941 HashMap::new(),
942 )
943 .await
944 .unwrap();
945
946 assert!(!entry_id.is_empty());
947
948 let response_data = ResponseData {
950 content: "The weather is sunny".to_string(),
951 tool_result: None,
952 confidence: Some(0.95),
953 metadata: HashMap::new(),
954 };
955
956 let result = logger
957 .log_response(
958 &entry_id,
959 response_data,
960 Duration::from_millis(150),
961 Some(TokenUsage {
962 input_tokens: 10,
963 output_tokens: 15,
964 total_tokens: 25,
965 }),
966 None,
967 )
968 .await;
969
970 assert!(result.is_ok());
971
972 assert!(tokio::fs::metadata(&log_path).await.is_ok());
974 }
975
976 #[tokio::test]
977 async fn test_complete_interaction_logging() {
978 let temp_dir = tempdir().unwrap();
979 let log_path = temp_dir.path().join("test_complete_interaction.json");
980
981 let config = LoggingConfig {
982 log_file_path: log_path.to_string_lossy().to_string(),
983 ..Default::default()
984 };
985
986 let logger = ModelLogger::new(config, None).unwrap();
987 let agent_id = AgentId::new();
988
989 let request_data = RequestData {
990 prompt: "Generate code for sorting".to_string(),
991 tool_name: Some("code_generator".to_string()),
992 tool_arguments: Some(serde_json::json!({"language": "python"})),
993 parameters: {
994 let mut params = HashMap::new();
995 params.insert("temperature".to_string(), serde_json::json!(0.7));
996 params
997 },
998 };
999
1000 let response_data = ResponseData {
1001 content: "def sort_list(lst): return sorted(lst)".to_string(),
1002 tool_result: Some(serde_json::json!({"status": "success"})),
1003 confidence: Some(0.92),
1004 metadata: {
1005 let mut meta = HashMap::new();
1006 meta.insert("language".to_string(), serde_json::json!("python"));
1007 meta
1008 },
1009 };
1010
1011 let result = logger
1012 .log_interaction(
1013 agent_id,
1014 ModelInteractionType::ToolCall,
1015 "test-code-model",
1016 request_data,
1017 response_data,
1018 Duration::from_millis(350),
1019 {
1020 let mut meta = HashMap::new();
1021 meta.insert("session_id".to_string(), "test_session".to_string());
1022 meta
1023 },
1024 Some(TokenUsage {
1025 input_tokens: 25,
1026 output_tokens: 40,
1027 total_tokens: 65,
1028 }),
1029 None,
1030 )
1031 .await;
1032
1033 assert!(result.is_ok());
1034
1035 assert!(tokio::fs::metadata(&log_path).await.is_ok());
1037 }
1038
1039 #[tokio::test]
1040 async fn test_logging_disabled() {
1041 let config = LoggingConfig {
1042 enabled: false,
1043 ..Default::default()
1044 };
1045
1046 let logger = ModelLogger::new(config, None).unwrap();
1047 let agent_id = AgentId::new();
1048
1049 let request_data = RequestData {
1050 prompt: "Test prompt".to_string(),
1051 tool_name: None,
1052 tool_arguments: None,
1053 parameters: HashMap::new(),
1054 };
1055
1056 let entry_id = logger
1058 .log_request(
1059 agent_id,
1060 ModelInteractionType::Completion,
1061 "test-model",
1062 request_data,
1063 HashMap::new(),
1064 )
1065 .await
1066 .unwrap();
1067
1068 assert!(entry_id.is_empty());
1069 }
1070
1071 #[tokio::test]
1072 async fn test_logging_with_error() {
1073 let temp_dir = tempdir().unwrap();
1074 let log_path = temp_dir.path().join("test_error_logging.json");
1075
1076 let config = LoggingConfig {
1077 log_file_path: log_path.to_string_lossy().to_string(),
1078 ..Default::default()
1079 };
1080
1081 let logger = ModelLogger::new(config, None).unwrap();
1082 let agent_id = AgentId::new();
1083
1084 let request_data = RequestData {
1085 prompt: "Error test".to_string(),
1086 tool_name: None,
1087 tool_arguments: None,
1088 parameters: HashMap::new(),
1089 };
1090
1091 let response_data = ResponseData {
1092 content: "Error occurred".to_string(),
1093 tool_result: None,
1094 confidence: None,
1095 metadata: HashMap::new(),
1096 };
1097
1098 let result = logger
1099 .log_interaction(
1100 agent_id,
1101 ModelInteractionType::Completion,
1102 "test-model",
1103 request_data,
1104 response_data,
1105 Duration::from_millis(50),
1106 HashMap::new(),
1107 None,
1108 Some("Model execution failed".to_string()),
1109 )
1110 .await;
1111
1112 assert!(result.is_ok());
1113 assert!(tokio::fs::metadata(&log_path).await.is_ok());
1114 }
1115
1116 #[tokio::test]
1117 async fn test_logging_config_validation() {
1118 let config = LoggingConfig::default();
1120 assert!(config.enabled);
1121 assert_eq!(config.log_file_path, "logs/model_io.encrypted.log");
1122 assert_eq!(
1123 config.encryption_key_name,
1124 "symbiont/logging/encryption_key"
1125 );
1126 assert_eq!(config.max_entry_size, 1024 * 1024);
1127 assert_eq!(config.retention_days, 90);
1128 assert!(config.enable_pii_masking);
1129 assert_eq!(config.batch_size, 100);
1130 }
1131
1132 #[tokio::test]
1133 async fn test_model_interaction_types() {
1134 let types = vec![
1136 ModelInteractionType::Completion,
1137 ModelInteractionType::ToolCall,
1138 ModelInteractionType::RagQuery,
1139 ModelInteractionType::AgentExecution,
1140 ];
1141
1142 for interaction_type in types {
1143 let serialized = serde_json::to_string(&interaction_type).unwrap();
1145 let deserialized: ModelInteractionType = serde_json::from_str(&serialized).unwrap();
1146 assert_eq!(interaction_type, deserialized);
1147 }
1148 }
1149
1150 #[tokio::test]
1151 async fn test_token_usage_tracking() {
1152 let token_usage = TokenUsage {
1153 input_tokens: 100,
1154 output_tokens: 50,
1155 total_tokens: 150,
1156 };
1157
1158 let serialized = serde_json::to_string(&token_usage).unwrap();
1160 let deserialized: TokenUsage = serde_json::from_str(&serialized).unwrap();
1161
1162 assert_eq!(token_usage.input_tokens, deserialized.input_tokens);
1163 assert_eq!(token_usage.output_tokens, deserialized.output_tokens);
1164 assert_eq!(token_usage.total_tokens, deserialized.total_tokens);
1165 }
1166
1167 #[tokio::test]
1168 async fn test_request_response_data_structures() {
1169 let request_data = RequestData {
1170 prompt: "Test prompt".to_string(),
1171 tool_name: Some("test_tool".to_string()),
1172 tool_arguments: Some(serde_json::json!({"arg": "value"})),
1173 parameters: {
1174 let mut params = HashMap::new();
1175 params.insert("temp".to_string(), serde_json::json!(0.8));
1176 params
1177 },
1178 };
1179
1180 let response_data = ResponseData {
1181 content: "Test response".to_string(),
1182 tool_result: Some(serde_json::json!({"result": "success"})),
1183 confidence: Some(0.9),
1184 metadata: {
1185 let mut meta = HashMap::new();
1186 meta.insert("model".to_string(), serde_json::json!("test"));
1187 meta
1188 },
1189 };
1190
1191 let req_serialized = serde_json::to_string(&request_data).unwrap();
1193 let req_deserialized: RequestData = serde_json::from_str(&req_serialized).unwrap();
1194 assert_eq!(request_data.prompt, req_deserialized.prompt);
1195
1196 let resp_serialized = serde_json::to_string(&response_data).unwrap();
1197 let resp_deserialized: ResponseData = serde_json::from_str(&resp_serialized).unwrap();
1198 assert_eq!(response_data.content, resp_deserialized.content);
1199 }
1200
1201 #[tokio::test]
1202 async fn test_pii_masking_request_data() {
1203 let logger = ModelLogger::with_defaults().unwrap();
1204
1205 let request_data = RequestData {
1206 prompt: "My SSN is 123-45-6789 and email is user@example.com".to_string(),
1207 tool_name: Some("sensitive_tool".to_string()),
1208 tool_arguments: Some(serde_json::json!({
1209 "user_password": "secret123",
1210 "api_token": "xyz789",
1211 "safe_data": "public_info"
1212 })),
1213 parameters: {
1214 let mut params = HashMap::new();
1215 params.insert("auth_key".to_string(), serde_json::json!("sensitive_key"));
1216 params.insert("username".to_string(), serde_json::json!("john_doe"));
1217 params
1218 },
1219 };
1220
1221 let masked_request = logger.mask_pii_in_request(request_data).unwrap();
1222
1223 assert!(!masked_request.prompt.contains("123-45-6789"));
1225 assert!(!masked_request.prompt.contains("user@example.com"));
1226
1227 if let Some(args) = &masked_request.tool_arguments {
1229 assert_eq!(args["user_password"], "***");
1230 assert_eq!(args["api_token"], "***");
1231 assert_eq!(args["safe_data"], "public_info");
1232 }
1233
1234 assert_eq!(masked_request.parameters["auth_key"], "***");
1236 assert_eq!(masked_request.parameters["username"], "john_doe");
1237 }
1238
1239 #[tokio::test]
1240 async fn test_pii_masking_response_data() {
1241 let logger = ModelLogger::with_defaults().unwrap();
1242
1243 let response_data = ResponseData {
1244 content: "Your SSN is 123-45-6789 and email is user@example.com".to_string(),
1245 tool_result: Some(serde_json::json!({
1246 "password": "hidden123",
1247 "result": "success"
1248 })),
1249 confidence: Some(0.95),
1250 metadata: {
1251 let mut meta = HashMap::new();
1252 meta.insert("secret".to_string(), serde_json::json!("confidential"));
1253 meta.insert("public".to_string(), serde_json::json!("open"));
1254 meta
1255 },
1256 };
1257
1258 let masked_response = logger.mask_pii_in_response(response_data).unwrap();
1259
1260 assert!(!masked_response.content.contains("123-45-6789"));
1262 assert!(!masked_response.content.contains("user@example.com"));
1263
1264 if let Some(result) = &masked_response.tool_result {
1266 assert_eq!(result["password"], "***");
1267 assert_eq!(result["result"], "success");
1268 }
1269
1270 assert_eq!(masked_response.metadata["secret"], "***");
1272 assert_eq!(masked_response.metadata["public"], "open");
1273 }
1274}