1use std::ops::{Deref, DerefMut};
2use std::time::Instant;
3
4use chrono_tz::Tz;
5
6use crate::key::Key;
7use crate::value::Value;
8use crate::wide_event::WideEvent;
9
10pub struct ScopedGuard<K: Key, F>
27where
28 F: FnOnce(&WideEvent<K>) + Send + 'static,
29{
30 event: WideEvent<K>,
31 start: Instant,
32 tz: Tz,
33 emit_fn: Option<F>,
34}
35
36impl<K: Key, F> ScopedGuard<K, F>
37where
38 F: FnOnce(&WideEvent<K>) + Send + 'static,
39{
40 pub fn new_with_tz(emit_fn: F, tz: Tz) -> Self {
44 Self {
45 event: WideEvent::new(),
46 start: Instant::now(),
47 tz,
48 emit_fn: Some(emit_fn),
49 }
50 }
51
52 pub fn event_ptr(&self) -> *const WideEvent<K> {
58 &self.event as *const _
59 }
60}
61
62#[cfg(test)]
63impl<K: Key, F> ScopedGuard<K, F>
64where
65 F: FnOnce(&WideEvent<K>) + Send + 'static,
66{
67 pub(crate) fn new(emit_fn: F) -> Self {
74 Self::new_with_tz(emit_fn, Tz::UTC)
75 }
76
77 pub(crate) fn new_with_warnings(
85 emit_fn: F,
86 tz: Tz,
87 on_type_conflict: crate::wide_event::ConflictFn<K>,
88 ) -> Self {
89 Self {
90 event: WideEvent::new_with_warnings(on_type_conflict),
91 start: Instant::now(),
92 tz,
93 emit_fn: Some(emit_fn),
94 }
95 }
96}
97
98impl<K: Key, F> Deref for ScopedGuard<K, F>
99where
100 F: FnOnce(&WideEvent<K>) + Send + 'static,
101{
102 type Target = WideEvent<K>;
103
104 #[inline]
105 fn deref(&self) -> &WideEvent<K> {
106 &self.event
107 }
108}
109
110impl<K: Key, F> DerefMut for ScopedGuard<K, F>
111where
112 F: FnOnce(&WideEvent<K>) + Send + 'static,
113{
114 #[inline]
115 fn deref_mut(&mut self) -> &mut WideEvent<K> {
116 &mut self.event
117 }
118}
119
120impl<K: Key, F> Drop for ScopedGuard<K, F>
121where
122 F: FnOnce(&WideEvent<K>) + Send + 'static,
123{
124 fn drop(&mut self) {
125 let duration_ms: u64 = self
131 .start
132 .elapsed()
133 .as_millis()
134 .try_into()
135 .unwrap_or(u64::MAX);
136 if K::DURATION_PATH.len() == 2 && K::TIMESTAMP_PATH.len() == 2 {
140 let dur_idx = K::DURATION_PATH[0].as_index();
141 let ts_idx = K::TIMESTAMP_PATH[0].as_index();
142 self.event.ensure_capacity(dur_idx.max(ts_idx));
143
144 let need_dur_child = match &self.event.values.get(dur_idx) {
152 Some(Some(v)) => !v.is_object(),
153 _ => true,
154 };
155 let need_ts_child = match &self.event.values.get(ts_idx) {
156 Some(Some(v)) => !v.is_object(),
157 _ => true,
158 };
159 if need_dur_child {
160 let child = self.event.new_child();
161 self.event.values[dur_idx] = Some(crate::value::Value::from_object(child));
162 self.event.present_count += 1;
163 }
164 if need_ts_child {
165 let child = self.event.new_child();
166 self.event.values[ts_idx] = Some(crate::value::Value::from_object(child));
167 self.event.present_count += 1;
168 }
169
170 if let Some(Some(Value::Object(obj))) = self.event.values.get_mut(dur_idx) {
172 obj.add(K::DURATION_PATH[1], duration_ms);
173 }
174 let now = chrono::Utc::now().with_timezone(&self.tz);
175 if let Some(Some(Value::Object(obj))) = self.event.values.get_mut(ts_idx) {
176 obj.add(K::TIMESTAMP_PATH[1], now.to_rfc3339());
177 }
178 } else {
179 self.event.add_path(K::DURATION_PATH, duration_ms);
180 let now = chrono::Utc::now().with_timezone(&self.tz);
181 self.event.add_path(K::TIMESTAMP_PATH, now.to_rfc3339());
182 }
183 if let Some(emit) = self.emit_fn.take() {
184 emit(&self.event);
185 }
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use std::sync::{Arc, Mutex};
192
193 use super::*;
194 use crate::key::test_support::TestKey;
195
196 type CaptureSlot = Arc<Mutex<Option<String>>>;
197
198 fn capture_json() -> (
199 CaptureSlot,
200 impl FnOnce(&WideEvent<TestKey>) + Send + 'static,
201 ) {
202 let slot: CaptureSlot = Arc::new(Mutex::new(None));
203 let slot_clone = slot.clone();
204 let emit = move |we: &WideEvent<TestKey>| {
205 *slot_clone.lock().unwrap() = Some(we.to_json().unwrap());
206 };
207 (slot, emit)
208 }
209
210 #[test]
211 fn guard_sets_duration_path() {
212 let (slot, emit) = capture_json();
213 drop(ScopedGuard::<TestKey, _>::new(emit));
214 let json = slot.lock().unwrap().clone().unwrap();
215 assert!(json.contains("\"duration\""));
216 assert!(json.contains("\"total_ms\""));
217 }
218
219 #[test]
220 fn guard_sets_timestamp() {
221 let (slot, emit) = capture_json();
222 drop(ScopedGuard::<TestKey, _>::new(emit));
223 let json = slot.lock().unwrap().clone().unwrap();
224 let parsed: sonic_rs::Value = sonic_rs::from_str(&json).unwrap();
225 use sonic_rs::JsonValueTrait;
226 let ts = parsed["event"]["timestamp"].as_str().unwrap();
227 assert!(!ts.is_empty(), "timestamp should not be empty");
228 assert!(
229 ts.contains('T'),
230 "timestamp should be RFC 3339 (contain 'T'): {ts}"
231 );
232 }
233
234 #[test]
235 fn guard_deref_add() {
236 let (slot, emit) = capture_json();
237 let mut g = ScopedGuard::<TestKey, _>::new(emit);
238 g.add(TestKey::Status, "ok");
239 drop(g);
240 let json = slot.lock().unwrap().clone().unwrap();
241 assert!(json.contains(r#""status":"ok""#));
242 }
243
244 #[test]
245 fn guard_emit_called_exactly_once() {
246 let counter = Arc::new(Mutex::new(0u32));
247 let c = counter.clone();
248 let emit = move |_: &WideEvent<TestKey>| {
249 *c.lock().unwrap() += 1;
250 };
251 drop(ScopedGuard::<TestKey, _>::new(emit));
252 assert_eq!(*counter.lock().unwrap(), 1);
253 }
254
255 #[test]
256 fn guard_new_with_warnings_fires_callback() {
257 use std::sync::atomic::{AtomicU32, Ordering};
258 static COUNTER: AtomicU32 = AtomicU32::new(0);
259 COUNTER.store(0, Ordering::SeqCst);
260
261 fn cb(_event: &mut WideEvent<TestKey>, _key: TestKey) {
262 COUNTER.fetch_add(1, Ordering::SeqCst);
263 }
264
265 let (slot, emit) = capture_json();
266 let mut g = ScopedGuard::<TestKey, _>::new_with_warnings(emit, Tz::UTC, cb);
267 g.add(TestKey::Details, true);
268 g.object(TestKey::Details);
269 drop(g);
270 assert_eq!(COUNTER.load(Ordering::SeqCst), 1);
271 assert!(slot.lock().unwrap().is_some());
272 }
273
274 #[test]
275 fn guard_new_with_warnings_callback_can_mutate_event() {
276 let (slot, emit) = capture_json();
277 let mut g = ScopedGuard::<TestKey, _>::new_with_warnings(emit, Tz::UTC, |event, _key| {
278 event.add(TestKey::Flag, true);
279 });
280 g.add(TestKey::Details, 42u64);
281 g.object(TestKey::Details);
282 drop(g);
283 let json = slot.lock().unwrap().clone().unwrap();
284 assert!(json.contains("\"flag\":true"));
285 }
286
287 #[test]
288 fn guard_new_with_warnings_callback_appends_warning_string() {
289 use crate::value::Value;
290 use smallvec::smallvec;
291
292 let (slot, emit) = capture_json();
293 let mut g = ScopedGuard::<TestKey, _>::new_with_warnings(emit, Tz::UTC, |event, key| {
294 let warning = format!("{} type conflict", key.as_str());
295 let idx = TestKey::Tag.as_index();
296 let existing = event.values.get(idx).and_then(|v| v.as_ref());
298 if let Some(v) = existing
299 && let Some(arr) = v.as_array_ref()
300 {
301 let mut new_arr = arr.clone();
302 new_arr.push(Value::from(warning));
303 event.add(TestKey::Tag, Value::from_array(new_arr));
304 return;
305 }
306 event.add(
307 TestKey::Tag,
308 Value::from_array(smallvec![Value::from(warning)]),
309 );
310 });
311 g.add(TestKey::Details, true);
312 g.object(TestKey::Details);
313 drop(g);
314 let json = slot.lock().unwrap().clone().unwrap();
315 assert!(
316 json.contains("\"details type conflict\""),
317 "warning string should appear in JSON"
318 );
319 }
320
321 #[test]
322 fn guard_emit_not_called_on_forget() {
323 let counter = Arc::new(Mutex::new(0u32));
324 let c = counter.clone();
325 let emit = move |_: &WideEvent<TestKey>| {
326 *c.lock().unwrap() += 1;
327 };
328 let g = ScopedGuard::<TestKey, _>::new(emit);
329 std::mem::forget(g);
330 assert_eq!(*counter.lock().unwrap(), 0);
331 }
332
333 #[test]
334 fn guard_duration_is_milliseconds() {
335 let (slot, emit) = capture_json();
336 let g = ScopedGuard::<TestKey, _>::new(emit);
337 std::thread::sleep(std::time::Duration::from_millis(2));
338 drop(g);
339 let json = slot.lock().unwrap().clone().unwrap();
340 let parsed: sonic_rs::Value = sonic_rs::from_str(&json).unwrap();
341 use sonic_rs::JsonValueTrait;
342 let total_ms = parsed["duration"]["total_ms"].as_u64().unwrap();
343 assert!(
344 total_ms >= 1,
345 "duration.total_ms should be >= 1, got {total_ms}"
346 );
347 }
348
349 #[test]
350 fn guard_timestamp_reflects_timezone() {
351 let (slot, emit) = capture_json();
352 let g = ScopedGuard::<TestKey, _>::new_with_tz(emit, Tz::America__New_York);
353 drop(g);
354 let json = slot.lock().unwrap().clone().unwrap();
355 let parsed: sonic_rs::Value = sonic_rs::from_str(&json).unwrap();
356 use sonic_rs::JsonValueTrait;
357 let ts = parsed["event"]["timestamp"].as_str().unwrap();
358 assert!(
359 ts.contains("-04:00") || ts.contains("-05:00"),
360 "timestamp should reflect America/New_York offset: {ts}"
361 );
362 }
363}