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