Skip to main content

nodedb_lite/nodedb/
diagnostic.rs

1//! Diagnostic dump — JSON blob for remote bug reports.
2//!
3//! `db.diagnostic_dump()` produces a structured report containing:
4//! - Redacted sync config (tokens masked)
5//! - Storage stats (namespace counts)
6//! - Engine stats (from health API)
7//! - Pending delta summary (count + oldest timestamp, not actual data)
8//! - Flow control state
9//! - Recent error summary
10//!
11//! Designed for production debugging: safe to send to support (no user data,
12//! no embeddings, no document contents).
13
14use serde::Serialize;
15
16use crate::storage::engine::StorageEngine;
17
18use super::core::NodeDbLite;
19use super::health::HealthStatus;
20use super::lock_ext::LockExt;
21
22/// Full diagnostic dump for bug reports.
23#[derive(Debug, Serialize)]
24pub struct DiagnosticDump {
25    /// Timestamp of this dump (epoch ms).
26    pub timestamp_ms: u64,
27    /// Health status snapshot.
28    pub health: HealthStatus,
29    /// Pending delta summary (no actual data).
30    pub pending_summary: PendingSummary,
31    /// Storage namespace entry counts.
32    pub storage_counts: StorageCounts,
33    /// Engine version and build info.
34    pub build_info: BuildInfo,
35}
36
37/// Pending delta summary — safe to include in bug reports.
38#[derive(Debug, Serialize)]
39pub struct PendingSummary {
40    /// Number of pending deltas.
41    pub count: usize,
42    /// Total bytes of pending deltas.
43    pub total_bytes: usize,
44    /// Oldest pending mutation_id (0 if no pending).
45    pub oldest_mutation_id: u64,
46    /// Newest pending mutation_id (0 if no pending).
47    pub newest_mutation_id: u64,
48    /// Collections with pending deltas.
49    pub collections: Vec<String>,
50}
51
52/// Storage namespace entry counts.
53#[derive(Debug, Serialize)]
54pub struct StorageCounts {
55    pub meta: u64,
56    pub vector: u64,
57    pub graph: u64,
58    pub crdt: u64,
59    pub loro_state: u64,
60}
61
62/// Build and version info.
63#[derive(Debug, Serialize)]
64pub struct BuildInfo {
65    pub version: &'static str,
66    pub target: &'static str,
67    pub profile: &'static str,
68}
69
70impl<S: StorageEngine> NodeDbLite<S> {
71    /// Generate a diagnostic dump suitable for bug reports.
72    ///
73    /// Contains NO user data, NO document contents, NO embeddings.
74    /// Safe to send to support channels.
75    pub async fn diagnostic_dump(&self) -> DiagnosticDump {
76        let health = self.health();
77
78        let pending_summary = {
79            let crdt = self.crdt.lock_or_recover();
80            let pending = crdt.pending_deltas();
81            let total_bytes: usize = pending.iter().map(|d| d.delta_bytes.len()).sum();
82            let oldest = pending.iter().map(|d| d.mutation_id).min().unwrap_or(0);
83            let newest = pending.iter().map(|d| d.mutation_id).max().unwrap_or(0);
84            let mut collections: Vec<String> = pending
85                .iter()
86                .map(|d| d.collection.clone())
87                .collect::<std::collections::HashSet<_>>()
88                .into_iter()
89                .collect();
90            collections.sort();
91
92            PendingSummary {
93                count: pending.len(),
94                total_bytes,
95                oldest_mutation_id: oldest,
96                newest_mutation_id: newest,
97                collections,
98            }
99        };
100
101        let storage_counts = StorageCounts {
102            meta: self
103                .storage
104                .count(nodedb_types::Namespace::Meta)
105                .await
106                .unwrap_or(0),
107            vector: self
108                .storage
109                .count(nodedb_types::Namespace::Vector)
110                .await
111                .unwrap_or(0),
112            graph: self
113                .storage
114                .count(nodedb_types::Namespace::Graph)
115                .await
116                .unwrap_or(0),
117            crdt: self
118                .storage
119                .count(nodedb_types::Namespace::Crdt)
120                .await
121                .unwrap_or(0),
122            loro_state: self
123                .storage
124                .count(nodedb_types::Namespace::LoroState)
125                .await
126                .unwrap_or(0),
127        };
128
129        let build_info = BuildInfo {
130            version: env!("CARGO_PKG_VERSION"),
131            target: std::env::consts::ARCH,
132            profile: if cfg!(debug_assertions) {
133                "debug"
134            } else {
135                "release"
136            },
137        };
138
139        DiagnosticDump {
140            timestamp_ms: crate::runtime::now_millis(),
141            health,
142            pending_summary,
143            storage_counts,
144            build_info,
145        }
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use crate::PagedbStorageMem;
153
154    async fn make_db() -> std::sync::Arc<NodeDbLite<PagedbStorageMem>> {
155        let storage = PagedbStorageMem::open_in_memory().await.unwrap();
156        NodeDbLite::open(storage).await.unwrap()
157    }
158
159    #[tokio::test]
160    async fn diagnostic_dump_empty_db() {
161        let db = make_db().await;
162        let dump = db.diagnostic_dump().await;
163
164        assert_eq!(dump.pending_summary.count, 0);
165        assert!(!dump.build_info.version.is_empty());
166        assert!(dump.timestamp_ms > 0);
167
168        // Should serialize to JSON cleanly.
169        let json = sonic_rs::to_string_pretty(&dump).unwrap();
170        assert!(json.contains("\"health\""));
171        assert!(json.contains("\"pending_summary\""));
172        assert!(json.contains("\"storage_counts\""));
173    }
174
175    #[tokio::test]
176    async fn diagnostic_dump_with_pending() {
177        use nodedb_client::NodeDb;
178        use nodedb_types::document::Document;
179
180        let db = make_db().await;
181        let mut doc = Document::new("d1");
182        doc.set("key", nodedb_types::Value::String("value".into()));
183        db.document_put("test_coll", doc).await.unwrap();
184
185        let dump = db.diagnostic_dump().await;
186        assert!(dump.pending_summary.count > 0);
187        assert!(
188            dump.pending_summary
189                .collections
190                .contains(&"test_coll".to_string())
191        );
192        assert!(dump.pending_summary.oldest_mutation_id > 0);
193    }
194}