snm_brightdata_client/extras/
logger.rs

1// crates/snm-brightdata-client/src/extras/logger.rs
2use serde::{Deserialize, Serialize};
3use serde_json::{json, Value};
4use std::collections::HashMap;
5use tokio::fs;
6use chrono::{DateTime, Utc};
7use log::{info, error};
8use std::path::Path;
9
10#[derive(Debug, Serialize, Deserialize)]
11pub struct ExecutionLog {
12    pub execution_id: String,
13    pub tool_name: String,
14    pub timestamp: DateTime<Utc>,
15    pub request_data: RequestData,
16    pub response_data: Option<ResponseData>,
17    pub execution_metadata: ExecutionMetadata,
18    pub brightdata_details: Option<BrightDataDetails>,
19}
20
21#[derive(Debug, Serialize, Deserialize)]
22pub struct RequestData {
23    pub parameters: Value,
24    pub method: String,
25    pub user_agent: Option<String>,
26    pub ip_address: Option<String>,
27    pub headers: HashMap<String, String>,
28}
29
30#[derive(Debug, Serialize, Deserialize)]
31pub struct ResponseData {
32    pub content: Value,
33    pub status: String,
34    pub error: Option<String>,
35    pub content_length: usize,
36    pub processing_time_ms: u64,
37}
38
39#[derive(Debug, Serialize, Deserialize)]
40pub struct ExecutionMetadata {
41    pub start_time: DateTime<Utc>,
42    pub end_time: Option<DateTime<Utc>>,
43    pub duration_ms: Option<u64>,
44    pub memory_usage_mb: Option<f64>,
45    pub cpu_usage_percent: Option<f64>,
46    pub success: bool,
47}
48
49#[derive(Debug, Serialize, Deserialize)]
50pub struct BrightDataDetails {
51    pub zone_used: String,
52    pub target_url: String,
53    pub request_payload: Value,
54    pub response_status: u16,
55    pub response_headers: HashMap<String, String>,
56    pub data_format: String,
57    pub cost_estimate: Option<f64>,
58}
59
60pub struct JsonLogger {
61    log_directory: String,
62}
63
64impl JsonLogger {
65    pub fn new(log_directory: Option<String>) -> Self {
66        let log_dir = log_directory.unwrap_or_else(|| "logs".to_string());
67        Self {
68            log_directory: log_dir,
69        }
70    }
71
72    pub async fn ensure_log_directory(&self) -> Result<(), std::io::Error> {
73        if !Path::new(&self.log_directory).exists() {
74            fs::create_dir_all(&self.log_directory).await?;
75        }
76        Ok(())
77    }
78
79    pub async fn start_execution(&self, tool_name: &str, parameters: Value) -> ExecutionLog {
80        let execution_id = self.generate_execution_id(tool_name);
81        let timestamp = Utc::now();
82
83        ExecutionLog {
84            execution_id: execution_id.clone(),
85            tool_name: tool_name.to_string(),
86            timestamp,
87            request_data: RequestData {
88                parameters,
89                method: "execute".to_string(),
90                user_agent: None,
91                ip_address: None,
92                headers: HashMap::new(),
93            },
94            response_data: None,
95            execution_metadata: ExecutionMetadata {
96                start_time: timestamp,
97                end_time: None,
98                duration_ms: None,
99                memory_usage_mb: None,
100                cpu_usage_percent: None,
101                success: false,
102            },
103            brightdata_details: None,
104        }
105    }
106
107    pub async fn complete_execution(
108        &self,
109        mut log: ExecutionLog,
110        response: Value,
111        success: bool,
112        error: Option<String>,
113    ) -> Result<(), Box<dyn std::error::Error>> {
114        let end_time = Utc::now();
115        let duration = (end_time - log.execution_metadata.start_time).num_milliseconds() as u64;
116
117        log.execution_metadata.end_time = Some(end_time);
118        log.execution_metadata.duration_ms = Some(duration);
119        log.execution_metadata.success = success;
120
121        log.response_data = Some(ResponseData {
122            content_length: response.to_string().len(),
123            content: response,
124            status: if success { "success".to_string() } else { "error".to_string() },
125            error,
126            processing_time_ms: duration,
127        });
128
129        self.save_execution_log(&log).await?;
130        Ok(())
131    }
132
133    pub async fn log_brightdata_request(
134        &self,
135        execution_id: &str,
136        zone: &str,
137        url: &str,
138        payload: Value,
139        response_status: u16,
140        response_headers: HashMap<String, String>,
141        data_format: &str,
142    ) -> Result<(), Box<dyn std::error::Error>> {
143        let brightdata_log = json!({
144            "execution_id": execution_id,
145            "timestamp": Utc::now().to_rfc3339(),
146            "zone_used": zone,
147            "target_url": url,
148            "request_payload": payload,
149            "response_status": response_status,
150            "response_headers": response_headers,
151            "data_format": data_format,
152            "cost_estimate": self.estimate_cost(zone, &payload)
153        });
154
155        let filename = format!("{}/brightdata_{}.json", self.log_directory, execution_id);
156        self.write_json_file(&filename, &brightdata_log).await?;
157        
158        info!("💾 BrightData request logged: {}", filename);
159        Ok(())
160    }
161
162    async fn save_execution_log(&self, log: &ExecutionLog) -> Result<(), Box<dyn std::error::Error>> {
163        self.ensure_log_directory().await?;
164
165        // Save main execution log
166        let main_filename = format!("{}/execution_{}.json", self.log_directory, log.execution_id);
167        let log_json = serde_json::to_value(log)?;
168        self.write_json_file(&main_filename, &log_json).await?;
169
170        // Save separate request file
171        let request_filename = format!("{}/request_{}.json", self.log_directory, log.execution_id);
172        let request_json = serde_json::to_value(&log.request_data)?;
173        self.write_json_file(&request_filename, &request_json).await?;
174
175        // Save separate response file (if available)
176        if let Some(ref response_data) = log.response_data {
177            let response_filename = format!("{}/response_{}.json", self.log_directory, log.execution_id);
178            let response_json = serde_json::to_value(response_data)?;
179            self.write_json_file(&response_filename, &response_json).await?;
180        }
181
182        // Save daily summary
183        self.update_daily_summary(log).await?;
184
185        info!("💾 Execution logged: {}", main_filename);
186        Ok(())
187    }
188
189    async fn write_json_file(&self, filename: &str, data: &Value) -> Result<(), Box<dyn std::error::Error>> {
190        let pretty_json = serde_json::to_string_pretty(data)?;
191        fs::write(filename, pretty_json).await?;
192        Ok(())
193    }
194
195    async fn update_daily_summary(&self, log: &ExecutionLog) -> Result<(), Box<dyn std::error::Error>> {
196        let date = log.timestamp.format("%Y-%m-%d");
197        let summary_filename = format!("{}/daily_summary_{}.json", self.log_directory, date);
198
199        // Load existing summary or create new
200        let mut summary = if Path::new(&summary_filename).exists() {
201            let content = fs::read_to_string(&summary_filename).await?;
202            serde_json::from_str::<Value>(&content).unwrap_or_else(|_| json!({}))
203        } else {
204            json!({
205                "date": date.to_string(),
206                "total_executions": 0,
207                "successful_executions": 0,
208                "failed_executions": 0,
209                "tools_used": {},
210                "total_processing_time_ms": 0,
211                "average_processing_time_ms": 0.0
212            })
213        };
214
215        // Update summary
216        summary["total_executions"] = json!(summary["total_executions"].as_u64().unwrap_or(0) + 1);
217        
218        if log.execution_metadata.success {
219            summary["successful_executions"] = json!(summary["successful_executions"].as_u64().unwrap_or(0) + 1);
220        } else {
221            summary["failed_executions"] = json!(summary["failed_executions"].as_u64().unwrap_or(0) + 1);
222        }
223
224        // Update tool usage
225        let mut tools_used = summary["tools_used"].as_object().cloned().unwrap_or_default();
226        let current_count = tools_used.get(&log.tool_name)
227            .and_then(|v| v.as_u64())
228            .unwrap_or(0);
229        tools_used.insert(log.tool_name.clone(), json!(current_count + 1));
230        summary["tools_used"] = json!(tools_used);
231
232        // Update timing
233        if let Some(duration) = log.execution_metadata.duration_ms {
234            let total_time = summary["total_processing_time_ms"].as_u64().unwrap_or(0) + duration;
235            summary["total_processing_time_ms"] = json!(total_time);
236            
237            let total_executions = summary["total_executions"].as_u64().unwrap_or(1);
238            let avg_time = total_time as f64 / total_executions as f64;
239            summary["average_processing_time_ms"] = json!(avg_time);
240        }
241
242        summary["last_updated"] = json!(Utc::now().to_rfc3339());
243
244        self.write_json_file(&summary_filename, &summary).await?;
245        Ok(())
246    }
247
248    fn generate_execution_id(&self, tool_name: &str) -> String {
249        let timestamp = Utc::now().format("%Y%m%d_%H%M%S%.3f");
250        format!("{}_{}", tool_name, timestamp)
251    }
252
253    fn estimate_cost(&self, zone: &str, _payload: &Value) -> Option<f64> {
254        // Simple cost estimation based on zone
255        match zone {
256            z if z.contains("serp") => Some(0.01), // $0.01 per SERP request
257            z if z.contains("browser") => Some(0.05), // $0.05 per browser request
258            _ => Some(0.001), // $0.001 per web unlocker request
259        }
260    }
261
262    // Utility methods for querying logs
263    pub async fn get_execution_logs_by_tool(&self, tool_name: &str) -> Result<Vec<ExecutionLog>, Box<dyn std::error::Error>> {
264        let mut logs = Vec::new();
265        let mut dir = fs::read_dir(&self.log_directory).await?;
266
267        while let Some(entry) = dir.next_entry().await? {
268            let filename = entry.file_name().to_string_lossy().to_string();
269            if filename.starts_with("execution_") && filename.contains(tool_name) {
270                let content = fs::read_to_string(entry.path()).await?;
271                if let Ok(log) = serde_json::from_str::<ExecutionLog>(&content) {
272                    logs.push(log);
273                }
274            }
275        }
276
277        logs.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
278        Ok(logs)
279    }
280
281    pub async fn get_daily_summary(&self, date: &str) -> Result<Option<Value>, Box<dyn std::error::Error>> {
282        let summary_filename = format!("{}/daily_summary_{}.json", self.log_directory, date);
283        
284        if Path::new(&summary_filename).exists() {
285            let content = fs::read_to_string(summary_filename).await?;
286            let summary = serde_json::from_str::<Value>(&content)?;
287            Ok(Some(summary))
288        } else {
289            Ok(None)
290        }
291    }
292}
293
294// Singleton instance for global access
295lazy_static::lazy_static! {
296    pub static ref JSON_LOGGER: JsonLogger = JsonLogger::new(
297        std::env::var("BRIGHTDATA_JSON_LOG_PATH").ok()
298            .or_else(|| std::env::var("LOG_DIRECTORY").ok())
299    );
300}