1use poll_promise::Promise;
7use rustc_hash::{FxHashMap, FxHashSet};
8
9use crate::error::ClientError;
10use crate::now_unix_secs;
11use crate::prometheus::response::MetricLabels;
12use crate::request::QueryRequest;
13use crate::types::{MetricsBucket, MetricsGroup, ResultType};
14use crate::{
15 BackendInfo, HealthCheckResult, LabelsResult, MetricLabelsResult, MetricsClient, QueryResponse,
16 QueryResult,
17};
18
19#[derive(Debug, Clone)]
21struct DemoMetric {
22 name: String,
24 #[allow(dead_code)]
26 category: MetricCategory,
27 labels: Vec<String>,
29 label_values: FxHashMap<String, Vec<String>>,
31 metric_type: MetricType,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37enum MetricCategory {
38 System,
39 Http,
40 Runtime,
41 Application,
42 Database,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47enum MetricType {
48 Counter,
50 Gauge,
52 Histogram,
54}
55
56impl DemoMetric {
57 fn new(name: &str, category: MetricCategory, metric_type: MetricType) -> Self {
58 Self {
59 name: name.to_string(),
60 category,
61 labels: Vec::new(),
62 label_values: FxHashMap::default(),
63 metric_type,
64 }
65 }
66
67 fn with_label(mut self, name: &str, values: &[&str]) -> Self {
68 self.labels.push(name.to_string());
69 self.label_values.insert(
70 name.to_string(),
71 values.iter().map(|s| (*s).to_string()).collect(),
72 );
73 self
74 }
75}
76
77pub struct DemoMetricsClient {
93 metrics: Vec<DemoMetric>,
95}
96
97impl Default for DemoMetricsClient {
98 fn default() -> Self {
99 Self::new()
100 }
101}
102
103impl DemoMetricsClient {
104 #[must_use]
106 pub fn new() -> Self {
107 Self {
108 metrics: build_metrics_catalog(),
109 }
110 }
111
112 fn metric_names(&self) -> Vec<String> {
114 self.metrics.iter().map(|m| m.name.clone()).collect()
115 }
116
117 fn all_label_names(&self) -> Vec<String> {
119 let mut labels: Vec<String> = self
120 .metrics
121 .iter()
122 .flat_map(|m| m.labels.iter().cloned())
123 .collect();
124 labels.sort();
125 labels.dedup();
126 labels
127 }
128
129 fn get_metric_labels(&self, metric_name: &str) -> Option<MetricLabels> {
131 self.metrics
132 .iter()
133 .find(|m| m.name == metric_name)
134 .map(|m| MetricLabels {
135 labels: m
136 .label_values
137 .iter()
138 .map(|(k, v)| (k.clone(), v.clone()))
139 .collect(),
140 })
141 }
142
143 fn get_metric(&self, name: &str) -> Option<&DemoMetric> {
145 self.metrics.iter().find(|m| m.name == name)
146 }
147
148 fn generate_data(&self, request: &QueryRequest) -> QueryResponse {
150 let now_secs = now_unix_secs();
151 let end_secs = request
152 .end
153 .map(|ns| (ns / 1_000_000_000) as u64)
154 .unwrap_or(now_secs);
155 let start_secs = request
156 .start
157 .map(|ns| (ns / 1_000_000_000) as u64)
158 .unwrap_or(end_secs.saturating_sub(3600));
159
160 let step = request.step_secs.max(1);
161 let num_points = ((end_secs - start_secs) / step).min(1000) as usize;
162
163 let metric = self.get_metric(&request.metric);
165 let metric_type = metric.map(|m| m.metric_type).unwrap_or(MetricType::Gauge);
166
167 let series_labels = self.generate_series_labels(&request.metric);
169
170 let groups: Vec<MetricsGroup> = series_labels
171 .iter()
172 .enumerate()
173 .map(|(idx, labels)| {
174 let buckets = generate_buckets(
175 start_secs,
176 step,
177 num_points,
178 metric_type,
179 idx,
180 &request.query,
181 );
182
183 let group_str = labels
184 .iter()
185 .map(|(k, v)| format!("{k}=\"{v}\""))
186 .collect::<Vec<_>>()
187 .join(", ");
188
189 MetricsGroup {
190 group: format!("{{{group_str}}}"),
191 buckets,
192 }
193 })
194 .collect();
195
196 let start_ns = (start_secs as u128) * 1_000_000_000;
197 let end_ns = (end_secs as u128) * 1_000_000_000;
198 let granularity_ns = (step as u128) * 1_000_000_000;
199
200 QueryResponse {
201 metric: request.metric.clone(),
202 query: request.query.clone(),
203 parsed_agg: None,
204 parsed_filter: String::new(),
205 parsed_grouping: None,
206 parsed_time_range: None,
207 start: Some(start_ns),
208 end: Some(end_ns),
209 granularity_ns,
210 groups,
211 result_type: ResultType::Matrix,
213 }
214 }
215
216 fn generate_series_labels(&self, metric_name: &str) -> Vec<FxHashMap<String, String>> {
218 let Some(metric) = self.get_metric(metric_name) else {
219 let mut labels = FxHashMap::default();
221 labels.insert("env".to_string(), "prod".to_string());
222 labels.insert("host".to_string(), "server-1".to_string());
223 return vec![labels];
224 };
225
226 let mut combinations = Vec::new();
228
229 if metric.labels.is_empty() {
230 combinations.push(FxHashMap::default());
231 } else if metric.labels.len() == 1 {
232 let label = &metric.labels[0];
233 if let Some(values) = metric.label_values.get(label) {
234 for value in values.iter().take(4) {
235 let mut map = FxHashMap::default();
236 map.insert(label.clone(), value.clone());
237 combinations.push(map);
238 }
239 }
240 } else {
241 let label1 = &metric.labels[0];
243 let label2 = &metric.labels[1];
244 let values1 = metric.label_values.get(label1).cloned().unwrap_or_default();
245 let values2 = metric.label_values.get(label2).cloned().unwrap_or_default();
246
247 for v1 in values1.iter().take(2) {
248 for v2 in values2.iter().take(2) {
249 let mut map = FxHashMap::default();
250 map.insert(label1.clone(), v1.clone());
251 map.insert(label2.clone(), v2.clone());
252 combinations.push(map);
253 }
254 }
255 }
256
257 if combinations.is_empty() {
258 combinations.push(FxHashMap::default());
259 }
260
261 combinations
262 }
263}
264
265fn generate_buckets(
267 start_secs: u64,
268 step: u64,
269 num_points: usize,
270 metric_type: MetricType,
271 series_idx: usize,
272 query: &str,
273) -> Vec<MetricsBucket> {
274 let hash = query
276 .bytes()
277 .fold(0u64, |acc, b| acc.wrapping_add(u64::from(b)));
278 let series_offset = series_idx as f64 * 17.3;
279
280 (0..num_points)
281 .map(|i| {
282 let t = start_secs + (i as u64) * step;
283 let t_f = t as f64;
284
285 let value = match metric_type {
286 MetricType::Counter => {
287 let base_rate = 100.0 + (hash % 200) as f64;
289 let variation = (t_f / 300.0 + series_offset).sin() * 20.0;
290 (i as f64) * (base_rate + variation) / 60.0
291 }
292 MetricType::Gauge => {
293 let base = 50.0 + (hash % 50) as f64 + series_offset.abs() % 30.0;
295 let slow_wave = (t_f / 600.0 + series_offset).sin() * 15.0;
296 let fast_wave = (t_f / 60.0 + series_offset * 2.0).sin() * 5.0;
297 let spike = if (t_f / 1800.0 + series_offset).sin() > 0.95 {
298 30.0
299 } else {
300 0.0
301 };
302 (base + slow_wave + fast_wave + spike).max(0.0)
303 }
304 MetricType::Histogram => {
305 let base = 0.05 + (hash % 10) as f64 * 0.01;
307 let jitter = (t_f * 7.0 + series_offset).sin().abs() * 0.02;
308 let spike = if (t_f / 900.0 + series_offset).sin() > 0.9 {
309 0.5
310 } else {
311 0.0
312 };
313 base + jitter + spike
314 }
315 };
316
317 let start_ns = (t as u128) * 1_000_000_000;
318 let end_ns = start_ns + (step as u128) * 1_000_000_000;
319 MetricsBucket {
320 start: start_ns,
321 end: end_ns,
322 value,
323 count: 1,
324 }
325 })
326 .collect()
327}
328
329impl MetricsClient for DemoMetricsClient {
330 fn query(&self, request: QueryRequest, _ctx: &egui::Context) -> Promise<QueryResult> {
331 let response = self.generate_data(&request);
332 Promise::from_ready(Ok(response))
333 }
334
335 fn fetch_label_names(&self, _ctx: &egui::Context) -> Promise<LabelsResult> {
336 Promise::from_ready(Ok(self.all_label_names()))
337 }
338
339 fn fetch_label_values(&self, label: &str, _ctx: &egui::Context) -> Promise<LabelsResult> {
340 let values: Vec<String> = self
342 .metrics
343 .iter()
344 .filter_map(|m| m.label_values.get(label))
345 .flatten()
346 .cloned()
347 .collect::<FxHashSet<_>>()
348 .into_iter()
349 .collect();
350
351 Promise::from_ready(Ok(values))
352 }
353
354 fn fetch_metric_names(&self, _ctx: &egui::Context) -> Promise<LabelsResult> {
355 Promise::from_ready(Ok(self.metric_names()))
356 }
357
358 fn fetch_metric_labels(
359 &self,
360 metric: &str,
361 _ctx: &egui::Context,
362 ) -> Promise<MetricLabelsResult> {
363 match self.get_metric_labels(metric) {
364 Some(labels) => Promise::from_ready(Ok(labels)),
365 None => Promise::from_ready(Err(ClientError::BackendError {
366 status: 404,
367 message: format!("metric '{metric}' not found in demo catalog"),
368 })),
369 }
370 }
371
372 fn backend_type(&self) -> &'static str {
373 "demo"
374 }
375
376 fn health_check(&self, _ctx: &egui::Context) -> Promise<HealthCheckResult> {
377 Promise::from_ready(Ok(BackendInfo {
379 backend_type: "demo".to_string(),
380 version: "offline".to_string(),
381 }))
382 }
383}
384
385fn build_metrics_catalog() -> Vec<DemoMetric> {
387 vec![
388 DemoMetric::new(
390 "node_cpu_seconds_total",
391 MetricCategory::System,
392 MetricType::Counter,
393 )
394 .with_label("cpu", &["0", "1", "2", "3"])
395 .with_label("mode", &["user", "system", "idle", "iowait"]),
396 DemoMetric::new(
397 "node_memory_bytes",
398 MetricCategory::System,
399 MetricType::Gauge,
400 )
401 .with_label("type", &["used", "free", "cached", "buffers"]),
402 DemoMetric::new(
403 "node_disk_read_bytes_total",
404 MetricCategory::System,
405 MetricType::Counter,
406 )
407 .with_label("device", &["sda", "sdb", "nvme0n1"]),
408 DemoMetric::new(
409 "node_disk_write_bytes_total",
410 MetricCategory::System,
411 MetricType::Counter,
412 )
413 .with_label("device", &["sda", "sdb", "nvme0n1"]),
414 DemoMetric::new(
415 "node_network_receive_bytes_total",
416 MetricCategory::System,
417 MetricType::Counter,
418 )
419 .with_label("device", &["eth0", "eth1", "lo"]),
420 DemoMetric::new(
421 "node_network_transmit_bytes_total",
422 MetricCategory::System,
423 MetricType::Counter,
424 )
425 .with_label("device", &["eth0", "eth1", "lo"]),
426 DemoMetric::new("node_load1", MetricCategory::System, MetricType::Gauge),
427 DemoMetric::new("node_load5", MetricCategory::System, MetricType::Gauge),
428 DemoMetric::new("node_load15", MetricCategory::System, MetricType::Gauge),
429 DemoMetric::new(
431 "http_requests_total",
432 MetricCategory::Http,
433 MetricType::Counter,
434 )
435 .with_label("method", &["GET", "POST", "PUT", "DELETE"])
436 .with_label(
437 "path",
438 &["/api/users", "/api/orders", "/api/products", "/health"],
439 )
440 .with_label("status_code", &["200", "201", "400", "404", "500"]),
441 DemoMetric::new(
442 "http_request_duration_seconds",
443 MetricCategory::Http,
444 MetricType::Histogram,
445 )
446 .with_label("method", &["GET", "POST", "PUT", "DELETE"])
447 .with_label("path", &["/api/users", "/api/orders", "/api/products"])
448 .with_label("quantile", &["0.5", "0.9", "0.99"]),
449 DemoMetric::new(
450 "http_requests_in_flight",
451 MetricCategory::Http,
452 MetricType::Gauge,
453 )
454 .with_label("service", &["api", "web", "worker"]),
455 DemoMetric::new(
456 "http_response_size_bytes",
457 MetricCategory::Http,
458 MetricType::Histogram,
459 )
460 .with_label("method", &["GET", "POST"])
461 .with_label("quantile", &["0.5", "0.9", "0.99"]),
462 DemoMetric::new(
464 "tokio_runtime_workers_count",
465 MetricCategory::Runtime,
466 MetricType::Gauge,
467 )
468 .with_label("runtime", &["main", "blocking"]),
469 DemoMetric::new(
470 "tokio_runtime_blocking_threads",
471 MetricCategory::Runtime,
472 MetricType::Gauge,
473 )
474 .with_label("runtime", &["main"]),
475 DemoMetric::new(
476 "tokio_tasks_spawned_total",
477 MetricCategory::Runtime,
478 MetricType::Counter,
479 )
480 .with_label("runtime", &["main", "blocking"]),
481 DemoMetric::new(
482 "tokio_task_poll_duration_seconds",
483 MetricCategory::Runtime,
484 MetricType::Histogram,
485 )
486 .with_label("quantile", &["0.5", "0.9", "0.99"]),
487 DemoMetric::new(
489 "app_cache_hits_total",
490 MetricCategory::Application,
491 MetricType::Counter,
492 )
493 .with_label("cache", &["users", "sessions", "products"]),
494 DemoMetric::new(
495 "app_cache_misses_total",
496 MetricCategory::Application,
497 MetricType::Counter,
498 )
499 .with_label("cache", &["users", "sessions", "products"]),
500 DemoMetric::new(
501 "app_queue_depth",
502 MetricCategory::Application,
503 MetricType::Gauge,
504 )
505 .with_label("queue", &["orders", "notifications", "emails"]),
506 DemoMetric::new(
507 "app_active_users",
508 MetricCategory::Application,
509 MetricType::Gauge,
510 )
511 .with_label("env", &["prod", "staging"]),
512 DemoMetric::new(
514 "db_connections_active",
515 MetricCategory::Database,
516 MetricType::Gauge,
517 )
518 .with_label("pool", &["primary", "replica"])
519 .with_label("database", &["users", "orders"]),
520 DemoMetric::new(
521 "db_connections_idle",
522 MetricCategory::Database,
523 MetricType::Gauge,
524 )
525 .with_label("pool", &["primary", "replica"]),
526 DemoMetric::new(
527 "db_query_duration_seconds",
528 MetricCategory::Database,
529 MetricType::Histogram,
530 )
531 .with_label("query_type", &["select", "insert", "update", "delete"])
532 .with_label("quantile", &["0.5", "0.9", "0.99"]),
533 DemoMetric::new(
534 "db_transactions_total",
535 MetricCategory::Database,
536 MetricType::Counter,
537 )
538 .with_label("status", &["commit", "rollback"]),
539 ]
540}
541
542#[cfg(test)]
543mod tests {
544 use super::*;
545
546 #[test]
547 fn test_demo_client_metric_names() {
548 let client = DemoMetricsClient::new();
549 let names = client.metric_names();
550 assert!(!names.is_empty());
551 assert!(names.contains(&"http_requests_total".to_string()));
552 assert!(names.contains(&"node_cpu_seconds_total".to_string()));
553 }
554
555 #[test]
556 fn test_demo_client_label_names() {
557 let client = DemoMetricsClient::new();
558 let labels = client.all_label_names();
559 assert!(labels.contains(&"method".to_string()));
560 assert!(labels.contains(&"status_code".to_string()));
561 assert!(labels.contains(&"cpu".to_string()));
562 }
563
564 #[test]
565 fn test_demo_client_metric_labels() {
566 let client = DemoMetricsClient::new();
567 let labels = client.get_metric_labels("http_requests_total");
568 assert!(labels.is_some());
569 let labels = labels.unwrap();
570 assert!(labels.labels.contains_key("method"));
571 assert!(labels.labels.contains_key("status_code"));
572 }
573
574 #[test]
575 fn test_demo_client_backend_type() {
576 let client = DemoMetricsClient::new();
577 assert_eq!(client.backend_type(), "demo");
578 }
579
580 #[test]
581 fn test_generate_buckets_counter() {
582 let buckets = generate_buckets(1000, 60, 10, MetricType::Counter, 0, "test");
583 assert_eq!(buckets.len(), 10);
584 for window in buckets.windows(2) {
586 assert!(window[1].value >= window[0].value);
587 }
588 }
589
590 #[test]
591 fn test_generate_buckets_gauge() {
592 let buckets = generate_buckets(1000, 60, 10, MetricType::Gauge, 0, "test");
593 assert_eq!(buckets.len(), 10);
594 for bucket in &buckets {
596 assert!(bucket.value >= 0.0);
597 }
598 }
599}