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