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}