incular_runtime/
app_log.rs1use std::fmt;
8
9pub const LOG_QUEUE_CAPACITY: usize = 1024;
11pub const LOG_MESSAGE_LIMIT: usize = 4096;
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub enum LogLevel {
17 Debug,
18 Info,
19 Warn,
20 Error,
21}
22
23impl LogLevel {
24 #[must_use]
26 pub const fn as_str(self) -> &'static str {
27 match self {
28 Self::Debug => "debug",
29 Self::Info => "info",
30 Self::Warn => "warn",
31 Self::Error => "error",
32 }
33 }
34}
35
36impl fmt::Display for LogLevel {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 f.write_str(self.as_str())
39 }
40}
41
42#[derive(Clone, Debug, PartialEq, Eq)]
44pub struct LogRecord {
45 pub level: LogLevel,
46 pub target: String,
47 pub message: String,
48}
49
50#[derive(Clone, Debug, Default, PartialEq, Eq)]
52pub struct LogBatch {
53 pub records: Vec<LogRecord>,
54 pub dropped: u64,
55}
56
57#[derive(Debug)]
59pub struct LogQueue {
60 capacity: usize,
61 records: std::collections::VecDeque<LogRecord>,
62 dropped: u64,
63}
64
65impl LogQueue {
66 #[must_use]
67 pub fn new(capacity: usize) -> Self {
68 Self {
69 capacity: capacity.max(1),
70 records: std::collections::VecDeque::new(),
71 dropped: 0,
72 }
73 }
74
75 pub fn push(&mut self, level: LogLevel, target: &str, message: &str) {
78 if self.records.len() >= self.capacity {
79 self.records.pop_front();
80 self.dropped = self.dropped.saturating_add(1);
81 }
82 self.records.push_back(LogRecord {
83 level,
84 target: truncate(target, 256).to_owned(),
85 message: truncate(message, LOG_MESSAGE_LIMIT).to_owned(),
86 });
87 }
88
89 pub fn drain(&mut self, max: usize) -> LogBatch {
91 let take = max.min(self.records.len());
92 LogBatch {
93 records: self.records.drain(..take).collect(),
94 dropped: std::mem::take(&mut self.dropped),
95 }
96 }
97
98 #[must_use]
99 pub fn len(&self) -> usize {
100 self.records.len()
101 }
102
103 #[must_use]
104 pub fn is_empty(&self) -> bool {
105 self.records.is_empty()
106 }
107}
108
109fn truncate(text: &str, limit: usize) -> &str {
111 if text.len() <= limit {
112 return text;
113 }
114 let mut end = limit;
115 while !text.is_char_boundary(end) {
116 end -= 1;
117 }
118 &text[..end]
119}
120
121#[cfg(feature = "devtools")]
122mod global {
123 use super::{LOG_QUEUE_CAPACITY, LogBatch, LogLevel, LogQueue};
124 use std::sync::{
125 Arc, Mutex, OnceLock,
126 atomic::{AtomicBool, Ordering},
127 };
128
129 type Wake = Arc<dyn Fn() + Send + Sync>;
130
131 struct Global {
132 queue: Mutex<LogQueue>,
133 wake: Mutex<Option<Wake>>,
134 wake_pending: AtomicBool,
135 }
136
137 fn global() -> &'static Global {
138 static GLOBAL: OnceLock<Global> = OnceLock::new();
139 GLOBAL.get_or_init(|| Global {
140 queue: Mutex::new(LogQueue::new(LOG_QUEUE_CAPACITY)),
141 wake: Mutex::new(None),
142 wake_pending: AtomicBool::new(false),
143 })
144 }
145
146 pub fn log(level: LogLevel, target: &str, message: &str) {
147 let global = global();
148 if let Ok(mut queue) = global.queue.lock() {
149 queue.push(level, target, message);
150 }
151 if !global.wake_pending.swap(true, Ordering::AcqRel) {
153 let wake = global.wake.lock().ok().and_then(|wake| wake.clone());
154 if let Some(wake) = wake {
155 wake();
156 }
157 }
158 }
159
160 pub fn set_wake(wake: Wake) {
161 let global = global();
162 if let Ok(mut slot) = global.wake.lock() {
163 *slot = Some(wake);
164 }
165 global.wake_pending.store(false, Ordering::Release);
167 if !global.queue.lock().map(|q| q.is_empty()).unwrap_or(true) {
168 global.wake_pending.store(true, Ordering::Release);
169 if let Some(wake) = global.wake.lock().ok().and_then(|w| w.clone()) {
170 wake();
171 }
172 }
173 }
174
175 pub fn drain(max: usize) -> LogBatch {
176 let global = global();
177 global.wake_pending.store(false, Ordering::Release);
179 let (batch, remaining) = global
180 .queue
181 .lock()
182 .map(|mut queue| (queue.drain(max), !queue.is_empty()))
183 .unwrap_or_default();
184 if remaining
185 && !global.wake_pending.swap(true, Ordering::AcqRel)
186 && let Some(wake) = global.wake.lock().ok().and_then(|w| w.clone())
187 {
188 wake();
189 }
190 batch
191 }
192
193 pub fn pending() -> bool {
194 global()
195 .queue
196 .lock()
197 .map(|q| !q.is_empty())
198 .unwrap_or(false)
199 }
200}
201
202pub fn log(level: LogLevel, target: &str, message: impl AsRef<str>) {
206 #[cfg(feature = "devtools")]
207 global::log(level, target, message.as_ref());
208 #[cfg(not(feature = "devtools"))]
209 let _ = (level, target, message);
210}
211
212#[cfg(feature = "devtools")]
215pub fn set_log_wake(wake: std::sync::Arc<dyn Fn() + Send + Sync>) {
216 global::set_wake(wake);
217}
218
219#[cfg(feature = "devtools")]
222#[must_use]
223pub fn drain_logs(max: usize) -> LogBatch {
224 global::drain(max)
225}
226
227#[cfg(feature = "devtools")]
229#[must_use]
230pub fn logs_pending() -> bool {
231 global::pending()
232}