Skip to main content

wide_log/
wide_event.rs

1use smallvec::SmallVec;
2
3use crate::error::Error;
4use crate::key::Key;
5use crate::log::LogEntry;
6use crate::value::Value;
7
8pub(crate) type ConflictFn<K> = fn(&mut WideEvent<K>, K);
9
10const INLINE_CAP: usize = 32;
11const LOG_INLINE_CAP: usize = 16;
12
13/// A wide event — a structured log record that accumulates fields throughout
14/// a request/task lifecycle and is emitted as a single JSON line on completion.
15///
16/// Fields are stored in an array indexed by `K::as_index()` — O(1) lookup
17/// with no linear scan. Up to `INLINE_CAP` (24) `Option<Value>` slots are
18/// inline on the stack; zero heap allocation in the common case. Log entries
19/// are stored in a separate `SmallVec` with inline capacity of 16.
20///
21/// The `Serialize` impl emits all user entries in enum-variant order, then
22/// the `"log"` key (if any log entries have been accumulated). The `"log"`
23/// key is never declared by the user — it appears automatically.
24///
25/// `present_count` is a cached O(1) count of `Some(_)` slots, maintained
26/// incrementally by `add`, `inc`, `dec`, `add_n`, and `object()`. It is
27/// excluded from the serialized form.
28///
29/// This type is not constructed directly by users. The `wide_log!` macro
30/// generates a `WideLogGuard` that owns a `WideEvent` and manages the
31/// thread-local/task-local pointer via `current()`.
32#[derive(Clone)]
33pub struct WideEvent<K: Key> {
34    /// Indexed value slots. `values[key.as_index()]` holds the value for that
35    /// key, or `None` if not yet set.
36    pub(crate) values: SmallVec<[Option<Value<K>>; INLINE_CAP]>,
37    /// Log entries accumulated by `info!`, `warn!`, etc. (up to 16 inline).
38    pub(crate) log_entries: SmallVec<[LogEntry<K>; LOG_INLINE_CAP]>,
39    /// Optional callback fired when a key's value type conflicts.
40    pub(crate) on_type_conflict: Option<ConflictFn<K>>,
41    /// Cached count of `Some(_)` slots in `values`. Maintained by mutation
42    /// methods so `len()` / `count_present()` are O(1) instead of O(K).
43    /// Not included in serialization (the custom `Serialize` impl iterates
44    /// the `values` array and emits the `log` key separately).
45    pub(crate) present_count: usize,
46}
47
48impl<K: Key> WideEvent<K> {
49    /// Creates a new empty wide event with no conflict callback.
50    #[inline]
51    pub(crate) fn new() -> Self {
52        Self {
53            values: SmallVec::new(),
54            log_entries: SmallVec::new(),
55            on_type_conflict: None,
56            present_count: 0,
57        }
58    }
59
60    /// Ensures the values SmallVec is large enough to index by `key.as_index()`.
61    #[inline]
62    pub(crate) fn ensure_capacity(&mut self, idx: usize) {
63        if idx >= self.values.len() {
64            self.values.resize(idx + 1, None);
65        }
66    }
67
68    /// Sets or replaces a field value at the given key. O(1) indexed access.
69    #[inline]
70    pub(crate) fn add<V: Into<Value<K>>>(&mut self, key: K, value: V) {
71        let idx = key.as_index();
72        self.ensure_capacity(idx);
73        let prev = self.values[idx].is_none();
74        self.values[idx] = Some(value.into());
75        if prev {
76            self.present_count += 1;
77        }
78    }
79
80    /// Gets or creates a nested object at the given key, returning `&mut` to it.
81    #[inline]
82    pub(crate) fn object(&mut self, key: K) -> &mut WideEvent<K> {
83        let idx = key.as_index();
84        self.ensure_capacity(idx);
85        let prev_was_none = self.values[idx].is_none();
86        let needs_replace = match &self.values[idx] {
87            None => true,
88            Some(v) => !v.is_object(),
89        };
90        if needs_replace {
91            if let Some(cb) = self.on_type_conflict
92                && self.values[idx].is_some()
93            {
94                cb(self, key);
95            }
96            self.values[idx] = Some(Value::from_object(self.new_child()));
97            if prev_was_none {
98                self.present_count += 1;
99            }
100        }
101        match &mut self.values[idx] {
102            Some(Value::Object(boxed)) => boxed,
103            _ => unreachable!(),
104        }
105    }
106
107    #[inline]
108    pub(crate) fn new_child(&self) -> WideEvent<K> {
109        WideEvent {
110            values: SmallVec::new(),
111            log_entries: SmallVec::new(),
112            on_type_conflict: self.on_type_conflict,
113            present_count: 0,
114        }
115    }
116
117    /// Set a value at a nested path. Traverses/creates intermediate objects.
118    #[inline]
119    pub fn add_path<V: Into<Value<K>>>(&mut self, path: &[K], value: V) {
120        debug_assert!(!path.is_empty(), "path must have at least one segment");
121        let value = value.into();
122        // Phase 5 §5.3: inline the common 2-segment case. This
123        // avoids a loop + bounds check for the most common
124        // pattern (e.g. "duration.total_ms", "event.id",
125        // "service.name"). For longer paths we fall back to the
126        // generic `descend_mut` loop.
127        if path.len() == 2 {
128            let target = self.object(path[0]);
129            target.add(path[1], value);
130            return;
131        }
132        if path.len() == 1 {
133            self.add(path[0], value);
134            return;
135        }
136        let target = self.descend_mut(path);
137        target.add(path[path.len() - 1], value);
138    }
139
140    #[inline]
141    fn descend_mut(&mut self, path: &[K]) -> &mut WideEvent<K> {
142        let mut current: &mut WideEvent<K> = self;
143        for &seg in &path[..path.len() - 1] {
144            current = current.object(seg);
145        }
146        current
147    }
148
149    /// Increment a numeric field by 1. Initializes to 1 if absent.
150    #[inline]
151    pub(crate) fn inc(&mut self, key: K) {
152        let idx = key.as_index();
153        self.ensure_capacity(idx);
154        let prev_none = self.values[idx].is_none();
155        let new_val = match &self.values[idx] {
156            Some(Value::U64(n)) => Value::from(*n + 1),
157            Some(Value::I64(n)) => Value::from(*n + 1),
158            Some(_) => Value::from(1u64),
159            None => Value::from(1u64),
160        };
161        self.values[idx] = Some(new_val);
162        if prev_none {
163            self.present_count += 1;
164        }
165    }
166
167    /// Decrement a numeric field by 1. Initializes to -1 if absent.
168    #[inline]
169    pub(crate) fn dec(&mut self, key: K) {
170        let idx = key.as_index();
171        self.ensure_capacity(idx);
172        let prev_none = self.values[idx].is_none();
173        let new_val = match &self.values[idx] {
174            Some(Value::U64(n)) => {
175                if *n > 0 {
176                    Value::from(*n - 1)
177                } else {
178                    Value::from(0u64)
179                }
180            }
181            Some(Value::I64(n)) => Value::from(*n - 1),
182            Some(_) => Value::from(-1i64),
183            None => Value::from(-1i64),
184        };
185        self.values[idx] = Some(new_val);
186        if prev_none {
187            self.present_count += 1;
188        }
189    }
190
191    /// Add a number to a numeric field. Initializes to `n` if absent.
192    #[inline]
193    pub(crate) fn add_n(&mut self, key: K, n: i64) {
194        let idx = key.as_index();
195        self.ensure_capacity(idx);
196        let prev_none = self.values[idx].is_none();
197        let new_val = match &self.values[idx] {
198            Some(Value::U64(u)) => Value::from(u.saturating_add_signed(n)),
199            Some(Value::I64(i)) => Value::from(*i + n),
200            Some(_) => {
201                if n >= 0 {
202                    Value::from(n as u64)
203                } else {
204                    Value::from(n)
205                }
206            }
207            None => {
208                if n >= 0 {
209                    Value::from(n as u64)
210                } else {
211                    Value::from(n)
212                }
213            }
214        };
215        self.values[idx] = Some(new_val);
216        if prev_none {
217            self.present_count += 1;
218        }
219    }
220
221    #[inline]
222    pub fn inc_path(&mut self, path: &[K]) {
223        debug_assert!(!path.is_empty(), "path must have at least one segment");
224        // Phase 5 §5.3: inline the 1- and 2-segment cases.
225        if path.len() == 2 {
226            let target = self.object(path[0]);
227            target.inc(path[1]);
228            return;
229        }
230        if path.len() == 1 {
231            self.inc(path[0]);
232            return;
233        }
234        let target = self.descend_mut(path);
235        target.inc(path[path.len() - 1]);
236    }
237
238    #[inline]
239    pub fn dec_path(&mut self, path: &[K]) {
240        debug_assert!(!path.is_empty(), "path must have at least one segment");
241        if path.len() == 2 {
242            let target = self.object(path[0]);
243            target.dec(path[1]);
244            return;
245        }
246        if path.len() == 1 {
247            self.dec(path[0]);
248            return;
249        }
250        let target = self.descend_mut(path);
251        target.dec(path[path.len() - 1]);
252    }
253
254    #[inline]
255    pub fn add_n_path(&mut self, path: &[K], n: i64) {
256        debug_assert!(!path.is_empty(), "path must have at least one segment");
257        if path.len() == 2 {
258            let target = self.object(path[0]);
259            target.add_n(path[1], n);
260            return;
261        }
262        if path.len() == 1 {
263            self.add_n(path[0], n);
264            return;
265        }
266        let target = self.descend_mut(path);
267        target.add_n(path[path.len() - 1], n);
268    }
269
270    #[inline]
271    pub fn append_log_entry(&mut self, level: &'static str, message: &str) {
272        self.log_entries.push(LogEntry::new(
273            level,
274            crate::log::LogMsg::Owned(faststr::FastStr::new(message)),
275        ));
276    }
277
278    /// Append a log entry with a `&'static str` message — zero-copy.
279    #[inline]
280    pub fn append_log_entry_static(&mut self, level: &'static str, message: &'static str) {
281        self.log_entries
282            .push(LogEntry::new(level, crate::log::LogMsg::Static(message)));
283    }
284
285    #[inline]
286    pub(crate) fn len(&self) -> usize {
287        self.present_count
288    }
289
290    pub fn to_json(&self) -> Result<String, Error> {
291        sonic_rs::to_string(self).map_err(|e| Error::Serialize(e.to_string()))
292    }
293
294    /// Serialize directly to a writer, bypassing serde entirely.
295    /// Uses itoa/ryu for zero-allocation number formatting.
296    pub fn serialize_to<W: std::io::Write>(&self, w: &mut W) -> Result<(), Error> {
297        write_event(self, w).map_err(|e| Error::Serialize(e.to_string()))
298    }
299
300    /// Count of present entries (alias for `len()`).
301    #[inline]
302    pub(crate) fn count_present(&self) -> usize {
303        self.present_count
304    }
305}
306
307#[cfg(test)]
308impl<K: Key> WideEvent<K> {
309    /// Creates a new empty wide event with a type-conflict callback.
310    #[inline]
311    pub(crate) fn new_with_warnings(f: ConflictFn<K>) -> Self {
312        Self {
313            values: SmallVec::new(),
314            log_entries: SmallVec::new(),
315            on_type_conflict: Some(f),
316            present_count: 0,
317        }
318    }
319
320    #[inline]
321    pub(crate) fn is_empty(&self) -> bool {
322        self.values.iter().all(|v| v.is_none())
323    }
324
325    #[inline]
326    pub(crate) fn log_len(&self) -> usize {
327        self.log_entries.len()
328    }
329
330    #[inline]
331    pub(crate) fn has_logs(&self) -> bool {
332        !self.log_entries.is_empty()
333    }
334}
335
336impl<K: Key> Default for WideEvent<K> {
337    fn default() -> Self {
338        Self::new()
339    }
340}
341
342impl<K: Key> serde::Serialize for WideEvent<K> {
343    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
344        use serde::ser::SerializeMap;
345        let present = self.count_present();
346        let total = present + if self.log_entries.is_empty() { 0 } else { 1 };
347        let mut map = serializer.serialize_map(Some(total))?;
348        for (i, key) in K::KEYS.iter().enumerate() {
349            if i < self.values.len()
350                && let Some(value) = &self.values[i]
351            {
352                map.serialize_entry(key.as_str(), value)?;
353            }
354        }
355        if !self.log_entries.is_empty() {
356            map.serialize_entry(K::LOG_KEY, &self.log_entries)?;
357        }
358        map.end()
359    }
360}
361
362impl<K: Key> std::fmt::Debug for WideEvent<K> {
363    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
364        f.debug_struct("WideEvent")
365            .field("present", &self.len())
366            .field("log_entries_len", &self.log_entries.len())
367            .field("has_conflict_callback", &self.on_type_conflict.is_some())
368            .finish()
369    }
370}
371
372// ── Direct serializer (bypasses serde) ──
373
374#[inline]
375fn write_event<K: Key, W: std::io::Write>(ev: &WideEvent<K>, w: &mut W) -> std::io::Result<()> {
376    w.write_all(b"{")?;
377    let mut first = true;
378    for (i, key) in K::KEYS.iter().enumerate() {
379        if i < ev.values.len()
380            && let Some(val) = &ev.values[i]
381        {
382            if !first {
383                w.write_all(b",")?;
384            }
385            first = false;
386            write_json_str(w, key.as_str())?;
387            w.write_all(b":")?;
388            write_value(val, w)?;
389        }
390    }
391    if !ev.log_entries.is_empty() {
392        if !first {
393            w.write_all(b",")?;
394        }
395        write_json_str(w, K::LOG_KEY)?;
396        w.write_all(b":")?;
397        w.write_all(b"[")?;
398        for (j, entry) in ev.log_entries.iter().enumerate() {
399            if j > 0 {
400                w.write_all(b",")?;
401            }
402            w.write_all(b"{")?;
403            write_json_str(w, K::LEVEL_KEY)?;
404            w.write_all(b":")?;
405            write_json_str(w, entry.level)?;
406            w.write_all(b",")?;
407            write_json_str(w, K::MESSAGE_KEY)?;
408            w.write_all(b":")?;
409            write_json_str(w, entry.message.as_str())?;
410            w.write_all(b"}")?;
411        }
412        w.write_all(b"]")?;
413    }
414    w.write_all(b"}")?;
415    Ok(())
416}
417
418#[inline]
419fn write_value<K: Key, W: std::io::Write>(
420    val: &crate::value::Value<K>,
421    w: &mut W,
422) -> std::io::Result<()> {
423    match val {
424        Value::Null => w.write_all(b"null"),
425        Value::Bool(b) => {
426            if *b {
427                w.write_all(b"true")
428            } else {
429                w.write_all(b"false")
430            }
431        }
432        Value::I64(i) => {
433            let mut buf = itoa::Buffer::new();
434            w.write_all(buf.format(*i).as_bytes())
435        }
436        Value::U64(u) => {
437            let mut buf = itoa::Buffer::new();
438            w.write_all(buf.format(*u).as_bytes())
439        }
440        Value::F64(f) => {
441            // JSON has no representation for NaN or ±Infinity. The
442            // `to_json` path (sonic-rs) emits `null` for non-finite
443            // floats; the direct `serialize_to` path must match so
444            // the two paths produce byte-identical output for the
445            // same event. Without this guard, `ryu::Buffer::format`
446            // would emit the literal text "NaN" / "inf" / "-inf",
447            // which is not valid JSON.
448            if !f.is_finite() {
449                w.write_all(b"null")
450            } else {
451                let mut buf = ryu::Buffer::new();
452                w.write_all(buf.format(*f).as_bytes())
453            }
454        }
455        Value::Str(s) => write_json_str(w, s.as_str()),
456        Value::StaticStr(s) => write_json_str(w, s),
457        Value::Array(arr) => {
458            w.write_all(b"[")?;
459            for (i, v) in arr.iter().enumerate() {
460                if i > 0 {
461                    w.write_all(b",")?;
462                }
463                write_value(v, w)?;
464            }
465            w.write_all(b"]")?;
466            Ok(())
467        }
468        Value::Object(obj) => write_event(obj, w),
469    }
470}
471
472#[inline]
473fn write_json_str<W: std::io::Write>(w: &mut W, s: &str) -> std::io::Result<()> {
474    w.write_all(b"\"")?;
475    for c in s.chars() {
476        match c {
477            '"' => w.write_all(b"\\\"")?,
478            '\\' => w.write_all(b"\\\\")?,
479            '\n' => w.write_all(b"\\n")?,
480            '\t' => w.write_all(b"\\t")?,
481            '\r' => w.write_all(b"\\r")?,
482            '\u{0008}' => w.write_all(b"\\b")?,
483            '\u{000C}' => w.write_all(b"\\f")?,
484            // U+2028 (LINE SEPARATOR) and U+2029 (PARAGRAPH SEPARATOR)
485            // are valid in JSON strings but are treated as line terminators
486            // by JavaScript engines, which can enable JSON hijacking
487            // attacks when JSON is interpreted as JS. Escape them.
488            '\u{2028}' => w.write_all(b"\\u2028")?,
489            '\u{2029}' => w.write_all(b"\\u2029")?,
490            c if (c as u32) < 0x20 => {
491                w.write_all(format!("\\u{:04x}", c as u32).as_bytes())?;
492            }
493            c => {
494                let mut buf = [0u8; 4];
495                let s = c.encode_utf8(&mut buf);
496                w.write_all(s.as_bytes())?;
497            }
498        }
499    }
500    w.write_all(b"\"")?;
501    Ok(())
502}
503
504#[cfg(test)]
505mod tests {
506    use super::*;
507    use crate::key::test_support::TestKey;
508
509    // 25-variant key used to verify the inline SmallVec capacity.
510    #[derive(Copy, Clone, PartialEq, Eq, Debug)]
511    #[repr(u8)]
512    #[allow(dead_code)]
513    enum BigKey {
514        Duration,
515        TotalMs,
516        Event,
517        Timestamp,
518        Id,
519        K2,
520        K3,
521        K4,
522        K5,
523        K6,
524        K7,
525        K8,
526        K9,
527        K10,
528        K11,
529        K12,
530        K13,
531        K14,
532        K15,
533        K16,
534        K17,
535        K18,
536        K19,
537        K20,
538        K21,
539        K22,
540    }
541
542    impl crate::key::Key for BigKey {
543        fn as_str(self) -> &'static str {
544            match self {
545                BigKey::Duration => "duration",
546                BigKey::TotalMs => "total_ms",
547                BigKey::Event => "event",
548                BigKey::Timestamp => "timestamp",
549                BigKey::Id => "id",
550                BigKey::K2 => "k2",
551                BigKey::K3 => "k3",
552                BigKey::K4 => "k4",
553                BigKey::K5 => "k5",
554                BigKey::K6 => "k6",
555                BigKey::K7 => "k7",
556                BigKey::K8 => "k8",
557                BigKey::K9 => "k9",
558                BigKey::K10 => "k10",
559                BigKey::K11 => "k11",
560                BigKey::K12 => "k12",
561                BigKey::K13 => "k13",
562                BigKey::K14 => "k14",
563                BigKey::K15 => "k15",
564                BigKey::K16 => "k16",
565                BigKey::K17 => "k17",
566                BigKey::K18 => "k18",
567                BigKey::K19 => "k19",
568                BigKey::K20 => "k20",
569                BigKey::K21 => "k21",
570                BigKey::K22 => "k22",
571            }
572        }
573        const MAX_KEYS: usize = 25;
574        const KEYS: &'static [Self] = &[
575            BigKey::Duration,
576            BigKey::TotalMs,
577            BigKey::Event,
578            BigKey::Timestamp,
579            BigKey::Id,
580            BigKey::K2,
581            BigKey::K3,
582            BigKey::K4,
583            BigKey::K5,
584            BigKey::K6,
585            BigKey::K7,
586            BigKey::K8,
587            BigKey::K9,
588            BigKey::K10,
589            BigKey::K11,
590            BigKey::K12,
591            BigKey::K13,
592            BigKey::K14,
593            BigKey::K15,
594            BigKey::K16,
595            BigKey::K17,
596            BigKey::K18,
597            BigKey::K19,
598            BigKey::K20,
599            BigKey::K21,
600            BigKey::K22,
601        ];
602        const KEY_STRS: &'static [&'static str] = &[
603            "duration",
604            "total_ms",
605            "event",
606            "timestamp",
607            "id",
608            "k2",
609            "k3",
610            "k4",
611            "k5",
612            "k6",
613            "k7",
614            "k8",
615            "k9",
616            "k10",
617            "k11",
618            "k12",
619            "k13",
620            "k14",
621            "k15",
622            "k16",
623            "k17",
624            "k18",
625            "k19",
626            "k20",
627            "k21",
628            "k22",
629        ];
630        fn as_index(self) -> usize {
631            self as usize
632        }
633        const DURATION_PATH: &'static [Self] = &[BigKey::Duration, BigKey::TotalMs];
634        const TIMESTAMP_PATH: &'static [Self] = &[BigKey::Event, BigKey::Timestamp];
635        const ID_PATH: &'static [Self] = &[BigKey::Event, BigKey::Id];
636        const LOG_KEY: &'static str = "log";
637        const LEVEL_KEY: &'static str = "level";
638        const MESSAGE_KEY: &'static str = "message";
639    }
640
641    // ── Basic tests ──
642
643    #[test]
644    fn new_is_empty() {
645        let e = WideEvent::<TestKey>::new();
646        assert!(e.is_empty());
647        assert_eq!(e.len(), 0);
648        assert_eq!(e.log_len(), 0);
649        assert!(!e.has_logs());
650    }
651
652    #[test]
653    fn default_is_empty() {
654        let e = WideEvent::<TestKey>::default();
655        assert!(e.is_empty());
656    }
657
658    #[test]
659    fn add_single() {
660        let mut e = WideEvent::<TestKey>::new();
661        e.add(TestKey::Status, "ok");
662        assert_eq!(e.len(), 1);
663    }
664
665    #[test]
666    fn add_updates_existing() {
667        let mut e = WideEvent::<TestKey>::new();
668        e.add(TestKey::Status, "ok");
669        e.add(TestKey::Status, "err");
670        assert_eq!(e.len(), 1);
671        assert_eq!(e.to_json().unwrap(), r#"{"status":"err"}"#);
672    }
673
674    #[test]
675    fn add_multiple_keys() {
676        let mut e = WideEvent::<TestKey>::new();
677        e.add(TestKey::Requests, 42u64);
678        e.add(TestKey::Status, "ok");
679        e.add(TestKey::Tag, "web");
680        assert_eq!(e.len(), 3);
681    }
682
683    // ── Object tests ──
684
685    #[test]
686    fn object_creates_nested() {
687        let mut e = WideEvent::<TestKey>::new();
688        e.object(TestKey::Details).add(TestKey::Status, "ok");
689        let json = e.to_json().unwrap();
690        assert!(json.contains("\"details\""));
691        assert!(json.contains("\"status\""));
692    }
693
694    #[test]
695    fn object_returns_existing() {
696        let mut e = WideEvent::<TestKey>::new();
697        e.object(TestKey::Details).add(TestKey::Requests, 1u64);
698        e.object(TestKey::Details).add(TestKey::Status, "ok");
699        let json = e.to_json().unwrap();
700        assert!(json.contains("\"requests\""));
701        assert!(json.contains("\"status\""));
702        assert_eq!(e.len(), 1, "only one top-level entry");
703    }
704
705    // ── Type conflict tests (fn pointer version) ──
706
707    #[test]
708    fn object_type_conflict_replaces_with_object() {
709        let mut e = WideEvent::<TestKey>::new();
710        e.add(TestKey::Details, true);
711        e.object(TestKey::Details).add(TestKey::Status, "ok");
712        assert!(matches!(&e.values[TestKey::Details.as_index()], Some(v) if v.is_object()));
713    }
714
715    #[test]
716    fn object_type_conflict_fires_callback() {
717        use std::sync::atomic::{AtomicU32, Ordering};
718
719        static COUNTER: AtomicU32 = AtomicU32::new(0);
720        COUNTER.store(0, Ordering::SeqCst);
721
722        fn cb(_event: &mut WideEvent<TestKey>, _key: TestKey) {
723            COUNTER.fetch_add(1, Ordering::SeqCst);
724        }
725
726        let mut e = WideEvent::new_with_warnings(cb);
727        e.add(TestKey::Details, true);
728        e.object(TestKey::Details);
729        assert_eq!(COUNTER.load(Ordering::SeqCst), 1);
730    }
731
732    // ── Serialization tests ──
733
734    #[test]
735    fn to_json_valid() {
736        let mut e = WideEvent::<TestKey>::new();
737        e.add(TestKey::Requests, 7u64);
738        e.add(TestKey::Status, "ok");
739        e.add(TestKey::Flag, false);
740        let json = e.to_json().unwrap();
741        assert!(json.starts_with('{'));
742        assert!(json.ends_with('}'));
743    }
744
745    #[test]
746    fn to_json_roundtrip() {
747        let mut e = WideEvent::<TestKey>::new();
748        e.add(TestKey::Requests, 42u64);
749        e.add(TestKey::Status, "active");
750        let json = e.to_json().unwrap();
751        let parsed: sonic_rs::Value = sonic_rs::from_str(&json).unwrap();
752        assert_eq!(parsed["requests"], 42u64);
753        assert_eq!(parsed["status"], "active");
754    }
755
756    #[test]
757    fn serialize_nested_object() {
758        let mut e = WideEvent::<TestKey>::new();
759        e.object(TestKey::Service).add(TestKey::Name, "svc");
760        assert_eq!(e.to_json().unwrap(), r#"{"service":{"name":"svc"}}"#);
761    }
762
763    #[test]
764    fn inline_capacity_not_spilled() {
765        let mut e = WideEvent::<BigKey>::new();
766        for i in 0..BigKey::MAX_KEYS {
767            e.add(BigKey::KEYS[i], i as u64);
768        }
769        assert_eq!(e.len(), BigKey::MAX_KEYS);
770        assert!(
771            !e.values.spilled(),
772            "{} entries must fit in inline storage",
773            BigKey::MAX_KEYS
774        );
775    }
776
777    // ── Path method tests ──
778
779    #[test]
780    fn add_path_single_segment() {
781        let mut e = WideEvent::<TestKey>::new();
782        e.add_path(&[TestKey::Status], "ok");
783        assert_eq!(e.len(), 1);
784        assert_eq!(e.to_json().unwrap(), r#"{"status":"ok"}"#);
785    }
786
787    #[test]
788    fn add_path_two_segments() {
789        let mut e = WideEvent::<TestKey>::new();
790        e.add_path(&[TestKey::Service, TestKey::Name], "my-service");
791        assert_eq!(e.to_json().unwrap(), r#"{"service":{"name":"my-service"}}"#);
792    }
793
794    #[test]
795    fn add_path_updates_existing_nested() {
796        let mut e = WideEvent::<TestKey>::new();
797        e.add_path(&[TestKey::Service, TestKey::Name], "a");
798        e.add_path(&[TestKey::Service, TestKey::Version], "1.0.0");
799        e.add_path(&[TestKey::Service, TestKey::Name], "b");
800        let json = e.to_json().unwrap();
801        // Index-ordered: service object has name and version
802        assert!(json.contains(r#""name":"b""#));
803        assert!(json.contains(r#""version":"1.0.0""#));
804    }
805
806    #[test]
807    fn add_path_duration_path() {
808        use crate::key::Key;
809        let mut e = WideEvent::<TestKey>::new();
810        e.add_path(<TestKey as Key>::DURATION_PATH, 42u64);
811        let json = e.to_json().unwrap();
812        assert_eq!(json, r#"{"duration":{"total_ms":42}}"#);
813    }
814
815    // ── inc / dec / add_n tests ──
816
817    #[test]
818    fn inc_initializes_to_one() {
819        let mut e = WideEvent::<TestKey>::new();
820        e.inc(TestKey::Requests);
821        assert_eq!(e.to_json().unwrap(), r#"{"requests":1}"#);
822    }
823
824    #[test]
825    fn inc_increments_existing() {
826        let mut e = WideEvent::<TestKey>::new();
827        e.add(TestKey::Requests, 41u64);
828        e.inc(TestKey::Requests);
829        assert_eq!(e.to_json().unwrap(), r#"{"requests":42}"#);
830    }
831
832    #[test]
833    fn dec_initializes_to_minus_one() {
834        let mut e = WideEvent::<TestKey>::new();
835        e.dec(TestKey::Requests);
836        assert_eq!(e.to_json().unwrap(), r#"{"requests":-1}"#);
837    }
838
839    #[test]
840    fn dec_decrements_existing() {
841        let mut e = WideEvent::<TestKey>::new();
842        e.add(TestKey::Requests, 5u64);
843        e.dec(TestKey::Requests);
844        assert_eq!(e.to_json().unwrap(), r#"{"requests":4}"#);
845    }
846
847    #[test]
848    fn add_n_initializes() {
849        let mut e = WideEvent::<TestKey>::new();
850        e.add_n(TestKey::Requests, 10);
851        assert_eq!(e.to_json().unwrap(), r#"{"requests":10}"#);
852    }
853
854    #[test]
855    fn add_n_adds_to_existing() {
856        let mut e = WideEvent::<TestKey>::new();
857        e.add(TestKey::Requests, 40u64);
858        e.add_n(TestKey::Requests, 2);
859        assert_eq!(e.to_json().unwrap(), r#"{"requests":42}"#);
860    }
861
862    #[test]
863    fn add_n_negative_from_u64() {
864        let mut e = WideEvent::<TestKey>::new();
865        e.add(TestKey::Requests, 10u64);
866        e.add_n(TestKey::Requests, -3);
867        assert_eq!(e.to_json().unwrap(), r#"{"requests":7}"#);
868    }
869
870    // ── Path inc/dec/add_n tests ──
871
872    #[test]
873    fn inc_path_single_segment() {
874        let mut e = WideEvent::<TestKey>::new();
875        e.inc_path(&[TestKey::Requests]);
876        e.inc_path(&[TestKey::Requests]);
877        e.inc_path(&[TestKey::Requests]);
878        assert_eq!(e.to_json().unwrap(), r#"{"requests":3}"#);
879    }
880
881    #[test]
882    fn dec_path_single_segment() {
883        let mut e = WideEvent::<TestKey>::new();
884        e.dec_path(&[TestKey::Requests]);
885        assert_eq!(e.to_json().unwrap(), r#"{"requests":-1}"#);
886    }
887
888    #[test]
889    fn add_n_path_single_segment() {
890        let mut e = WideEvent::<TestKey>::new();
891        e.add_n_path(&[TestKey::Requests], 5);
892        e.add_n_path(&[TestKey::Requests], -2);
893        assert_eq!(e.to_json().unwrap(), r#"{"requests":3}"#);
894    }
895
896    #[test]
897    fn inc_path_two_segments() {
898        let mut e = WideEvent::<TestKey>::new();
899        e.add_path(&[TestKey::Service, TestKey::Requests], 0u64);
900        e.inc_path(&[TestKey::Service, TestKey::Requests]);
901        e.inc_path(&[TestKey::Service, TestKey::Requests]);
902        let json = e.to_json().unwrap();
903        assert!(json.contains(r#""requests":2"#));
904    }
905
906    #[test]
907    fn add_n_path_two_segments() {
908        let mut e = WideEvent::<TestKey>::new();
909        e.add_n_path(&[TestKey::Service, TestKey::Requests], 10);
910        e.add_n_path(&[TestKey::Service, TestKey::Requests], -3);
911        let json = e.to_json().unwrap();
912        assert!(json.contains(r#""requests":7"#));
913    }
914
915    // ── Log entry tests ──
916
917    #[test]
918    fn append_log_entry_single() {
919        let mut e = WideEvent::<TestKey>::new();
920        e.append_log_entry("info", "request received");
921        assert!(e.has_logs());
922        assert_eq!(e.log_len(), 1);
923        let json = e.to_json().unwrap();
924        assert_eq!(
925            json,
926            r#"{"log":[{"level":"info","message":"request received"}]}"#
927        );
928    }
929
930    #[test]
931    fn append_log_entry_multiple_preserves_order() {
932        let mut e = WideEvent::<TestKey>::new();
933        e.append_log_entry("info", "first");
934        e.append_log_entry("warn", "second");
935        e.append_log_entry("error", "third");
936        assert_eq!(e.log_len(), 3);
937        let json = e.to_json().unwrap();
938        assert_eq!(
939            json,
940            concat!(
941                r#"{"log":["#,
942                r#"{"level":"info","message":"first"},"#,
943                r#"{"level":"warn","message":"second"},"#,
944                r#"{"level":"error","message":"third"}"#,
945                r#"]}"#,
946            )
947        );
948    }
949
950    #[test]
951    fn log_entries_after_user_entries() {
952        let mut e = WideEvent::<TestKey>::new();
953        e.add(TestKey::Status, "ok");
954        e.append_log_entry("info", "done");
955        let json = e.to_json().unwrap();
956        assert_eq!(
957            json,
958            r#"{"status":"ok","log":[{"level":"info","message":"done"}]}"#
959        );
960    }
961
962    #[test]
963    fn no_log_key_when_empty() {
964        let mut e = WideEvent::<TestKey>::new();
965        e.add(TestKey::Status, "ok");
966        let json = e.to_json().unwrap();
967        assert_eq!(json, r#"{"status":"ok"}"#);
968        assert!(!json.contains("log"));
969    }
970}