vector/sources/apache_metrics/
parser.rs

1use std::{collections::HashMap, error, fmt, iter, num, sync::LazyLock};
2
3use chrono::{DateTime, Utc};
4
5use crate::event::metric::{Metric, MetricKind, MetricTags, MetricValue};
6
7static SCOREBOARD: LazyLock<HashMap<char, &'static str>> = LazyLock::new(|| {
8    vec![
9        ('_', "waiting"),
10        ('S', "starting"),
11        ('R', "reading"),
12        ('W', "sending"),
13        ('K', "keepalive"),
14        ('D', "dnslookup"),
15        ('C', "closing"),
16        ('L', "logging"),
17        ('G', "finishing"),
18        ('I', "idle_cleanup"),
19        ('.', "open"),
20    ]
21    .into_iter()
22    .collect()
23});
24
25/// enum of mod_status fields we care about
26enum StatusFieldStatistic<'a> {
27    ServerUptimeSeconds(u64),
28    TotalAccesses(u64),
29    TotalKBytes(u64),
30    TotalDuration(u64),
31    CpuUser(f64),
32    CpuSystem(f64),
33    CpuChildrenUser(f64),
34    CpuChildrenSystem(f64),
35    CpuLoad(f64),
36    IdleWorkers(u64),
37    BusyWorkers(u64),
38    ConnsTotal(u64),
39    ConnsAsyncWriting(u64),
40    ConnsAsyncKeepAlive(u64),
41    ConnsAsyncClosing(u64),
42    Scoreboard(&'a str),
43}
44
45impl<'a> StatusFieldStatistic<'a> {
46    fn from_key_value(
47        key: &str,
48        value: &'a str,
49    ) -> Option<Result<StatusFieldStatistic<'a>, ParseError>> {
50        match key {
51            "ServerUptimeSeconds" => {
52                Some(parse_numeric_value(key, value).map(StatusFieldStatistic::ServerUptimeSeconds))
53            }
54            "Total Accesses" => {
55                Some(parse_numeric_value(key, value).map(StatusFieldStatistic::TotalAccesses))
56            }
57            "Total kBytes" => {
58                Some(parse_numeric_value(key, value).map(StatusFieldStatistic::TotalKBytes))
59            }
60            "Total Duration" => {
61                Some(parse_numeric_value(key, value).map(StatusFieldStatistic::TotalDuration))
62            }
63            "CPUUser" => Some(parse_numeric_value(key, value).map(StatusFieldStatistic::CpuUser)),
64            "CPUSystem" => {
65                Some(parse_numeric_value(key, value).map(StatusFieldStatistic::CpuSystem))
66            }
67            "CPUChildrenUser" => {
68                Some(parse_numeric_value(key, value).map(StatusFieldStatistic::CpuChildrenUser))
69            }
70            "CPUChildrenSystem" => {
71                Some(parse_numeric_value(key, value).map(StatusFieldStatistic::CpuChildrenSystem))
72            }
73            "CPULoad" => Some(parse_numeric_value(key, value).map(StatusFieldStatistic::CpuLoad)),
74            "IdleWorkers" => {
75                Some(parse_numeric_value(key, value).map(StatusFieldStatistic::IdleWorkers))
76            }
77            "BusyWorkers" => {
78                Some(parse_numeric_value(key, value).map(StatusFieldStatistic::BusyWorkers))
79            }
80            "ConnsTotal" => {
81                Some(parse_numeric_value(key, value).map(StatusFieldStatistic::ConnsTotal))
82            }
83            "ConnsAsyncWriting" => {
84                Some(parse_numeric_value(key, value).map(StatusFieldStatistic::ConnsAsyncWriting))
85            }
86            "ConnsAsyncClosing" => {
87                Some(parse_numeric_value(key, value).map(StatusFieldStatistic::ConnsAsyncClosing))
88            }
89            "ConnsAsyncKeepAlive" => {
90                Some(parse_numeric_value(key, value).map(StatusFieldStatistic::ConnsAsyncKeepAlive))
91            }
92            "Scoreboard" => Some(Ok(StatusFieldStatistic::Scoreboard(value))),
93
94            _ => None,
95        }
96    }
97}
98
99/// Parses the text output from Apache's mod_status and returns:
100///
101/// - A list of metrics generated from the output
102/// - A list of parse errors that were encountered
103///
104/// # Arguments
105///
106/// - `payload` - the mod_status output
107/// - `namespace` - the namespace to put the generated metrics in
108/// - `now` - the time the payload was fetched
109/// - `tags` - any base tags to apply to the metrics
110pub fn parse(
111    payload: &str,
112    namespace: Option<&str>,
113    now: DateTime<Utc>,
114    tags: Option<&MetricTags>,
115) -> impl Iterator<Item = Result<Metric, ParseError>> + use<> {
116    // We use a HashMap rather than a Vector as mod_status has
117    // BusyWorkers/IdleWorkers repeated
118    // https://bz.apache.org/bugzilla/show_bug.cgi?id=63300
119    let parsed = payload
120        .lines()
121        .filter_map(|l| {
122            let mut parts = l.splitn(2, ':');
123            let key = parts.next();
124            let value = parts.next();
125            match (key, value) {
126                (Some(k), Some(v)) => Some((k, v.trim())),
127                _ => None,
128            }
129        })
130        .collect::<HashMap<_, _>>();
131
132    parsed
133        .iter()
134        .filter_map(|(key, value)| line_to_metrics(key, value, namespace, now, tags))
135        .fold(vec![], |mut acc, v| {
136            match v {
137                Ok(metrics) => metrics.for_each(|v| acc.push(Ok(v))),
138                Err(error) => acc.push(Err(error)),
139            };
140            acc
141        })
142        .into_iter()
143}
144
145fn line_to_metrics<'a>(
146    key: &str,
147    value: &str,
148    namespace: Option<&'a str>,
149    now: DateTime<Utc>,
150    tags: Option<&'a MetricTags>,
151) -> Option<Result<Box<dyn Iterator<Item = Metric> + 'a>, ParseError>> {
152    StatusFieldStatistic::from_key_value(key, value).map(move |result| {
153        result.map(move |statistic| match statistic {
154            StatusFieldStatistic::ServerUptimeSeconds(value) => Box::new(iter::once(
155                Metric::new(
156                    "uptime_seconds_total",
157                    MetricKind::Absolute,
158                    MetricValue::Counter {
159                        value: value as f64,
160                    },
161                )
162                .with_namespace(namespace.map(str::to_string))
163                .with_tags(tags.cloned())
164                .with_timestamp(Some(now)),
165            )),
166            StatusFieldStatistic::TotalAccesses(value) => Box::new(iter::once(
167                Metric::new(
168                    "access_total",
169                    MetricKind::Absolute,
170                    MetricValue::Counter {
171                        value: value as f64,
172                    },
173                )
174                .with_namespace(namespace.map(str::to_string))
175                .with_tags(tags.cloned())
176                .with_timestamp(Some(now)),
177            )),
178            StatusFieldStatistic::TotalKBytes(value) => Box::new(iter::once(
179                Metric::new(
180                    "sent_bytes_total",
181                    MetricKind::Absolute,
182                    MetricValue::Counter {
183                        value: (value * 1024) as f64,
184                    },
185                )
186                .with_namespace(namespace.map(str::to_string))
187                .with_tags(tags.cloned())
188                .with_timestamp(Some(now)),
189            )),
190            StatusFieldStatistic::TotalDuration(value) => Box::new(iter::once(
191                Metric::new(
192                    "duration_seconds_total",
193                    MetricKind::Absolute,
194                    MetricValue::Counter {
195                        value: value as f64,
196                    },
197                )
198                .with_namespace(namespace.map(str::to_string))
199                .with_tags(tags.cloned())
200                .with_timestamp(Some(now)),
201            )),
202            StatusFieldStatistic::CpuUser(value) => Box::new(iter::once(
203                Metric::new(
204                    "cpu_seconds_total",
205                    MetricKind::Absolute,
206                    MetricValue::Gauge { value },
207                )
208                .with_namespace(namespace.map(str::to_string))
209                .with_tags({
210                    let mut tags = tags.cloned().unwrap_or_default();
211                    tags.replace("type".to_string(), "user".to_string());
212                    Some(tags)
213                })
214                .with_timestamp(Some(now)),
215            ))
216                as Box<dyn Iterator<Item = Metric>>,
217            StatusFieldStatistic::CpuSystem(value) => Box::new(iter::once(
218                Metric::new(
219                    "cpu_seconds_total",
220                    MetricKind::Absolute,
221                    MetricValue::Gauge { value },
222                )
223                .with_namespace(namespace.map(str::to_string))
224                .with_tags({
225                    let mut tags = tags.cloned().unwrap_or_default();
226                    tags.replace("type".to_string(), "system".to_string());
227                    Some(tags)
228                })
229                .with_timestamp(Some(now)),
230            ))
231                as Box<dyn Iterator<Item = Metric>>,
232            StatusFieldStatistic::CpuChildrenUser(value) => Box::new(iter::once(
233                Metric::new(
234                    "cpu_seconds_total",
235                    MetricKind::Absolute,
236                    MetricValue::Gauge { value },
237                )
238                .with_namespace(namespace.map(str::to_string))
239                .with_tags({
240                    let mut tags = tags.cloned().unwrap_or_default();
241                    tags.replace("type".to_string(), "children_user".to_string());
242                    Some(tags)
243                })
244                .with_timestamp(Some(now)),
245            ))
246                as Box<dyn Iterator<Item = Metric>>,
247            StatusFieldStatistic::CpuChildrenSystem(value) => Box::new(iter::once(
248                Metric::new(
249                    "cpu_seconds_total",
250                    MetricKind::Absolute,
251                    MetricValue::Gauge { value },
252                )
253                .with_namespace(namespace.map(str::to_string))
254                .with_tags({
255                    let mut tags = tags.cloned().unwrap_or_default();
256                    tags.replace("type".to_string(), "children_system".to_string());
257                    Some(tags)
258                })
259                .with_timestamp(Some(now)),
260            ))
261                as Box<dyn Iterator<Item = Metric>>,
262            StatusFieldStatistic::CpuLoad(value) => Box::new(iter::once(
263                Metric::new(
264                    "cpu_load",
265                    MetricKind::Absolute,
266                    MetricValue::Gauge { value },
267                )
268                .with_namespace(namespace.map(str::to_string))
269                .with_tags(tags.cloned())
270                .with_timestamp(Some(now)),
271            ))
272                as Box<dyn Iterator<Item = Metric>>,
273            StatusFieldStatistic::IdleWorkers(value) => Box::new(iter::once(
274                Metric::new(
275                    "workers",
276                    MetricKind::Absolute,
277                    MetricValue::Gauge {
278                        value: value as f64,
279                    },
280                )
281                .with_namespace(namespace.map(str::to_string))
282                .with_tags({
283                    let mut tags = tags.cloned().unwrap_or_default();
284                    tags.replace("state".to_string(), "idle".to_string());
285                    Some(tags)
286                })
287                .with_timestamp(Some(now)),
288            ))
289                as Box<dyn Iterator<Item = Metric>>,
290            StatusFieldStatistic::BusyWorkers(value) => Box::new(iter::once(
291                Metric::new(
292                    "workers",
293                    MetricKind::Absolute,
294                    MetricValue::Gauge {
295                        value: value as f64,
296                    },
297                )
298                .with_namespace(namespace.map(str::to_string))
299                .with_tags({
300                    let mut tags = tags.cloned().unwrap_or_default();
301                    tags.replace("state".to_string(), "busy".to_string());
302                    Some(tags)
303                })
304                .with_timestamp(Some(now)),
305            )),
306            StatusFieldStatistic::ConnsTotal(value) => Box::new(iter::once(
307                Metric::new(
308                    "connections",
309                    MetricKind::Absolute,
310                    MetricValue::Gauge {
311                        value: value as f64,
312                    },
313                )
314                .with_namespace(namespace.map(str::to_string))
315                .with_tags({
316                    let mut tags = tags.cloned().unwrap_or_default();
317                    tags.replace("state".to_string(), "total".to_string());
318                    Some(tags)
319                })
320                .with_timestamp(Some(now)),
321            )),
322            StatusFieldStatistic::ConnsAsyncWriting(value) => Box::new(iter::once(
323                Metric::new(
324                    "connections",
325                    MetricKind::Absolute,
326                    MetricValue::Gauge {
327                        value: value as f64,
328                    },
329                )
330                .with_namespace(namespace.map(str::to_string))
331                .with_tags({
332                    let mut tags = tags.cloned().unwrap_or_default();
333                    tags.replace("state".to_string(), "writing".to_string());
334                    Some(tags)
335                })
336                .with_timestamp(Some(now)),
337            )),
338            StatusFieldStatistic::ConnsAsyncClosing(value) => Box::new(iter::once(
339                Metric::new(
340                    "connections",
341                    MetricKind::Absolute,
342                    MetricValue::Gauge {
343                        value: value as f64,
344                    },
345                )
346                .with_namespace(namespace.map(str::to_string))
347                .with_tags({
348                    let mut tags = tags.cloned().unwrap_or_default();
349                    tags.replace("state".to_string(), "closing".to_string());
350                    Some(tags)
351                })
352                .with_timestamp(Some(now)),
353            )),
354            StatusFieldStatistic::ConnsAsyncKeepAlive(value) => Box::new(iter::once(
355                Metric::new(
356                    "connections",
357                    MetricKind::Absolute,
358                    MetricValue::Gauge {
359                        value: value as f64,
360                    },
361                )
362                .with_namespace(namespace.map(str::to_string))
363                .with_tags({
364                    let mut tags = tags.cloned().unwrap_or_default();
365                    tags.replace("state".to_string(), "keepalive".to_string());
366                    Some(tags)
367                })
368                .with_timestamp(Some(now)),
369            )),
370            StatusFieldStatistic::Scoreboard(value) => {
371                let scores = value.chars().fold(HashMap::new(), |mut m, c| {
372                    *m.entry(c).or_insert(0u32) += 1;
373                    m
374                });
375
376                Box::new(SCOREBOARD.iter().map(move |(c, name)| {
377                    score_to_metric(
378                        namespace,
379                        now,
380                        tags,
381                        name,
382                        scores.get(c).copied().unwrap_or_default(),
383                    )
384                })) as Box<dyn Iterator<Item = Metric>>
385            }
386        })
387    })
388}
389
390fn parse_numeric_value<T: std::str::FromStr>(key: &str, value: &str) -> Result<T, ParseError>
391where
392    T::Err: Into<ValueParseError> + 'static,
393{
394    value.parse::<T>().map_err(|error| ParseError {
395        key: key.to_string(),
396        error: error.into(),
397    })
398}
399
400fn score_to_metric(
401    namespace: Option<&str>,
402    now: DateTime<Utc>,
403    tags: Option<&MetricTags>,
404    state: &str,
405    count: u32,
406) -> Metric {
407    Metric::new(
408        "scoreboard",
409        MetricKind::Absolute,
410        MetricValue::Gauge {
411            value: count.into(),
412        },
413    )
414    .with_namespace(namespace.map(str::to_string))
415    .with_tags({
416        let mut tags = tags.cloned().unwrap_or_default();
417        tags.replace("state".to_string(), state.to_string());
418        Some(tags)
419    })
420    .with_timestamp(Some(now))
421}
422
423#[derive(Debug)]
424enum ValueParseError {
425    Float(num::ParseFloatError),
426    Int(num::ParseIntError),
427}
428
429impl From<num::ParseFloatError> for ValueParseError {
430    fn from(error: num::ParseFloatError) -> ValueParseError {
431        ValueParseError::Float(error)
432    }
433}
434
435impl From<num::ParseIntError> for ValueParseError {
436    fn from(error: num::ParseIntError) -> ValueParseError {
437        ValueParseError::Int(error)
438    }
439}
440
441impl error::Error for ValueParseError {
442    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
443        match *self {
444            ValueParseError::Float(ref e) => Some(e),
445            ValueParseError::Int(ref e) => Some(e),
446        }
447    }
448}
449
450impl fmt::Display for ValueParseError {
451    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
452        match *self {
453            ValueParseError::Float(ref e) => e.fmt(f),
454            ValueParseError::Int(ref e) => e.fmt(f),
455        }
456    }
457}
458
459#[derive(Debug)]
460pub struct ParseError {
461    key: String,
462    error: ValueParseError,
463}
464
465impl fmt::Display for ParseError {
466    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
467        write!(f, "could not parse value for {}: {}", self.key, self.error)
468    }
469}
470
471impl error::Error for ParseError {
472    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
473        Some(&self.error)
474    }
475}
476
477#[cfg(test)]
478mod test {
479    use chrono::{DateTime, Utc};
480    use similar_asserts::assert_eq;
481    use vector_lib::{assert_event_data_eq, metric_tags};
482
483    use super::*;
484    use crate::event::metric::{Metric, MetricKind, MetricValue};
485
486    // Test ExtendedStatus: Off
487    // https://httpd.apache.org/docs/2.4/mod/core.html#extendedstatus
488    #[test]
489    fn test_not_extended() {
490        let payload = r"
491localhost
492ServerVersion: Apache/2.4.46 (Unix)
493ServerMPM: event
494Server Built: Aug  5 2020 23:20:17
495CurrentTime: Thursday, 03-Sep-2020 20:48:54 UTC
496RestartTime: Thursday, 03-Sep-2020 20:48:41 UTC
497ParentServerConfigGeneration: 1
498ParentServerMPMGeneration: 0
499ServerUptimeSeconds: 12
500ServerUptime: 12 seconds
501Load1: 0.75
502Load5: 0.59
503Load15: 0.76
504BusyWorkers: 1
505IdleWorkers: 74
506Processes: 3
507Stopping: 0
508BusyWorkers: 1
509IdleWorkers: 74
510ConnsTotal: 1
511ConnsAsyncWriting: 0
512ConnsAsyncKeepAlive: 0
513ConnsAsyncClosing: 0
514Scoreboard: ____S_____I______R____I_______KK___D__C__G_L____________W__________________.....................................................................................................................................................................................................................................................................................................................................
515            ";
516
517        let (now, metrics, errors) = parse_sort(payload);
518
519        assert_event_data_eq!(
520            metrics,
521            vec![
522                Metric::new(
523                    "connections",
524                    MetricKind::Absolute,
525                    MetricValue::Gauge { value: 0.0 },
526                )
527                .with_namespace(Some("apache"))
528                .with_tags(Some(metric_tags!("state" => "closing")))
529                .with_timestamp(Some(now)),
530                Metric::new(
531                    "connections",
532                    MetricKind::Absolute,
533                    MetricValue::Gauge { value: 0.0 },
534                )
535                .with_namespace(Some("apache"))
536                .with_tags(Some(metric_tags!("state" => "keepalive")))
537                .with_timestamp(Some(now)),
538                Metric::new(
539                    "connections",
540                    MetricKind::Absolute,
541                    MetricValue::Gauge { value: 1.0 },
542                )
543                .with_namespace(Some("apache"))
544                .with_tags(Some(metric_tags!("state" => "total")))
545                .with_timestamp(Some(now)),
546                Metric::new(
547                    "connections",
548                    MetricKind::Absolute,
549                    MetricValue::Gauge { value: 0.0 },
550                )
551                .with_namespace(Some("apache"))
552                .with_tags(Some(metric_tags!("state" => "writing")))
553                .with_timestamp(Some(now)),
554                Metric::new(
555                    "scoreboard",
556                    MetricKind::Absolute,
557                    MetricValue::Gauge { value: 1.0 },
558                )
559                .with_namespace(Some("apache"))
560                .with_tags(Some(metric_tags!("state" => "closing")))
561                .with_timestamp(Some(now)),
562                Metric::new(
563                    "scoreboard",
564                    MetricKind::Absolute,
565                    MetricValue::Gauge { value: 1.0 },
566                )
567                .with_namespace(Some("apache"))
568                .with_tags(Some(metric_tags!("state" => "dnslookup")))
569                .with_timestamp(Some(now)),
570                Metric::new(
571                    "scoreboard",
572                    MetricKind::Absolute,
573                    MetricValue::Gauge { value: 1.0 },
574                )
575                .with_namespace(Some("apache"))
576                .with_tags(Some(metric_tags!("state" => "finishing")))
577                .with_timestamp(Some(now)),
578                Metric::new(
579                    "scoreboard",
580                    MetricKind::Absolute,
581                    MetricValue::Gauge { value: 2.0 },
582                )
583                .with_namespace(Some("apache"))
584                .with_tags(Some(metric_tags!("state" => "idle_cleanup")))
585                .with_timestamp(Some(now)),
586                Metric::new(
587                    "scoreboard",
588                    MetricKind::Absolute,
589                    MetricValue::Gauge { value: 2.0 },
590                )
591                .with_namespace(Some("apache"))
592                .with_tags(Some(metric_tags!("state" => "keepalive")))
593                .with_timestamp(Some(now)),
594                Metric::new(
595                    "scoreboard",
596                    MetricKind::Absolute,
597                    MetricValue::Gauge { value: 1.0 },
598                )
599                .with_namespace(Some("apache"))
600                .with_tags(Some(metric_tags!("state" => "logging")))
601                .with_timestamp(Some(now)),
602                Metric::new(
603                    "scoreboard",
604                    MetricKind::Absolute,
605                    MetricValue::Gauge { value: 325.0 },
606                )
607                .with_namespace(Some("apache"))
608                .with_tags(Some(metric_tags!("state" => "open")))
609                .with_timestamp(Some(now)),
610                Metric::new(
611                    "scoreboard",
612                    MetricKind::Absolute,
613                    MetricValue::Gauge { value: 1.0 },
614                )
615                .with_namespace(Some("apache"))
616                .with_tags(Some(metric_tags!("state" => "reading")))
617                .with_timestamp(Some(now)),
618                Metric::new(
619                    "scoreboard",
620                    MetricKind::Absolute,
621                    MetricValue::Gauge { value: 1.0 },
622                )
623                .with_namespace(Some("apache"))
624                .with_tags(Some(metric_tags!("state" => "sending")))
625                .with_timestamp(Some(now)),
626                Metric::new(
627                    "scoreboard",
628                    MetricKind::Absolute,
629                    MetricValue::Gauge { value: 1.0 },
630                )
631                .with_namespace(Some("apache"))
632                .with_tags(Some(metric_tags!("state" => "starting")))
633                .with_timestamp(Some(now)),
634                Metric::new(
635                    "scoreboard",
636                    MetricKind::Absolute,
637                    MetricValue::Gauge { value: 64.0 },
638                )
639                .with_namespace(Some("apache"))
640                .with_tags(Some(metric_tags!("state" => "waiting")))
641                .with_timestamp(Some(now)),
642                Metric::new(
643                    "uptime_seconds_total",
644                    MetricKind::Absolute,
645                    MetricValue::Counter { value: 12.0 },
646                )
647                .with_namespace(Some("apache"))
648                .with_timestamp(Some(now)),
649                Metric::new(
650                    "workers",
651                    MetricKind::Absolute,
652                    MetricValue::Gauge { value: 1.0 },
653                )
654                .with_namespace(Some("apache"))
655                .with_tags(Some(metric_tags!("state" => "busy")))
656                .with_timestamp(Some(now)),
657                Metric::new(
658                    "workers",
659                    MetricKind::Absolute,
660                    MetricValue::Gauge { value: 74.0 },
661                )
662                .with_namespace(Some("apache"))
663                .with_tags(Some(metric_tags!("state" => "idle")))
664                .with_timestamp(Some(now)),
665            ]
666        );
667        assert_eq!(errors.len(), 0);
668    }
669
670    // Test ExtendedStatus: On
671    // https://httpd.apache.org/docs/2.4/mod/core.html#extendedstatus
672    #[test]
673    fn test_extended() {
674        let payload = r"
675localhost
676ServerVersion: Apache/2.4.46 (Unix)
677ServerMPM: event
678Server Built: Aug  5 2020 23:20:17
679CurrentTime: Friday, 21-Aug-2020 18:41:34 UTC
680RestartTime: Friday, 21-Aug-2020 18:41:08 UTC
681ParentServerConfigGeneration: 1
682ParentServerMPMGeneration: 0
683ServerUptimeSeconds: 26
684ServerUptime: 26 seconds
685Load1: 0.00
686Load5: 0.03
687Load15: 0.03
688Total Accesses: 30
689Total kBytes: 217
690Total Duration: 11
691CPUUser: .2
692CPUSystem: .02
693CPUChildrenUser: 0
694CPUChildrenSystem: 0
695CPULoad: .846154
696Uptime: 26
697ReqPerSec: 1.15385
698BytesPerSec: 8546.46
699BytesPerReq: 7406.93
700DurationPerReq: .366667
701BusyWorkers: 1
702IdleWorkers: 74
703Processes: 3
704Stopping: 0
705BusyWorkers: 1
706IdleWorkers: 74
707ConnsTotal: 1
708ConnsAsyncWriting: 0
709ConnsAsyncKeepAlive: 0
710ConnsAsyncClosing: 0
711Scoreboard: ____S_____I______R____I_______KK___D__C__G_L____________W__________________.....................................................................................................................................................................................................................................................................................................................................
712            ";
713
714        let (now, metrics, errors) = parse_sort(payload);
715
716        assert_event_data_eq!(
717            metrics,
718            vec![
719                Metric::new(
720                    "access_total",
721                    MetricKind::Absolute,
722                    MetricValue::Counter { value: 30.0 },
723                )
724                .with_namespace(Some("apache"))
725                .with_timestamp(Some(now)),
726                Metric::new(
727                    "connections",
728                    MetricKind::Absolute,
729                    MetricValue::Gauge { value: 0.0 },
730                )
731                .with_namespace(Some("apache"))
732                .with_tags(Some(metric_tags!("state" => "closing")))
733                .with_timestamp(Some(now)),
734                Metric::new(
735                    "connections",
736                    MetricKind::Absolute,
737                    MetricValue::Gauge { value: 0.0 },
738                )
739                .with_namespace(Some("apache"))
740                .with_tags(Some(metric_tags!("state" => "keepalive")))
741                .with_timestamp(Some(now)),
742                Metric::new(
743                    "connections",
744                    MetricKind::Absolute,
745                    MetricValue::Gauge { value: 1.0 },
746                )
747                .with_namespace(Some("apache"))
748                .with_tags(Some(metric_tags!("state" => "total")))
749                .with_timestamp(Some(now)),
750                Metric::new(
751                    "connections",
752                    MetricKind::Absolute,
753                    MetricValue::Gauge { value: 0.0 },
754                )
755                .with_namespace(Some("apache"))
756                .with_tags(Some(metric_tags!("state" => "writing")))
757                .with_timestamp(Some(now)),
758                Metric::new(
759                    "cpu_load",
760                    MetricKind::Absolute,
761                    MetricValue::Gauge { value: 0.846154 },
762                )
763                .with_namespace(Some("apache"))
764                .with_timestamp(Some(now)),
765                Metric::new(
766                    "cpu_seconds_total",
767                    MetricKind::Absolute,
768                    MetricValue::Gauge { value: 0.0 },
769                )
770                .with_namespace(Some("apache"))
771                .with_tags(Some(metric_tags!("type" => "children_system")))
772                .with_timestamp(Some(now)),
773                Metric::new(
774                    "cpu_seconds_total",
775                    MetricKind::Absolute,
776                    MetricValue::Gauge { value: 0.0 },
777                )
778                .with_namespace(Some("apache"))
779                .with_tags(Some(metric_tags!("type" => "children_user")))
780                .with_timestamp(Some(now)),
781                Metric::new(
782                    "cpu_seconds_total",
783                    MetricKind::Absolute,
784                    MetricValue::Gauge { value: 0.02 },
785                )
786                .with_namespace(Some("apache"))
787                .with_tags(Some(metric_tags!("type" => "system")))
788                .with_timestamp(Some(now)),
789                Metric::new(
790                    "cpu_seconds_total",
791                    MetricKind::Absolute,
792                    MetricValue::Gauge { value: 0.2 },
793                )
794                .with_namespace(Some("apache"))
795                .with_tags(Some(metric_tags!("type" => "user")))
796                .with_timestamp(Some(now)),
797                Metric::new(
798                    "duration_seconds_total",
799                    MetricKind::Absolute,
800                    MetricValue::Counter { value: 11.0 },
801                )
802                .with_namespace(Some("apache"))
803                .with_timestamp(Some(now)),
804                Metric::new(
805                    "scoreboard",
806                    MetricKind::Absolute,
807                    MetricValue::Gauge { value: 1.0 },
808                )
809                .with_namespace(Some("apache"))
810                .with_tags(Some(metric_tags!("state" => "closing")))
811                .with_timestamp(Some(now)),
812                Metric::new(
813                    "scoreboard",
814                    MetricKind::Absolute,
815                    MetricValue::Gauge { value: 1.0 },
816                )
817                .with_namespace(Some("apache"))
818                .with_tags(Some(metric_tags!("state" => "dnslookup")))
819                .with_timestamp(Some(now)),
820                Metric::new(
821                    "scoreboard",
822                    MetricKind::Absolute,
823                    MetricValue::Gauge { value: 1.0 },
824                )
825                .with_namespace(Some("apache"))
826                .with_tags(Some(metric_tags!("state" => "finishing")))
827                .with_timestamp(Some(now)),
828                Metric::new(
829                    "scoreboard",
830                    MetricKind::Absolute,
831                    MetricValue::Gauge { value: 2.0 },
832                )
833                .with_namespace(Some("apache"))
834                .with_tags(Some(metric_tags!("state" => "idle_cleanup")))
835                .with_timestamp(Some(now)),
836                Metric::new(
837                    "scoreboard",
838                    MetricKind::Absolute,
839                    MetricValue::Gauge { value: 2.0 },
840                )
841                .with_namespace(Some("apache"))
842                .with_tags(Some(metric_tags!("state" => "keepalive")))
843                .with_timestamp(Some(now)),
844                Metric::new(
845                    "scoreboard",
846                    MetricKind::Absolute,
847                    MetricValue::Gauge { value: 1.0 },
848                )
849                .with_namespace(Some("apache"))
850                .with_tags(Some(metric_tags!("state" => "logging")))
851                .with_timestamp(Some(now)),
852                Metric::new(
853                    "scoreboard",
854                    MetricKind::Absolute,
855                    MetricValue::Gauge { value: 325.0 },
856                )
857                .with_namespace(Some("apache"))
858                .with_tags(Some(metric_tags!("state" => "open")))
859                .with_timestamp(Some(now)),
860                Metric::new(
861                    "scoreboard",
862                    MetricKind::Absolute,
863                    MetricValue::Gauge { value: 1.0 },
864                )
865                .with_namespace(Some("apache"))
866                .with_tags(Some(metric_tags!("state" => "reading")))
867                .with_timestamp(Some(now)),
868                Metric::new(
869                    "scoreboard",
870                    MetricKind::Absolute,
871                    MetricValue::Gauge { value: 1.0 },
872                )
873                .with_namespace(Some("apache"))
874                .with_tags(Some(metric_tags!("state" => "sending")))
875                .with_timestamp(Some(now)),
876                Metric::new(
877                    "scoreboard",
878                    MetricKind::Absolute,
879                    MetricValue::Gauge { value: 1.0 },
880                )
881                .with_namespace(Some("apache"))
882                .with_tags(Some(metric_tags!("state" => "starting")))
883                .with_timestamp(Some(now)),
884                Metric::new(
885                    "scoreboard",
886                    MetricKind::Absolute,
887                    MetricValue::Gauge { value: 64.0 },
888                )
889                .with_namespace(Some("apache"))
890                .with_tags(Some(metric_tags!("state" => "waiting")))
891                .with_timestamp(Some(now)),
892                Metric::new(
893                    "sent_bytes_total",
894                    MetricKind::Absolute,
895                    MetricValue::Counter { value: 222208.0 },
896                )
897                .with_namespace(Some("apache"))
898                .with_timestamp(Some(now)),
899                Metric::new(
900                    "uptime_seconds_total",
901                    MetricKind::Absolute,
902                    MetricValue::Counter { value: 26.0 },
903                )
904                .with_namespace(Some("apache"))
905                .with_timestamp(Some(now)),
906                Metric::new(
907                    "workers",
908                    MetricKind::Absolute,
909                    MetricValue::Gauge { value: 1.0 },
910                )
911                .with_namespace(Some("apache"))
912                .with_tags(Some(metric_tags!("state" => "busy")))
913                .with_timestamp(Some(now)),
914                Metric::new(
915                    "workers",
916                    MetricKind::Absolute,
917                    MetricValue::Gauge { value: 74.0 },
918                )
919                .with_namespace(Some("apache"))
920                .with_tags(Some(metric_tags!("state" => "idle")))
921                .with_timestamp(Some(now)),
922            ]
923        );
924        assert_eq!(errors.len(), 0);
925    }
926
927    #[test]
928    fn test_parse_failure() {
929        let payload = r"
930ServerUptimeSeconds: not a number
931ConnsTotal: 1
932            ";
933
934        let (now, metrics, errors) = parse_sort(payload);
935
936        assert_event_data_eq!(
937            metrics,
938            vec![
939                Metric::new(
940                    "connections",
941                    MetricKind::Absolute,
942                    MetricValue::Gauge { value: 1.0 },
943                )
944                .with_namespace(Some("apache"))
945                .with_tags(Some(metric_tags!("state" => "total")))
946                .with_timestamp(Some(now)),
947            ]
948        );
949        assert_eq!(errors.len(), 1);
950    }
951
952    fn parse_sort(payload: &str) -> (DateTime<Utc>, Vec<Metric>, Vec<ParseError>) {
953        let now: DateTime<Utc> = Utc::now();
954        let (mut metrics, errors) = parse(payload, Some("apache"), now, None).fold(
955            (vec![], vec![]),
956            |(mut metrics, mut errors), v| {
957                match v {
958                    Ok(m) => metrics.push(m),
959                    Err(e) => errors.push(e),
960                }
961                (metrics, errors)
962            },
963        );
964
965        metrics.sort_by_key(|metric| metric.series().to_string());
966
967        (now, metrics, errors)
968    }
969}