harness/contexts/
event.rs

1//! Event contexts: the stable `_e{}` identity a driver renders text and timestamps against.
2//!
3//! The Agent does not aggregate events, but each event has a natural stable identity — title, tags,
4//! and its option fields (aggregation key, host, source, alert type, priority) — distinct from the
5//! per-occurrence text and timestamp. The pool recurs the identity; the driver varies text and
6//! timestamp each render.
7
8use rand::{Rng, RngExt};
9
10use super::{fresh_timestamp, get_bytes, get_tags, put_bytes, put_tags};
11use super::{LEN_DIGITS, TS_DIGITS};
12use crate::payload::dogstatsd::common;
13
14/// Identity option prefixes: aggregation key, hostname, source type, alert type, priority. Bad values
15/// are logged and defaulted by the Agent, never dropped, so any content forwards.
16const OPT_PREFIXES: &[&[u8]] = &[b"k:", b"h:", b"s:", b"t:", b"p:"];
17
18/// Identity option counts: mostly none, a small body.
19const OPT_COUNTS: &[usize] = &[0, 0, 1, 1, 2, 3];
20
21/// An event identity: title, tags, and fixed option chunks. Text and timestamp vary per render.
22#[derive(Clone, Debug, PartialEq, Eq, Hash)]
23pub struct EventContext {
24    /// Title content, non-empty.
25    pub title: Vec<u8>,
26    /// `key:value` tags.
27    pub tags: Vec<Vec<u8>>,
28    /// Fixed option chunks, each carrying its own prefix.
29    pub options: Vec<Vec<u8>>,
30}
31
32impl EventContext {
33    /// Mint an event identity that renders within `budget`, or `None` when the budget cannot hold the
34    /// smallest one. Title, options and tags are built against the room the render's header, timestamp
35    /// and separators leave.
36    pub(crate) fn mint_within(rng: &mut (impl Rng + ?Sized), budget: usize) -> Option<Self> {
37        const RESERVED: usize = "_e{".len() + LEN_DIGITS + 1 + LEN_DIGITS + "}:".len() + 1 + "|d:".len() + TS_DIGITS;
38        let title = common::identifier_within(rng, budget.checked_sub(RESERVED)?);
39        if title.is_empty() {
40            return None;
41        }
42        let mut room = budget - RESERVED - title.len();
43        let count = OPT_COUNTS[rng.random_range(0..OPT_COUNTS.len())];
44        let mut options = Vec::new();
45        for _ in 0..count {
46            let prefix = OPT_PREFIXES[rng.random_range(0..OPT_PREFIXES.len())];
47            let Some(body_room) = room.checked_sub(1 + prefix.len()) else {
48                break;
49            };
50            let mut chunk = prefix.to_vec();
51            chunk.extend_from_slice(&common::optional_text_within(rng, body_room));
52            room -= 1 + chunk.len();
53            options.push(chunk);
54        }
55        Some(Self {
56            title,
57            tags: common::tags_within(rng, room),
58            options,
59        })
60    }
61
62    /// Render `_e{title_len,text_len}:title|text[|opt...]|d:ts[|#tags]` for a fresh text and
63    /// timestamp. The header lengths are the true byte lengths, so the Agent never rejects on a
64    /// length mismatch. Returns zero (events carry no packed run).
65    /// Bytes every render of this identity must spend: the header, the title, the fixed options, the
66    /// timestamp and the tag set. Only the text is variable. Length and timestamp digits are allowed
67    /// generously so the floor is never an underestimate.
68    pub(crate) fn floor(&self) -> usize {
69        const HEADER: usize = "_e{".len() + LEN_DIGITS + 1 + LEN_DIGITS + "}:".len();
70        HEADER
71            + self.title.len()
72            + 1
73            + self.options.iter().map(|opt| 1 + opt.len()).sum::<usize>()
74            + "|d:".len()
75            + TS_DIGITS
76            + common::tags_len(&self.tags)
77    }
78
79    /// Render within `budget`, or `None` when the budget cannot hold the identity. Only the text is
80    /// sampled, and it is sampled against the room left rather than trimmed afterwards.
81    pub(crate) fn render_within(
82        &self, rng: &mut (impl Rng + ?Sized), out: &mut Vec<u8>, budget: usize,
83    ) -> Option<usize> {
84        let text_room = budget.checked_sub(self.floor())?;
85        let text = common::optional_text_within(rng, text_room);
86        let mut itoa = itoa::Buffer::new();
87        out.extend_from_slice(b"_e{");
88        out.extend_from_slice(itoa.format(self.title.len()).as_bytes());
89        out.push(b',');
90        out.extend_from_slice(itoa.format(text.len()).as_bytes());
91        out.extend_from_slice(b"}:");
92        out.extend_from_slice(&self.title);
93        out.push(b'|');
94        out.extend_from_slice(&text);
95        for opt in &self.options {
96            out.push(b'|');
97            out.extend_from_slice(opt);
98        }
99        out.extend_from_slice(b"|d:");
100        out.extend_from_slice(itoa.format(fresh_timestamp(rng)).as_bytes());
101        common::serialize_tags(&self.tags, out);
102        Some(0)
103    }
104
105    pub(crate) fn render(&self, rng: &mut (impl Rng + ?Sized), out: &mut Vec<u8>) -> usize {
106        let text = common::optional_text(rng);
107        let mut itoa = itoa::Buffer::new();
108        out.extend_from_slice(b"_e{");
109        out.extend_from_slice(itoa.format(self.title.len()).as_bytes());
110        out.push(b',');
111        out.extend_from_slice(itoa.format(text.len()).as_bytes());
112        out.extend_from_slice(b"}:");
113        out.extend_from_slice(&self.title);
114        out.push(b'|');
115        out.extend_from_slice(&text);
116        for opt in &self.options {
117            out.push(b'|');
118            out.extend_from_slice(opt);
119        }
120        out.extend_from_slice(b"|d:");
121        out.extend_from_slice(itoa.format(fresh_timestamp(rng)).as_bytes());
122        common::serialize_tags(&self.tags, out);
123        0
124    }
125
126    /// Append this context's length-prefixed encoding.
127    pub(crate) fn encode(&self, out: &mut Vec<u8>) {
128        put_bytes(out, &self.title);
129        put_tags(out, &self.tags);
130        put_tags(out, &self.options);
131    }
132
133    /// Decode one event context, advancing `*pos`.
134    pub(crate) fn decode(buf: &[u8], pos: &mut usize) -> Option<Self> {
135        let title = get_bytes(buf, pos)?.to_vec();
136        let tags = get_tags(buf, pos)?;
137        let options = get_tags(buf, pos)?;
138        Some(Self { title, tags, options })
139    }
140}