Skip to main content

symbi_runtime/
logging.rs

1//! Encrypted Logging Module for Model I/O
2//!
3//! This module provides secure logging capabilities for all model interactions
4//! including prompts, tool calls, outputs, and latency metrics. All sensitive
5//! data is encrypted using AES-256-GCM before being written to logs.
6//!
7//! # Security Features
8//! - Automatic encryption of all sensitive log data
9//! - PII/PHI detection and masking
10//! - Secure key management integration
11//! - Structured logging with metadata
12//! - Configurable retention policies
13
14use 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/// Errors that can occur during logging operations
28#[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/// Configuration for the logging module
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct LoggingConfig {
55    /// Enable/disable encrypted logging
56    pub enabled: bool,
57    /// Log file path
58    pub log_file_path: String,
59    /// Secret key name in SecretStore for encryption key
60    pub encryption_key_name: String,
61    /// Environment variable for encryption key (fallback only)
62    pub encryption_key_env: Option<String>,
63    /// Maximum log entry size in bytes
64    pub max_entry_size: usize,
65    /// Log retention period in days
66    pub retention_days: u32,
67    /// Enable PII detection and masking
68    pub enable_pii_masking: bool,
69    /// Batch size for log writes
70    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, // 1MB
81            retention_days: 90,
82            enable_pii_masking: true,
83            batch_size: 100,
84        }
85    }
86}
87
88/// Type of model interaction being logged
89#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
90pub enum ModelInteractionType {
91    /// Direct model prompt/completion
92    Completion,
93    /// Tool call execution
94    ToolCall,
95    /// RAG query processing
96    RagQuery,
97    /// Agent task execution
98    AgentExecution,
99}
100
101/// Log entry for model I/O operations
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct ModelLogEntry {
104    /// Unique identifier for this log entry
105    pub id: String,
106    /// Agent that initiated the request
107    pub agent_id: AgentId,
108    /// Type of model interaction
109    pub interaction_type: ModelInteractionType,
110    /// Timestamp when the interaction started
111    pub timestamp: DateTime<Utc>,
112    /// Duration of the interaction
113    pub latency_ms: u64,
114    /// Model/service used
115    pub model_identifier: String,
116    /// Encrypted request data
117    pub request_data: EncryptedData,
118    /// Encrypted response data
119    pub response_data: Option<EncryptedData>,
120    /// Metadata (non-sensitive)
121    pub metadata: HashMap<String, String>,
122    /// Error information if the interaction failed
123    pub error: Option<String>,
124    /// Token usage statistics
125    pub token_usage: Option<TokenUsage>,
126}
127
128/// Raw (unencrypted) request data structure
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct RequestData {
131    /// The prompt or query sent to the model
132    pub prompt: String,
133    /// Tool name (if applicable)
134    pub tool_name: Option<String>,
135    /// Tool arguments (if applicable)
136    pub tool_arguments: Option<serde_json::Value>,
137    /// Additional parameters
138    pub parameters: HashMap<String, serde_json::Value>,
139}
140
141/// Raw (unencrypted) response data structure
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct ResponseData {
144    /// Model's response content
145    pub content: String,
146    /// Tool execution result (if applicable)
147    pub tool_result: Option<serde_json::Value>,
148    /// Confidence score (if available)
149    pub confidence: Option<f64>,
150    /// Additional response metadata
151    pub metadata: HashMap<String, serde_json::Value>,
152}
153
154/// Token usage statistics
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct TokenUsage {
157    /// Input tokens consumed
158    pub input_tokens: u32,
159    /// Output tokens generated
160    pub output_tokens: u32,
161    /// Total tokens used
162    pub total_tokens: u32,
163}
164
165/// Encrypted model I/O logger
166pub 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    /// Create a new model logger with the given configuration and secret store
177    pub fn new(
178        config: LoggingConfig,
179        secret_store: Option<Arc<dyn SecretStore>>,
180    ) -> Result<Self, LoggingError> {
181        let crypto = Aes256GcmCrypto::new();
182
183        // Get encryption key
184        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    /// Create a new logger with default configuration (no secret store)
195    pub fn with_defaults() -> Result<Self, LoggingError> {
196        Self::new(LoggingConfig::default(), None)
197    }
198
199    /// Get encryption key from SecretStore, environment variable, or generate new one
200    fn get_encryption_key(
201        config: &LoggingConfig,
202        secret_store: &Option<Arc<dyn SecretStore>>,
203    ) -> Result<String, LoggingError> {
204        // Try SecretStore first if available
205        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        // Try environment variable as fallback
217        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        // Final fallback: generate or retrieve from keychain
225        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    /// Log a model request (before execution)
234    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        // Mask PII if enabled
250        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        // Encrypt request data
257        let encrypted_request = self.encrypt_request_data(&sanitized_request)?;
258
259        // Create log entry (without response data initially)
260        let log_entry = ModelLogEntry {
261            id: entry_id.clone(),
262            agent_id,
263            interaction_type,
264            timestamp,
265            latency_ms: 0, // Will be updated when response is logged
266            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    /// Log a model response (after execution)
281    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        // Mask PII if enabled
294        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        // Encrypt response data
301        let encrypted_response = self.encrypt_response_data(&sanitized_response)?;
302
303        // Create update entry
304        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    /// Convenience method to log a complete interaction
320    #[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        // Mask PII if enabled
341        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        // Encrypt data
354        let encrypted_request = self.encrypt_request_data(&sanitized_request)?;
355        let encrypted_response = self.encrypt_response_data(&sanitized_response)?;
356
357        // Create complete log entry
358        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    /// Encrypt request data
379    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    /// Encrypt response data
391    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    /// Basic PII masking for request data
403    fn mask_pii_in_request(&self, mut data: RequestData) -> Result<RequestData, LoggingError> {
404        // Basic patterns for common PII
405        data.prompt = self.mask_sensitive_patterns(&data.prompt);
406
407        // Mask tool arguments if they contain sensitive data
408        if let Some(ref mut args) = data.tool_arguments {
409            *args = self.mask_json_values(args.clone());
410        }
411
412        // Mask parameters (check key names for sensitivity)
413        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    /// Basic PII masking for response data
425    fn mask_pii_in_response(&self, mut data: ResponseData) -> Result<ResponseData, LoggingError> {
426        data.content = self.mask_sensitive_patterns(&data.content);
427
428        // Mask tool results
429        if let Some(ref mut result) = data.tool_result {
430            *result = self.mask_json_values(result.clone());
431        }
432
433        // Mask metadata (check key names for sensitivity)
434        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    /// Mask common sensitive patterns in text
446    fn mask_sensitive_patterns(&self, text: &str) -> String {
447        use regex::Regex;
448
449        // Common patterns to mask
450        let patterns = [
451            (r"\b\d{3}-\d{2}-\d{4}\b", "***-**-****"), // SSN
452            (
453                r"\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b",
454                "****-****-****-****",
455            ), // Credit card
456            (
457                r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b",
458                "***@***.***",
459            ), // Email
460            (r"\b\d{3}[\s-]?\d{3}[\s-]?\d{4}\b", "***-***-****"), // Phone
461            (r"\bAPI[_\s]*KEY[\s:=]*[A-Za-z0-9+/]{20,}\b", "API_KEY=***"), // API keys
462            (r"\bTOKEN[\s:=]*[A-Za-z0-9+/]{20,}\b", "TOKEN=***"), // Tokens
463        ];
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    /// Mask sensitive values in JSON structures
476    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                    // Mask known sensitive keys completely
484                    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    /// Check if a key name indicates sensitive data
500    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    /// Write a log entry to storage
525    async fn write_log_entry(&self, entry: &ModelLogEntry) -> Result<(), LoggingError> {
526        // Ensure log directory exists
527        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        // Serialize and append to log file
532        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    /// Write a log update (for response data)
541    async fn write_log_update(&self, update: &serde_json::Value) -> Result<(), LoggingError> {
542        // In a production implementation, this would update the existing entry
543        // For now, we'll append an update record
544        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    /// Decrypt and read log entries (for debugging/analysis)
560    pub async fn decrypt_log_entry(
561        &self,
562        encrypted_entry: &ModelLogEntry,
563    ) -> Result<(RequestData, Option<ResponseData>), LoggingError> {
564        // Decrypt request data
565        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        // Decrypt response data if present
576        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
592/// Helper trait for timing model operations
593pub trait TimedOperation {
594    /// Execute an operation and return the result with timing
595    #[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    // Mock SecretStore for testing
622    #[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        // Set environment variable for fallback
696        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        // Test SecretStore priority
713        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        // Should get from secret store, not environment
724        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        // Test request encryption/decryption
774        let encrypted_request = logger.encrypt_request_data(&request_data).unwrap();
775        let encrypted_response = logger.encrypt_response_data(&response_data).unwrap();
776
777        // Create a mock log entry for decryption testing
778        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        // Test various PII patterns
808        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        // Sensitive keys should be masked
846        assert_eq!(masked_json["password"], "***");
847        assert_eq!(masked_json["api_key"], "***");
848        assert_eq!(masked_json["nested"]["token"], "***");
849
850        // Non-sensitive keys should remain
851        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        // Sensitive keys
861        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        // Non-sensitive keys
893        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        // Log request
935        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        // Log response
949        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        // Verify log file was created and updated
973        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        // Verify log file was created
1036        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        // When logging is disabled, should return empty string
1057        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        // Test default config
1119        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        // Test all ModelInteractionType variants
1135        let types = vec![
1136            ModelInteractionType::Completion,
1137            ModelInteractionType::ToolCall,
1138            ModelInteractionType::RagQuery,
1139            ModelInteractionType::AgentExecution,
1140        ];
1141
1142        for interaction_type in types {
1143            // Ensure they can be serialized/deserialized
1144            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        // Test serialization
1159        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        // Test serialization/deserialization
1192        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        // Check prompt masking
1224        assert!(!masked_request.prompt.contains("123-45-6789"));
1225        assert!(!masked_request.prompt.contains("user@example.com"));
1226
1227        // Check tool arguments masking
1228        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        // Check parameters masking
1235        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        // Check content masking
1261        assert!(!masked_response.content.contains("123-45-6789"));
1262        assert!(!masked_response.content.contains("user@example.com"));
1263
1264        // Check tool result masking
1265        if let Some(result) = &masked_response.tool_result {
1266            assert_eq!(result["password"], "***");
1267            assert_eq!(result["result"], "success");
1268        }
1269
1270        // Check metadata masking
1271        assert_eq!(masked_response.metadata["secret"], "***");
1272        assert_eq!(masked_response.metadata["public"], "open");
1273    }
1274}