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}