harness/contexts/
service_check.rs

1//! Service-check contexts: the stable `_sc` identity a driver renders status, message, and timestamp
2//! against.
3//!
4//! The Agent does not aggregate service checks, but each has a natural stable identity — name, tags,
5//! and host — distinct from the per-occurrence status, message, and timestamp. The pool recurs the
6//! identity; the driver varies status, message, and timestamp each render.
7
8use rand::{Rng, RngExt};
9
10use super::TS_DIGITS;
11use super::{fresh_timestamp, get_bytes, get_tags, put_bytes, put_tags};
12use crate::payload::dogstatsd::common;
13
14/// The status symbols: OK, warning, critical, unknown.
15const STATUS: &[&[u8]] = &[b"0", b"1", b"2", b"3"];
16
17/// Identity option counts: usually none, sometimes a host.
18const OPT_COUNTS: &[usize] = &[0, 0, 0, 1];
19
20/// A service-check identity: name, tags, and fixed option chunks (a host). Status, message, and
21/// timestamp vary per render.
22#[derive(Clone, Debug, PartialEq, Eq, Hash)]
23pub struct ServiceCheckContext {
24    /// Name content, non-empty.
25    pub name: Vec<u8>,
26    /// `key:value` tags.
27    pub tags: Vec<Vec<u8>>,
28    /// Fixed option chunks (a `h:` host), each carrying its own prefix.
29    pub options: Vec<Vec<u8>>,
30}
31
32impl ServiceCheckContext {
33    /// Mint a service-check identity that renders within `budget`, or `None` when the budget cannot
34    /// hold the smallest one. Name, options and tags are built against the room the render's skeleton,
35    /// timestamp and trailing message leave.
36    pub(crate) fn mint_within(rng: &mut (impl Rng + ?Sized), budget: usize) -> Option<Self> {
37        const RESERVED: usize = "_sc|".len() + 1 + 1 + "|d:".len() + TS_DIGITS + "|m:".len();
38        let name = common::identifier_within(rng, budget.checked_sub(RESERVED)?);
39        if name.is_empty() {
40            return None;
41        }
42        let mut room = budget - RESERVED - name.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 Some(body_room) = room.checked_sub(1 + "h:".len()) else {
47                break;
48            };
49            let mut chunk = b"h:".to_vec();
50            chunk.extend_from_slice(&common::optional_text_within(rng, body_room));
51            room -= 1 + chunk.len();
52            options.push(chunk);
53        }
54        Some(Self {
55            name,
56            tags: common::tags_within(rng, room),
57            options,
58        })
59    }
60
61    /// Render `_sc|name|status[|opt...]|d:ts[|#tags]|m:message` for a fresh status, message, and
62    /// timestamp. Returns zero (service checks carry no packed run).
63    /// Bytes every render of this identity must spend: `_sc|name|status`, the fixed options, the
64    /// timestamp, the tag set and the empty `|m:` message. Only the message body is variable.
65    pub(crate) fn floor(&self) -> usize {
66        "_sc|".len()
67            + self.name.len()
68            + 1
69            + 1
70            + self.options.iter().map(|opt| 1 + opt.len()).sum::<usize>()
71            + "|d:".len()
72            + TS_DIGITS
73            + common::tags_len(&self.tags)
74            + "|m:".len()
75    }
76
77    /// Render within `budget`, or `None` when the budget cannot hold the identity. Only the message is
78    /// sampled, against the room left.
79    pub(crate) fn render_within(
80        &self, rng: &mut (impl Rng + ?Sized), out: &mut Vec<u8>, budget: usize,
81    ) -> Option<usize> {
82        let message_room = budget.checked_sub(self.floor())?;
83        out.extend_from_slice(b"_sc|");
84        out.extend_from_slice(&self.name);
85        out.push(b'|');
86        out.extend_from_slice(STATUS[rng.random_range(0..STATUS.len())]);
87        for opt in &self.options {
88            out.push(b'|');
89            out.extend_from_slice(opt);
90        }
91        let mut itoa = itoa::Buffer::new();
92        out.extend_from_slice(b"|d:");
93        out.extend_from_slice(itoa.format(fresh_timestamp(rng)).as_bytes());
94        common::serialize_tags(&self.tags, out);
95        out.extend_from_slice(b"|m:");
96        out.extend_from_slice(&common::optional_text_within(rng, message_room));
97        Some(0)
98    }
99
100    pub(crate) fn render(&self, rng: &mut (impl Rng + ?Sized), out: &mut Vec<u8>) -> usize {
101        out.extend_from_slice(b"_sc|");
102        out.extend_from_slice(&self.name);
103        out.push(b'|');
104        out.extend_from_slice(STATUS[rng.random_range(0..STATUS.len())]);
105        for opt in &self.options {
106            out.push(b'|');
107            out.extend_from_slice(opt);
108        }
109        let mut itoa = itoa::Buffer::new();
110        out.extend_from_slice(b"|d:");
111        out.extend_from_slice(itoa.format(fresh_timestamp(rng)).as_bytes());
112        common::serialize_tags(&self.tags, out);
113        out.extend_from_slice(b"|m:");
114        out.extend_from_slice(&common::optional_text(rng));
115        0
116    }
117
118    /// Append this context's length-prefixed encoding.
119    pub(crate) fn encode(&self, out: &mut Vec<u8>) {
120        put_bytes(out, &self.name);
121        put_tags(out, &self.tags);
122        put_tags(out, &self.options);
123    }
124
125    /// Decode one service-check context, advancing `*pos`.
126    pub(crate) fn decode(buf: &[u8], pos: &mut usize) -> Option<Self> {
127        let name = get_bytes(buf, pos)?.to_vec();
128        let tags = get_tags(buf, pos)?;
129        let options = get_tags(buf, pos)?;
130        Some(Self { name, tags, options })
131    }
132}