linkrs_server/http/handlers/statistics/
database.rs1use axum::{extract::State, response::Json as JsonResponse};
6
7use crate::http::{error::HttpError, state::AppState};
8use crate::storage::{
9 StorageClient, StorageOperationContextOps, StorageSchemaContextOps, StorageSyncContextOps,
10};
11use linkrs_metrics::MetricType;
12use linkrs_wire::meta::{
13 CheckpointSummary, DatabaseOverviewResponse, DatabasePerformance, DatabaseSearchSummary,
14 DatabaseSpaces, DatabaseStorage, LatencyPercentilesUs,
15};
16
17#[utoipa::path(
18 get,
19 path = "/v1/statistics/database",
20 tag = "Statistics",
21 responses(
22 (status = 200, body = DatabaseOverviewResponse, description = "Database statistics"),
23 (status = 500, description = "Internal error")
24 )
25)]
26pub async fn database<
28 S: StorageClient
29 + StorageSchemaContextOps
30 + StorageSyncContextOps
31 + StorageOperationContextOps
32 + Clone
33 + Send
34 + Sync
35 + 'static,
36>(
37 State(state): State<AppState<S>>,
38) -> Result<JsonResponse<DatabaseOverviewResponse>, HttpError> {
39 let stats_manager = state.server.get_stats_manager();
40 let storage = state.server.get_storage();
41
42 let storage_stats = {
44 let storage = storage.clone();
45 tokio::task::spawn_blocking(move || {
46 let storage = storage.read();
47 storage.get_storage_stats()
48 })
49 .await
50 .map_err(|e| HttpError::internal(format!("Failed to get storage statistics: {:?}", e)))?
51 };
52
53 Ok(JsonResponse(collect_database_overview(
54 stats_manager,
55 &storage_stats,
56 )))
57}
58
59pub(crate) fn collect_database_overview(
60 stats_manager: &linkrs_metrics::StatsManager,
61 storage_stats: &crate::storage::StorageStats,
62) -> DatabaseOverviewResponse {
63 let total_queries = stats_manager.get_value(MetricType::NumQueries).unwrap_or(0);
65 let active_queries = stats_manager
66 .get_value(MetricType::NumActiveQueries)
67 .unwrap_or(0);
68
69 let cache_size = stats_manager.query_cache_size();
71
72 let recent_queries = stats_manager.get_recent_queries(100);
74 let avg_latency_ms = if recent_queries.is_empty() {
75 0.0
76 } else {
77 recent_queries
78 .iter()
79 .map(|q| q.total_duration_us)
80 .sum::<u64>() as f64
81 / recent_queries.len() as f64
82 / 1000.0
83 };
84
85 let qps = stats_manager.get_current_qps() as f64;
87
88 let num_search_queries = stats_manager
90 .get_value(MetricType::NumSearchQueries)
91 .unwrap_or(0);
92 let num_search_errors = stats_manager
93 .get_value(MetricType::NumSearchErrors)
94 .unwrap_or(0);
95 let search_latency_ms = stats_manager
96 .get_value(MetricType::SearchLatencyMs)
97 .unwrap_or(0);
98 let num_index_operations = stats_manager
99 .get_value(MetricType::NumIndexOperations)
100 .unwrap_or(0);
101 let num_delete_operations = stats_manager
102 .get_value(MetricType::NumDeleteOperations)
103 .unwrap_or(0);
104 let cache_hit_count = stats_manager
105 .get_value(MetricType::SearchCacheHitCount)
106 .unwrap_or(0);
107 let cache_miss_count = stats_manager
108 .get_value(MetricType::SearchCacheMissCount)
109 .unwrap_or(0);
110
111 let avg_search_latency_ms = if num_search_queries > 0 {
112 search_latency_ms as f64 / num_search_queries as f64
113 } else {
114 0.0
115 };
116
117 let search_cache_hit_rate = if cache_hit_count + cache_miss_count > 0 {
118 cache_hit_count as f64 / (cache_hit_count + cache_miss_count) as f64
119 } else {
120 0.0
121 };
122
123 let latency = stats_manager.query_latency_snapshot();
124 let errors = stats_manager.error_snapshot();
125 let storage_snapshot = stats_manager.storage_snapshot();
126 let checkpoint = stats_manager.checkpoint_snapshot();
127
128 DatabaseOverviewResponse {
129 spaces: DatabaseSpaces {
130 count: storage_stats.total_spaces,
131 total_vertices: storage_stats.total_vertices,
132 total_edges: storage_stats.total_edges,
133 },
134 storage: DatabaseStorage {
135 total_size_bytes: storage_stats.total_size_bytes,
139 index_size_bytes: storage_stats.index_size_bytes,
140 data_size_bytes: storage_stats.data_size_bytes,
141 fragmentation_permille: storage_snapshot.fragmentation_permille,
142 wasted_bytes: storage_snapshot.wasted_bytes,
143 tombstone_count: storage_snapshot.tombstone_count,
144 tombstone_memory_bytes: storage_snapshot.tombstone_memory_bytes,
145 dirty_pages: storage_snapshot.dirty_pages,
146 dirty_pages_total: storage_snapshot.dirty_pages_total,
147 checkpoint: CheckpointSummary {
148 success_count: checkpoint.success_count,
149 failure_count: checkpoint.failure_count,
150 trigger_count: checkpoint.trigger_count,
151 avg_duration_us: checkpoint.avg_duration_us,
152 triggered_by_wal_size: checkpoint.triggered_by_wal_size,
153 triggered_by_interval: checkpoint.triggered_by_interval,
154 triggered_explicit: checkpoint.triggered_explicit,
155 },
156 },
157 performance: DatabasePerformance {
158 total_queries,
159 active_queries,
160 query_cache_size: cache_size,
161 queries_per_second: qps,
162 avg_latency_ms,
163 cache_hit_rate: search_cache_hit_rate,
164 cache_hit_rate_source: "search_cache".to_string(),
165 error_total: errors.total_errors,
166 latency_percentiles_us: Some(LatencyPercentilesUs {
167 avg: latency.avg_us,
168 p50: latency.p50_us,
169 p95: latency.p95_us,
170 p99: latency.p99_us,
171 }),
172 },
173 search: DatabaseSearchSummary {
174 total_queries: num_search_queries,
175 total_errors: num_search_errors,
176 avg_latency_ms: avg_search_latency_ms,
177 total_index_operations: num_index_operations,
178 total_delete_operations: num_delete_operations,
179 cache_hit_count,
180 cache_miss_count,
181 cache_hit_rate: search_cache_hit_rate,
182 },
183 }
184}