below_render/
lib.rs

1// Copyright (c) Facebook, Inc. and its affiliates.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![deny(clippy::all)]
16
17use std::collections::BTreeMap;
18use std::fmt::Display;
19use std::fmt::Formatter;
20use std::fmt::Write;
21
22mod default_configs;
23
24use common::open_source_shim;
25use common::util::convert_bytes;
26use common::util::convert_duration;
27use common::util::convert_freq;
28use common::util::fold_string;
29use model::Field;
30use model::Queriable;
31
32open_source_shim!();
33
34/// Specifies how to format a Field into String
35#[derive(Clone)]
36pub enum RenderFormat {
37    /// Truncates String to a certain width.
38    Precision(usize),
39    /// Only works on numeric Fields. Format as human-readable size with
40    /// suffixes (KB, MB, GB etc).
41    ReadableSize,
42    /// Only works on numeric Fields. Format number of 4K pages as
43    /// human-readable size with suffixes (KB, MB, GB, etc).
44    PageReadableSize,
45    /// Only works on numeric Fields. Formats number of 512b sectors as
46    /// human-readable size with suffixes (KB, MB, GB, etc).
47    SectorReadableSize,
48    /// Only works on int Fields. Same as ReadableSize except when Field is -1,
49    /// in which case "max" is returned.
50    MaxOrReadableSize,
51    /// Frequency. Format with human-readable freq with suffixes (MHz, GHz etc.)
52    ReadableFrequency,
53    /// Only works on int Fields. Displays duration with human readable
54    /// suffixes (us, ms, s, etc.)
55    Duration,
56    /// Only works on int Fields. -1 displays "max" else displays duration with
57    /// human readable suffixes (us, ms, s, etc.)
58    MaxOrDuration,
59}
60
61/// Specifies how a long string is folded to fit into a shorter width.
62#[derive(Clone)]
63pub enum FoldOption {
64    /// Starts elision from first non-alphanumeric character.
65    Name,
66    /// Starts elision from first subdirectory (second '/' as we skip root).
67    Path,
68}
69
70/// Config object for specifying how to render a Field. Options are ordered
71/// roughly by their order of processing.
72#[derive(Default, Clone)]
73pub struct RenderConfig {
74    pub title: Option<String>,
75    /// Converting Field to String.
76    pub format: Option<RenderFormat>,
77    /// Prefix when rendered with indent. Each extra level adds same number of
78    /// spaces equal to the length of this prefix. This allows us to render:
79    /// <root>
80    /// -+ branch
81    ///    -* leaf
82    /// -* another_leaf
83    /// The example above use two prefixes, "-+ " and "-* ". Root has no prefix.
84    pub indented_prefix: Option<String>,
85    pub suffix: Option<String>,
86    /// Fit a long rendered Field into smaller width by omitting some characters
87    /// in the middle instead of truncating. Only applies when rendering Field
88    /// with fixed width. Taken indent, prefix and suffix len into account.
89    pub fold: Option<FoldOption>,
90    /// For fixed width rendering. Truncate or pad whitespace to output.
91    pub width: Option<usize>,
92}
93
94#[derive(Default, Clone)]
95pub struct RenderConfigBuilder {
96    rc: RenderConfig,
97}
98
99#[derive(Clone)]
100pub enum OpenMetricsType {
101    /// Counters measure discrete events. Common examples are the number of HTTP requests received,
102    /// CPU seconds spent, or bytes sent. For counters how quickly they are increasing over time is
103    /// what is of interest to a user.
104    Counter,
105    /// Gauges are current measurements, such as bytes of memory currently used or the number of
106    /// items in a queue. For gauges the absolute value is what is of interest to a user.
107    Gauge,
108}
109
110/// Configuration for rendering fields in OpenMetrics format.
111///
112/// See the OpenMetrics spec for more details:
113/// https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md
114#[derive(Clone)]
115pub struct RenderOpenMetricsConfig {
116    ty: OpenMetricsType,
117    help: Option<String>,
118    unit: Option<String>,
119    labels: BTreeMap<String, String>,
120}
121
122#[derive(Clone)]
123pub struct RenderOpenMetricsConfigBuilder {
124    config: RenderOpenMetricsConfig,
125}
126
127impl Display for OpenMetricsType {
128    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
129        match self {
130            OpenMetricsType::Counter => write!(f, "counter"),
131            OpenMetricsType::Gauge => write!(f, "gauge"),
132        }
133    }
134}
135
136impl RenderOpenMetricsConfigBuilder {
137    pub fn new(ty: OpenMetricsType) -> Self {
138        Self {
139            config: RenderOpenMetricsConfig {
140                ty,
141                help: None,
142                unit: None,
143                labels: BTreeMap::new(),
144            },
145        }
146    }
147
148    /// Help text for the metric
149    pub fn help(mut self, help: &str) -> Self {
150        self.config.help = Some(help.to_owned());
151        self
152    }
153
154    /// Unit for the metric
155    pub fn unit(mut self, unit: &str) -> Self {
156        self.config.unit = Some(unit.to_owned());
157        self
158    }
159
160    /// Add a label to the metric
161    ///
162    /// Note multiple labels for a single metric is supported
163    pub fn label(mut self, key: &str, value: &str) -> Self {
164        // Escape value according to spec:
165        // https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#escaping
166        let mut value_escaped = String::with_capacity(value.len());
167        for c in value.chars() {
168            match c {
169                '\\' => value_escaped.push_str("\\\\"),
170                '\"' => value_escaped.push_str("\\\""),
171                '\n' => value_escaped.push_str("\\n"),
172                _ => value_escaped.push(c),
173            }
174        }
175
176        self.config.labels.insert(key.to_owned(), value_escaped);
177        self
178    }
179
180    /// Build the config
181    pub fn build(self) -> RenderOpenMetricsConfig {
182        self.config
183    }
184}
185
186fn gauge() -> RenderOpenMetricsConfigBuilder {
187    RenderOpenMetricsConfigBuilder::new(OpenMetricsType::Gauge)
188}
189
190fn counter() -> RenderOpenMetricsConfigBuilder {
191    RenderOpenMetricsConfigBuilder::new(OpenMetricsType::Counter)
192}
193
194impl RenderOpenMetricsConfig {
195    /// Returns the normalized key name for this metric
196    fn normalize_key(&self, key: &str) -> String {
197        let mut ret = key.to_owned();
198        if let Some(unit) = &self.unit {
199            // If a unit is provided, it _must_ be suffixed on the key separated
200            // by an underscore. The spec requires it.
201            if !key.ends_with(unit) {
202                ret = format!("{}_{}", key, unit);
203            }
204        }
205        ret
206    }
207
208    fn render_field(&self, key: &str, field: Field, timestamp: i64) -> String {
209        let mut res = String::new();
210
211        let key = self.normalize_key(key);
212        let metric_type = self.ty.to_string();
213        let labels = if self.labels.is_empty() {
214            "".to_owned()
215        } else {
216            let body = self
217                .labels
218                .iter()
219                .map(|(k, v)| format!("{}=\"{}\"", k, v))
220                .collect::<Vec<_>>()
221                .join(",");
222            format!("{{{}}}", body)
223        };
224
225        // Appending to a string can never fail so unwrap() is safe here
226        writeln!(&mut res, "# TYPE {key} {metric_type}").unwrap();
227        if let Some(help) = &self.help {
228            writeln!(&mut res, "# HELP {key} {help}").unwrap();
229        }
230        if let Some(unit) = &self.unit {
231            writeln!(&mut res, "# UNIT {key} {unit}").unwrap();
232        }
233        writeln!(&mut res, "{key}{labels} {field} {timestamp}").unwrap();
234
235        res
236    }
237
238    /// Render the field as an openmetrics field in string form
239    pub fn render(&self, key: &str, field: Field, timestamp: i64) -> String {
240        match field {
241            Field::StrU64Map(map) => {
242                let mut res = String::new();
243                for (k, v) in map {
244                    let key = format!("{}_{}", key, k);
245                    let out = self.render_field(&key, Field::U64(v), timestamp);
246                    res.push_str(&out);
247                }
248                res
249            }
250            _ => self.render_field(key, field, timestamp),
251        }
252    }
253}
254
255impl RenderConfigBuilder {
256    pub fn new() -> Self {
257        Default::default()
258    }
259    pub fn get(self) -> RenderConfig {
260        self.rc
261    }
262    pub fn title<T: AsRef<str>>(mut self, title: T) -> Self {
263        self.rc.title = Some(title.as_ref().to_owned());
264        self
265    }
266    pub fn format(mut self, format: RenderFormat) -> Self {
267        self.rc.format = Some(format);
268        self
269    }
270    pub fn indented_prefix<T: AsRef<str>>(mut self, indented_prefix: T) -> Self {
271        self.rc.indented_prefix = Some(indented_prefix.as_ref().to_owned());
272        self
273    }
274    pub fn suffix<T: AsRef<str>>(mut self, suffix: T) -> Self {
275        self.rc.suffix = Some(suffix.as_ref().to_owned());
276        self
277    }
278    pub fn fold(mut self, fold: FoldOption) -> Self {
279        self.rc.fold = Some(fold);
280        self
281    }
282    pub fn width(mut self, width: usize) -> Self {
283        self.rc.width = Some(width);
284        self
285    }
286}
287
288impl From<RenderConfigBuilder> for RenderConfig {
289    fn from(b: RenderConfigBuilder) -> Self {
290        b.rc
291    }
292}
293
294impl From<RenderConfig> for RenderConfigBuilder {
295    fn from(rc: RenderConfig) -> Self {
296        RenderConfigBuilder { rc }
297    }
298}
299
300pub fn get_fixed_width(val: &str, width: usize) -> String {
301    format!("{val:width$.width$}", val = val, width = width)
302}
303
304impl RenderConfig {
305    pub fn update<T: Into<Self>>(mut self, overrides: T) -> Self {
306        let overrides = overrides.into();
307        self.title = overrides.title.or(self.title);
308        self.format = overrides.format.or(self.format);
309        self.indented_prefix = overrides.indented_prefix.or(self.indented_prefix);
310        self.suffix = overrides.suffix.or(self.suffix);
311        self.fold = overrides.fold.or(self.fold);
312        self.width = overrides.width.or(self.width);
313        self
314    }
315
316    pub fn get_title(&self) -> &str {
317        self.title.as_deref().unwrap_or("unknown")
318    }
319
320    /// Value for fixed-width rendering, with default as title width + 2 and
321    /// minimum width 10.
322    fn get_width(&self) -> usize {
323        const MIN_WIDTH: usize = 10;
324        std::cmp::max(MIN_WIDTH, self.width.unwrap_or(self.get_title().len() + 2))
325    }
326
327    pub fn render_title(&self, fixed_width: bool) -> String {
328        if fixed_width {
329            get_fixed_width(self.get_title(), self.get_width())
330        } else {
331            self.get_title().to_owned()
332        }
333    }
334
335    /// Applies format to render a Field into a String.
336    fn format(&self, field: Field) -> String {
337        use RenderFormat::*;
338        match &self.format {
339            Some(format) => match format {
340                Precision(precision) => format!("{:.precision$}", field, precision = precision),
341                ReadableSize => convert_bytes(f64::from(field)),
342                PageReadableSize => convert_bytes(4096.0 * f64::from(field)),
343                SectorReadableSize => convert_bytes(512.0 * f64::from(field)),
344                MaxOrReadableSize => {
345                    let field = i64::from(field);
346                    if field == -1 {
347                        "max".to_owned()
348                    } else {
349                        convert_bytes(field as f64)
350                    }
351                }
352                ReadableFrequency => convert_freq(u64::from(field)),
353                Duration => {
354                    let field = u64::from(field);
355                    convert_duration(field)
356                }
357                MaxOrDuration => {
358                    let field = i64::from(field);
359                    if field == -1 {
360                        "max".to_owned()
361                    } else {
362                        convert_duration(field as u64)
363                    }
364                }
365            },
366            None => field.to_string(),
367        }
368    }
369
370    fn fold_str(&self, val: &str, width: usize) -> String {
371        match self.fold {
372            Some(FoldOption::Name) => fold_string(val, width, 0, |c: char| !c.is_alphanumeric()),
373            Some(FoldOption::Path) => fold_string(val, width, 1, |c: char| c == '/'),
374            None => val.to_owned(),
375        }
376    }
377
378    /// Renders Field with all options applied. `depth` specifies the depth of
379    /// the model of this Field, where the model is Recursive, i.e. it works as
380    /// a node in a tree. Currently this only affects indented_prefix.
381    pub fn render_indented(&self, field: Option<Field>, fixed_width: bool, depth: usize) -> String {
382        let res = match field {
383            Some(field) => self.format(field),
384            None => {
385                return if fixed_width {
386                    get_fixed_width("?", self.get_width())
387                } else {
388                    "?".to_owned()
389                };
390            }
391        };
392        let indented_prefix = self.indented_prefix.as_deref().unwrap_or("");
393        let suffix = self.suffix.as_deref().unwrap_or("");
394        // May contain UTF8 chars
395        let indented_prefix_len = indented_prefix.chars().count();
396        let suffix_len = suffix.chars().count();
397        // When depth == 0, neither indent nor prefix is rendered.
398        let indented_prefix_width = indented_prefix_len * depth;
399        if fixed_width {
400            // The folded string has target len be fixed width subtracts indent,
401            // indented_prefix, and suffix.
402            let remain_len = self
403                .get_width()
404                .saturating_sub(indented_prefix_width + suffix_len);
405            let res = self.fold_str(&res, remain_len);
406            let res = format!(
407                "{:>prefix_width$.prefix_width$}{}{}",
408                indented_prefix,
409                res,
410                suffix,
411                prefix_width = indented_prefix_width
412            );
413            get_fixed_width(&res, self.get_width())
414        } else {
415            format!(
416                "{:>prefix_width$.prefix_width$}{}{}",
417                indented_prefix,
418                res,
419                suffix,
420                prefix_width = indented_prefix_width
421            )
422        }
423    }
424
425    /// Renders Field with all options without indent.
426    pub fn render(&self, field: Option<Field>, fixed_width: bool) -> String {
427        self.render_indented(field, fixed_width, 0)
428    }
429}
430
431/// Provide default RenderConfig for each Field in a Model
432pub trait HasRenderConfig: Queriable {
433    fn get_render_config_builder(field_id: &Self::FieldId) -> RenderConfigBuilder;
434    fn get_render_config(field_id: &Self::FieldId) -> RenderConfig {
435        Self::get_render_config_builder(field_id).get()
436    }
437}
438
439pub trait HasRenderConfigForDump: HasRenderConfig {
440    fn get_render_config_for_dump(field_id: &Self::FieldId) -> RenderConfig {
441        Self::get_render_config(field_id)
442    }
443
444    /// Configures how to dump model fields in OpenMetrics format
445    ///
446    /// Some fields cannot be dumped in openmetrics format, for example strings. For those,
447    /// return None.
448    fn get_openmetrics_config_for_dump(
449        &self,
450        _field_id: &Self::FieldId,
451    ) -> Option<RenderOpenMetricsConfigBuilder>;
452}
453
454#[test]
455fn test_openmetrics_gauge() {
456    let config = RenderOpenMetricsConfigBuilder::new(OpenMetricsType::Gauge)
457        .help("gauge help")
458        .build();
459    let text = config.render("my_key", Field::U64(123), 1234);
460    let expected = r#"# TYPE my_key gauge
461# HELP my_key gauge help
462my_key 123 1234
463"#;
464    assert_eq!(text, expected);
465}
466
467#[test]
468fn test_openmetrics_counter() {
469    let config = RenderOpenMetricsConfigBuilder::new(OpenMetricsType::Counter)
470        .help("counter help")
471        .build();
472    let text = config.render("my_key", Field::F32(1.23), 1234);
473    let expected = r#"# TYPE my_key counter
474# HELP my_key counter help
475my_key 1.23 1234
476"#;
477    assert_eq!(text, expected);
478}
479
480// Unit suffix should be appended
481#[test]
482fn test_openmetrics_unit() {
483    let config = RenderOpenMetricsConfigBuilder::new(OpenMetricsType::Counter)
484        .unit("foobars")
485        .build();
486    let text = config.render("my_key", Field::F32(1.23), 1234);
487    let expected = r#"# TYPE my_key_foobars counter
488# UNIT my_key_foobars foobars
489my_key_foobars 1.23 1234
490"#;
491    assert_eq!(text, expected);
492}
493
494// Unit suffix already present, so should not be doubled
495#[test]
496fn test_openmetrics_unit_exists() {
497    let config = RenderOpenMetricsConfigBuilder::new(OpenMetricsType::Counter)
498        .unit("foobars")
499        .build();
500    let text = config.render("my_key_foobars", Field::F32(1.23), 1234);
501    let expected = r#"# TYPE my_key_foobars counter
502# UNIT my_key_foobars foobars
503my_key_foobars 1.23 1234
504"#;
505    assert_eq!(text, expected);
506}
507
508#[test]
509fn test_openmetrics_label() {
510    let config = RenderOpenMetricsConfigBuilder::new(OpenMetricsType::Counter)
511        .help("counter help")
512        .label("label1", "value1")
513        .build();
514    let text = config.render("my_key", Field::F32(1.23), 1234);
515    let expected = r#"# TYPE my_key counter
516# HELP my_key counter help
517my_key{label1="value1"} 1.23 1234
518"#;
519    assert_eq!(text, expected);
520}
521
522#[test]
523fn test_openmetrics_label_escaped_quotes() {
524    let config = RenderOpenMetricsConfigBuilder::new(OpenMetricsType::Counter)
525        .help("counter help")
526        .label("label1", r#"quotes""between"#)
527        .build();
528    let text = config.render("my_key", Field::F32(1.23), 1234);
529    let expected = r#"# TYPE my_key counter
530# HELP my_key counter help
531my_key{label1="quotes\"\"between"} 1.23 1234
532"#;
533    assert_eq!(text, expected);
534}
535
536#[test]
537fn test_openmetrics_label_escaped_newline() {
538    let config = RenderOpenMetricsConfigBuilder::new(OpenMetricsType::Counter)
539        .help("counter help")
540        .label("label1", "newline\nbetween")
541        .build();
542    let text = config.render("my_key", Field::F32(1.23), 1234);
543    let expected = r#"# TYPE my_key counter
544# HELP my_key counter help
545my_key{label1="newline\nbetween"} 1.23 1234
546"#;
547    assert_eq!(text, expected);
548}
549
550#[test]
551fn test_openmetrics_label_escaped_backslash() {
552    let config = RenderOpenMetricsConfigBuilder::new(OpenMetricsType::Counter)
553        .help("counter help")
554        .label("label1", r#"newline\between"#)
555        .build();
556    let text = config.render("my_key", Field::F32(1.23), 1234);
557    let expected = r#"# TYPE my_key counter
558# HELP my_key counter help
559my_key{label1="newline\\between"} 1.23 1234
560"#;
561    assert_eq!(text, expected);
562}
563
564#[test]
565fn test_openmetrics_label_escaped_newline_and_backslash() {
566    let config = RenderOpenMetricsConfigBuilder::new(OpenMetricsType::Counter)
567        .help("counter help")
568        .label("label1", "newline\\\nbetween")
569        .build();
570    let text = config.render("my_key", Field::F32(1.23), 1234);
571    let expected = r#"# TYPE my_key counter
572# HELP my_key counter help
573my_key{label1="newline\\\nbetween"} 1.23 1234
574"#;
575    assert_eq!(text, expected);
576}
577
578#[test]
579fn test_openmetrics_labels() {
580    let config = RenderOpenMetricsConfigBuilder::new(OpenMetricsType::Counter)
581        .help("counter help")
582        .label("label1", "value1")
583        .label("label2", "value2")
584        .label("label3", "zzz")
585        .build();
586    let text = config.render("my_key", Field::F32(1.23), 1234);
587    let expected = r#"# TYPE my_key counter
588# HELP my_key counter help
589my_key{label1="value1",label2="value2",label3="zzz"} 1.23 1234
590"#;
591    assert_eq!(text, expected);
592}