foundationdb/
metrics.rs

1use crate::transaction::ConflictingKeyRange;
2use std::collections::HashMap;
3use std::sync::{Arc, Mutex};
4
5/// Label key-value pairs for metrics
6pub type Labels = Vec<(String, String)>;
7
8/// Unique key for a metric: name + labels
9#[derive(Clone, Hash, Eq, PartialEq, Debug)]
10pub struct MetricKey {
11    pub name: String,
12    pub labels: Labels,
13}
14
15impl MetricKey {
16    /// Create a new MetricKey
17    ///
18    /// # Arguments
19    /// * `name` - The name of the metric
20    /// * `labels` - Key-value pairs for labeling the metric
21    ///
22    /// # Returns
23    /// * `MetricKey` - A new MetricKey instance
24    pub fn new(name: &str, labels: &[(&str, &str)]) -> Self {
25        // Convert labels to owned strings
26        let mut sorted_labels: Labels = labels
27            .iter()
28            .map(|(k, v)| (k.to_string(), v.to_string()))
29            .collect();
30
31        // Sort labels by key to ensure consistent ordering
32        sorted_labels.sort_by(|a, b| a.0.cmp(&b.0));
33
34        Self {
35            name: name.to_string(),
36            labels: sorted_labels,
37        }
38    }
39}
40
41/// Tracks metrics for a transaction.
42///
43/// This struct maintains transaction metrics protected by a mutex to allow safe concurrent access.
44#[derive(Debug, Clone, Default)]
45pub struct TransactionMetrics {
46    /// All metrics for the transaction, organized by category
47    pub metrics: Arc<Mutex<MetricsReport>>,
48}
49
50/// Transaction-level information that persists across retries
51#[derive(Debug, Default, Clone)]
52pub struct TransactionInfo {
53    /// Number of retries performed
54    pub retries: u64,
55    /// Number of retries caused by commit conflicts (`not_committed`, error 1020)
56    pub conflict_count: u64,
57    /// Transaction read version
58    pub read_version: Option<i64>,
59    /// Transaction commit version
60    pub commit_version: Option<i64>,
61}
62
63/// Data structure containing the actual metrics for a transaction,
64/// organized into different categories
65#[derive(Debug, Clone, Default)]
66pub struct MetricsReport {
67    /// Metrics for the current transaction attempt
68    pub current: CounterMetrics,
69    /// Aggregated metrics across all transaction attempts, including retries
70    pub total: CounterMetrics,
71    /// Time-related metrics for performance analysis
72    pub time: TimingMetrics,
73    /// Custom metrics for the current transaction attempt
74    pub custom_metrics: HashMap<MetricKey, u64>,
75    /// Transaction-level information
76    pub transaction: TransactionInfo,
77    /// Conflicting key ranges from the last commit conflict.
78    /// Empty if `TransactionOption::ReportConflictingKeys` was not enabled or the read failed.
79    pub conflicting_keys: Vec<ConflictingKeyRange>,
80}
81
82impl MetricsReport {
83    /// Set the read version for the transaction
84    ///
85    /// # Arguments
86    /// * `version` - The read version
87    pub fn set_read_version(&mut self, version: i64) {
88        self.transaction.read_version = Some(version);
89    }
90
91    /// Set the commit version for the transaction
92    ///
93    /// # Arguments
94    /// * `version` - The commit version
95    pub fn set_commit_version(&mut self, version: i64) {
96        self.transaction.commit_version = Some(version);
97    }
98
99    /// Increment the retry counter
100    pub fn increment_retries(&mut self) {
101        self.transaction.retries += 1;
102    }
103}
104
105/// Time-related metrics for performance analysis of transaction execution phases
106#[derive(Debug, Default, Clone)]
107pub struct TimingMetrics {
108    /// Time spent in commit phase execution
109    pub commit_execution_ms: u64,
110    /// Time spent handling errors and retries
111    pub on_error_execution_ms: Vec<u64>,
112    /// Total transaction duration from start to finish
113    pub total_execution_ms: u64,
114}
115
116impl TimingMetrics {
117    /// Record commit execution time
118    ///
119    /// # Arguments
120    /// * `duration_ms` - The duration of the commit execution in milliseconds
121    pub fn record_commit_time(&mut self, duration_ms: u64) {
122        self.commit_execution_ms = duration_ms;
123    }
124
125    /// Add an error execution time to the list
126    ///
127    /// # Arguments
128    /// * `duration_ms` - The duration of the error handling in milliseconds
129    pub fn add_error_time(&mut self, duration_ms: u64) {
130        self.on_error_execution_ms.push(duration_ms);
131    }
132
133    /// Set the total execution time
134    ///
135    /// # Arguments
136    /// * `duration_ms` - The total duration of the transaction in milliseconds
137    pub fn set_execution_time(&mut self, duration_ms: u64) {
138        self.total_execution_ms = duration_ms;
139    }
140
141    /// Get the sum of all error handling times
142    ///
143    /// # Returns
144    /// * `u64` - The total time spent handling errors in milliseconds
145    pub fn get_total_error_time(&self) -> u64 {
146        self.on_error_execution_ms.iter().sum()
147    }
148}
149
150/// Represents a FoundationDB command.
151pub enum FdbCommand {
152    /// Atomic operation
153    Atomic,
154    /// Clear operation
155    Clear,
156    /// Clear range operation
157    ClearRange,
158    /// Get operation
159    Get(u64, u64),
160    GetRange(u64, u64),
161    Set(u64),
162}
163
164const INCREMENT: u64 = 1;
165
166impl TransactionMetrics {
167    /// Create a new instance of TransactionMetrics
168    pub fn new() -> Self {
169        Self {
170            metrics: Arc::new(Mutex::new(MetricsReport::default())),
171        }
172    }
173
174    /// Set the read version for the transaction
175    ///
176    /// # Arguments
177    /// * `version` - The read version
178    pub fn set_read_version(&self, version: i64) {
179        let mut data = self
180            .metrics
181            .lock()
182            .unwrap_or_else(|poisoned| poisoned.into_inner());
183        data.set_read_version(version);
184    }
185
186    /// Set the commit version for the transaction
187    ///
188    /// # Arguments
189    /// * `version` - The commit version
190    pub fn set_commit_version(&self, version: i64) {
191        let mut data = self
192            .metrics
193            .lock()
194            .unwrap_or_else(|poisoned| poisoned.into_inner());
195        data.set_commit_version(version);
196    }
197
198    /// Resets the current metrics and increments the retry counter in total metrics.
199    ///
200    /// This method is called when a transaction is retried due to a conflict or other retryable error.
201    /// It increments the retry count in the transaction info and resets the current metrics to zero.
202    /// It also merges current custom metrics into total custom metrics before clearing them.
203    pub fn reset_current(&self) {
204        let mut data = self
205            .metrics
206            .lock()
207            .unwrap_or_else(|poisoned| poisoned.into_inner());
208        data.increment_retries();
209        data.current = CounterMetrics::default();
210        data.custom_metrics.clear();
211    }
212
213    /// Increment the retry counter
214    pub fn increment_retries(&self) {
215        let mut data = self
216            .metrics
217            .lock()
218            .unwrap_or_else(|poisoned| poisoned.into_inner());
219        data.increment_retries();
220    }
221
222    /// Increment the conflict counter
223    pub fn increment_conflict_count(&self) {
224        let mut data = self
225            .metrics
226            .lock()
227            .unwrap_or_else(|poisoned| poisoned.into_inner());
228        data.transaction.conflict_count += 1;
229    }
230
231    /// Reports metrics for a specific FDB command by incrementing the appropriate counters.
232    ///
233    /// This method updates both the current and total metrics for the given command.
234    ///
235    /// # Arguments
236    /// * `fdb_command` - The FDB command to report metrics for
237    pub fn report_metrics(&self, fdb_command: FdbCommand) {
238        let mut data = self
239            .metrics
240            .lock()
241            .unwrap_or_else(|poisoned| poisoned.into_inner());
242        data.current.increment(&fdb_command);
243        data.total.increment(&fdb_command);
244    }
245
246    /// Get the number of retries
247    ///
248    /// # Returns
249    /// * `u64` - The number of retries
250    pub fn get_retries(&self) -> u64 {
251        let data = self
252            .metrics
253            .lock()
254            .unwrap_or_else(|poisoned| poisoned.into_inner());
255        data.transaction.retries
256    }
257
258    /// Returns a clone of all metrics data.
259    ///
260    /// # Returns
261    /// * `MetricsData` - A clone of all metrics data
262    pub fn get_metrics_data(&self) -> MetricsReport {
263        let data = self
264            .metrics
265            .lock()
266            .unwrap_or_else(|poisoned| poisoned.into_inner());
267        data.clone()
268    }
269
270    /// Returns a clone of the transaction information.
271    ///
272    /// # Returns
273    /// * `TransactionInfo` - A clone of the transaction information
274    pub fn get_transaction_info(&self) -> TransactionInfo {
275        let data = self
276            .metrics
277            .lock()
278            .unwrap_or_else(|poisoned| poisoned.into_inner());
279        data.transaction.clone()
280    }
281
282    /// Set a custom metric
283    ///
284    /// # Arguments
285    /// * `name` - The name of the metric
286    /// * `value` - The value to set
287    /// * `labels` - Key-value pairs for labeling the metric
288    pub fn set_custom(&self, name: &str, value: u64, labels: &[(&str, &str)]) {
289        let mut data = self
290            .metrics
291            .lock()
292            .unwrap_or_else(|poisoned| poisoned.into_inner());
293        let key = MetricKey::new(name, labels);
294        data.custom_metrics.insert(key.clone(), value);
295    }
296
297    /// Increment a custom metric
298    ///
299    /// # Arguments
300    /// * `name` - The name of the metric
301    /// * `amount` - The amount to increment by
302    /// * `labels` - Key-value pairs for labeling the metric
303    pub fn increment_custom(&self, name: &str, amount: u64, labels: &[(&str, &str)]) {
304        let mut data = self
305            .metrics
306            .lock()
307            .unwrap_or_else(|poisoned| poisoned.into_inner());
308        let key = MetricKey::new(name, labels);
309        // Increment in both current and total custom metrics
310        *data.custom_metrics.entry(key.clone()).or_insert(0) += amount;
311    }
312
313    /// Set the conflicting key ranges from a commit conflict.
314    pub fn set_conflicting_keys(&self, keys: Vec<ConflictingKeyRange>) {
315        let mut data = self
316            .metrics
317            .lock()
318            .unwrap_or_else(|poisoned| poisoned.into_inner());
319        data.conflicting_keys = keys;
320    }
321
322    /// Record commit execution time
323    ///
324    /// # Arguments
325    /// * `duration_ms` - The duration of the commit execution in milliseconds
326    pub fn record_commit_time(&self, duration_ms: u64) {
327        let mut data = self
328            .metrics
329            .lock()
330            .unwrap_or_else(|poisoned| poisoned.into_inner());
331        data.time.commit_execution_ms = duration_ms;
332    }
333
334    /// Add an error execution time to the list
335    ///
336    /// # Arguments
337    /// * `duration_ms` - The duration of the error handling in milliseconds
338    pub fn add_error_time(&self, duration_ms: u64) {
339        let mut data = self
340            .metrics
341            .lock()
342            .unwrap_or_else(|poisoned| poisoned.into_inner());
343        data.time.on_error_execution_ms.push(duration_ms);
344    }
345
346    /// Set the total execution time
347    ///
348    /// # Arguments
349    /// * `duration_ms` - The total duration of the transaction in milliseconds
350    pub fn set_execution_time(&self, duration_ms: u64) {
351        let mut data = self
352            .metrics
353            .lock()
354            .unwrap_or_else(|poisoned| poisoned.into_inner());
355        data.time.set_execution_time(duration_ms);
356    }
357
358    /// Get the sum of all error handling times
359    ///
360    /// # Returns
361    /// * `u64` - The total time spent handling errors in milliseconds
362    pub fn get_total_error_time(&self) -> u64 {
363        let data = self
364            .metrics
365            .lock()
366            .unwrap_or_else(|poisoned| poisoned.into_inner());
367        data.time.on_error_execution_ms.iter().sum()
368    }
369}
370
371/// `CounterMetrics` is used in two contexts within the metrics system:
372/// - As `current` metrics: tracking operations for the current transaction attempt
373/// - As `total` metrics: aggregating operations across all transaction attempts including retries
374///
375/// Each counter is incremented when the corresponding operation is performed, allowing
376/// for detailed analysis of transaction behavior and performance characteristics.
377#[derive(Debug, Default, Clone)]
378pub struct CounterMetrics {
379    // ATOMIC OP
380    /// Number of atomic operations performed (add, and, or, etc.)
381    pub call_atomic_op: u64,
382    // CLEAR
383    /// Number of key clear operations performed
384    pub call_clear: u64,
385    // CLEAR RANGE
386    /// Number of range clear operations performed
387    pub call_clear_range: u64,
388    // GET
389    /// Number of get operations performed
390    pub call_get: u64,
391    /// Total number of key-value pairs fetched across all read operations
392    pub keys_values_fetched: u64,
393    /// Total number of bytes read from the database
394    pub bytes_read: u64,
395    // SET
396    /// Number of set operations performed
397    pub call_set: u64,
398    /// Total number of bytes written to the database
399    pub bytes_written: u64,
400}
401
402impl CounterMetrics {
403    /// Increments the metrics for a given FDB command.
404    ///
405    /// This method updates the metrics counters based on the type of FDB command.
406    ///
407    /// # Arguments
408    /// * `fdb_command` - The FDB command to increment metrics for
409    pub fn increment(&mut self, fdb_command: &FdbCommand) {
410        match fdb_command {
411            FdbCommand::Atomic => self.call_atomic_op += INCREMENT,
412            FdbCommand::Clear => self.call_clear += INCREMENT,
413            FdbCommand::ClearRange => self.call_clear_range += INCREMENT,
414            FdbCommand::Get(bytes_count, kv_fetched) => {
415                self.keys_values_fetched += *kv_fetched;
416                self.bytes_read += *bytes_count;
417                self.call_get += INCREMENT
418            }
419            FdbCommand::GetRange(bytes_count, keys_values_fetched) => {
420                self.keys_values_fetched += *keys_values_fetched;
421                self.bytes_read += *bytes_count;
422            }
423            FdbCommand::Set(bytes_count) => {
424                self.bytes_written += *bytes_count;
425                self.call_set += INCREMENT
426            }
427        }
428    }
429}
430
431#[cfg(test)]
432mod tests {
433    use super::*;
434    use std::collections::HashSet;
435
436    #[test]
437    fn test_metric_key_equality() {
438        // Test that different combinations of the same labels are considered equal
439        let key1 = MetricKey::new("counter", &[("region", "us-west"), ("service", "api")]);
440        let key2 = MetricKey::new("counter", &[("service", "api"), ("region", "us-west")]);
441
442        // Same labels in different order should be considered equal
443        assert_eq!(key1, key2);
444
445        // Different label values should produce different keys
446        let key3 = MetricKey::new("counter", &[("region", "us-east"), ("service", "api")]);
447        assert_ne!(key1, key3);
448
449        // Different label keys should produce different keys
450        let key4 = MetricKey::new("counter", &[("zone", "us-west"), ("service", "api")]);
451        assert_ne!(key1, key4);
452
453        // Different metric names should produce different keys
454        let key5 = MetricKey::new("timer", &[("region", "us-west"), ("service", "api")]);
455        assert_ne!(key1, key5);
456    }
457
458    #[test]
459    fn test_metric_key_in_hashmap() {
460        let mut metrics = HashMap::new();
461
462        // Insert metrics with different label combinations
463        metrics.insert(
464            MetricKey::new("counter", &[("region", "us-west"), ("service", "api")]),
465            100,
466        );
467        metrics.insert(
468            MetricKey::new("counter", &[("region", "us-east"), ("service", "api")]),
469            200,
470        );
471        metrics.insert(
472            MetricKey::new("timer", &[("region", "us-west"), ("service", "api")]),
473            300,
474        );
475
476        // Verify we can retrieve metrics with the same label combinations
477        assert_eq!(
478            metrics.get(&MetricKey::new(
479                "counter",
480                &[("region", "us-west"), ("service", "api")]
481            )),
482            Some(&100)
483        );
484
485        // Verify we can retrieve metrics with labels in different order
486        assert_eq!(
487            metrics.get(&MetricKey::new(
488                "counter",
489                &[("service", "api"), ("region", "us-west")]
490            )),
491            Some(&100)
492        );
493
494        // Verify different label values produce different keys
495        assert_eq!(
496            metrics.get(&MetricKey::new(
497                "counter",
498                &[("region", "us-east"), ("service", "api")]
499            )),
500            Some(&200)
501        );
502
503        // Verify different metric names produce different keys
504        assert_eq!(
505            metrics.get(&MetricKey::new(
506                "timer",
507                &[("region", "us-west"), ("service", "api")]
508            )),
509            Some(&300)
510        );
511    }
512
513    #[test]
514    fn test_metric_key_label_order_independence() {
515        // Create a HashSet to verify uniqueness
516        let mut unique_keys = HashSet::new();
517
518        // Add keys with the same labels in different orders
519        unique_keys.insert(MetricKey::new(
520            "counter",
521            &[("a", "1"), ("b", "2"), ("c", "3")],
522        ));
523        unique_keys.insert(MetricKey::new(
524            "counter",
525            &[("a", "1"), ("c", "3"), ("b", "2")],
526        ));
527        unique_keys.insert(MetricKey::new(
528            "counter",
529            &[("b", "2"), ("a", "1"), ("c", "3")],
530        ));
531        unique_keys.insert(MetricKey::new(
532            "counter",
533            &[("b", "2"), ("c", "3"), ("a", "1")],
534        ));
535        unique_keys.insert(MetricKey::new(
536            "counter",
537            &[("c", "3"), ("a", "1"), ("b", "2")],
538        ));
539        unique_keys.insert(MetricKey::new(
540            "counter",
541            &[("c", "3"), ("b", "2"), ("a", "1")],
542        ));
543
544        // All permutations should be considered the same key
545        assert_eq!(unique_keys.len(), 1);
546    }
547
548    #[test]
549    fn test_custom_metrics_operations() {
550        // Create a metrics instance
551        let metrics = TransactionMetrics::new();
552
553        // Set initial value for a custom metric
554        metrics.set_custom(
555            "api_calls",
556            100,
557            &[("endpoint", "users"), ("method", "GET")],
558        );
559
560        // Get the metrics data and verify the value was set
561        let data = metrics.get_metrics_data();
562        let key = MetricKey::new("api_calls", &[("endpoint", "users"), ("method", "GET")]);
563        assert_eq!(data.custom_metrics.get(&key).copied(), Some(100));
564
565        // Increment the same metric
566        metrics.increment_custom("api_calls", 50, &[("endpoint", "users"), ("method", "GET")]);
567
568        // Verify the value was incremented
569        let data = metrics.get_metrics_data();
570        assert_eq!(data.custom_metrics.get(&key).copied(), Some(150));
571
572        // Set the same metric to a new value (overwrite)
573        metrics.set_custom(
574            "api_calls",
575            200,
576            &[("endpoint", "users"), ("method", "GET")],
577        );
578
579        // Verify the value was overwritten
580        let data = metrics.get_metrics_data();
581        assert_eq!(data.custom_metrics.get(&key).copied(), Some(200));
582
583        // Test with different label order
584        metrics.increment_custom("api_calls", 75, &[("method", "GET"), ("endpoint", "users")]);
585
586        // Verify the value was incremented regardless of label order
587        let data = metrics.get_metrics_data();
588        assert_eq!(data.custom_metrics.get(&key).copied(), Some(275));
589    }
590}