Skip to main content

wide_log/
guard.rs

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
10/// RAII guard that owns a [`WideEvent`] and emits it on drop.
11///
12/// The guard starts a timer on creation. On drop, it sets the duration field
13/// (via [`Key::DURATION_PATH`]) to the elapsed milliseconds, sets the
14/// timestamp field (via [`Key::TIMESTAMP_PATH`]) to the current time as an
15/// RFC 3339 string in the guard's timezone, and calls the emit function
16/// with a reference to the event.
17///
18/// Implements [`Deref`] / [`DerefMut`] to [`WideEvent`] so you can call
19/// `add`, `add_path`, `inc`, etc. directly on the guard.
20///
21/// The `wide_log!` macro generates a `WideLogGuard` that wraps this guard
22/// and manages the thread-local / task-local pointer. Users typically
23/// interact with `WideLogGuard`, not `ScopedGuard` directly.
24///
25/// [`WideEvent`]: crate::WideEvent
26pub 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    /// Creates a new guard with the given emit function and timezone.
41    ///
42    /// The timezone is used to format the timestamp set on drop.
43    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    /// Returns a raw pointer to the inner `WideEvent<K>`. The pointer
53    /// is valid for the lifetime of this `ScopedGuard` and is intended
54    /// to be stored in a [`ContextCell`] for lookup via `current()`.
55    ///
56    /// [`ContextCell`]: crate::ContextCell
57    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    /// Creates a new guard with the given emit function and UTC timezone.
68    ///
69    /// The timer starts immediately. On drop, the guard sets
70    /// `K::DURATION_PATH` to the elapsed milliseconds, sets
71    /// `K::TIMESTAMP_PATH` to the current UTC time as RFC 3339, and
72    /// calls `emit_fn`.
73    pub(crate) fn new(emit_fn: F) -> Self {
74        Self::new_with_tz(emit_fn, Tz::UTC)
75    }
76
77    /// Creates a new guard with the given emit function, timezone, and a
78    /// type-conflict callback.
79    ///
80    /// The callback is fired when `object()` is called on a key that already
81    /// has a non-object value. See [`WideEvent::new_with_warnings`].
82    ///
83    /// [`WideEvent::new_with_warnings`]: crate::WideEvent::new_with_warnings
84    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        // Phase 3 §2.5: `as_millis() as u64` silently truncates on
126        // platforms where `u128` is wider than `u64` (effectively never
127        // on 64-bit, but the conversion is lossy in principle). Use
128        // `try_into` to saturate at `u64::MAX` instead of panicking
129        // in debug builds or wrapping in release.
130        let duration_ms: u64 = self
131            .start
132            .elapsed()
133            .as_millis()
134            .try_into()
135            .unwrap_or(u64::MAX);
136        // Fast path for common 2-segment DURATION_PATH and TIMESTAMP_PATH.
137        // Uses direct indexed writes instead of object() to avoid the overhead
138        // of creating and returning &mut references through the ManuallyDrop chain.
139        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            // Phase 3 §2.4: skip `new_child()` when the child slot is
145            // already a valid `Object`. Previously, the code created a
146            // new child whenever `values[idx]` was `None` or held a
147            // non-object, which would clobber an existing object the
148            // user had already populated. We now read the existing
149            // child if it is already an object and only allocate a new
150            // one when the slot is empty or wrong-typed.
151            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            // Now write the leaf values
171            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            // Check if there's already an array at Tag
297            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}