netdata_plugin/
lib.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3use std::fmt;
4use std::fmt::{Debug, Display};
5use validator::{Validate, ValidationError};
6
7/// High-Level interface to run the data loop
8/// and setup Chart and Dimension info in an efficient manner.  
9/// (The [Collector](collector::Collector) section includes an example.)
10pub mod collector;
11
12/// A private function used for the validation of `type_id` entries.
13///
14/// tests:
15/// * field is not empty.
16/// * there is only one dot present and it's neither the first nor the last character.
17/// * it doesn't contain illegal characters: [' ', '\t', '\n', '\r', '\\', '\'', '"', ','].
18fn validate_type_id(v: &str) -> Result<(), ValidationError> {
19    if v.is_empty() {
20        return Err(ValidationError::new("Empty type_id field"));
21    };
22
23    if v.chars().filter(|x| x == &'.').count() != 1
24        || v.chars().next() == Some('.')
25        || v.chars().last() == Some('.')
26    {
27        return Err(ValidationError::new(
28            "Requires one single dot in the middle of type_id field",
29        ));
30    }
31
32    if v.matches(&[' ', '\t', '\n', '\r', '\\', '\'', '"', ','])
33        .count()
34        != 0
35    {
36        return Err(ValidationError::new("Illegal character"));
37    }
38
39    Ok(())
40}
41
42/// A private function used for the validation of `id` entries.
43///
44/// tests:
45/// * field is not empty.
46/// * there is no dot present.
47/// * it doesn't contain other illegal characters: [' ', '\t', '\n', '\r', '\\', '\'', '"', ','].
48fn validate_id(v: &str) -> Result<(), ValidationError> {
49    if v.is_empty() {
50        return Err(ValidationError::new("Empty id field"));
51    };
52
53    if v.matches(&['.', ' ', '\t', '\n', '\r', '\\', '\'', '"', ','])
54        .count()
55        != 0
56    {
57        return Err(ValidationError::new("Illegal character"));
58    }
59
60    Ok(())
61}
62
63/// Command literals used for plugin communication.
64///
65/// Netdata parses `stdout` output of plugins looking for lines starting with
66/// this instruction codes.
67///
68/// See also <https://learn.netdata.cloud/docs/agent/collectors/plugins.d#external-plugins-api>
69#[allow(non_camel_case_types)]
70#[derive(Debug, Clone)]
71pub enum Instruction {
72    /// Create or update a [Chart].
73    CHART,
74    /// Add or update a [Dimension] associated to a chart.
75    DIMENSION,
76    /// signify [Begin] of a data collection sequence.
77    BEGIN,
78    /// [Set] the value of a dimension for the initialized chart.
79    SET,
80    /// Complete data collection for the initialized chart.
81    END,
82    /// Ignore the last collected values.
83    FLUSH,
84    /// Disable this plugin. This will prevent Netdata from restarting
85    /// the plugin.
86    ///
87    /// You can also exit with the value 1 to have the same effect.
88    DISABLE,
89    /// define [Variables](Variable).
90    VARIABLE,
91}
92
93/// Support simple serialization `.to_string()`.
94impl fmt::Display for Instruction {
95    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
96        write!(f, "{:?}", self)
97    }
98}
99
100/// Interpretion of sequential values for a given [Dimension].
101#[allow(non_camel_case_types)]
102#[derive(Debug, Clone)]
103pub enum Algorithm {
104    /// The value is drawn as-is (interpolated to second boundary).
105    /// This is the the default behavior.
106    absolute,
107    /// The value increases over time, the difference from the last value is
108    /// presented in the chart, the server interpolates the value and calculates
109    /// a per second figure.
110    incremental,
111    /// The % of this value compared to the total of all dimensions.
112    percentage_of_absolute_row, // kabab-case!
113    /// The % of this value compared to the incremental total of all dimensions.
114    percentage_of_incremental_row, // kabab-case!
115}
116
117/// The label of all variants can be printed by `{}` placeholders in format strings.
118///
119/// A conversion from `snake_case` to `kebab-case` will be performed for the output.
120impl fmt::Display for Algorithm {
121    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
122        let replaced = format!("{:?}", self).replace("_", "-");
123        write!(f, "{}", replaced)
124    }
125}
126
127#[cfg(test)]
128mod algorithm_tests {
129    use super::Algorithm;
130    #[test]
131    fn algorithm_kebab_display_output() {
132        let a = Algorithm::percentage_of_absolute_row;
133        assert_eq!(a.to_string(), "percentage-of-absolute-row");
134    }
135}
136
137/// Auxilary options available for [Dimension].
138#[allow(non_camel_case_types)]
139#[derive(Debug, Clone)]
140pub enum DimensionOption {
141    /// Mark a dimension as obsolete. Netdata will delete it after some time.
142    obsolete,
143    /// Make this dimension hidden,
144    /// it will take part in the calculations but will not be presented in the chart.
145    hidden,
146}
147
148/// Defines a new dimension for the [Chart].
149///
150/// The template of this instruction looks like:
151///
152/// `DIMENSION id [name [algorithm [multiplier [divisor [options]]]]]`
153///
154/// See also: <https://learn.netdata.cloud/docs/agent/collectors/plugins.d#dimension>
155///
156/// The [trait@Display] trait resp. the `.to_string()`-method should
157/// be used to compose the final command string
158///
159/// ```
160/// # use netdata_plugin::{Dimension};
161/// let d = Dimension{
162///     id: "test_id",
163///     name: "test_name",
164///     multiplier: Some(42),
165///     ..Dimension::default()
166/// };
167/// assert_eq!(d.to_string(), r#"DIMENSION "test_id" "test_name" "" "42""# );
168/// ```
169
170///
171#[derive(Debug, Default, Clone, Validate)]
172pub struct Dimension<'a> {
173    /// The id of this dimension (it is a text value, not numeric).
174    /// It will be needed later to add values to the dimension.
175    ///
176    /// We suggest to avoid using `"."` in dimension ids.
177    /// External databases expect metrics to be `"."` separated and people
178    /// will get confused if a dimension id contains a dot.
179    ///
180    /// You can utilize [validate()](Self::validate()) to prevent this kind of issue.
181    #[validate(custom = "validate_id")]
182    pub id: &'a str,
183    /// The name of the dimension as it will appear at the legend of the chart,
184    /// if empty or missing the id will be used.
185    pub name: &'a str,
186    /// One of the [Algorithm] variantes.
187    pub algorithm: Option<Algorithm>,
188    /// An integer value to multiply the collected value, if empty or missing, 1 is used.
189    pub multiplier: Option<i32>,
190    /// An integer value to divide the collected value, if empty or missing, 1 is used.
191    pub divisor: Option<i32>,
192    /// A list of options.
193    ///
194    /// Options supported: [obsolete](DimensionOption::obsolete) to mark a dimension as obsolete
195    /// (Netdata will delete it after some time) and [hidden](DimensionOption::hidden)
196    /// to make this dimension hidden, it will take part in the calculations but will not
197    /// be presented in the chart.
198    pub options: Vec<DimensionOption>,
199}
200
201/// This will generate the final command text string.
202///
203/// Optional fields will be communicated as empty string or simply skipped.
204///
205impl<'a> fmt::Display for Dimension<'a> {
206    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
207        let all_fields = format!(
208            "{:?} {:?} {:?} {:?} {:?} {:?} {:?}",
209            Instruction::DIMENSION,
210            self.id,
211            self.name,
212            some_to_textfield(&self.algorithm),
213            some_to_textfield(&self.multiplier),
214            some_to_textfield(&self.divisor),
215            options_to_textfield(&self.options),
216        );
217        write!(f, "{}", all_fields.trim_end_matches(" \"\""))
218    }
219}
220
221#[cfg(test)]
222mod dimension_tests {
223    use super::Algorithm;
224    use super::Dimension;
225    use super::DimensionOption;
226    use pretty_assertions::assert_eq;
227    use validator::Validate;
228
229    #[test]
230    fn dimension_display_output() {
231        let d = Dimension {
232            id: "test_id",
233            name: "test_name",
234            ..Dimension::default()
235        };
236        assert_eq!(d.to_string(), r#"DIMENSION "test_id" "test_name""#);
237    }
238
239    #[test]
240    fn dimension_validate_id() {
241        let d = Dimension {
242            id: "contains_dot.",
243            ..Dimension::default()
244        };
245        assert!(d.validate().is_err());
246        let d = Dimension {
247            id: "contains space",
248            ..Dimension::default()
249        };
250        assert!(d.validate().is_err());
251        let d = Dimension {
252            id: r#"contains"quote"#,
253            ..Dimension::default()
254        };
255        assert!(d.validate().is_err());
256        let d = Dimension {
257            id: r#"contains\backslash"#,
258            ..Dimension::default()
259        };
260        assert!(d.validate().is_err());
261        let d = Dimension {
262            id: "legitim",
263            ..Dimension::default()
264        };
265        d.validate().unwrap()
266    }
267
268    #[test]
269    fn dimension_display_output_missing_name() {
270        let d = Dimension {
271            id: "test_id",
272            ..Dimension::default()
273        };
274        assert_eq!(d.to_string(), r#"DIMENSION "test_id""#);
275    }
276
277    #[test]
278    fn dimension_display_output_with_algorithm() {
279        let d = Dimension {
280            id: "test_id",
281            name: "test_name",
282            algorithm: Some(Algorithm::percentage_of_absolute_row),
283            ..Dimension::default()
284        };
285        assert_eq!(
286            d.to_string(),
287            r#"DIMENSION "test_id" "test_name" "percentage-of-absolute-row""#
288        );
289    }
290
291    #[test]
292    fn dimension_display_output_with_options() {
293        let d = Dimension {
294            id: "test_label",
295            options: vec![DimensionOption::obsolete, DimensionOption::hidden],
296            ..Dimension::default()
297        };
298        assert_eq!(
299            d.to_string(),
300            r#"DIMENSION "test_label" "" "" "" "" "obsolete hidden""#
301        );
302    }
303
304    #[test]
305    fn dimension_display_output_with_multiplier() {
306        let d = Dimension {
307            id: "test_id",
308            name: "test_name",
309            algorithm: Some(Algorithm::absolute),
310            multiplier: Some(42),
311            ..Dimension::default()
312        };
313        assert_eq!(
314            d.to_string(),
315            r#"DIMENSION "test_id" "test_name" "absolute" "42""#
316        );
317    }
318
319    #[test]
320    fn dimension_display_output_empty_inner_fields() {
321        let d = Dimension {
322            id: "test_string",
323            divisor: Some(42),
324            ..Dimension::default()
325        };
326        assert_eq!(d.to_string(), r#"DIMENSION "test_string" "" "" "" "42""#);
327    }
328
329    #[test]
330    fn dimension_clone() {
331        let d = Dimension {
332            id: "test_id",
333            algorithm: Some(Algorithm::absolute),
334            options: vec![DimensionOption::obsolete],
335            ..Dimension::default()
336        };
337        let clone = d.clone();
338        assert_eq!(
339            clone.to_string(),
340            r#"DIMENSION "test_id" "" "absolute" "" "" "obsolete""#
341        );
342    }
343}
344
345/// The type of graphical rendering.
346#[allow(non_camel_case_types)]
347#[derive(Debug, Clone)]
348pub enum ChartType {
349    /// Displays information as a series of data points connected by straight line segments.
350    line,
351    /// When multiple attributes are included, the first attribute is plotted as a line
352    /// with a color fill followed by the second attribute, and so on. Technically,
353    /// this chart type is based on the Line Chart and represents a filled area between
354    /// the zero line and the line that connects data points.
355    area,
356    /// Stacked Area Chart is plotted in the form of several area series stacked on top
357    /// of one another. The height of each series is determined by the value in each data point.
358    stacked,
359}
360
361impl fmt::Display for ChartType {
362    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
363        write!(f, "{:?}", self)
364    }
365}
366
367/// Auxilary options available for [Chart].
368#[allow(non_camel_case_types)]
369#[derive(Debug, Clone)]
370pub enum ChartOption {
371    /// Mark a chart as obsolete (Netdata will hide it and delete it after some time).
372    obsolete,
373    /// Mark a chart as insignificant (this may be used by dashboards to make the charts smaller,
374    /// or somehow visualize properly a less important chart).  
375    detail,
376    /// make Netdata store the first collected value, assuming there was an invisible
377    /// previous value set to zero (this is used by `statsd` charts - if the first data
378    /// collected value of incremental dimensions is not zero based, unrealistic spikes
379    /// will appear with this option set).
380    store_first,
381    /// to perform all operations on a chart, but do not offer
382    /// it on dashboards (the chart will be send to external databases).
383    hidden,
384}
385
386/// This structure defines a new chart.
387///
388/// The template of this instruction looks like:
389///
390/// `CHART type.id name title units [family [context [charttype [priority [update_every [options [plugin [module]]]]]]]]`
391///
392/// See also: <https://learn.netdata.cloud/docs/agent/collectors/plugins.d#chart>
393///
394/// Use the formating features provided by the [trait@Display] trait or `to_string()`
395/// to generate the final command string output.
396///
397/// Example:
398///
399/// ```
400/// # use netdata_plugin::Chart;
401/// let c = Chart {
402///    type_id: "test_type.id",
403///    name: "test_name",
404///    title: "caption_text",
405///    units: "test_units",
406///    ..Chart::default()
407/// };
408/// assert_eq!(
409///    c.to_string(),
410///    r#"CHART "test_type.id" "test_name" "caption_text" "test_units""#
411/// );
412/// ```
413
414/// See also: <https://learn.netdata.cloud/docs/agent/collectors/plugins.d#chart>
415#[derive(Debug, Default, Clone, Validate)]
416pub struct Chart<'a> {
417    /// A dot-separated compound of `type.id` identifier strings.
418    ///
419    /// The `type` part controls the menu the charts will appear in.  
420    /// `Id` Uniquely identifies the chart, this is what will be needed to add values to the chart.
421    ///
422    /// Use [validate()](Self::validate()) to test the formal correctness.
423    #[validate(custom = "validate_type_id")]
424    pub type_id: &'a str,
425    /// The name that will be presented to the user instead of `id` in `type.id`.
426    /// This means that only the `id` part of `type.id` is changed. When a name has been given,
427    /// the chart is index (and can be referred) as both `type.id` and `type.name`.
428    /// You can set name to `""` to disable it.
429    pub name: &'a str,
430    /// The text above the chart.
431    pub title: &'a str,
432    /// The label of the vertical axis of the chart, all dimensions added to a chart should have
433    /// the same units of measurement.
434    pub units: &'a str,
435    /// This entry is used to group charts together (for example all `eth0` charts should say:
436    /// `eth0`), if empty or missing, the `id` part of `type.id will` be used.
437    /// This controls the sub-menu on the dashboard.
438    pub familiy: &'a str,
439    /// The `context` is giving the template of the chart. For example, if multiple charts present
440    /// the same information for a different `family`, they should have the same `context`.
441    ///
442    /// This is used for looking up rendering information for the chart (colors, sizes,
443    /// informational texts) and also apply alarms to it.
444    pub context: &'a str,
445    /// One of [ChartType] ([line](ChartType::line), [area](ChartType::area) or
446    /// [stacked](ChartType::stacked)), if empty or missing, [line](ChartType::line) will be used.
447    pub charttype: Option<ChartType>,
448    /// The relative priority of the charts as rendered on the web page,
449    /// lower numbers make the charts appear before the ones with higher numbers,
450    /// if empty or missing, `1000` will be used.
451    pub priority: Option<u64>,
452    /// Overwrite the update frequency set by the server.
453    ///
454    /// Note: To force fields with strong typing, a numeric value of `1` is inserted as the default
455    /// when subsequent given fields require such an output.
456    pub update_every: Option<u64>,
457    /// List of options. 4 options are currently supported:
458    ///
459    /// [obsolete](ChartOption::obsolete) to mark a chart as obsolete (Netdata will hide
460    /// it and delete it after some time), [detail](ChartOption::detail) to mark a
461    /// chart as insignificant (this may be used by dashboards to make the charts smaller,
462    /// or somehow visualize properly a less important chart),
463    /// [store_first](ChartOption::store_first) to make Netdata
464    /// store the first collected value, assuming there was an invisible previous value
465    /// set to zero (this is used by `statsd` charts - if the first data collected value
466    /// of incremental dimensions is not zero based, unrealistic spikes will appear with
467    /// this option set) and [hidden](ChartOption::hidden) to perform all operations on a chart,
468    /// but do not offer it on dashboards (the chart will be send to external databases).
469    pub options: Vec<ChartOption>,
470    /// Let the user identify the plugin that generated the chart.
471    /// If plugin is unset or empty, Netdata will automatically set the filename
472    /// of the plugin that generated the chart.
473    pub plugin: &'a str,
474    /// Let the user identify the module that generated the chart. Module has not default.
475    pub module: &'a str,
476}
477
478impl<'a> fmt::Display for Chart<'a> {
479    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
480        let all_fields = format!(
481            // the double space after the 5th placeholder prevents removal of
482            // empty mandatory fields by trim_end_matches() and shouldn't do
483            // much harm ;)
484            "{:?} {:?} {:?} {:?} {:?}  {:?} {:?} {:?} {:?} {:?} {:?} {:?} {:?}",
485            Instruction::CHART,
486            self.type_id,
487            self.name,
488            self.title,
489            self.units,
490            self.familiy,
491            self.context,
492            some_to_textfield(&self.charttype),
493            some_to_textfield(&self.priority),
494            some_to_textfield(&self.update_every),
495            options_to_textfield(&self.options),
496            self.plugin,
497            self.module
498        );
499        write!(
500            f,
501            "{}",
502            all_fields
503                .trim_end_matches(" \"\"")
504                .replace("\"  \"", "\" \"")
505                .trim()
506        )
507    }
508}
509
510#[cfg(test)]
511mod chart_tests {
512    use super::Chart;
513    use super::ChartOption;
514    use super::ChartType;
515    use pretty_assertions::assert_eq;
516    use validator::Validate;
517
518    #[test]
519    fn minimal_chart() {
520        let c = Chart {
521            type_id: "test_type.id",
522            name: "test_name",
523            title: "caption_text",
524            units: "test_units",
525            ..Chart::default()
526        };
527        assert_eq!(
528            c.to_string(),
529            r#"CHART "test_type.id" "test_name" "caption_text" "test_units""#
530        );
531    }
532
533    #[test]
534    fn chart_manadatory_field_output() {
535        let c = Chart {
536            type_id: "test_type.id",
537            ..Chart::default()
538        };
539        assert_eq!(c.to_string(), r#"CHART "test_type.id" "" "" """#);
540    }
541
542    #[test]
543    fn chart_validate_test_id() {
544        let c = Chart {
545            type_id: "test_type.id",
546            ..Chart::default()
547        };
548        assert!(c.validate().is_ok());
549        let c = Chart {
550            type_id: "double.dot.id",
551            ..Chart::default()
552        };
553        assert!(c.validate().is_err());
554        let c = Chart {
555            type_id: ".start_dot",
556            ..Chart::default()
557        };
558        assert!(c.validate().is_err());
559        let c = Chart {
560            type_id: "end_dot.",
561            ..Chart::default()
562        };
563        assert!(c.validate().is_err());
564        let c = Chart {
565            type_id: "nodot",
566            ..Chart::default()
567        };
568        assert!(c.validate().is_err());
569    }
570
571    #[test]
572    fn chart_defaults() {
573        let c = Chart {
574            type_id: "test_type.id",
575            name: "test_name",
576            title: "test_title",
577            units: "test_units",
578            charttype: Some(ChartType::area),
579            options: vec![ChartOption::hidden, ChartOption::obsolete],
580            module: "module_name",
581            ..Chart::default()
582        };
583        let clone = c.clone();
584        assert_eq!(
585            clone.to_string(),
586            r#"CHART "test_type.id" "test_name" "test_title" "test_units" "" "" "area" "" "" "hidden obsolete" "" "module_name""#
587        );
588    }
589}
590
591/// [Variables](Variable) can claim validity for different scopes.
592///
593/// * [GLOBAL](Scope::GLOBAL) or [HOST](Scope::HOST) to define
594///   the variable at the host level.
595/// * [LOCAL](Scope::LOCAL) or [CHART](Scope::CHART) to define
596///   the variable at the chart level.
597///   Use chart-local variables when the same variable may exist
598///   for different charts (i.e. Netdata monitors 2 mysql servers,
599///   and you need to set the max_connections each server accepts).
600///   Using chart-local variables is the ideal to build alarm templates.
601///
602/// The position of the VARIABLE line output, sets its default scope
603/// (in case you do not specify a scope).
604#[allow(non_camel_case_types)]
605#[derive(Debug, Clone)]
606pub enum Scope {
607    GLOBAL,
608    HOST,
609    LOCAL,
610    CHART,
611}
612
613/// Define and publish variables and constants.
614///
615/// `VARIABLE [SCOPE] name = value`
616///
617/// It defines a variable that can be used in alarms.  
618/// This is also used for setting constants (like the max connections
619/// a server may accept).
620///
621/// Examples:
622///
623/// ```
624/// use netdata_plugin::{Variable, Scope};
625/// let v = Variable {
626///    scope: Some(Scope::GLOBAL),
627///    name: "variable_name",
628///    value: 3.14f64,
629/// };
630/// assert_eq!(v.to_string(), "VARIABLE GLOBAL variable_name = 3.14");
631/// ```
632///
633/// Variables support 2 Scopes:
634///
635/// * [GLOBAL](Scope::GLOBAL) or [HOST](Scope::HOST) to define the
636///   variable at the host level.
637/// * [LOCAL](Scope::LOCAL) or [CHART](Scope::CHART) to define the
638///   variable at the chart level.
639///   Use chart-local variables when the same variable may exist
640///   for different charts (i.e. Netdata monitors 2 mysql servers,
641///   and you need to set the max_connections each server accepts).
642///   Using chart-local variables is the ideal to build alarm templates.
643///
644/// The position of the VARIABLE line output, sets its default scope
645/// (in case you do not specify a scope). So, defining a VARIABLE before
646/// any [CHART](Chart), or between [END](Instruction::END) and
647/// [BEGIN](Instruction::BEGIN) (outside any chart), sets `GLOBAL`
648/// scope, while defining a VARIABLE just after a [CHART](Chart) or
649/// a [DIMENSION](Dimension),
650/// or within the BEGIN - END block of a chart, sets `LOCAL` scope.
651///
652/// These variables can be set and updated at any point.
653///
654/// Variable names should use alphanumeric characters, the `.` and the `_`.
655///
656/// The value is floating point (Netdata used long double).
657///
658/// Variables are transferred to upstream Netdata servers
659/// (streaming and database replication).
660#[derive(Debug, Default, Clone)]
661pub struct Variable<'a> {
662    pub scope: Option<Scope>,
663    pub name: &'a str,
664    pub value: f64,
665}
666
667impl<'a> fmt::Display for Variable<'a> {
668    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
669        write!(
670            f,
671            "{}",
672            format!(
673                "VARIABLE {}{} = {}",
674                match &self.scope {
675                    Some(s) => format!("{:?} ", s),
676                    _ => "".to_owned(),
677                },
678                self.name,
679                self.value
680            )
681        )
682    }
683}
684
685#[cfg(test)]
686mod variable_tests {
687    use super::Scope;
688    use super::Variable;
689
690    #[test]
691    fn minimal_variable_output() {
692        let v = Variable {
693            scope: None,
694            name: "test_name",
695            value: 0f64,
696        };
697        assert_eq!(v.to_string(), "VARIABLE test_name = 0");
698    }
699
700    #[test]
701    fn scoped_variable_output() {
702        let v = Variable {
703            scope: Some(Scope::GLOBAL),
704            name: "test_name",
705            value: 3.14f64,
706        };
707        assert_eq!(v.to_string(), "VARIABLE GLOBAL test_name = 3.14");
708    }
709}
710
711/// This Opens the [Begin] -> [Set] -> [End](Instruction::END) data collection sequence.
712///
713/// `BEGIN type.id [microseconds]`
714///
715/// See also: <https://learn.netdata.cloud/docs/agent/collectors/plugins.d#data-collection>
716#[derive(Debug, Default, Clone)]
717pub struct Begin<'a> {
718    /// `type.id` Identifier as given in [Chart].
719    pub type_id: &'a str,
720    /// The number of microseconds since the last update of the chart.
721    pub microseconds: Option<u128>,
722}
723
724impl<'a> fmt::Display for Begin<'a> {
725    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
726        write!(
727            f,
728            "{}",
729            format!(
730                "{:?} {:?}{}",
731                Instruction::BEGIN,
732                self.type_id,
733                match &self.microseconds {
734                    Some(us) => format!(" {}", us),
735                    _ => "".to_owned(),
736                }
737            )
738        )
739    }
740}
741
742#[cfg(test)]
743mod begin_tests {
744    use super::Begin;
745
746    #[test]
747    fn begin_output() {
748        let b = Begin {
749            type_id: "test_type.id",
750            ..Default::default()
751        };
752        assert_eq!(b.to_string(), r#"BEGIN "test_type.id""#);
753    }
754
755    #[test]
756    fn begin_with_us_output() {
757        let b = Begin {
758            type_id: "test_type.id",
759            microseconds: Some(42),
760        };
761        assert_eq!(b.to_string(), r#"BEGIN "test_type.id" 42"#);
762    }
763}
764
765/// Store the collected values.
766///
767/// `SET id = [value]`
768///
769/// If a value is not collected, leave it empty, like this: `SET id =`
770/// or do not output the line at all.
771#[derive(Debug, Default, Clone)]
772pub struct Set<'a> {
773    /// The unique identification of the [Dimension] (of the chart just began)
774    pub id: &'a str,
775    /// the collected value, only integer values are collected.  
776    /// If you want to push fractional values, multiply this value
777    /// by 100 or 1000 and set the DIMENSION divider to 1000.
778    pub value: Option<i64>,
779}
780
781impl<'a> fmt::Display for Set<'a> {
782    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
783        write!(
784            f,
785            "{}",
786            format!(
787                "{:?} {:?} ={}",
788                Instruction::SET,
789                self.id,
790                match &self.value {
791                    Some(v) => format!(" {}", v),
792                    _ => "".to_owned(),
793                }
794            )
795        )
796    }
797}
798
799#[cfg(test)]
800mod set_tests {
801    use super::Set;
802
803    #[test]
804    fn incomplete_set_output() {
805        let s = Set {
806            id: "test_id",
807            ..Default::default()
808        };
809        assert_eq!(s.to_string(), r#"SET "test_id" ="#);
810    }
811
812    #[test]
813    fn set_with_value_output() {
814        let s = Set {
815            id: "test_id",
816            value: Some(-42),
817        };
818        assert_eq!(s.to_string(), r#"SET "test_id" = -42"#);
819    }
820}
821
822/// Text representation of a given optional field value or
823/// an empty string instead of `None`.
824fn some_to_textfield<T: Display>(opt: &Option<T>) -> String {
825    match opt {
826        Some(o) => format!("{}", o),
827        _ => format!(""),
828    }
829}
830
831/// A space delimited concatenation of all given options as string.
832fn options_to_textfield<T: Debug>(opts: &Vec<T>) -> String {
833    opts.iter()
834        .map(|o| format!("{:?}", o))
835        .collect::<Vec<String>>()
836        .join(" ")
837}