harness/contexts/
metric.rs

1//! Metric contexts: the `(kind, name, tags)` identity a driver renders values against.
2
3use rand::{Rng, RngExt};
4
5use super::{get_bytes, get_tags, get_u8, put_bytes, put_tags, put_u8};
6use crate::payload::dogstatsd::common;
7
8/// The six metric types.
9const METRIC_TYPES: [MetricType; 6] = [
10    MetricType::Count,
11    MetricType::Gauge,
12    MetricType::Timing,
13    MetricType::Histogram,
14    MetricType::Set,
15    MetricType::Distribution,
16];
17
18/// Extension-chunk counts per render: mostly none, with a boundary tail.
19const EXT_COUNTS: &[usize] = &[0, 0, 0, 0, 1, 1, 2, 3, 127, 255];
20
21/// A metric type.
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
23pub enum MetricType {
24    /// Count.
25    Count,
26    /// Gauge.
27    Gauge,
28    /// Timing.
29    Timing,
30    /// Histogram.
31    Histogram,
32    /// Set.
33    Set,
34    /// Distribution.
35    Distribution,
36}
37
38impl MetricType {
39    /// The on-wire type symbol.
40    fn token(self) -> &'static [u8] {
41        match self {
42            MetricType::Count => b"c",
43            MetricType::Gauge => b"g",
44            MetricType::Timing => b"ms",
45            MetricType::Histogram => b"h",
46            MetricType::Set => b"s",
47            MetricType::Distribution => b"d",
48        }
49    }
50
51    /// A stable codec byte.
52    fn to_byte(self) -> u8 {
53        match self {
54            MetricType::Count => 0,
55            MetricType::Gauge => 1,
56            MetricType::Timing => 2,
57            MetricType::Histogram => 3,
58            MetricType::Set => 4,
59            MetricType::Distribution => 5,
60        }
61    }
62
63    /// Decode a codec byte.
64    fn from_byte(b: u8) -> Option<MetricType> {
65        Some(match b {
66            0 => MetricType::Count,
67            1 => MetricType::Gauge,
68            2 => MetricType::Timing,
69            3 => MetricType::Histogram,
70            4 => MetricType::Set,
71            5 => MetricType::Distribution,
72            _ => return None,
73        })
74    }
75
76    /// Whether this is the set type, whose value the Agent never parses.
77    fn is_set(self) -> bool {
78        matches!(self, MetricType::Set)
79    }
80}
81
82/// A metric identity: type, name, and tags. The value and extensions vary per render.
83#[derive(Clone, Debug, PartialEq, Eq, Hash)]
84pub struct MetricContext {
85    /// The metric type.
86    pub kind: MetricType,
87    /// Name content.
88    pub name: Vec<u8>,
89    /// `key:value` tags.
90    pub tags: Vec<Vec<u8>>,
91}
92
93impl MetricContext {
94    /// Mint a metric identity that renders within `budget`, or `None` when the budget cannot hold the
95    /// smallest one. The name and tags are built against the room the render's own skeleton leaves, so
96    /// the identity fits by construction and no probe is needed to find that out.
97    pub(crate) fn mint_within(rng: &mut (impl Rng + ?Sized), budget: usize) -> Option<Self> {
98        let kind = METRIC_TYPES[rng.random_range(0..METRIC_TYPES.len())];
99        // `:value|type` is what every render of this identity must carry beyond the name and tags.
100        let reserved = 1 + common::min_value_token() + 1 + kind.token().len();
101        let name = common::identifier_within(rng, budget.checked_sub(reserved)?);
102        if name.is_empty() {
103            return None;
104        }
105        let tags = common::tags_within(rng, budget - reserved - name.len());
106        Some(Self { kind, name, tags })
107    }
108
109    /// Bytes every render of this identity must spend: `name:value|type` and the tag set. A render
110    /// spends anything past this on extra packed values and extension chunks.
111    pub(crate) fn floor(&self) -> usize {
112        self.fixed() + common::min_value_token()
113    }
114
115    /// The identity's cost without the value placeholder.
116    fn fixed(&self) -> usize {
117        self.name.len() + 1 + 1 + self.kind.token().len() + common::tags_len(&self.tags)
118    }
119
120    /// Render `name:value|type[|#tags][|ext...]` within `budget`, or `None` when the budget cannot
121    /// hold the identity. Values and extensions are sampled against the room left, so no byte is built
122    /// and then thrown away.
123    pub(crate) fn render_within(
124        &self, rng: &mut (impl Rng + ?Sized), out: &mut Vec<u8>, budget: usize,
125    ) -> Option<usize> {
126        let fixed = self.fixed();
127        let value_room = budget.checked_sub(fixed)?;
128        if value_room < common::min_value_token() {
129            return None;
130        }
131        out.extend_from_slice(&self.name);
132        out.push(b':');
133        let mut used = 0;
134        let packed = if self.kind.is_set() {
135            let value = common::opaque_value_within(rng, value_room);
136            used += value.len();
137            out.extend_from_slice(&value);
138            0
139        } else {
140            let first = common::float_token_within(rng, value_room);
141            used += first.len();
142            out.extend_from_slice(&first);
143            let mut count = 1;
144            for _ in 1..value_count(rng) {
145                let room = value_room - used;
146                let Some(token_room) = room.checked_sub(1) else {
147                    break;
148                };
149                let value = common::float_token_within(rng, token_room);
150                if value.is_empty() {
151                    break;
152                }
153                out.push(b':');
154                out.extend_from_slice(&value);
155                used += 1 + value.len();
156                count += 1;
157            }
158            if count > 1 {
159                count
160            } else {
161                0
162            }
163        };
164        out.push(b'|');
165        out.extend_from_slice(self.kind.token());
166        common::serialize_tags(&self.tags, out);
167        let mut room = budget - (fixed + used);
168        let ext_count = EXT_COUNTS[rng.random_range(0..EXT_COUNTS.len())];
169        for _ in 0..ext_count {
170            let Some(chunk_room) = room.checked_sub(1) else {
171                break;
172            };
173            let Some(chunk) = ext_chunk_within(rng, chunk_room) else {
174                break;
175            };
176            out.push(b'|');
177            out.extend_from_slice(&chunk);
178            room -= 1 + chunk.len();
179        }
180        Some(packed)
181    }
182
183    /// Render `name:value|type[|#tags][|ext...]` for a fresh value and extensions. Returns the packed
184    /// multi-value run length, or zero for a single value or a set.
185    pub(crate) fn render(&self, rng: &mut (impl Rng + ?Sized), out: &mut Vec<u8>) -> usize {
186        out.extend_from_slice(&self.name);
187        out.push(b':');
188        let packed = if self.kind.is_set() {
189            out.extend_from_slice(&common::opaque_value(rng));
190            0
191        } else {
192            let count = value_count(rng);
193            for i in 0..count {
194                if i > 0 {
195                    out.push(b':');
196                }
197                out.extend_from_slice(&common::float_token(rng));
198            }
199            if count > 1 {
200                count
201            } else {
202                0
203            }
204        };
205        out.push(b'|');
206        out.extend_from_slice(self.kind.token());
207        common::serialize_tags(&self.tags, out);
208        let ext_count = EXT_COUNTS[rng.random_range(0..EXT_COUNTS.len())];
209        for _ in 0..ext_count {
210            out.push(b'|');
211            ext_chunk(rng, out);
212        }
213        packed
214    }
215
216    /// Append this context's length-prefixed encoding.
217    pub(crate) fn encode(&self, out: &mut Vec<u8>) {
218        put_u8(out, self.kind.to_byte());
219        put_bytes(out, &self.name);
220        put_tags(out, &self.tags);
221    }
222
223    /// Decode one metric context, advancing `*pos`.
224    pub(crate) fn decode(buf: &[u8], pos: &mut usize) -> Option<Self> {
225        let kind = MetricType::from_byte(get_u8(buf, pos)?)?;
226        let name = get_bytes(buf, pos)?.to_vec();
227        let tags = get_tags(buf, pos)?;
228        Some(Self { kind, name, tags })
229    }
230}
231
232/// The `:`-packed value run length. Overwhelmingly one, with a short tail.
233fn value_count(rng: &mut (impl Rng + ?Sized)) -> usize {
234    match rng.random_range(0..800u16) {
235        0..792 => 1,
236        792..796 => 2,
237        796..798 => 3,
238        798 => 4,
239        _ => 5,
240    }
241}
242
243/// One extension chunk within `budget`, or `None` when the budget cannot hold the shortest one.
244fn ext_chunk_within(rng: &mut (impl Rng + ?Sized), budget: usize) -> Option<Vec<u8>> {
245    let prefix: &[u8] = match rng.random_range(0..4u8) {
246        0 => b"@",
247        1 => b"c:",
248        2 => b"e:",
249        _ => b"card:",
250    };
251    let body_room = budget.checked_sub(prefix.len())?;
252    let body = if prefix == b"@" {
253        common::rate_token_within(rng, body_room)?
254    } else {
255        common::optional_text_within(rng, body_room)
256    };
257    let mut chunk = prefix.to_vec();
258    chunk.extend_from_slice(&body);
259    Some(chunk)
260}
261
262/// Append one extension chunk (prefix + body). `@` is a parseable rate; the origin chunks carry free
263/// content the Agent never drops on.
264fn ext_chunk(rng: &mut (impl Rng + ?Sized), out: &mut Vec<u8>) {
265    let (prefix, body): (&[u8], Vec<u8>) = match rng.random_range(0..4u8) {
266        0 => (b"@", common::rate_token(rng)),
267        1 => (b"c:", common::optional_text(rng)),
268        2 => (b"e:", common::optional_text(rng)),
269        _ => (b"card:", common::optional_text(rng)),
270    };
271    out.extend_from_slice(prefix);
272    out.extend_from_slice(&body);
273}