term_guard/repository/
mod.rs

1//! Metrics repository framework for persisting and querying analyzer results.
2//!
3//! This module provides a flexible framework for storing and retrieving metrics
4//! across different storage backends (e.g., filesystem, S3, databases).
5
6use async_trait::async_trait;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10use crate::analyzers::context::AnalyzerContext;
11use crate::error::{Result, TermError};
12
13pub mod datafusion_executor;
14pub mod in_memory;
15pub mod query;
16pub mod result_key;
17
18pub use datafusion_executor::{DataFusionQueryExecutor, DataFusionQueryExecutorExt};
19pub use in_memory::InMemoryRepository;
20pub use query::{MetricsQuery, SortOrder};
21pub use result_key::ResultKey;
22
23/// Trait for implementing metrics storage backends.
24///
25/// This trait defines the interface that all metrics repositories must implement,
26/// allowing for flexible storage options while maintaining a consistent API.
27///
28/// # Example
29///
30/// ```rust,ignore
31/// use term_guard::repository::{MetricsRepository, ResultKey};
32/// use term_guard::analyzers::AnalyzerContext;
33///
34/// #[derive(Clone)]
35/// struct InMemoryRepository {
36///     metrics: Arc<RwLock<HashMap<ResultKey, AnalyzerContext>>>,
37/// }
38///
39/// #[async_trait]
40/// impl MetricsRepository for InMemoryRepository {
41///     async fn save(&self, key: ResultKey, metrics: AnalyzerContext) -> Result<()> {
42///         let mut store = self.metrics.write().await;
43///         store.insert(key, metrics);
44///         Ok(())
45///     }
46///
47///     async fn load(&self) -> MetricsQuery {
48///         MetricsQuery::new(Arc::new(self.clone()))
49///     }
50///
51///     async fn delete(&self, key: ResultKey) -> Result<()> {
52///         let mut store = self.metrics.write().await;
53///         store.remove(&key);
54///         Ok(())
55///     }
56/// }
57/// ```
58#[async_trait]
59pub trait MetricsRepository: Send + Sync {
60    /// Saves metrics with the given key.
61    ///
62    /// # Arguments
63    ///
64    /// * `key` - The result key containing timestamp and tags
65    /// * `metrics` - The analyzer context containing computed metrics
66    ///
67    /// # Errors
68    ///
69    /// Returns an error if the save operation fails (e.g., I/O error, permission issue).
70    async fn save(&self, key: ResultKey, metrics: AnalyzerContext) -> Result<()>;
71
72    /// Creates a query builder for retrieving metrics.
73    ///
74    /// The returned `MetricsQuery` can be used to filter and retrieve metrics
75    /// based on various criteria such as time range, tags, and analyzer types.
76    ///
77    /// # Example
78    ///
79    /// ```rust,ignore
80    /// let query = repository.load().await
81    ///     .after(start_timestamp)
82    ///     .before(end_timestamp)
83    ///     .with_tag("environment", "production");
84    ///
85    /// let results = query.execute().await?;
86    /// ```
87    async fn load(&self) -> MetricsQuery;
88
89    /// Deletes metrics with the given key.
90    ///
91    /// # Arguments
92    ///
93    /// * `key` - The result key identifying the metrics to delete
94    ///
95    /// # Errors
96    ///
97    /// Returns an error if the delete operation fails or if the key doesn't exist.
98    async fn delete(&self, key: ResultKey) -> Result<()>;
99
100    /// Lists all available keys in the repository.
101    ///
102    /// This method is optional and may not be implemented by all backends.
103    /// It's primarily useful for debugging and management operations.
104    ///
105    /// # Errors
106    ///
107    /// Returns an error if the listing operation fails.
108    async fn list_keys(&self) -> Result<Vec<ResultKey>> {
109        Err(TermError::NotSupported(
110            "list_keys not implemented for this repository".to_string(),
111        ))
112    }
113
114    /// Loads a specific metric by key.
115    ///
116    /// # Arguments
117    ///
118    /// * `key` - The result key to load
119    ///
120    /// # Returns
121    ///
122    /// Returns the AnalyzerContext if found, None otherwise.
123    async fn get(&self, _key: &ResultKey) -> Result<Option<AnalyzerContext>> {
124        // Default implementation returns None
125        // Repositories should override this for actual data retrieval
126        Ok(None)
127    }
128
129    /// Checks if a key exists in the repository.
130    ///
131    /// # Arguments
132    ///
133    /// * `key` - The result key to check
134    ///
135    /// # Returns
136    ///
137    /// Returns `true` if the key exists, `false` otherwise.
138    async fn exists(&self, key: &ResultKey) -> Result<bool> {
139        // Default implementation using list_keys
140        let keys = self.list_keys().await?;
141        Ok(keys.iter().any(|k| k == key))
142    }
143
144    /// Returns metadata about the repository.
145    ///
146    /// This can include information such as the backend type, configuration,
147    /// and storage statistics.
148    async fn metadata(&self) -> Result<RepositoryMetadata> {
149        Ok(RepositoryMetadata::default())
150    }
151}
152
153/// Metadata about a metrics repository.
154#[derive(Debug, Clone, Serialize, Deserialize, Default)]
155pub struct RepositoryMetadata {
156    /// The type of repository backend (e.g., "filesystem", "s3", "memory").
157    pub backend_type: Option<String>,
158
159    /// Total number of stored metrics.
160    pub total_metrics: Option<usize>,
161
162    /// Total storage size in bytes.
163    pub storage_size_bytes: Option<u64>,
164
165    /// Repository-specific configuration.
166    pub config: HashMap<String, String>,
167
168    /// Last modification timestamp.
169    pub last_modified: Option<chrono::DateTime<chrono::Utc>>,
170}
171
172impl RepositoryMetadata {
173    /// Creates a new repository metadata instance.
174    pub fn new(backend_type: impl Into<String>) -> Self {
175        Self {
176            backend_type: Some(backend_type.into()),
177            ..Default::default()
178        }
179    }
180
181    /// Sets the total number of metrics.
182    pub fn with_total_metrics(mut self, count: usize) -> Self {
183        self.total_metrics = Some(count);
184        self
185    }
186
187    /// Sets the storage size in bytes.
188    pub fn with_storage_size(mut self, size: u64) -> Self {
189        self.storage_size_bytes = Some(size);
190        self
191    }
192
193    /// Adds a configuration parameter.
194    pub fn with_config(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
195        self.config.insert(key.into(), value.into());
196        self
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn test_repository_metadata_builder() {
206        let metadata = RepositoryMetadata::new("filesystem")
207            .with_total_metrics(100)
208            .with_storage_size(1024 * 1024)
209            .with_config("path", "/var/metrics");
210
211        assert_eq!(metadata.backend_type, Some("filesystem".to_string()));
212        assert_eq!(metadata.total_metrics, Some(100));
213        assert_eq!(metadata.storage_size_bytes, Some(1024 * 1024));
214        assert_eq!(
215            metadata.config.get("path"),
216            Some(&"/var/metrics".to_string())
217        );
218    }
219}