1use crate::collector::{LogLine, RequestMeta, RequestSnapshot};
4use serde_json::json;
5use sova_sse::{SseChannel, SseEvent};
6use std::collections::{HashMap, VecDeque};
7use std::sync::{
8 atomic::{AtomicU64, Ordering},
9 Arc, Mutex,
10};
11
12static SEQ: AtomicU64 = AtomicU64::new(1);
13
14pub fn next_id() -> String {
15 format!("dt-{}", SEQ.fetch_add(1, Ordering::Relaxed))
16}
17
18struct HubInner {
19 requests: VecDeque<RequestSnapshot>,
20 by_id: HashMap<String, RequestSnapshot>,
21 logs: VecDeque<LogLine>,
22 plugins: Vec<String>,
23 profile: String,
24 event_seq: u64,
25}
26
27#[derive(Clone)]
29pub struct DevToolsHub {
30 inner: Arc<Mutex<HubInner>>,
31 pub channel: SseChannel,
32 request_cap: usize,
33 log_cap: usize,
34}
35
36impl DevToolsHub {
37 pub fn new(request_cap: usize, log_cap: usize) -> Self {
38 let channel = SseChannel::new(256).history_cap(100);
39 Self {
40 inner: Arc::new(Mutex::new(HubInner {
41 requests: VecDeque::new(),
42 by_id: HashMap::new(),
43 logs: VecDeque::new(),
44 plugins: Vec::new(),
45 profile: String::new(),
46 event_seq: 0,
47 })),
48 channel,
49 request_cap: request_cap.max(10),
50 log_cap: log_cap.max(50),
51 }
52 }
53
54 pub fn set_config_info(&self, plugins: Vec<String>, profile: String) {
55 let mut g = self.inner.lock().unwrap();
56 g.plugins = plugins;
57 g.profile = profile;
58 }
59
60 pub fn push_snapshot(&self, snap: RequestSnapshot) {
61 let meta = RequestMeta::from(&snap);
62 let mut g = self.inner.lock().unwrap();
63 g.event_seq += 1;
64 let eid = g.event_seq.to_string();
65 g.by_id.insert(snap.id.clone(), snap.clone());
66 g.requests.push_back(snap);
67 while g.requests.len() > self.request_cap {
68 if let Some(old) = g.requests.pop_front() {
69 g.by_id.remove(&old.id);
70 }
71 }
72 drop(g);
73 let data = serde_json::to_string(&json!({
74 "type": "request.finished",
75 "meta": meta,
76 }))
77 .unwrap_or_else(|_| "{}".into());
78 self.channel.publish(
79 SseEvent::data(data)
80 .id(eid)
81 .event("request.finished"),
82 );
83 }
84
85 pub fn push_log(&self, line: LogLine) {
86 let mut g = self.inner.lock().unwrap();
87 g.event_seq += 1;
88 let eid = g.event_seq.to_string();
89 g.logs.push_back(line.clone());
91 while g.logs.len() > self.log_cap {
92 g.logs.pop_front();
93 }
94 drop(g);
95 let data = serde_json::to_string(&json!({
96 "type": "log.line",
97 "line": line,
98 }))
99 .unwrap_or_else(|_| "{}".into());
100 self.channel
101 .publish(SseEvent::data(data).id(eid).event("log.line"));
102 }
103
104 pub fn get(&self, id: &str) -> Option<RequestSnapshot> {
105 self.inner.lock().unwrap().by_id.get(id).cloned()
106 }
107
108 pub fn list_meta(&self, limit: usize) -> Vec<RequestMeta> {
109 let g = self.inner.lock().unwrap();
110 g.requests
111 .iter()
112 .rev()
113 .take(limit)
114 .map(RequestMeta::from)
115 .collect()
116 }
117
118 pub fn recent_logs(&self, limit: usize) -> Vec<LogLine> {
119 let g = self.inner.lock().unwrap();
120 g.logs.iter().rev().take(limit).cloned().collect()
121 }
122
123 pub fn config_json(&self) -> serde_json::Value {
124 let g = self.inner.lock().unwrap();
125 json!({
126 "profile": g.profile,
127 "plugins": g.plugins,
128 "features": compile_features(),
129 })
130 }
131}
132
133fn compile_features() -> Vec<&'static str> {
134 #[allow(unused_mut)]
135 let mut v = Vec::new();
136 #[cfg(feature = "session")]
137 v.push("session");
138 #[cfg(feature = "mail")]
139 v.push("mail");
140 #[cfg(feature = "http")]
141 v.push("http");
142 #[cfg(feature = "db")]
143 v.push("db");
144 #[cfg(feature = "tasks")]
145 v.push("tasks");
146 #[cfg(feature = "auth")]
147 v.push("auth");
148 #[cfg(feature = "i18n")]
149 v.push("i18n");
150 #[cfg(feature = "csrf")]
151 v.push("csrf");
152 #[cfg(feature = "passport")]
153 v.push("passport");
154 #[cfg(feature = "store")]
155 v.push("store");
156 #[cfg(feature = "redis")]
157 v.push("redis");
158 #[cfg(feature = "rate-limit")]
159 v.push("rate-limit");
160 v
161}