llm_cost_ops/export/
config.rs

1// Export and reporting configuration
2
3use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6use super::formats::ExportFormat;
7use super::reports::ReportType;
8
9/// Export configuration
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ExportConfig {
12    /// Default export format
13    pub default_format: ExportFormat,
14
15    /// Output directory for file exports
16    pub output_dir: PathBuf,
17
18    /// Maximum export size in bytes
19    pub max_export_size: usize,
20
21    /// Enable compression for exports
22    pub enable_compression: bool,
23
24    /// Email delivery configuration
25    pub email: Option<EmailConfig>,
26
27    /// Storage configuration
28    pub storage: StorageConfig,
29
30    /// Scheduled reports configuration
31    pub scheduled_reports: Vec<ScheduledReportConfig>,
32
33    /// Report templates directory
34    pub templates_dir: PathBuf,
35}
36
37impl Default for ExportConfig {
38    fn default() -> Self {
39        Self {
40            default_format: ExportFormat::Csv,
41            output_dir: PathBuf::from("./exports"),
42            max_export_size: 100 * 1024 * 1024, // 100MB
43            enable_compression: true,
44            email: None,
45            storage: StorageConfig::default(),
46            scheduled_reports: Vec::new(),
47            templates_dir: PathBuf::from("./templates"),
48        }
49    }
50}
51
52/// Email delivery configuration
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct EmailConfig {
55    /// SMTP server host
56    pub smtp_host: String,
57
58    /// SMTP server port
59    pub smtp_port: u16,
60
61    /// SMTP username
62    pub smtp_username: String,
63
64    /// SMTP password
65    pub smtp_password: String,
66
67    /// Use TLS
68    pub use_tls: bool,
69
70    /// Use STARTTLS
71    pub use_starttls: bool,
72
73    /// From email address
74    pub from_email: String,
75
76    /// From name
77    pub from_name: String,
78
79    /// Default recipients for reports
80    pub default_recipients: Vec<String>,
81
82    /// Email template for reports
83    pub template_name: String,
84
85    /// Connection timeout in seconds
86    pub timeout_secs: u64,
87}
88
89impl Default for EmailConfig {
90    fn default() -> Self {
91        Self {
92            smtp_host: "localhost".to_string(),
93            smtp_port: 587,
94            smtp_username: String::new(),
95            smtp_password: String::new(),
96            use_tls: false,
97            use_starttls: true,
98            from_email: "reports@llm-cost-ops.local".to_string(),
99            from_name: "LLM Cost Ops".to_string(),
100            default_recipients: Vec::new(),
101            template_name: "default".to_string(),
102            timeout_secs: 30,
103        }
104    }
105}
106
107/// Storage configuration
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct StorageConfig {
110    /// Storage backend type
111    pub backend: StorageBackend,
112
113    /// Local filesystem configuration
114    pub local: Option<LocalStorageConfig>,
115
116    /// S3 configuration
117    pub s3: Option<S3StorageConfig>,
118
119    /// Retention policy in days
120    pub retention_days: u32,
121
122    /// Enable automatic cleanup
123    pub auto_cleanup: bool,
124}
125
126impl Default for StorageConfig {
127    fn default() -> Self {
128        Self {
129            backend: StorageBackend::Local,
130            local: Some(LocalStorageConfig::default()),
131            s3: None,
132            retention_days: 90,
133            auto_cleanup: true,
134        }
135    }
136}
137
138/// Storage backend type
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
140#[serde(rename_all = "lowercase")]
141pub enum StorageBackend {
142    Local,
143    S3,
144}
145
146/// Local storage configuration
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct LocalStorageConfig {
149    /// Base directory for storing reports
150    pub base_dir: PathBuf,
151
152    /// Use subdirectories by date
153    pub use_date_subdirs: bool,
154
155    /// File naming pattern
156    pub file_pattern: String,
157}
158
159impl Default for LocalStorageConfig {
160    fn default() -> Self {
161        Self {
162            base_dir: PathBuf::from("./reports"),
163            use_date_subdirs: true,
164            file_pattern: "{report_type}_{date}_{id}.{ext}".to_string(),
165        }
166    }
167}
168
169/// S3 storage configuration
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct S3StorageConfig {
172    /// S3 bucket name
173    pub bucket: String,
174
175    /// S3 region
176    pub region: String,
177
178    /// S3 access key
179    pub access_key: String,
180
181    /// S3 secret key
182    pub secret_key: String,
183
184    /// S3 endpoint (for S3-compatible services)
185    pub endpoint: Option<String>,
186
187    /// Prefix for report objects
188    pub prefix: String,
189
190    /// Storage class
191    pub storage_class: String,
192}
193
194impl Default for S3StorageConfig {
195    fn default() -> Self {
196        Self {
197            bucket: String::new(),
198            region: "us-east-1".to_string(),
199            access_key: String::new(),
200            secret_key: String::new(),
201            endpoint: None,
202            prefix: "reports/".to_string(),
203            storage_class: "STANDARD".to_string(),
204        }
205    }
206}
207
208/// Scheduled report configuration
209#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct ScheduledReportConfig {
211    /// Unique identifier for the scheduled report
212    pub id: String,
213
214    /// Report type
215    pub report_type: ReportType,
216
217    /// Schedule in cron format
218    pub schedule: String,
219
220    /// Export format
221    pub format: ExportFormat,
222
223    /// Delivery methods
224    pub delivery: Vec<DeliveryTarget>,
225
226    /// Report filters
227    pub filters: ReportFiltersConfig,
228
229    /// Enabled flag
230    pub enabled: bool,
231
232    /// Timezone for scheduling
233    pub timezone: String,
234}
235
236/// Delivery target
237#[derive(Debug, Clone, Serialize, Deserialize)]
238#[serde(tag = "type", rename_all = "lowercase")]
239pub enum DeliveryTarget {
240    Email {
241        recipients: Vec<String>,
242        subject: String,
243        body_template: Option<String>,
244    },
245    Storage {
246        path: Option<String>,
247    },
248    Webhook {
249        url: String,
250        headers: std::collections::HashMap<String, String>,
251    },
252}
253
254/// Report filters configuration
255#[derive(Debug, Clone, Default, Serialize, Deserialize)]
256pub struct ReportFiltersConfig {
257    pub provider: Option<String>,
258    pub model: Option<String>,
259    pub user_id: Option<String>,
260    pub resource_type: Option<String>,
261    pub organization_id: Option<String>,
262    pub tags: std::collections::HashMap<String, String>,
263}
264
265/// Report template configuration
266#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct ReportTemplate {
268    /// Template name
269    pub name: String,
270
271    /// Template file path
272    pub file_path: PathBuf,
273
274    /// Template format
275    pub format: TemplateFormat,
276
277    /// Default variables
278    pub default_vars: std::collections::HashMap<String, String>,
279}
280
281/// Template format
282#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
283#[serde(rename_all = "lowercase")]
284pub enum TemplateFormat {
285    Html,
286    Text,
287    Markdown,
288}
289
290/// Compression configuration
291#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct CompressionConfig {
293    /// Compression algorithm
294    pub algorithm: CompressionAlgorithm,
295
296    /// Compression level (1-9)
297    pub level: u32,
298
299    /// Minimum size for compression (bytes)
300    pub min_size: usize,
301}
302
303impl Default for CompressionConfig {
304    fn default() -> Self {
305        Self {
306            algorithm: CompressionAlgorithm::Gzip,
307            level: 6,
308            min_size: 1024, // 1KB
309        }
310    }
311}
312
313/// Compression algorithm
314#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
315#[serde(rename_all = "lowercase")]
316pub enum CompressionAlgorithm {
317    Gzip,
318    Brotli,
319    Zstd,
320}
321
322impl ExportConfig {
323    /// Load configuration from file
324    pub fn from_file(path: impl AsRef<std::path::Path>) -> Result<Self, Box<dyn std::error::Error>> {
325        let contents = std::fs::read_to_string(path)?;
326        let config = toml::from_str(&contents)?;
327        Ok(config)
328    }
329
330    /// Save configuration to file
331    pub fn to_file(&self, path: impl AsRef<std::path::Path>) -> Result<(), Box<dyn std::error::Error>> {
332        let contents = toml::to_string_pretty(self)?;
333        std::fs::write(path, contents)?;
334        Ok(())
335    }
336
337    /// Get email configuration
338    pub fn email_config(&self) -> Option<&EmailConfig> {
339        self.email.as_ref()
340    }
341
342    /// Get storage configuration
343    pub fn storage_config(&self) -> &StorageConfig {
344        &self.storage
345    }
346
347    /// Get scheduled reports
348    pub fn scheduled_reports(&self) -> &[ScheduledReportConfig] {
349        &self.scheduled_reports
350    }
351
352    /// Add scheduled report
353    pub fn add_scheduled_report(&mut self, config: ScheduledReportConfig) {
354        self.scheduled_reports.push(config);
355    }
356
357    /// Remove scheduled report
358    pub fn remove_scheduled_report(&mut self, id: &str) -> Option<ScheduledReportConfig> {
359        if let Some(pos) = self.scheduled_reports.iter().position(|r| r.id == id) {
360            Some(self.scheduled_reports.remove(pos))
361        } else {
362            None
363        }
364    }
365
366    /// Get scheduled report by ID
367    pub fn get_scheduled_report(&self, id: &str) -> Option<&ScheduledReportConfig> {
368        self.scheduled_reports.iter().find(|r| r.id == id)
369    }
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375
376    #[test]
377    fn test_default_config() {
378        let config = ExportConfig::default();
379        assert_eq!(config.default_format, ExportFormat::Csv);
380        assert_eq!(config.max_export_size, 100 * 1024 * 1024);
381        assert!(config.enable_compression);
382    }
383
384    #[test]
385    fn test_email_config_default() {
386        let config = EmailConfig::default();
387        assert_eq!(config.smtp_port, 587);
388        assert!(config.use_starttls);
389        assert!(!config.use_tls);
390    }
391
392    #[test]
393    fn test_storage_config_default() {
394        let config = StorageConfig::default();
395        assert_eq!(config.backend, StorageBackend::Local);
396        assert_eq!(config.retention_days, 90);
397        assert!(config.auto_cleanup);
398    }
399
400    #[test]
401    fn test_add_remove_scheduled_report() {
402        let mut config = ExportConfig::default();
403
404        let scheduled = ScheduledReportConfig {
405            id: "test-report".to_string(),
406            report_type: ReportType::Cost,
407            schedule: "0 0 * * *".to_string(),
408            format: ExportFormat::Csv,
409            delivery: vec![],
410            filters: ReportFiltersConfig::default(),
411            enabled: true,
412            timezone: "UTC".to_string(),
413        };
414
415        config.add_scheduled_report(scheduled.clone());
416        assert_eq!(config.scheduled_reports().len(), 1);
417
418        let removed = config.remove_scheduled_report("test-report");
419        assert!(removed.is_some());
420        assert_eq!(config.scheduled_reports().len(), 0);
421    }
422
423    #[test]
424    fn test_compression_config() {
425        let config = CompressionConfig::default();
426        assert_eq!(config.algorithm, CompressionAlgorithm::Gzip);
427        assert_eq!(config.level, 6);
428        assert_eq!(config.min_size, 1024);
429    }
430}