nmbrs_runtime/readouts/builtins/
metric.rs1use crate::lifecycle::SubjectKind;
24use crate::readouts::buf::ReadoutBuf;
25use crate::readouts::context::ReadoutContext;
26use crate::readouts::readout::{ContentMode, Lod, Readout, ReadoutOptions};
27
28pub struct Metric;
29
30impl Readout for Metric {
31 fn name(&self) -> &'static str {
32 "metric"
33 }
34 fn accepts(&self) -> &'static [SubjectKind] {
35 &[SubjectKind::Phase]
36 }
37
38 fn render(
39 &self,
40 ctx: &dyn ReadoutContext,
41 _lod: Lod,
42 mode: ContentMode,
43 opts: &ReadoutOptions,
44 out: &mut dyn ReadoutBuf,
45 ) -> usize {
46 match mode {
47 ContentMode::Value => {
48 let chips = ctx.status_metric_chips();
49 if chips.is_empty() {
50 return 0;
51 }
52 let filtered = match opts.get_str("pattern") {
59 Some(pat) => filter_chips(&chips, pat),
60 None => chips,
61 };
62 let bytes = filtered.len();
63 let _ = out.write_str(&filtered);
64 bytes
65 }
66 ContentMode::Explanation => {
67 if ctx.status_metric_chips().is_empty() {
73 return 0;
74 }
75 let s = match opts.get_str("pattern") {
76 Some(pat) => format!(" (live aggregates matching '{pat}')"),
77 None => " (live aggregates: name:value pairs)".to_string(),
78 };
79 let _ = out.write_str(&s);
80 s.len()
81 }
82 }
83 }
84}
85
86fn filter_chips(chips: &str, pattern: &str) -> String {
97 let mut out = String::with_capacity(chips.len());
102 for chip in chips.split_whitespace() {
103 let name = chip.split_once(':').map(|(n, _)| n).unwrap_or(chip);
104 if glob_match(pattern, name) {
105 out.push(' ');
106 out.push_str(chip);
107 }
108 }
109 out
110}
111
112fn glob_match(pattern: &str, candidate: &str) -> bool {
116 glob_match_bytes(pattern.as_bytes(), candidate.as_bytes())
117}
118
119fn glob_match_bytes(pat: &[u8], s: &[u8]) -> bool {
120 match (pat.first(), s.first()) {
121 (None, None) => true,
122 (Some(b'*'), _) => {
123 glob_match_bytes(&pat[1..], s) || (!s.is_empty() && glob_match_bytes(pat, &s[1..]))
124 }
125 (Some(b'?'), Some(_)) => glob_match_bytes(&pat[1..], &s[1..]),
126 (Some(p), Some(c)) if p == c => glob_match_bytes(&pat[1..], &s[1..]),
127 _ => false,
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use crate::readouts::buf::StringBuf;
135
136 struct Ctx {
137 chips: String,
138 }
139 impl ReadoutContext for Ctx {
140 fn subject_name(&self) -> &str {
141 "x"
142 }
143 fn subject_seq(&self) -> Option<(usize, usize)> {
144 None
145 }
146 fn subject_labels(&self) -> &str {
147 ""
148 }
149 fn cycles_completed(&self) -> u64 {
150 0
151 }
152 fn cycles_total(&self) -> u64 {
153 0
154 }
155 fn ops_ok(&self) -> u64 {
156 0
157 }
158 fn errors(&self) -> u64 {
159 0
160 }
161 fn retries(&self) -> u64 {
162 0
163 }
164 fn concurrency(&self) -> usize {
165 0
166 }
167 fn elapsed_secs(&self) -> f64 {
168 0.0
169 }
170 fn consumed(&self) -> u64 {
171 0
172 }
173 fn status_metric_chips(&self) -> String {
174 self.chips.clone()
175 }
176 fn depth_indent(&self) -> &str {
177 ""
178 }
179 fn use_color(&self) -> bool {
180 false
181 }
182 fn event(&self) -> crate::lifecycle::EventType {
183 crate::lifecycle::EventType::Update
184 }
185 }
186
187 #[test]
188 fn renders_chips_string() {
189 let ctx = Ctx {
190 chips: " recall_at_10:79.62% latency_p99:1.23ms".into(),
191 };
192 let mut s = String::new();
193 let mut buf = StringBuf::new(&mut s);
194 let n = Metric.render(
195 &ctx,
196 Lod::Labeled,
197 ContentMode::Value,
198 &ReadoutOptions::new(),
199 &mut buf,
200 );
201 assert!(n > 0);
202 assert_eq!(s, " recall_at_10:79.62% latency_p99:1.23ms");
203 }
204
205 #[test]
206 fn empty_chips_renders_zero() {
207 let ctx = Ctx {
208 chips: String::new(),
209 };
210 let mut s = String::new();
211 let mut buf = StringBuf::new(&mut s);
212 let n = Metric.render(
213 &ctx,
214 Lod::Labeled,
215 ContentMode::Value,
216 &ReadoutOptions::new(),
217 &mut buf,
218 );
219 assert_eq!(n, 0);
220 assert!(s.is_empty());
221 }
222
223 #[test]
224 fn explanation_mode_emits_descriptor_when_chips_present() {
225 let ctx = Ctx {
226 chips: " latency_p99:1ms".into(),
227 };
228 let mut s = String::new();
229 let mut buf = StringBuf::new(&mut s);
230 let n = Metric.render(
231 &ctx,
232 Lod::Labeled,
233 ContentMode::Explanation,
234 &ReadoutOptions::new(),
235 &mut buf,
236 );
237 assert!(n > 0);
238 assert!(
239 s.contains("live aggregates"),
240 "expected descriptor, got {s}"
241 );
242 }
243
244 #[test]
245 fn explanation_mode_zero_when_no_chips() {
246 let ctx = Ctx {
247 chips: String::new(),
248 };
249 let mut s = String::new();
250 let mut buf = StringBuf::new(&mut s);
251 let n = Metric.render(
252 &ctx,
253 Lod::Labeled,
254 ContentMode::Explanation,
255 &ReadoutOptions::new(),
256 &mut buf,
257 );
258 assert_eq!(n, 0);
259 }
260
261 #[test]
262 fn pattern_filter_keeps_only_matching_chips() {
263 let ctx = Ctx {
264 chips: " recall_at_10:79.62% latency_p99:1.23ms recall_at_1:42.00% latency_max:5ms"
265 .into(),
266 };
267 let mut opts = ReadoutOptions::new();
268 opts.set(
269 "pattern",
270 super::super::super::OptionValue::Str("recall*".into()),
271 );
272 let mut s = String::new();
273 let mut buf = StringBuf::new(&mut s);
274 Metric.render(&ctx, Lod::Labeled, ContentMode::Value, &opts, &mut buf);
275 assert_eq!(s, " recall_at_10:79.62% recall_at_1:42.00%");
276 }
277
278 #[test]
279 fn pattern_filter_latency_family() {
280 let ctx = Ctx {
281 chips: " recall_at_10:79.62% latency_p50:1ms latency_p99:2ms".into(),
282 };
283 let mut opts = ReadoutOptions::new();
284 opts.set(
285 "pattern",
286 super::super::super::OptionValue::Str("latency*".into()),
287 );
288 let mut s = String::new();
289 let mut buf = StringBuf::new(&mut s);
290 Metric.render(&ctx, Lod::Labeled, ContentMode::Value, &opts, &mut buf);
291 assert_eq!(s, " latency_p50:1ms latency_p99:2ms");
292 }
293
294 #[test]
295 fn pattern_filter_exact_name_only_one_chip() {
296 let ctx = Ctx {
297 chips: " recall_at_10:79.62% recall_at_1:42.00%".into(),
298 };
299 let mut opts = ReadoutOptions::new();
300 opts.set(
301 "pattern",
302 super::super::super::OptionValue::Str("recall_at_1".into()),
303 );
304 let mut s = String::new();
305 let mut buf = StringBuf::new(&mut s);
306 Metric.render(&ctx, Lod::Labeled, ContentMode::Value, &opts, &mut buf);
307 assert_eq!(s, " recall_at_1:42.00%");
310 }
311
312 #[test]
313 fn pattern_with_no_match_returns_zero() {
314 let ctx = Ctx {
315 chips: " recall_at_10:79.62%".into(),
316 };
317 let mut opts = ReadoutOptions::new();
318 opts.set(
319 "pattern",
320 super::super::super::OptionValue::Str("nonexistent*".into()),
321 );
322 let mut s = String::new();
323 let mut buf = StringBuf::new(&mut s);
324 let n = Metric.render(&ctx, Lod::Labeled, ContentMode::Value, &opts, &mut buf);
325 assert_eq!(n, 0);
326 assert_eq!(s, "");
327 }
328
329 #[test]
330 fn no_pattern_emits_all_chips() {
331 let ctx = Ctx {
332 chips: " recall_at_10:79.62% latency_p99:1ms".into(),
333 };
334 let mut s = String::new();
335 let mut buf = StringBuf::new(&mut s);
336 Metric.render(
337 &ctx,
338 Lod::Labeled,
339 ContentMode::Value,
340 &ReadoutOptions::new(),
341 &mut buf,
342 );
343 assert_eq!(s, " recall_at_10:79.62% latency_p99:1ms");
346 }
347}