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(Clone, PartialEq, Eq)]
48pub struct Sensitive<T>(T);
49
50impl<T> Sensitive<T> {
51 pub fn new(value: T) -> Self {
53 Self(value)
54 }
55
56 pub fn expose_secret(&self) -> &T {
59 &self.0
60 }
61
62 pub fn into_inner(self) -> T {
64 self.0
65 }
66}
67
68impl<T> From<T> for Sensitive<T> {
69 fn from(value: T) -> Self {
70 Self(value)
71 }
72}
73
74impl<T> std::fmt::Debug for Sensitive<T> {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 f.write_str("Sensitive([REDACTED])")
77 }
78}
79
80impl<T> std::fmt::Display for Sensitive<T> {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 f.write_str("[REDACTED]")
83 }
84}
85
86impl<T> Serialize for Sensitive<T> {
87 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
88 where
89 S: serde::Serializer,
90 {
91 serializer.serialize_str("[REDACTED]")
92 }
93}
94
95#[derive(Debug, Error)]
102pub enum LoggingError {
103 #[error("Encryption failed: {message}")]
104 EncryptionFailed { message: String },
105
106 #[error("Key management error: {message}")]
107 KeyManagementError { message: String },
108
109 #[error("Serialization error: {source}")]
110 SerializationError {
111 #[from]
112 source: serde_json::Error,
113 },
114
115 #[error("I/O error: {source}")]
116 IoError {
117 #[from]
118 source: std::io::Error,
119 },
120
121 #[error("Configuration error: {message}")]
122 ConfigurationError { message: String },
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct LoggingConfig {
128 pub enabled: bool,
130 pub log_file_path: String,
132 pub encryption_key_name: String,
134 pub encryption_key_env: Option<String>,
136 pub max_entry_size: usize,
138 pub retention_days: u32,
140 pub enable_pii_masking: bool,
142 pub batch_size: usize,
144}
145
146impl Default for LoggingConfig {
147 fn default() -> Self {
148 Self {
149 enabled: true,
150 log_file_path: "logs/model_io.encrypted.log".to_string(),
151 encryption_key_name: "symbiont/logging/encryption_key".to_string(),
152 encryption_key_env: Some("SYMBIONT_LOGGING_KEY".to_string()),
153 max_entry_size: 1024 * 1024, retention_days: 90,
155 enable_pii_masking: true,
156 batch_size: 100,
157 }
158 }
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
163pub enum ModelInteractionType {
164 Completion,
166 ToolCall,
168 RagQuery,
170 AgentExecution,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct ModelLogEntry {
177 pub id: String,
179 pub agent_id: AgentId,
181 pub interaction_type: ModelInteractionType,
183 pub timestamp: DateTime<Utc>,
185 pub latency_ms: u64,
187 pub model_identifier: String,
189 pub request_data: EncryptedData,
191 pub response_data: Option<EncryptedData>,
193 pub metadata: HashMap<String, String>,
195 pub error: Option<String>,
197 pub token_usage: Option<TokenUsage>,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct RequestData {
204 pub prompt: String,
206 pub tool_name: Option<String>,
208 pub tool_arguments: Option<serde_json::Value>,
210 pub parameters: HashMap<String, serde_json::Value>,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct ResponseData {
217 pub content: String,
219 pub tool_result: Option<serde_json::Value>,
221 pub confidence: Option<f64>,
223 pub metadata: HashMap<String, serde_json::Value>,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct TokenUsage {
230 pub input_tokens: u32,
232 pub output_tokens: u32,
234 pub total_tokens: u32,
236}
237
238pub struct ModelLogger {
240 config: LoggingConfig,
241 #[allow(dead_code)]
242 crypto: Aes256GcmCrypto,
243 #[allow(dead_code)]
244 secret_store: Option<Arc<dyn SecretStore>>,
245 encryption_key: String,
246}
247
248impl std::fmt::Debug for ModelLogger {
249 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252 f.debug_struct("ModelLogger")
253 .field("config", &self.config)
254 .field("encryption_key", &"<redacted>")
255 .finish_non_exhaustive()
256 }
257}
258
259impl ModelLogger {
260 pub fn new(
262 config: LoggingConfig,
263 secret_store: Option<Arc<dyn SecretStore>>,
264 ) -> Result<Self, LoggingError> {
265 let crypto = Aes256GcmCrypto::new();
266
267 let encryption_key = Self::get_encryption_key(&config, &secret_store)?;
269
270 Ok(Self {
271 config,
272 crypto,
273 secret_store,
274 encryption_key,
275 })
276 }
277
278 pub fn with_defaults() -> Result<Self, LoggingError> {
280 Self::new(LoggingConfig::default(), None)
281 }
282
283 fn get_encryption_key(
285 config: &LoggingConfig,
286 secret_store: &Option<Arc<dyn SecretStore>>,
287 ) -> Result<String, LoggingError> {
288 if let Some(store) = secret_store {
290 if let Ok(secret) =
291 futures::executor::block_on(store.get_secret(&config.encryption_key_name))
292 {
293 log::debug!("Retrieved logging encryption key from SecretStore");
294 return Ok(secret.value().to_string());
295 } else {
296 log::warn!("Failed to retrieve logging encryption key from SecretStore, falling back to environment variable");
297 }
298 }
299
300 if let Some(env_var) = &config.encryption_key_env {
302 if let Ok(key) = KeyUtils::get_key_from_env(env_var) {
303 log::debug!("Retrieved logging encryption key from environment variable");
304 return Ok(key);
305 }
306 }
307
308 let key_utils = KeyUtils::new();
310 key_utils
311 .get_or_create_key()
312 .map_err(|e| LoggingError::KeyManagementError {
313 message: format!("Failed to get encryption key: {}", e),
314 })
315 }
316
317 pub async fn log_request(
319 &self,
320 agent_id: AgentId,
321 interaction_type: ModelInteractionType,
322 model_identifier: &str,
323 request_data: RequestData,
324 metadata: HashMap<String, String>,
325 ) -> Result<String, LoggingError> {
326 if !self.config.enabled {
327 return Ok(String::new());
328 }
329
330 let entry_id = Uuid::new_v4().to_string();
331 let timestamp = Utc::now();
332
333 let sanitized_request = if self.config.enable_pii_masking {
335 self.mask_pii_in_request(request_data)?
336 } else {
337 request_data
338 };
339
340 let encrypted_request = self.encrypt_request_data(&sanitized_request)?;
342
343 let log_entry = ModelLogEntry {
345 id: entry_id.clone(),
346 agent_id,
347 interaction_type,
348 timestamp,
349 latency_ms: 0, model_identifier: model_identifier.to_string(),
351 request_data: encrypted_request,
352 response_data: None,
353 metadata,
354 error: None,
355 token_usage: None,
356 };
357
358 self.write_log_entry(&log_entry).await?;
359
360 log::debug!("Logged model request {} for agent {}", entry_id, agent_id);
361 Ok(entry_id)
362 }
363
364 pub async fn log_response(
366 &self,
367 entry_id: &str,
368 response_data: ResponseData,
369 latency: Duration,
370 token_usage: Option<TokenUsage>,
371 error: Option<String>,
372 ) -> Result<(), LoggingError> {
373 if !self.config.enabled {
374 return Ok(());
375 }
376
377 let sanitized_response = if self.config.enable_pii_masking {
379 self.mask_pii_in_response(response_data)?
380 } else {
381 response_data
382 };
383
384 let encrypted_response = self.encrypt_response_data(&sanitized_response)?;
386
387 let update_entry = serde_json::json!({
389 "id": entry_id,
390 "response_data": encrypted_response,
391 "latency_ms": latency.as_millis() as u64,
392 "token_usage": token_usage,
393 "error": error,
394 "updated_at": Utc::now()
395 });
396
397 self.write_log_update(&update_entry).await?;
398
399 log::debug!("Logged model response for entry {}", entry_id);
400 Ok(())
401 }
402
403 #[allow(clippy::too_many_arguments)]
405 pub async fn log_interaction(
406 &self,
407 agent_id: AgentId,
408 interaction_type: ModelInteractionType,
409 model_identifier: &str,
410 request_data: RequestData,
411 response_data: ResponseData,
412 latency: Duration,
413 metadata: HashMap<String, String>,
414 token_usage: Option<TokenUsage>,
415 error: Option<String>,
416 ) -> Result<(), LoggingError> {
417 if !self.config.enabled {
418 return Ok(());
419 }
420
421 let entry_id = Uuid::new_v4().to_string();
422 let timestamp = Utc::now();
423
424 let sanitized_request = if self.config.enable_pii_masking {
426 self.mask_pii_in_request(request_data)?
427 } else {
428 request_data
429 };
430
431 let sanitized_response = if self.config.enable_pii_masking {
432 self.mask_pii_in_response(response_data)?
433 } else {
434 response_data
435 };
436
437 let encrypted_request = self.encrypt_request_data(&sanitized_request)?;
439 let encrypted_response = self.encrypt_response_data(&sanitized_response)?;
440
441 let log_entry = ModelLogEntry {
443 id: entry_id,
444 agent_id,
445 interaction_type,
446 timestamp,
447 latency_ms: latency.as_millis() as u64,
448 model_identifier: model_identifier.to_string(),
449 request_data: encrypted_request,
450 response_data: Some(encrypted_response),
451 metadata,
452 error,
453 token_usage,
454 };
455
456 self.write_log_entry(&log_entry).await?;
457
458 log::debug!("Logged complete model interaction for agent {}", agent_id);
459 Ok(())
460 }
461
462 fn encrypt_request_data(&self, data: &RequestData) -> Result<EncryptedData, LoggingError> {
464 let json_data = serde_json::to_string(data)?;
465 let encrypted =
466 Aes256GcmCrypto::encrypt_with_password(json_data.as_bytes(), &self.encryption_key)
467 .map_err(|e| LoggingError::EncryptionFailed {
468 message: format!("Failed to encrypt request data: {}", e),
469 })?;
470
471 Ok(encrypted)
472 }
473
474 fn encrypt_response_data(&self, data: &ResponseData) -> Result<EncryptedData, LoggingError> {
476 let json_data = serde_json::to_string(data)?;
477 let encrypted =
478 Aes256GcmCrypto::encrypt_with_password(json_data.as_bytes(), &self.encryption_key)
479 .map_err(|e| LoggingError::EncryptionFailed {
480 message: format!("Failed to encrypt response data: {}", e),
481 })?;
482
483 Ok(encrypted)
484 }
485
486 fn mask_pii_in_request(&self, mut data: RequestData) -> Result<RequestData, LoggingError> {
488 data.prompt = self.mask_sensitive_patterns(&data.prompt);
490
491 if let Some(ref mut args) = data.tool_arguments {
493 *args = self.mask_json_values(args.clone());
494 }
495
496 for (key, value) in data.parameters.iter_mut() {
498 if self.is_sensitive_key(key) {
499 *value = serde_json::Value::String("***".to_string());
500 } else {
501 *value = self.mask_json_values(value.clone());
502 }
503 }
504
505 Ok(data)
506 }
507
508 fn mask_pii_in_response(&self, mut data: ResponseData) -> Result<ResponseData, LoggingError> {
510 data.content = self.mask_sensitive_patterns(&data.content);
511
512 if let Some(ref mut result) = data.tool_result {
514 *result = self.mask_json_values(result.clone());
515 }
516
517 for (key, value) in data.metadata.iter_mut() {
519 if self.is_sensitive_key(key) {
520 *value = serde_json::Value::String("***".to_string());
521 } else {
522 *value = self.mask_json_values(value.clone());
523 }
524 }
525
526 Ok(data)
527 }
528
529 fn mask_sensitive_patterns(&self, text: &str) -> String {
543 use regex::Regex;
544
545 let patterns: &[(&str, &str)] = &[
549 (r"\bsk-[A-Za-z0-9_\-]{20,}\b", "[REDACTED:API_KEY]"),
552 (
554 r"\bxox[bapre]-[A-Za-z0-9-]{10,}\b",
555 "[REDACTED:SLACK_TOKEN]",
556 ),
557 (r"\bgh[pousr]_[A-Za-z0-9]{30,}\b", "[REDACTED:GITHUB_TOKEN]"),
559 (r"\bAKIA[0-9A-Z]{16}\b", "[REDACTED:AWS_KEY_ID]"),
561 (r"\bAIza[0-9A-Za-z_\-]{35}\b", "[REDACTED:GOOGLE_API_KEY]"),
563 (
565 r"\bsk_(live|test)_[A-Za-z0-9]{20,}\b",
566 "[REDACTED:STRIPE_KEY]",
567 ),
568 (
570 r"(?i)\bbearer\s+[A-Za-z0-9._\-]{16,}\b",
571 "Bearer [REDACTED]",
572 ),
573 (
576 r"-----BEGIN (RSA |EC |OPENSSH |PGP |)PRIVATE KEY-----[\s\S]*?-----END (RSA |EC |OPENSSH |PGP |)PRIVATE KEY-----",
577 "[REDACTED:PRIVATE_KEY]",
578 ),
579 (
581 r"\beyJ[A-Za-z0-9_\-]{5,}\.[A-Za-z0-9_\-]{5,}\.[A-Za-z0-9_\-]{5,}\b",
582 "[REDACTED:JWT]",
583 ),
584 (
586 r"(?i)\bapi[_\s-]*key[\s:=]+[A-Za-z0-9+/_\-]{12,}\b",
587 "api_key=[REDACTED]",
588 ),
589 (
590 r"(?i)\btoken[\s:=]+[A-Za-z0-9+/_\-]{12,}\b",
591 "token=[REDACTED]",
592 ),
593 (
594 r"(?i)\bsecret[\s:=]+[A-Za-z0-9+/_\-]{12,}\b",
595 "secret=[REDACTED]",
596 ),
597 (r"(?i)\bpassword[\s:=]+[^\s]{6,}\b", "password=[REDACTED]"),
598 (r"\b\d{3}-\d{2}-\d{4}\b", "[REDACTED:SSN]"),
601 (
603 r"\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b",
604 "[REDACTED:CC]",
605 ),
606 (
608 r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b",
609 "[REDACTED:EMAIL]",
610 ),
611 (r"\b\d{3}[\s-]?\d{3}[\s-]?\d{4}\b", "[REDACTED:PHONE]"),
613 ];
614
615 let mut masked_text = text.to_string();
616 for (pattern, replacement) in patterns {
617 if let Ok(re) = Regex::new(pattern) {
618 masked_text = re.replace_all(&masked_text, *replacement).to_string();
619 }
620 }
621
622 masked_text
623 }
624
625 fn mask_json_values(&self, value: serde_json::Value) -> serde_json::Value {
627 match value {
628 serde_json::Value::String(s) => {
629 serde_json::Value::String(self.mask_sensitive_patterns(&s))
630 }
631 serde_json::Value::Object(mut map) => {
632 for (key, val) in map.iter_mut() {
633 if self.is_sensitive_key(key) {
635 *val = serde_json::Value::String("***".to_string());
636 } else {
637 *val = self.mask_json_values(val.clone());
638 }
639 }
640 serde_json::Value::Object(map)
641 }
642 serde_json::Value::Array(arr) => serde_json::Value::Array(
643 arr.into_iter().map(|v| self.mask_json_values(v)).collect(),
644 ),
645 _ => value,
646 }
647 }
648
649 fn is_sensitive_key(&self, key: &str) -> bool {
660 const SENSITIVE_FRAGMENTS: &[&str] = &[
661 "password",
663 "passwd",
664 "passphrase",
665 "token",
666 "bearer",
667 "jwt",
668 "auth",
669 "authorization",
670 "session",
671 "cookie",
672 "set_cookie",
673 "api_key",
674 "apikey",
675 "access_key",
676 "private_key",
677 "client_secret",
678 "client_id",
679 "refresh_token",
680 "id_token",
681 "csrf",
682 "otp",
683 "totp",
684 "secret",
685 "credential",
686 "signature",
687 "hmac",
688 "hash",
689 "salt",
690 "key", "ssn",
693 "social_security",
694 "credit_card",
695 "card_number",
696 "cvv",
697 "pin",
698 "date_of_birth",
699 "dob",
700 "phone",
701 "address",
702 "email",
703 "dsn",
705 "connection_string",
706 "conn_str",
707 "database_url",
708 "db_url",
709 "redis_url",
710 "amqp_url",
711 "postgres_url",
712 "mongodb_uri",
713 "url", ];
715
716 let key_lower = key.to_lowercase();
717 SENSITIVE_FRAGMENTS
718 .iter()
719 .any(|&fragment| key_lower.contains(fragment))
720 }
721
722 async fn write_log_entry(&self, entry: &ModelLogEntry) -> Result<(), LoggingError> {
730 use tokio::io::AsyncWriteExt;
731
732 if let Some(parent) = std::path::Path::new(&self.config.log_file_path).parent() {
734 tokio::fs::create_dir_all(parent).await?;
735 }
736
737 let json_line = serde_json::to_string(entry)?;
738 let log_line = format!("{}\n", json_line);
739
740 let mut file = tokio::fs::OpenOptions::new()
741 .create(true)
742 .append(true)
743 .open(&self.config.log_file_path)
744 .await?;
745
746 #[cfg(unix)]
747 {
748 use std::os::unix::fs::PermissionsExt;
749 let perms = std::fs::Permissions::from_mode(0o600);
750 if let Err(e) = tokio::fs::set_permissions(&self.config.log_file_path, perms).await {
753 log::warn!(
754 "Failed to set 0o600 permissions on model log {}: {}",
755 self.config.log_file_path,
756 e
757 );
758 }
759 }
760
761 file.write_all(log_line.as_bytes()).await?;
762 file.sync_all().await?;
765
766 Ok(())
767 }
768
769 async fn write_log_update(&self, update: &serde_json::Value) -> Result<(), LoggingError> {
771 let update_line = format!("UPDATE: {}\n", serde_json::to_string(update)?);
774
775 use tokio::io::AsyncWriteExt;
776 let mut file = tokio::fs::OpenOptions::new()
777 .create(true)
778 .append(true)
779 .open(&self.config.log_file_path)
780 .await?;
781
782 file.write_all(update_line.as_bytes()).await?;
783 file.flush().await?;
784
785 Ok(())
786 }
787
788 pub async fn decrypt_log_entry(
790 &self,
791 encrypted_entry: &ModelLogEntry,
792 ) -> Result<(RequestData, Option<ResponseData>), LoggingError> {
793 let request_json = Aes256GcmCrypto::decrypt_with_password(
795 &encrypted_entry.request_data,
796 &self.encryption_key,
797 )
798 .map_err(|e| LoggingError::EncryptionFailed {
799 message: format!("Failed to decrypt request data: {}", e),
800 })?;
801
802 let request_data: RequestData = serde_json::from_slice(&request_json)?;
803
804 let response_data = if let Some(ref encrypted_response) = encrypted_entry.response_data {
806 let response_json =
807 Aes256GcmCrypto::decrypt_with_password(encrypted_response, &self.encryption_key)
808 .map_err(|e| LoggingError::EncryptionFailed {
809 message: format!("Failed to decrypt response data: {}", e),
810 })?;
811
812 Some(serde_json::from_slice(&response_json)?)
813 } else {
814 None
815 };
816
817 Ok((request_data, response_data))
818 }
819}
820
821pub trait TimedOperation {
823 #[allow(async_fn_in_trait)]
825 async fn timed<F, R, E>(&self, operation: F) -> (Result<R, E>, Duration)
826 where
827 F: std::future::Future<Output = Result<R, E>>;
828}
829
830impl TimedOperation for ModelLogger {
831 async fn timed<F, R, E>(&self, operation: F) -> (Result<R, E>, Duration)
832 where
833 F: std::future::Future<Output = Result<R, E>>,
834 {
835 let start = Instant::now();
836 let result = operation.await;
837 let duration = start.elapsed();
838 (result, duration)
839 }
840}
841
842#[cfg(test)]
843mod tests {
844 use super::*;
845 use crate::types::AgentId;
846 use std::collections::HashMap;
847 use std::sync::Arc;
848 use tempfile::tempdir;
849
850 #[derive(Debug, Clone)]
852 struct MockSecretStore {
853 secrets: HashMap<String, String>,
854 should_fail: bool,
855 }
856
857 impl MockSecretStore {
858 fn new() -> Self {
859 let mut secrets = HashMap::new();
860 secrets.insert(
861 "symbiont/logging/encryption_key".to_string(),
862 "test_key_123".to_string(),
863 );
864 Self {
865 secrets,
866 should_fail: false,
867 }
868 }
869
870 fn new_failing() -> Self {
871 Self {
872 secrets: HashMap::new(),
873 should_fail: true,
874 }
875 }
876 }
877
878 #[async_trait::async_trait]
879 impl crate::secrets::SecretStore for MockSecretStore {
880 async fn get_secret(
881 &self,
882 key: &str,
883 ) -> Result<crate::secrets::Secret, crate::secrets::SecretError> {
884 if self.should_fail {
885 return Err(crate::secrets::SecretError::NotFound {
886 key: key.to_string(),
887 });
888 }
889
890 if let Some(value) = self.secrets.get(key) {
891 Ok(crate::secrets::Secret::new(key.to_string(), value.clone()))
892 } else {
893 Err(crate::secrets::SecretError::NotFound {
894 key: key.to_string(),
895 })
896 }
897 }
898
899 async fn list_secrets(&self) -> Result<Vec<String>, crate::secrets::SecretError> {
900 Ok(self.secrets.keys().cloned().collect())
901 }
902 }
903
904 #[tokio::test]
905 async fn test_logger_creation_with_secret_store() {
906 let config = LoggingConfig {
907 log_file_path: "/tmp/test_model_logs.json".to_string(),
908 ..Default::default()
909 };
910
911 let secret_store: Arc<dyn crate::secrets::SecretStore> = Arc::new(MockSecretStore::new());
912 let logger = ModelLogger::new(config, Some(secret_store));
913 assert!(logger.is_ok());
914 }
915
916 #[tokio::test]
917 async fn test_logger_creation_without_secret_store() {
918 let config = LoggingConfig {
919 log_file_path: "/tmp/test_model_logs.json".to_string(),
920 encryption_key_env: Some("TEST_LOGGING_KEY".to_string()),
921 ..Default::default()
922 };
923
924 std::env::set_var("TEST_LOGGING_KEY", "fallback_key_456");
926
927 let logger = ModelLogger::new(config, None);
928 assert!(logger.is_ok());
929
930 std::env::remove_var("TEST_LOGGING_KEY");
931 }
932
933 #[tokio::test]
934 async fn test_logger_creation_with_defaults() {
935 let logger = ModelLogger::with_defaults();
936 assert!(logger.is_ok());
937 }
938
939 #[tokio::test]
940 async fn test_encryption_key_retrieval_priority() {
941 let config = LoggingConfig {
943 encryption_key_name: "test/key".to_string(),
944 encryption_key_env: Some("TEST_ENV_KEY".to_string()),
945 ..Default::default()
946 };
947
948 let secret_store: Arc<dyn crate::secrets::SecretStore> = Arc::new(MockSecretStore::new());
949 std::env::set_var("TEST_ENV_KEY", "env_key_value");
950
951 let key = ModelLogger::get_encryption_key(&config, &Some(secret_store));
952 assert!(key.is_ok());
954
955 std::env::remove_var("TEST_ENV_KEY");
956 }
957
958 #[tokio::test]
959 async fn test_encryption_key_fallback_to_env() {
960 let config = LoggingConfig {
961 encryption_key_name: "nonexistent/key".to_string(),
962 encryption_key_env: Some("TEST_FALLBACK_KEY".to_string()),
963 ..Default::default()
964 };
965
966 let secret_store: Arc<dyn crate::secrets::SecretStore> =
967 Arc::new(MockSecretStore::new_failing());
968 std::env::set_var("TEST_FALLBACK_KEY", "fallback_env_key");
969
970 let key = ModelLogger::get_encryption_key(&config, &Some(secret_store));
971 assert!(key.is_ok());
972
973 std::env::remove_var("TEST_FALLBACK_KEY");
974 }
975
976 #[tokio::test]
977 async fn test_encryption_decryption_roundtrip() {
978 let logger = ModelLogger::with_defaults().unwrap();
979
980 let request_data = RequestData {
981 prompt: "Test prompt".to_string(),
982 tool_name: Some("test_tool".to_string()),
983 tool_arguments: Some(serde_json::json!({"arg1": "value1"})),
984 parameters: {
985 let mut params = HashMap::new();
986 params.insert("param1".to_string(), serde_json::json!("value1"));
987 params
988 },
989 };
990
991 let response_data = ResponseData {
992 content: "Test response".to_string(),
993 tool_result: Some(serde_json::json!({"result": "success"})),
994 confidence: Some(0.95),
995 metadata: {
996 let mut meta = HashMap::new();
997 meta.insert("meta1".to_string(), serde_json::json!("value1"));
998 meta
999 },
1000 };
1001
1002 let encrypted_request = logger.encrypt_request_data(&request_data).unwrap();
1004 let encrypted_response = logger.encrypt_response_data(&response_data).unwrap();
1005
1006 let log_entry = ModelLogEntry {
1008 id: "test_id".to_string(),
1009 agent_id: AgentId::new(),
1010 interaction_type: ModelInteractionType::Completion,
1011 timestamp: chrono::Utc::now(),
1012 latency_ms: 100,
1013 model_identifier: "test_model".to_string(),
1014 request_data: encrypted_request,
1015 response_data: Some(encrypted_response),
1016 metadata: HashMap::new(),
1017 error: None,
1018 token_usage: None,
1019 };
1020
1021 let (decrypted_request, decrypted_response) =
1022 logger.decrypt_log_entry(&log_entry).await.unwrap();
1023
1024 assert_eq!(decrypted_request.prompt, request_data.prompt);
1025 assert_eq!(decrypted_request.tool_name, request_data.tool_name);
1026
1027 let decrypted_resp = decrypted_response.unwrap();
1028 assert_eq!(decrypted_resp.content, response_data.content);
1029 assert_eq!(decrypted_resp.confidence, response_data.confidence);
1030 }
1031
1032 #[tokio::test]
1033 async fn test_pii_masking_comprehensive() {
1034 let logger = ModelLogger::with_defaults().unwrap();
1035
1036 let test_cases = vec![
1040 ("SSN: 123-45-6789", "[REDACTED:SSN]"),
1041 ("Credit card: 4532-1234-5678-9012", "[REDACTED:CC]"),
1042 ("Email: user@example.com", "[REDACTED:EMAIL]"),
1043 ("Phone: 555-123-4567", "[REDACTED:PHONE]"),
1044 ("API_KEY: abc123def456ghi789abcdef", "api_key=[REDACTED]"),
1045 ("TOKEN: xyz789uvw456rst123abcdef", "token=[REDACTED]"),
1046 ];
1047
1048 for (input, expected_pattern) in test_cases {
1049 let masked = logger.mask_sensitive_patterns(input);
1050 assert!(
1051 masked.contains(expected_pattern),
1052 "Failed to mask '{}', got '{}'",
1053 input,
1054 masked
1055 );
1056 }
1057 }
1058
1059 #[tokio::test]
1060 async fn test_pii_masking_json_values() {
1061 let logger = ModelLogger::with_defaults().unwrap();
1062
1063 let json_data = serde_json::json!({
1064 "password": "secret123",
1065 "api_key": "abc123def456",
1066 "username": "john_doe",
1067 "data": "safe_content",
1068 "nested": {
1069 "token": "xyz789",
1070 "info": "public_info"
1071 }
1072 });
1073
1074 let masked_json = logger.mask_json_values(json_data);
1075
1076 assert_eq!(masked_json["password"], "***");
1078 assert_eq!(masked_json["api_key"], "***");
1079 assert_eq!(masked_json["nested"]["token"], "***");
1080
1081 assert_eq!(masked_json["username"], "john_doe");
1083 assert_eq!(masked_json["data"], "safe_content");
1084 assert_eq!(masked_json["nested"]["info"], "public_info");
1085 }
1086
1087 #[tokio::test]
1088 async fn test_sensitive_key_detection() {
1089 let logger = ModelLogger::with_defaults().unwrap();
1090
1091 let sensitive_keys = vec![
1093 "password",
1094 "PASSWORD",
1095 "Password",
1096 "token",
1097 "TOKEN",
1098 "auth_token",
1099 "key",
1100 "api_key",
1101 "API_KEY",
1102 "secret",
1103 "SECRET",
1104 "client_secret",
1105 "credential",
1106 "credentials",
1107 "ssn",
1108 "social_security",
1109 "credit_card",
1110 "card_number",
1111 "cvv",
1112 "pin",
1113 ];
1114
1115 for key in sensitive_keys {
1116 assert!(
1117 logger.is_sensitive_key(key),
1118 "Should detect '{}' as sensitive",
1119 key
1120 );
1121 }
1122
1123 let safe_keys = vec![
1125 "username",
1126 "user_id",
1127 "name",
1128 "data",
1129 "content",
1130 "message",
1131 "timestamp",
1132 "id",
1133 "status",
1134 ];
1135
1136 for key in safe_keys {
1137 assert!(
1138 !logger.is_sensitive_key(key),
1139 "Should not detect '{}' as sensitive",
1140 key
1141 );
1142 }
1143 }
1144
1145 #[tokio::test]
1146 async fn test_log_request_and_response() {
1147 let temp_dir = tempdir().unwrap();
1148 let log_path = temp_dir.path().join("test_request_response.json");
1149
1150 let config = LoggingConfig {
1151 log_file_path: log_path.to_string_lossy().to_string(),
1152 ..Default::default()
1153 };
1154
1155 let logger = ModelLogger::new(config, None).unwrap();
1156 let agent_id = AgentId::new();
1157
1158 let request_data = RequestData {
1159 prompt: "What is the weather?".to_string(),
1160 tool_name: None,
1161 tool_arguments: None,
1162 parameters: HashMap::new(),
1163 };
1164
1165 let entry_id = logger
1167 .log_request(
1168 agent_id,
1169 ModelInteractionType::Completion,
1170 "test-model",
1171 request_data,
1172 HashMap::new(),
1173 )
1174 .await
1175 .unwrap();
1176
1177 assert!(!entry_id.is_empty());
1178
1179 let response_data = ResponseData {
1181 content: "The weather is sunny".to_string(),
1182 tool_result: None,
1183 confidence: Some(0.95),
1184 metadata: HashMap::new(),
1185 };
1186
1187 let result = logger
1188 .log_response(
1189 &entry_id,
1190 response_data,
1191 Duration::from_millis(150),
1192 Some(TokenUsage {
1193 input_tokens: 10,
1194 output_tokens: 15,
1195 total_tokens: 25,
1196 }),
1197 None,
1198 )
1199 .await;
1200
1201 assert!(result.is_ok());
1202
1203 assert!(tokio::fs::metadata(&log_path).await.is_ok());
1205 }
1206
1207 #[tokio::test]
1208 async fn test_complete_interaction_logging() {
1209 let temp_dir = tempdir().unwrap();
1210 let log_path = temp_dir.path().join("test_complete_interaction.json");
1211
1212 let config = LoggingConfig {
1213 log_file_path: log_path.to_string_lossy().to_string(),
1214 ..Default::default()
1215 };
1216
1217 let logger = ModelLogger::new(config, None).unwrap();
1218 let agent_id = AgentId::new();
1219
1220 let request_data = RequestData {
1221 prompt: "Generate code for sorting".to_string(),
1222 tool_name: Some("code_generator".to_string()),
1223 tool_arguments: Some(serde_json::json!({"language": "python"})),
1224 parameters: {
1225 let mut params = HashMap::new();
1226 params.insert("temperature".to_string(), serde_json::json!(0.7));
1227 params
1228 },
1229 };
1230
1231 let response_data = ResponseData {
1232 content: "def sort_list(lst): return sorted(lst)".to_string(),
1233 tool_result: Some(serde_json::json!({"status": "success"})),
1234 confidence: Some(0.92),
1235 metadata: {
1236 let mut meta = HashMap::new();
1237 meta.insert("language".to_string(), serde_json::json!("python"));
1238 meta
1239 },
1240 };
1241
1242 let result = logger
1243 .log_interaction(
1244 agent_id,
1245 ModelInteractionType::ToolCall,
1246 "test-code-model",
1247 request_data,
1248 response_data,
1249 Duration::from_millis(350),
1250 {
1251 let mut meta = HashMap::new();
1252 meta.insert("session_id".to_string(), "test_session".to_string());
1253 meta
1254 },
1255 Some(TokenUsage {
1256 input_tokens: 25,
1257 output_tokens: 40,
1258 total_tokens: 65,
1259 }),
1260 None,
1261 )
1262 .await;
1263
1264 assert!(result.is_ok());
1265
1266 assert!(tokio::fs::metadata(&log_path).await.is_ok());
1268 }
1269
1270 #[tokio::test]
1271 async fn test_logging_disabled() {
1272 let config = LoggingConfig {
1273 enabled: false,
1274 ..Default::default()
1275 };
1276
1277 let logger = ModelLogger::new(config, None).unwrap();
1278 let agent_id = AgentId::new();
1279
1280 let request_data = RequestData {
1281 prompt: "Test prompt".to_string(),
1282 tool_name: None,
1283 tool_arguments: None,
1284 parameters: HashMap::new(),
1285 };
1286
1287 let entry_id = logger
1289 .log_request(
1290 agent_id,
1291 ModelInteractionType::Completion,
1292 "test-model",
1293 request_data,
1294 HashMap::new(),
1295 )
1296 .await
1297 .unwrap();
1298
1299 assert!(entry_id.is_empty());
1300 }
1301
1302 #[tokio::test]
1303 async fn test_logging_with_error() {
1304 let temp_dir = tempdir().unwrap();
1305 let log_path = temp_dir.path().join("test_error_logging.json");
1306
1307 let config = LoggingConfig {
1308 log_file_path: log_path.to_string_lossy().to_string(),
1309 ..Default::default()
1310 };
1311
1312 let logger = ModelLogger::new(config, None).unwrap();
1313 let agent_id = AgentId::new();
1314
1315 let request_data = RequestData {
1316 prompt: "Error test".to_string(),
1317 tool_name: None,
1318 tool_arguments: None,
1319 parameters: HashMap::new(),
1320 };
1321
1322 let response_data = ResponseData {
1323 content: "Error occurred".to_string(),
1324 tool_result: None,
1325 confidence: None,
1326 metadata: HashMap::new(),
1327 };
1328
1329 let result = logger
1330 .log_interaction(
1331 agent_id,
1332 ModelInteractionType::Completion,
1333 "test-model",
1334 request_data,
1335 response_data,
1336 Duration::from_millis(50),
1337 HashMap::new(),
1338 None,
1339 Some("Model execution failed".to_string()),
1340 )
1341 .await;
1342
1343 assert!(result.is_ok());
1344 assert!(tokio::fs::metadata(&log_path).await.is_ok());
1345 }
1346
1347 #[tokio::test]
1348 async fn test_logging_config_validation() {
1349 let config = LoggingConfig::default();
1351 assert!(config.enabled);
1352 assert_eq!(config.log_file_path, "logs/model_io.encrypted.log");
1353 assert_eq!(
1354 config.encryption_key_name,
1355 "symbiont/logging/encryption_key"
1356 );
1357 assert_eq!(config.max_entry_size, 1024 * 1024);
1358 assert_eq!(config.retention_days, 90);
1359 assert!(config.enable_pii_masking);
1360 assert_eq!(config.batch_size, 100);
1361 }
1362
1363 #[tokio::test]
1364 async fn test_model_interaction_types() {
1365 let types = vec![
1367 ModelInteractionType::Completion,
1368 ModelInteractionType::ToolCall,
1369 ModelInteractionType::RagQuery,
1370 ModelInteractionType::AgentExecution,
1371 ];
1372
1373 for interaction_type in types {
1374 let serialized = serde_json::to_string(&interaction_type).unwrap();
1376 let deserialized: ModelInteractionType = serde_json::from_str(&serialized).unwrap();
1377 assert_eq!(interaction_type, deserialized);
1378 }
1379 }
1380
1381 #[tokio::test]
1382 async fn test_token_usage_tracking() {
1383 let token_usage = TokenUsage {
1384 input_tokens: 100,
1385 output_tokens: 50,
1386 total_tokens: 150,
1387 };
1388
1389 let serialized = serde_json::to_string(&token_usage).unwrap();
1391 let deserialized: TokenUsage = serde_json::from_str(&serialized).unwrap();
1392
1393 assert_eq!(token_usage.input_tokens, deserialized.input_tokens);
1394 assert_eq!(token_usage.output_tokens, deserialized.output_tokens);
1395 assert_eq!(token_usage.total_tokens, deserialized.total_tokens);
1396 }
1397
1398 #[tokio::test]
1399 async fn test_request_response_data_structures() {
1400 let request_data = RequestData {
1401 prompt: "Test prompt".to_string(),
1402 tool_name: Some("test_tool".to_string()),
1403 tool_arguments: Some(serde_json::json!({"arg": "value"})),
1404 parameters: {
1405 let mut params = HashMap::new();
1406 params.insert("temp".to_string(), serde_json::json!(0.8));
1407 params
1408 },
1409 };
1410
1411 let response_data = ResponseData {
1412 content: "Test response".to_string(),
1413 tool_result: Some(serde_json::json!({"result": "success"})),
1414 confidence: Some(0.9),
1415 metadata: {
1416 let mut meta = HashMap::new();
1417 meta.insert("model".to_string(), serde_json::json!("test"));
1418 meta
1419 },
1420 };
1421
1422 let req_serialized = serde_json::to_string(&request_data).unwrap();
1424 let req_deserialized: RequestData = serde_json::from_str(&req_serialized).unwrap();
1425 assert_eq!(request_data.prompt, req_deserialized.prompt);
1426
1427 let resp_serialized = serde_json::to_string(&response_data).unwrap();
1428 let resp_deserialized: ResponseData = serde_json::from_str(&resp_serialized).unwrap();
1429 assert_eq!(response_data.content, resp_deserialized.content);
1430 }
1431
1432 #[tokio::test]
1433 async fn test_pii_masking_request_data() {
1434 let logger = ModelLogger::with_defaults().unwrap();
1435
1436 let request_data = RequestData {
1437 prompt: "My SSN is 123-45-6789 and email is user@example.com".to_string(),
1438 tool_name: Some("sensitive_tool".to_string()),
1439 tool_arguments: Some(serde_json::json!({
1440 "user_password": "secret123",
1441 "api_token": "xyz789",
1442 "safe_data": "public_info"
1443 })),
1444 parameters: {
1445 let mut params = HashMap::new();
1446 params.insert("auth_key".to_string(), serde_json::json!("sensitive_key"));
1447 params.insert("username".to_string(), serde_json::json!("john_doe"));
1448 params
1449 },
1450 };
1451
1452 let masked_request = logger.mask_pii_in_request(request_data).unwrap();
1453
1454 assert!(!masked_request.prompt.contains("123-45-6789"));
1456 assert!(!masked_request.prompt.contains("user@example.com"));
1457
1458 if let Some(args) = &masked_request.tool_arguments {
1460 assert_eq!(args["user_password"], "***");
1461 assert_eq!(args["api_token"], "***");
1462 assert_eq!(args["safe_data"], "public_info");
1463 }
1464
1465 assert_eq!(masked_request.parameters["auth_key"], "***");
1467 assert_eq!(masked_request.parameters["username"], "john_doe");
1468 }
1469
1470 #[tokio::test]
1471 async fn test_pii_masking_response_data() {
1472 let logger = ModelLogger::with_defaults().unwrap();
1473
1474 let response_data = ResponseData {
1475 content: "Your SSN is 123-45-6789 and email is user@example.com".to_string(),
1476 tool_result: Some(serde_json::json!({
1477 "password": "hidden123",
1478 "result": "success"
1479 })),
1480 confidence: Some(0.95),
1481 metadata: {
1482 let mut meta = HashMap::new();
1483 meta.insert("secret".to_string(), serde_json::json!("confidential"));
1484 meta.insert("public".to_string(), serde_json::json!("open"));
1485 meta
1486 },
1487 };
1488
1489 let masked_response = logger.mask_pii_in_response(response_data).unwrap();
1490
1491 assert!(!masked_response.content.contains("123-45-6789"));
1493 assert!(!masked_response.content.contains("user@example.com"));
1494
1495 if let Some(result) = &masked_response.tool_result {
1497 assert_eq!(result["password"], "***");
1498 assert_eq!(result["result"], "success");
1499 }
1500
1501 assert_eq!(masked_response.metadata["secret"], "***");
1503 assert_eq!(masked_response.metadata["public"], "open");
1504 }
1505}