Skip to main content

vector/transforms/
log_to_metric.rs

1use std::{collections::HashMap, num::ParseFloatError, sync::Arc};
2
3use chrono::Utc;
4use indexmap::IndexMap;
5use vector_lib::{
6    configurable::configurable_component,
7    event::{
8        DatadogMetricOriginMetadata, LogEvent,
9        metric::{Bucket, Quantile, Sample},
10    },
11};
12use vrl::{
13    event_path, path,
14    path::{PathParseError, parse_target_path},
15};
16
17use crate::{
18    common::expansion::pair_expansion,
19    config::{
20        DataType, GenerateConfig, Input, OutputId, TransformConfig, TransformContext,
21        TransformOutput, schema::Definition,
22    },
23    event::{
24        Event, Value,
25        metric::{Metric, MetricKind, MetricTags, MetricValue, StatisticKind, TagValue},
26    },
27    internal_events::{
28        DROP_EVENT, LogToMetricFieldNullError, LogToMetricParseFloatError,
29        MetricMetadataInvalidFieldValueError, MetricMetadataMetricDetailsNotFoundError,
30        MetricMetadataParseError, ParserMissingFieldError,
31    },
32    schema,
33    template::{Template, TemplateRenderingError},
34    transforms::{
35        FunctionTransform, OutputBuffer, Transform, log_to_metric::TransformError::PathNotFound,
36    },
37};
38
39const ORIGIN_SERVICE_VALUE: u32 = 3;
40
41/// Configuration for the `log_to_metric` transform.
42#[configurable_component(transform("log_to_metric", "Convert log events to metric events."))]
43#[derive(Clone, Debug)]
44#[serde(deny_unknown_fields)]
45pub struct LogToMetricConfig {
46    /// A list of metrics to generate.
47    pub metrics: Option<Vec<MetricConfig>>,
48
49    /// Setting this flag changes the behavior of this transformation.
50    /// Notably the `metrics` field will be ignored.
51    /// All incoming events will be processed and if possible they will be converted to log events.
52    /// Otherwise, only items specified in the `metrics` field will be processed.
53    ///
54    /// Example:
55    /// <pre class="chroma"><code class="language-toml" data-lang="toml">{
56    ///     "counter": {
57    ///         "value": 10.0
58    ///     },
59    ///     "kind": "incremental",
60    ///     "name": "test.transform.counter",
61    ///     "tags": {
62    ///         "env": "test_env",
63    ///         "host": "localhost"
64    ///     }
65    /// }
66    /// </code></pre>
67    ///
68    /// This is a JSON representation of a counter with the following properties:
69    ///
70    /// - `counter`: An object with a single property `value` representing the counter value, in this case, `10.0`).
71    /// - `kind`: A string indicating the kind of counter, in this case, "incremental".
72    /// - `name`: A string representing the name of the counter, here set to "test.transform.counter".
73    /// - `tags`: An object containing additional tags such as "env" and "host".
74    ///
75    /// Objects that can be processed include counter, histogram, gauge, set and summary.
76    pub all_metrics: Option<bool>,
77}
78
79/// Specification of a counter derived from a log event.
80#[configurable_component]
81#[derive(Clone, Debug)]
82pub struct CounterConfig {
83    /// Increments the counter by the value in `field`, instead of only by `1`.
84    #[serde(default = "default_increment_by_value")]
85    pub increment_by_value: bool,
86
87    #[configurable(derived)]
88    #[serde(default = "default_kind")]
89    pub kind: MetricKind,
90}
91
92/// Specification of a metric derived from a log event.
93// TODO: While we're resolving the schema for this enum somewhat reasonably (in
94// `generate-components-docs.rb`), we have a problem where an overlapping field (overlap between two
95// or more of the subschemas) takes the details of the last subschema to be iterated over that
96// contains that field, such that, for example, the `Summary` variant below is overriding the
97// description for almost all of the fields because they're shared across all of the variants.
98#[configurable_component]
99#[derive(Clone, Debug)]
100pub struct MetricConfig {
101    /// Name of the field in the event to generate the metric.
102    pub field: Template,
103
104    /// Overrides the name of the counter.
105    ///
106    /// If not specified, `field` is used as the name of the metric.
107    pub name: Option<Template>,
108
109    /// Sets the namespace for the metric.
110    pub namespace: Option<Template>,
111
112    /// Tags to apply to the metric.
113    ///
114    /// Both keys and values can be templated, allowing you to attach dynamic tags to events.
115    ///
116    #[configurable(metadata(docs::additional_props_description = "A metric tag."))]
117    pub tags: Option<IndexMap<Template, TagConfig>>,
118
119    #[configurable(derived)]
120    #[serde(flatten)]
121    pub metric: MetricTypeConfig,
122}
123
124/// Specification of the value of a created tag.
125///
126/// This may be a single value, a `null` for a bare tag, or an array of either.
127#[configurable_component]
128#[derive(Clone, Debug)]
129#[serde(untagged)]
130pub enum TagConfig {
131    /// A single tag value.
132    Plain(Option<Template>),
133
134    /// An array of values to give to the same tag name.
135    Multi(Vec<Option<Template>>),
136}
137
138/// Specification of the type of an individual metric, and any associated data.
139#[configurable_component]
140#[derive(Clone, Debug)]
141#[serde(tag = "type", rename_all = "snake_case")]
142#[configurable(metadata(docs::enum_tag_description = "The type of metric to create."))]
143pub enum MetricTypeConfig {
144    /// A counter.
145    Counter(CounterConfig),
146
147    /// A histogram.
148    Histogram,
149
150    /// A gauge.
151    Gauge,
152
153    /// A set.
154    Set,
155
156    /// A summary.
157    Summary,
158}
159
160impl MetricConfig {
161    fn field(&self) -> &str {
162        self.field.get_ref()
163    }
164}
165
166const fn default_increment_by_value() -> bool {
167    false
168}
169
170const fn default_kind() -> MetricKind {
171    MetricKind::Incremental
172}
173
174#[derive(Debug, Clone)]
175pub struct LogToMetric {
176    pub metrics: Vec<MetricConfig>,
177    pub all_metrics: bool,
178}
179
180impl GenerateConfig for LogToMetricConfig {
181    fn generate_config() -> toml::Value {
182        toml::Value::try_from(Self {
183            metrics: Some(vec![MetricConfig {
184                field: "field_name".try_into().expect("Fixed template"),
185                name: None,
186                namespace: None,
187                tags: None,
188                metric: MetricTypeConfig::Counter(CounterConfig {
189                    increment_by_value: false,
190                    kind: MetricKind::Incremental,
191                }),
192            }]),
193            all_metrics: Some(true),
194        })
195        .unwrap()
196    }
197}
198
199#[async_trait::async_trait]
200#[typetag::serde(name = "log_to_metric")]
201impl TransformConfig for LogToMetricConfig {
202    async fn build(&self, _context: &TransformContext) -> crate::Result<Transform> {
203        Ok(Transform::function(LogToMetric {
204            metrics: self.metrics.clone().unwrap_or_default(),
205            all_metrics: self.all_metrics.unwrap_or_default(),
206        }))
207    }
208
209    fn input(&self) -> Input {
210        Input::log()
211    }
212
213    fn outputs(
214        &self,
215        _: &TransformContext,
216        _: &[(OutputId, schema::Definition)],
217    ) -> Vec<TransformOutput> {
218        // Converting the log to a metric means we lose all incoming `Definition`s.
219        vec![TransformOutput::new(DataType::Metric, HashMap::new())]
220    }
221
222    fn enable_concurrency(&self) -> bool {
223        true
224    }
225}
226
227/// Kinds of TransformError for Parsing
228#[configurable_component]
229#[derive(Clone, Debug)]
230pub enum TransformParseErrorKind {
231    ///  Error when Parsing a Float
232    FloatError,
233    ///  Error when Parsing an Int
234    IntError,
235    /// Errors when Parsing Arrays
236    ArrayError,
237}
238
239impl std::fmt::Display for TransformParseErrorKind {
240    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
241        write!(f, "{self:?}")
242    }
243}
244
245enum TransformError {
246    PathNotFound {
247        path: String,
248    },
249    PathNull {
250        path: String,
251    },
252    MetricDetailsNotFound,
253    MetricValueError {
254        path: String,
255        path_value: String,
256    },
257    ParseError {
258        path: String,
259        kind: TransformParseErrorKind,
260    },
261    ParseFloatError {
262        path: String,
263        error: ParseFloatError,
264    },
265    TemplateRenderingError(TemplateRenderingError),
266    PairExpansionError {
267        key: String,
268        value: String,
269        error: serde_json::Error,
270    },
271}
272
273fn render_template(template: &Template, event: &Event) -> Result<String, TransformError> {
274    template
275        .render_string(event)
276        .map_err(TransformError::TemplateRenderingError)
277}
278
279fn render_tags(
280    tags: &Option<IndexMap<Template, TagConfig>>,
281    event: &Event,
282) -> Result<Option<MetricTags>, TransformError> {
283    let mut static_tags: HashMap<String, String> = HashMap::new();
284    let mut dynamic_tags: HashMap<String, String> = HashMap::new();
285    Ok(match tags {
286        None => None,
287        Some(tags) => {
288            let mut result = MetricTags::default();
289            for (name, config) in tags {
290                match config {
291                    TagConfig::Plain(template) => {
292                        render_tag_into(
293                            event,
294                            name,
295                            template.as_ref(),
296                            &mut result,
297                            &mut static_tags,
298                            &mut dynamic_tags,
299                        )?;
300                    }
301                    TagConfig::Multi(vec) => {
302                        for template in vec {
303                            render_tag_into(
304                                event,
305                                name,
306                                template.as_ref(),
307                                &mut result,
308                                &mut static_tags,
309                                &mut dynamic_tags,
310                            )?;
311                        }
312                    }
313                }
314            }
315            for (k, v) in static_tags {
316                if let Some(discarded_v) = dynamic_tags.insert(k.clone(), v.clone()) {
317                    warn!(
318                        "Static tags overrides dynamic tags. \
319                key: {}, value: {:?}, discarded value: {:?}",
320                        k, v, discarded_v
321                    );
322                };
323            }
324            result.as_option()
325        }
326    })
327}
328
329fn render_tag_into(
330    event: &Event,
331    key_template: &Template,
332    value_template: Option<&Template>,
333    result: &mut MetricTags,
334    static_tags: &mut HashMap<String, String>,
335    dynamic_tags: &mut HashMap<String, String>,
336) -> Result<(), TransformError> {
337    let key = match render_template(key_template, event) {
338        Ok(key_s) => key_s,
339        Err(TransformError::TemplateRenderingError(err)) => {
340            emit!(crate::internal_events::TemplateRenderingError {
341                error: err,
342                drop_event: false,
343                field: Some(key_template.get_ref()),
344            });
345            return Ok(());
346        }
347        Err(err) => return Err(err),
348    };
349    match value_template {
350        None => {
351            result.insert(key, TagValue::Bare);
352        }
353        Some(template) => match render_template(template, event) {
354            Ok(value) => {
355                let expanded_pairs = pair_expansion(&key, &value, static_tags, dynamic_tags)
356                    .map_err(|error| TransformError::PairExpansionError { key, value, error })?;
357                result.extend(expanded_pairs);
358            }
359            Err(TransformError::TemplateRenderingError(value_error)) => {
360                emit!(crate::internal_events::TemplateRenderingError {
361                    error: value_error,
362                    drop_event: false,
363                    field: Some(template.get_ref()),
364                });
365                return Ok(());
366            }
367            Err(other) => return Err(other),
368        },
369    };
370    Ok(())
371}
372
373fn to_metric_with_config(config: &MetricConfig, event: &Event) -> Result<Metric, TransformError> {
374    let log = event.as_log();
375
376    let timestamp = log
377        .get_timestamp()
378        .and_then(Value::as_timestamp)
379        .cloned()
380        .or_else(|| Some(Utc::now()));
381
382    // Assign the OriginService for the new metric
383    let metadata = event
384        .metadata()
385        .clone()
386        .with_schema_definition(&Arc::new(Definition::any()))
387        .with_origin_metadata(DatadogMetricOriginMetadata::new(
388            None,
389            None,
390            Some(ORIGIN_SERVICE_VALUE),
391        ));
392
393    let field = parse_target_path(config.field()).map_err(|_e| PathNotFound {
394        path: config.field().to_string(),
395    })?;
396
397    let value = match log.get(&field) {
398        None => Err(TransformError::PathNotFound {
399            path: field.to_string(),
400        }),
401        Some(Value::Null) => Err(TransformError::PathNull {
402            path: field.to_string(),
403        }),
404        Some(value) => Ok(value),
405    }?;
406
407    let name = config.name.as_ref().unwrap_or(&config.field);
408    let name = render_template(name, event)?;
409
410    let namespace = config.namespace.as_ref();
411    let namespace = namespace
412        .map(|namespace| render_template(namespace, event))
413        .transpose()?;
414
415    let tags = render_tags(&config.tags, event)?;
416
417    let (kind, value) = match &config.metric {
418        MetricTypeConfig::Counter(counter) => {
419            let value = if counter.increment_by_value {
420                value.to_string_lossy().parse().map_err(|error| {
421                    TransformError::ParseFloatError {
422                        path: config.field.get_ref().to_owned(),
423                        error,
424                    }
425                })?
426            } else {
427                1.0
428            };
429
430            (counter.kind, MetricValue::Counter { value })
431        }
432        MetricTypeConfig::Histogram => {
433            let value = value.to_string_lossy().parse().map_err(|error| {
434                TransformError::ParseFloatError {
435                    path: field.to_string(),
436                    error,
437                }
438            })?;
439
440            (
441                MetricKind::Incremental,
442                MetricValue::Distribution {
443                    samples: vector_lib::samples![value => 1],
444                    statistic: StatisticKind::Histogram,
445                },
446            )
447        }
448        MetricTypeConfig::Summary => {
449            let value = value.to_string_lossy().parse().map_err(|error| {
450                TransformError::ParseFloatError {
451                    path: field.to_string(),
452                    error,
453                }
454            })?;
455
456            (
457                MetricKind::Incremental,
458                MetricValue::Distribution {
459                    samples: vector_lib::samples![value => 1],
460                    statistic: StatisticKind::Summary,
461                },
462            )
463        }
464        MetricTypeConfig::Gauge => {
465            let value = value.to_string_lossy().parse().map_err(|error| {
466                TransformError::ParseFloatError {
467                    path: field.to_string(),
468                    error,
469                }
470            })?;
471
472            (MetricKind::Absolute, MetricValue::Gauge { value })
473        }
474        MetricTypeConfig::Set => {
475            let value = value.to_string_lossy().into_owned();
476
477            (
478                MetricKind::Incremental,
479                MetricValue::Set {
480                    values: std::iter::once(value).collect(),
481                },
482            )
483        }
484    };
485    Ok(Metric::new_with_metadata(name, kind, value, metadata)
486        .with_namespace(namespace)
487        .with_tags(tags)
488        .with_timestamp(timestamp))
489}
490
491fn bytes_to_str(value: &Value) -> Option<String> {
492    match value {
493        Value::Bytes(bytes) => std::str::from_utf8(bytes).ok().map(|s| s.to_string()),
494        _ => None,
495    }
496}
497
498fn try_get_string_from_log(log: &LogEvent, path: &str) -> Result<Option<String>, TransformError> {
499    // TODO: update returned errors after `TransformError` is refactored.
500    let maybe_value = log.parse_path_and_get_value(path).map_err(|e| match e {
501        PathParseError::InvalidPathSyntax { path } => PathNotFound {
502            path: path.to_string(),
503        },
504    })?;
505    match maybe_value {
506        None => Err(PathNotFound {
507            path: path.to_string(),
508        }),
509        Some(v) => Ok(bytes_to_str(v)),
510    }
511}
512
513fn get_counter_value(log: &LogEvent) -> Result<MetricValue, TransformError> {
514    let counter_value = log
515        .get(event_path!("counter", "value"))
516        .ok_or_else(|| TransformError::PathNotFound {
517            path: "counter.value".to_string(),
518        })?
519        .as_float()
520        .ok_or_else(|| TransformError::ParseError {
521            path: "counter.value".to_string(),
522            kind: TransformParseErrorKind::FloatError,
523        })?;
524
525    Ok(MetricValue::Counter {
526        value: *counter_value,
527    })
528}
529
530fn get_gauge_value(log: &LogEvent) -> Result<MetricValue, TransformError> {
531    let gauge_value = log
532        .get(event_path!("gauge", "value"))
533        .ok_or_else(|| TransformError::PathNotFound {
534            path: "gauge.value".to_string(),
535        })?
536        .as_float()
537        .ok_or_else(|| TransformError::ParseError {
538            path: "gauge.value".to_string(),
539            kind: TransformParseErrorKind::FloatError,
540        })?;
541    Ok(MetricValue::Gauge {
542        value: *gauge_value,
543    })
544}
545
546fn get_set_value(log: &LogEvent) -> Result<MetricValue, TransformError> {
547    let set_values = log
548        .get(event_path!("set", "values"))
549        .ok_or_else(|| TransformError::PathNotFound {
550            path: "set.values".to_string(),
551        })?
552        .as_array()
553        .ok_or_else(|| TransformError::ParseError {
554            path: "set.values".to_string(),
555            kind: TransformParseErrorKind::ArrayError,
556        })?;
557
558    let mut values: Vec<String> = Vec::new();
559    for e_value in set_values {
560        let value = e_value
561            .as_bytes()
562            .ok_or_else(|| TransformError::ParseError {
563                path: "set.values".to_string(),
564                kind: TransformParseErrorKind::ArrayError,
565            })?;
566        values.push(String::from_utf8_lossy(value).to_string());
567    }
568
569    Ok(MetricValue::Set {
570        values: values.into_iter().collect(),
571    })
572}
573
574fn get_distribution_value(log: &LogEvent) -> Result<MetricValue, TransformError> {
575    let event_samples = log
576        .get(event_path!("distribution", "samples"))
577        .ok_or_else(|| TransformError::PathNotFound {
578            path: "distribution.samples".to_string(),
579        })?
580        .as_array()
581        .ok_or_else(|| TransformError::ParseError {
582            path: "distribution.samples".to_string(),
583            kind: TransformParseErrorKind::ArrayError,
584        })?;
585
586    let mut samples: Vec<Sample> = Vec::new();
587    for e_sample in event_samples {
588        let value = e_sample
589            .get(path!("value"))
590            .ok_or_else(|| TransformError::PathNotFound {
591                path: "value".to_string(),
592            })?
593            .as_float()
594            .ok_or_else(|| TransformError::ParseError {
595                path: "value".to_string(),
596                kind: TransformParseErrorKind::FloatError,
597            })?;
598
599        let rate = e_sample
600            .get(path!("rate"))
601            .ok_or_else(|| TransformError::PathNotFound {
602                path: "rate".to_string(),
603            })?
604            .as_integer()
605            .ok_or_else(|| TransformError::ParseError {
606                path: "rate".to_string(),
607                kind: TransformParseErrorKind::IntError,
608            })?;
609
610        samples.push(Sample {
611            value: *value,
612            rate: rate as u32,
613        });
614    }
615
616    let statistic_str = match try_get_string_from_log(log, "distribution.statistic")? {
617        Some(n) => n,
618        None => {
619            return Err(TransformError::PathNotFound {
620                path: "distribution.statistic".to_string(),
621            });
622        }
623    };
624    let statistic_kind = match statistic_str.as_str() {
625        "histogram" => Ok(StatisticKind::Histogram),
626        "summary" => Ok(StatisticKind::Summary),
627        _ => Err(TransformError::MetricValueError {
628            path: "distribution.statistic".to_string(),
629            path_value: statistic_str.to_string(),
630        }),
631    }?;
632
633    Ok(MetricValue::Distribution {
634        samples,
635        statistic: statistic_kind,
636    })
637}
638
639fn get_histogram_value(log: &LogEvent) -> Result<MetricValue, TransformError> {
640    let event_buckets = log
641        .get(event_path!("aggregated_histogram", "buckets"))
642        .ok_or_else(|| TransformError::PathNotFound {
643            path: "aggregated_histogram.buckets".to_string(),
644        })?
645        .as_array()
646        .ok_or_else(|| TransformError::ParseError {
647            path: "aggregated_histogram.buckets".to_string(),
648            kind: TransformParseErrorKind::ArrayError,
649        })?;
650
651    let mut buckets: Vec<Bucket> = Vec::new();
652    for e_bucket in event_buckets {
653        let upper_limit = e_bucket
654            .get(path!("upper_limit"))
655            .ok_or_else(|| TransformError::PathNotFound {
656                path: "aggregated_histogram.buckets.upper_limit".to_string(),
657            })?
658            .as_float()
659            .ok_or_else(|| TransformError::ParseError {
660                path: "aggregated_histogram.buckets.upper_limit".to_string(),
661                kind: TransformParseErrorKind::FloatError,
662            })?;
663
664        let count = e_bucket
665            .get(path!("count"))
666            .ok_or_else(|| TransformError::PathNotFound {
667                path: "aggregated_histogram.buckets.count".to_string(),
668            })?
669            .as_integer()
670            .ok_or_else(|| TransformError::ParseError {
671                path: "aggregated_histogram.buckets.count".to_string(),
672                kind: TransformParseErrorKind::IntError,
673            })?;
674
675        buckets.push(Bucket {
676            upper_limit: *upper_limit,
677            count: count as u64,
678        });
679    }
680
681    let count = log
682        .get(event_path!("aggregated_histogram", "count"))
683        .ok_or_else(|| TransformError::PathNotFound {
684            path: "aggregated_histogram.count".to_string(),
685        })?
686        .as_integer()
687        .ok_or_else(|| TransformError::ParseError {
688            path: "aggregated_histogram.count".to_string(),
689            kind: TransformParseErrorKind::IntError,
690        })?;
691
692    let sum = log
693        .get(event_path!("aggregated_histogram", "sum"))
694        .ok_or_else(|| TransformError::PathNotFound {
695            path: "aggregated_histogram.sum".to_string(),
696        })?
697        .as_float()
698        .ok_or_else(|| TransformError::ParseError {
699            path: "aggregated_histogram.sum".to_string(),
700            kind: TransformParseErrorKind::FloatError,
701        })?;
702
703    Ok(MetricValue::AggregatedHistogram {
704        buckets,
705        count: count as u64,
706        sum: *sum,
707    })
708}
709
710fn get_summary_value(log: &LogEvent) -> Result<MetricValue, TransformError> {
711    let event_quantiles = log
712        .get(event_path!("aggregated_summary", "quantiles"))
713        .ok_or_else(|| TransformError::PathNotFound {
714            path: "aggregated_summary.quantiles".to_string(),
715        })?
716        .as_array()
717        .ok_or_else(|| TransformError::ParseError {
718            path: "aggregated_summary.quantiles".to_string(),
719            kind: TransformParseErrorKind::ArrayError,
720        })?;
721
722    let mut quantiles: Vec<Quantile> = Vec::new();
723    for e_quantile in event_quantiles {
724        let quantile = e_quantile
725            .get(path!("quantile"))
726            .ok_or_else(|| TransformError::PathNotFound {
727                path: "aggregated_summary.quantiles.quantile".to_string(),
728            })?
729            .as_float()
730            .ok_or_else(|| TransformError::ParseError {
731                path: "aggregated_summary.quantiles.quantile".to_string(),
732                kind: TransformParseErrorKind::FloatError,
733            })?;
734
735        let value = e_quantile
736            .get(path!("value"))
737            .ok_or_else(|| TransformError::PathNotFound {
738                path: "aggregated_summary.quantiles.value".to_string(),
739            })?
740            .as_float()
741            .ok_or_else(|| TransformError::ParseError {
742                path: "aggregated_summary.quantiles.value".to_string(),
743                kind: TransformParseErrorKind::FloatError,
744            })?;
745
746        quantiles.push(Quantile {
747            quantile: *quantile,
748            value: *value,
749        })
750    }
751
752    let count = log
753        .get(event_path!("aggregated_summary", "count"))
754        .ok_or_else(|| TransformError::PathNotFound {
755            path: "aggregated_summary.count".to_string(),
756        })?
757        .as_integer()
758        .ok_or_else(|| TransformError::ParseError {
759            path: "aggregated_summary.count".to_string(),
760            kind: TransformParseErrorKind::IntError,
761        })?;
762
763    let sum = log
764        .get(event_path!("aggregated_summary", "sum"))
765        .ok_or_else(|| TransformError::PathNotFound {
766            path: "aggregated_summary.sum".to_string(),
767        })?
768        .as_float()
769        .ok_or_else(|| TransformError::ParseError {
770            path: "aggregated_summary.sum".to_string(),
771            kind: TransformParseErrorKind::FloatError,
772        })?;
773
774    Ok(MetricValue::AggregatedSummary {
775        quantiles,
776        count: count as u64,
777        sum: *sum,
778    })
779}
780
781fn to_metrics(event: &Event) -> Result<Metric, TransformError> {
782    let log = event.as_log();
783    let timestamp = log
784        .get_timestamp()
785        .and_then(Value::as_timestamp)
786        .cloned()
787        .or_else(|| Some(Utc::now()));
788
789    let name = match try_get_string_from_log(log, "name")? {
790        Some(n) => n,
791        None => {
792            return Err(TransformError::PathNotFound {
793                path: "name".to_string(),
794            });
795        }
796    };
797
798    let mut tags = MetricTags::default();
799
800    if let Some(els) = log.get(event_path!("tags"))
801        && let Some(el) = els.as_object()
802    {
803        for (key, value) in el {
804            tags.insert(key.to_string(), bytes_to_str(value));
805        }
806    }
807    let tags_result = Some(tags);
808
809    let kind_str = match try_get_string_from_log(log, "kind")? {
810        Some(n) => n,
811        None => {
812            return Err(TransformError::PathNotFound {
813                path: "kind".to_string(),
814            });
815        }
816    };
817
818    let kind = match kind_str.as_str() {
819        "absolute" => Ok(MetricKind::Absolute),
820        "incremental" => Ok(MetricKind::Incremental),
821        value => Err(TransformError::MetricValueError {
822            path: "kind".to_string(),
823            path_value: value.to_string(),
824        }),
825    }?;
826
827    let mut value: Option<MetricValue> = None;
828    if let Some(root_event) = log.as_map() {
829        for key in root_event.keys() {
830            value = match key.as_str() {
831                "gauge" => Some(get_gauge_value(log)?),
832                "distribution" => Some(get_distribution_value(log)?),
833                "aggregated_histogram" => Some(get_histogram_value(log)?),
834                "aggregated_summary" => Some(get_summary_value(log)?),
835                "counter" => Some(get_counter_value(log)?),
836                "set" => Some(get_set_value(log)?),
837                _ => None,
838            };
839
840            if value.is_some() {
841                break;
842            }
843        }
844    }
845
846    let value = value.ok_or(TransformError::MetricDetailsNotFound)?;
847
848    let mut metric = Metric::new_with_metadata(name, kind, value, log.metadata().clone())
849        .with_tags(tags_result)
850        .with_timestamp(timestamp);
851
852    if let Ok(namespace) = try_get_string_from_log(log, "namespace") {
853        metric = metric.with_namespace(namespace);
854    }
855
856    Ok(metric)
857}
858
859impl FunctionTransform for LogToMetric {
860    fn transform(&mut self, output: &mut OutputBuffer, event: Event) {
861        // Metrics are "all or none" for a specific log. If a single fails, none are produced.
862        let mut buffer = Vec::with_capacity(self.metrics.len());
863        if self.all_metrics {
864            match to_metrics(&event) {
865                Ok(metric) => {
866                    output.push(Event::Metric(metric));
867                }
868                Err(err) => {
869                    match err {
870                        TransformError::MetricValueError { path, path_value } => {
871                            emit!(MetricMetadataInvalidFieldValueError {
872                                field: path.as_ref(),
873                                field_value: path_value.as_ref()
874                            })
875                        }
876                        TransformError::PathNotFound { path } => {
877                            emit!(ParserMissingFieldError::<DROP_EVENT> {
878                                field: path.as_ref()
879                            })
880                        }
881                        TransformError::ParseError { path, kind } => {
882                            emit!(MetricMetadataParseError {
883                                field: path.as_ref(),
884                                kind: &kind.to_string(),
885                            })
886                        }
887                        TransformError::MetricDetailsNotFound => {
888                            emit!(MetricMetadataMetricDetailsNotFoundError {})
889                        }
890                        TransformError::PairExpansionError { key, value, error } => {
891                            emit!(crate::internal_events::PairExpansionError {
892                                key: &key,
893                                value: &value,
894                                drop_event: true,
895                                error
896                            })
897                        }
898                        _ => {}
899                    };
900                }
901            }
902        } else {
903            for config in self.metrics.iter() {
904                match to_metric_with_config(config, &event) {
905                    Ok(metric) => {
906                        buffer.push(Event::Metric(metric));
907                    }
908                    Err(err) => {
909                        match err {
910                            TransformError::PathNull { path } => {
911                                emit!(LogToMetricFieldNullError {
912                                    field: path.as_ref()
913                                })
914                            }
915                            TransformError::PathNotFound { path } => {
916                                emit!(ParserMissingFieldError::<DROP_EVENT> {
917                                    field: path.as_ref()
918                                })
919                            }
920                            TransformError::ParseFloatError { path, error } => {
921                                emit!(LogToMetricParseFloatError {
922                                    field: path.as_ref(),
923                                    error
924                                })
925                            }
926                            TransformError::TemplateRenderingError(error) => {
927                                emit!(crate::internal_events::TemplateRenderingError {
928                                    error,
929                                    drop_event: true,
930                                    field: None,
931                                })
932                            }
933                            TransformError::PairExpansionError { key, value, error } => {
934                                emit!(crate::internal_events::PairExpansionError {
935                                    key: &key,
936                                    value: &value,
937                                    drop_event: true,
938                                    error
939                                })
940                            }
941                            _ => {}
942                        };
943                        // early return to prevent the partial buffer from being sent
944                        return;
945                    }
946                }
947            }
948        }
949
950        // Metric generation was successful, publish them all.
951        for event in buffer {
952            output.push(event);
953        }
954    }
955}
956
957#[cfg(test)]
958mod tests {
959    use std::{sync::Arc, time::Duration};
960
961    use chrono::{DateTime, Timelike, Utc, offset::TimeZone};
962    use similar_asserts::assert_eq;
963    use tokio::sync::mpsc;
964    use tokio_stream::wrappers::ReceiverStream;
965    use vector_lib::{
966        config::ComponentKey,
967        event::{EventMetadata, ObjectMap},
968        metric_tags,
969    };
970
971    use super::*;
972    use crate::{
973        config::log_schema,
974        event::{
975            Event, LogEvent,
976            metric::{Metric, MetricKind, MetricValue, StatisticKind},
977        },
978        test_util::components::assert_transform_compliance,
979        transforms::test::create_topology,
980    };
981
982    const TEST_SOURCE_COMPONENT_ID: &str = "in";
983    const TEST_UPSTREAM_COMPONENT_ID: &str = "transform";
984    const TEST_SOURCE_TYPE: &str = "unit_test_stream";
985    const TEST_NAMESPACE: &str = "test_namespace";
986
987    #[test]
988    fn generate_config() {
989        crate::test_util::test_generate_config::<LogToMetricConfig>();
990    }
991
992    fn parse_config(s: &str) -> LogToMetricConfig {
993        toml::from_str(s).unwrap()
994    }
995
996    fn parse_yaml_config(s: &str) -> LogToMetricConfig {
997        serde_yaml::from_str(s).unwrap()
998    }
999
1000    fn ts() -> DateTime<Utc> {
1001        Utc.with_ymd_and_hms(2018, 11, 14, 8, 9, 10)
1002            .single()
1003            .and_then(|t| t.with_nanosecond(11))
1004            .expect("invalid timestamp")
1005    }
1006
1007    fn create_event(key: &str, value: impl Into<Value> + std::fmt::Debug) -> Event {
1008        use vrl::path::{OwnedSegment, OwnedTargetPath, OwnedValuePath};
1009        let mut log = Event::Log(LogEvent::from("i am a log"));
1010        let path = OwnedTargetPath::event(OwnedValuePath::from(vec![OwnedSegment::field(key)]));
1011        log.as_mut_log().insert(&path, value);
1012        log.as_mut_log()
1013            .insert(log_schema().timestamp_key_target_path().unwrap(), ts());
1014        log
1015    }
1016
1017    fn set_test_source_metadata(metadata: &mut EventMetadata) {
1018        metadata.set_upstream_id(Arc::new(OutputId::from(TEST_UPSTREAM_COMPONENT_ID)));
1019        metadata.set_source_id(Arc::new(ComponentKey::from(TEST_SOURCE_COMPONENT_ID)));
1020        metadata.set_source_type(TEST_SOURCE_TYPE);
1021    }
1022
1023    async fn do_transform(config: LogToMetricConfig, event: Event) -> Option<Event> {
1024        assert_transform_compliance(async move {
1025            let (tx, rx) = mpsc::channel(1);
1026            let (topology, mut out) = create_topology(ReceiverStream::new(rx), config).await;
1027            tx.send(event).await.unwrap();
1028            let result = tokio::time::timeout(Duration::from_secs(5), out.recv())
1029                .await
1030                .unwrap_or(None);
1031            drop(tx);
1032            topology.stop().await;
1033            assert_eq!(out.recv().await, None);
1034            result
1035        })
1036        .await
1037    }
1038
1039    async fn do_transform_multiple_events(
1040        config: LogToMetricConfig,
1041        event: Event,
1042        count: usize,
1043    ) -> Vec<Event> {
1044        assert_transform_compliance(async move {
1045            let (tx, rx) = mpsc::channel(1);
1046            let (topology, mut out) = create_topology(ReceiverStream::new(rx), config).await;
1047            tx.send(event).await.unwrap();
1048
1049            let mut results = vec![];
1050            for _ in 0..count {
1051                let result = tokio::time::timeout(Duration::from_secs(5), out.recv())
1052                    .await
1053                    .unwrap_or(None);
1054                if let Some(event) = result {
1055                    results.push(event);
1056                }
1057            }
1058
1059            drop(tx);
1060            topology.stop().await;
1061            assert_eq!(out.recv().await, None);
1062            results
1063        })
1064        .await
1065    }
1066
1067    #[tokio::test]
1068    async fn count_http_status_codes() {
1069        let config = parse_config(
1070            r#"
1071            [[metrics]]
1072            type = "counter"
1073            field = "status"
1074            "#,
1075        );
1076
1077        let event = create_event("status", "42");
1078        let mut metadata =
1079            event
1080                .metadata()
1081                .clone()
1082                .with_origin_metadata(DatadogMetricOriginMetadata::new(
1083                    None,
1084                    None,
1085                    Some(ORIGIN_SERVICE_VALUE),
1086                ));
1087        // definitions aren't valid for metrics yet, it's just set to the default (anything).
1088        metadata.set_schema_definition(&Arc::new(Definition::any()));
1089        set_test_source_metadata(&mut metadata);
1090        let metric = do_transform(config, event).await.unwrap();
1091
1092        assert_eq!(
1093            metric.into_metric(),
1094            Metric::new_with_metadata(
1095                "status",
1096                MetricKind::Incremental,
1097                MetricValue::Counter { value: 1.0 },
1098                metadata,
1099            )
1100            .with_timestamp(Some(ts()))
1101        );
1102    }
1103
1104    #[tokio::test]
1105    async fn count_http_requests_with_tags() {
1106        let config = parse_config(
1107            r#"
1108            [[metrics]]
1109            type = "counter"
1110            field = "message"
1111            name = "http_requests_total"
1112            namespace = "app"
1113            tags = {method = "{{method}}", code = "{{code}}", missing_tag = "{{unknown}}", host = "localhost"}
1114            "#,
1115        );
1116
1117        let mut event = create_event("message", "i am log");
1118        event.as_mut_log().insert(event_path!("method"), "post");
1119        event.as_mut_log().insert(event_path!("code"), "200");
1120        let mut metadata =
1121            event
1122                .metadata()
1123                .clone()
1124                .with_origin_metadata(DatadogMetricOriginMetadata::new(
1125                    None,
1126                    None,
1127                    Some(ORIGIN_SERVICE_VALUE),
1128                ));
1129        // definitions aren't valid for metrics yet, it's just set to the default (anything).
1130        metadata.set_schema_definition(&Arc::new(Definition::any()));
1131        set_test_source_metadata(&mut metadata);
1132
1133        let metric = do_transform(config, event).await.unwrap();
1134
1135        assert_eq!(
1136            metric.into_metric(),
1137            Metric::new_with_metadata(
1138                "http_requests_total",
1139                MetricKind::Incremental,
1140                MetricValue::Counter { value: 1.0 },
1141                metadata,
1142            )
1143            .with_namespace(Some("app"))
1144            .with_tags(Some(metric_tags!(
1145                "method" => "post",
1146                "code" => "200",
1147                "host" => "localhost",
1148            )))
1149            .with_timestamp(Some(ts()))
1150        );
1151    }
1152
1153    #[tokio::test]
1154    async fn count_http_requests_with_tags_expansion() {
1155        let config = parse_config(
1156            r#"
1157            [[metrics]]
1158            type = "counter"
1159            field = "message"
1160            name = "http_requests_total"
1161            namespace = "app"
1162            tags = {"*" = "{{ dict }}"}
1163            "#,
1164        );
1165
1166        let mut event = create_event("message", "i am log");
1167        let log = event.as_mut_log();
1168
1169        let mut test_dict = ObjectMap::default();
1170        test_dict.insert("one".into(), Value::from("foo"));
1171        test_dict.insert("two".into(), Value::from("baz"));
1172        log.insert(event_path!("dict"), Value::from(test_dict));
1173
1174        let mut metadata =
1175            event
1176                .metadata()
1177                .clone()
1178                .with_origin_metadata(DatadogMetricOriginMetadata::new(
1179                    None,
1180                    None,
1181                    Some(ORIGIN_SERVICE_VALUE),
1182                ));
1183        // definitions aren't valid for metrics yet, it's just set to the default (anything).
1184        metadata.set_schema_definition(&Arc::new(Definition::any()));
1185        set_test_source_metadata(&mut metadata);
1186
1187        let metric = do_transform(config, event).await.unwrap();
1188
1189        assert_eq!(
1190            metric.into_metric(),
1191            Metric::new_with_metadata(
1192                "http_requests_total",
1193                MetricKind::Incremental,
1194                MetricValue::Counter { value: 1.0 },
1195                metadata,
1196            )
1197            .with_namespace(Some("app"))
1198            .with_tags(Some(metric_tags!(
1199                "one" => "foo",
1200                "two" => "baz",
1201            )))
1202            .with_timestamp(Some(ts()))
1203        );
1204    }
1205    #[tokio::test]
1206    async fn count_http_requests_with_colliding_dynamic_tags() {
1207        let config = parse_config(
1208            r#"
1209            [[metrics]]
1210            type = "counter"
1211            field = "message"
1212            name = "http_requests_total"
1213            namespace = "app"
1214            tags = {"l1_*" = "{{ map1 }}", "*" = "{{ map2 }}"}
1215            "#,
1216        );
1217
1218        let mut event = create_event("message", "i am log");
1219        let log = event.as_mut_log();
1220
1221        let mut map1 = ObjectMap::default();
1222        map1.insert("key1".into(), Value::from("val1"));
1223        log.insert(event_path!("map1"), Value::from(map1));
1224
1225        let mut map2 = ObjectMap::default();
1226        map2.insert("l1_key1".into(), Value::from("val2"));
1227        log.insert(event_path!("map2"), Value::from(map2));
1228
1229        let mut metadata =
1230            event
1231                .metadata()
1232                .clone()
1233                .with_origin_metadata(DatadogMetricOriginMetadata::new(
1234                    None,
1235                    None,
1236                    Some(ORIGIN_SERVICE_VALUE),
1237                ));
1238        // definitions aren't valid for metrics yet, it's just set to the default (anything).
1239        metadata.set_schema_definition(&Arc::new(Definition::any()));
1240        set_test_source_metadata(&mut metadata);
1241
1242        let metric = do_transform(config, event).await.unwrap().into_metric();
1243        let tags = metric.tags().expect("Metric should have tags");
1244
1245        assert_eq!(tags.iter_single().collect::<Vec<_>>()[0].0, "l1_key1");
1246
1247        assert_eq!(tags.iter_all().count(), 2);
1248        for (name, value) in tags.iter_all() {
1249            assert_eq!(name, "l1_key1");
1250            assert!(value == Some("val1") || value == Some("val2"));
1251        }
1252    }
1253    #[tokio::test]
1254    async fn multi_value_tags_yaml() {
1255        // Have to use YAML to represent bare tags
1256        let config = parse_yaml_config(
1257            r#"
1258            metrics:
1259            - field: "message"
1260              type: "counter"
1261              tags:
1262                tag:
1263                - "one"
1264                - null
1265                - "two"
1266            "#,
1267        );
1268
1269        let event = create_event("message", "I am log");
1270        let metric = do_transform(config, event).await.unwrap().into_metric();
1271        let tags = metric.tags().expect("Metric should have tags");
1272
1273        assert_eq!(tags.iter_single().collect::<Vec<_>>(), vec![("tag", "two")]);
1274
1275        assert_eq!(tags.iter_all().count(), 3);
1276        for (name, value) in tags.iter_all() {
1277            assert_eq!(name, "tag");
1278            assert!(value.is_none() || value == Some("one") || value == Some("two"));
1279        }
1280    }
1281    #[tokio::test]
1282    async fn multi_value_tags_expansion_yaml() {
1283        // Have to use YAML to represent bare tags
1284        let config = parse_yaml_config(
1285            r#"
1286            metrics:
1287            - field: "message"
1288              type: "counter"
1289              tags:
1290                "*": "{{dict}}"
1291            "#,
1292        );
1293
1294        let mut event = create_event("message", "I am log");
1295        let log = event.as_mut_log();
1296
1297        let mut test_dict = ObjectMap::default();
1298        test_dict.insert("one".into(), Value::from(vec!["foo", "baz"]));
1299        log.insert(event_path!("dict"), Value::from(test_dict));
1300
1301        let metric = do_transform(config, event).await.unwrap().into_metric();
1302        let tags = metric.tags().expect("Metric should have tags");
1303
1304        assert_eq!(
1305            tags.iter_single().collect::<Vec<_>>(),
1306            vec![("one", "[\"foo\",\"baz\"]")]
1307        );
1308
1309        assert_eq!(tags.iter_all().count(), 1);
1310        for (name, value) in tags.iter_all() {
1311            assert_eq!(name, "one");
1312            assert_eq!(value, Some("[\"foo\",\"baz\"]"));
1313        }
1314    }
1315
1316    #[tokio::test]
1317    async fn multi_value_tags_toml() {
1318        let config = parse_config(
1319            r#"
1320            [[metrics]]
1321            field = "message"
1322            type = "counter"
1323            [metrics.tags]
1324            tag = ["one", "two"]
1325            "#,
1326        );
1327
1328        let event = create_event("message", "I am log");
1329        let metric = do_transform(config, event).await.unwrap().into_metric();
1330        let tags = metric.tags().expect("Metric should have tags");
1331
1332        assert_eq!(tags.iter_single().collect::<Vec<_>>(), vec![("tag", "two")]);
1333
1334        assert_eq!(tags.iter_all().count(), 2);
1335        for (name, value) in tags.iter_all() {
1336            assert_eq!(name, "tag");
1337            assert!(value == Some("one") || value == Some("two"));
1338        }
1339    }
1340
1341    #[tokio::test]
1342    async fn count_exceptions() {
1343        let config = parse_config(
1344            r#"
1345            [[metrics]]
1346            type = "counter"
1347            field = "backtrace"
1348            name = "exception_total"
1349            "#,
1350        );
1351
1352        let event = create_event("backtrace", "message");
1353        let mut metadata =
1354            event
1355                .metadata()
1356                .clone()
1357                .with_origin_metadata(DatadogMetricOriginMetadata::new(
1358                    None,
1359                    None,
1360                    Some(ORIGIN_SERVICE_VALUE),
1361                ));
1362        // definitions aren't valid for metrics yet, it's just set to the default (anything).
1363        metadata.set_schema_definition(&Arc::new(Definition::any()));
1364        set_test_source_metadata(&mut metadata);
1365
1366        let metric = do_transform(config, event).await.unwrap();
1367
1368        assert_eq!(
1369            metric.into_metric(),
1370            Metric::new_with_metadata(
1371                "exception_total",
1372                MetricKind::Incremental,
1373                MetricValue::Counter { value: 1.0 },
1374                metadata
1375            )
1376            .with_timestamp(Some(ts()))
1377        );
1378    }
1379
1380    #[tokio::test]
1381    async fn count_exceptions_no_match() {
1382        let config = parse_config(
1383            r#"
1384            [[metrics]]
1385            type = "counter"
1386            field = "backtrace"
1387            name = "exception_total"
1388            "#,
1389        );
1390
1391        let event = create_event("success", "42");
1392        assert_eq!(do_transform(config, event).await, None);
1393    }
1394
1395    #[tokio::test]
1396    async fn sum_order_amounts() {
1397        let config = parse_config(
1398            r#"
1399            [[metrics]]
1400            type = "counter"
1401            field = "amount"
1402            name = "amount_total"
1403            increment_by_value = true
1404            "#,
1405        );
1406
1407        let event = create_event("amount", "33.99");
1408        let mut metadata =
1409            event
1410                .metadata()
1411                .clone()
1412                .with_origin_metadata(DatadogMetricOriginMetadata::new(
1413                    None,
1414                    None,
1415                    Some(ORIGIN_SERVICE_VALUE),
1416                ));
1417        // definitions aren't valid for metrics yet, it's just set to the default (anything).
1418        metadata.set_schema_definition(&Arc::new(Definition::any()));
1419        set_test_source_metadata(&mut metadata);
1420        let metric = do_transform(config, event).await.unwrap();
1421
1422        assert_eq!(
1423            metric.into_metric(),
1424            Metric::new_with_metadata(
1425                "amount_total",
1426                MetricKind::Incremental,
1427                MetricValue::Counter { value: 33.99 },
1428                metadata,
1429            )
1430            .with_timestamp(Some(ts()))
1431        );
1432    }
1433
1434    #[tokio::test]
1435    async fn count_absolute() {
1436        let config = parse_config(
1437            r#"
1438            [[metrics]]
1439            type = "counter"
1440            field = "amount"
1441            name = "amount_total"
1442            increment_by_value = true
1443            kind = "absolute"
1444            "#,
1445        );
1446
1447        let event = create_event("amount", "33.99");
1448        let mut metadata =
1449            event
1450                .metadata()
1451                .clone()
1452                .with_origin_metadata(DatadogMetricOriginMetadata::new(
1453                    None,
1454                    None,
1455                    Some(ORIGIN_SERVICE_VALUE),
1456                ));
1457        // definitions aren't valid for metrics yet, it's just set to the default (anything).
1458        metadata.set_schema_definition(&Arc::new(Definition::any()));
1459        set_test_source_metadata(&mut metadata);
1460
1461        let metric = do_transform(config, event).await.unwrap();
1462
1463        assert_eq!(
1464            metric.into_metric(),
1465            Metric::new_with_metadata(
1466                "amount_total",
1467                MetricKind::Absolute,
1468                MetricValue::Counter { value: 33.99 },
1469                metadata,
1470            )
1471            .with_timestamp(Some(ts()))
1472        );
1473    }
1474
1475    #[tokio::test]
1476    async fn memory_usage_gauge() {
1477        let config = parse_config(
1478            r#"
1479            [[metrics]]
1480            type = "gauge"
1481            field = "memory_rss"
1482            name = "memory_rss_bytes"
1483            "#,
1484        );
1485
1486        let event = create_event("memory_rss", "123");
1487        let mut metadata =
1488            event
1489                .metadata()
1490                .clone()
1491                .with_origin_metadata(DatadogMetricOriginMetadata::new(
1492                    None,
1493                    None,
1494                    Some(ORIGIN_SERVICE_VALUE),
1495                ));
1496
1497        // definitions aren't valid for metrics yet, it's just set to the default (anything).
1498        metadata.set_schema_definition(&Arc::new(Definition::any()));
1499
1500        set_test_source_metadata(&mut metadata);
1501
1502        let metric = do_transform(config, event).await.unwrap();
1503
1504        assert_eq!(
1505            metric.into_metric(),
1506            Metric::new_with_metadata(
1507                "memory_rss_bytes",
1508                MetricKind::Absolute,
1509                MetricValue::Gauge { value: 123.0 },
1510                metadata,
1511            )
1512            .with_timestamp(Some(ts()))
1513        );
1514    }
1515
1516    #[tokio::test]
1517    async fn parse_failure() {
1518        let config = parse_config(
1519            r#"
1520            [[metrics]]
1521            type = "counter"
1522            field = "status"
1523            name = "status_total"
1524            increment_by_value = true
1525            "#,
1526        );
1527
1528        let event = create_event("status", "not a number");
1529        assert_eq!(do_transform(config, event).await, None);
1530    }
1531
1532    #[tokio::test]
1533    async fn missing_field() {
1534        let config = parse_config(
1535            r#"
1536            [[metrics]]
1537            type = "counter"
1538            field = "status"
1539            name = "status_total"
1540            "#,
1541        );
1542
1543        let event = create_event("not foo", "not a number");
1544        assert_eq!(do_transform(config, event).await, None);
1545    }
1546
1547    #[tokio::test]
1548    async fn null_field() {
1549        let config = parse_config(
1550            r#"
1551            [[metrics]]
1552            type = "counter"
1553            field = "status"
1554            name = "status_total"
1555            "#,
1556        );
1557
1558        let event = create_event("status", Value::Null);
1559        assert_eq!(do_transform(config, event).await, None);
1560    }
1561
1562    #[tokio::test]
1563    async fn multiple_metrics() {
1564        let config = parse_config(
1565            r#"
1566            [[metrics]]
1567            type = "counter"
1568            field = "status"
1569
1570            [[metrics]]
1571            type = "counter"
1572            field = "backtrace"
1573            name = "exception_total"
1574            "#,
1575        );
1576
1577        let mut event = Event::Log(LogEvent::from("i am a log"));
1578        event
1579            .as_mut_log()
1580            .insert(log_schema().timestamp_key_target_path().unwrap(), ts());
1581        event.as_mut_log().insert(event_path!("status"), "42");
1582        event
1583            .as_mut_log()
1584            .insert(event_path!("backtrace"), "message");
1585        let mut metadata =
1586            event
1587                .metadata()
1588                .clone()
1589                .with_origin_metadata(DatadogMetricOriginMetadata::new(
1590                    None,
1591                    None,
1592                    Some(ORIGIN_SERVICE_VALUE),
1593                ));
1594
1595        // definitions aren't valid for metrics yet, it's just set to the default (anything).
1596        metadata.set_schema_definition(&Arc::new(Definition::any()));
1597        set_test_source_metadata(&mut metadata);
1598
1599        let output = do_transform_multiple_events(config, event, 2).await;
1600
1601        assert_eq!(2, output.len());
1602        assert_eq!(
1603            output[0].clone().into_metric(),
1604            Metric::new_with_metadata(
1605                "status",
1606                MetricKind::Incremental,
1607                MetricValue::Counter { value: 1.0 },
1608                metadata.clone(),
1609            )
1610            .with_timestamp(Some(ts()))
1611        );
1612        assert_eq!(
1613            output[1].clone().into_metric(),
1614            Metric::new_with_metadata(
1615                "exception_total",
1616                MetricKind::Incremental,
1617                MetricValue::Counter { value: 1.0 },
1618                metadata,
1619            )
1620            .with_timestamp(Some(ts()))
1621        );
1622    }
1623
1624    #[tokio::test]
1625    async fn multiple_metrics_with_multiple_templates() {
1626        let config = parse_config(
1627            r#"
1628            [[metrics]]
1629            type = "set"
1630            field = "status"
1631            name = "{{host}}_{{worker}}_status_set"
1632
1633            [[metrics]]
1634            type = "counter"
1635            field = "backtrace"
1636            name = "{{service}}_exception_total"
1637            namespace = "{{host}}"
1638            "#,
1639        );
1640
1641        let mut event = Event::Log(LogEvent::from("i am a log"));
1642        event
1643            .as_mut_log()
1644            .insert(log_schema().timestamp_key_target_path().unwrap(), ts());
1645        event.as_mut_log().insert(event_path!("status"), "42");
1646        event
1647            .as_mut_log()
1648            .insert(event_path!("backtrace"), "message");
1649        event.as_mut_log().insert(event_path!("host"), "local");
1650        event.as_mut_log().insert(event_path!("worker"), "abc");
1651        event.as_mut_log().insert(event_path!("service"), "xyz");
1652        let mut metadata =
1653            event
1654                .metadata()
1655                .clone()
1656                .with_origin_metadata(DatadogMetricOriginMetadata::new(
1657                    None,
1658                    None,
1659                    Some(ORIGIN_SERVICE_VALUE),
1660                ));
1661
1662        // definitions aren't valid for metrics yet, it's just set to the default (anything).
1663        metadata.set_schema_definition(&Arc::new(Definition::any()));
1664        set_test_source_metadata(&mut metadata);
1665
1666        let output = do_transform_multiple_events(config, event, 2).await;
1667
1668        assert_eq!(2, output.len());
1669        assert_eq!(
1670            output[0].as_metric(),
1671            &Metric::new_with_metadata(
1672                "local_abc_status_set",
1673                MetricKind::Incremental,
1674                MetricValue::Set {
1675                    values: vec!["42".into()].into_iter().collect()
1676                },
1677                metadata.clone(),
1678            )
1679            .with_timestamp(Some(ts()))
1680        );
1681        assert_eq!(
1682            output[1].as_metric(),
1683            &Metric::new_with_metadata(
1684                "xyz_exception_total",
1685                MetricKind::Incremental,
1686                MetricValue::Counter { value: 1.0 },
1687                metadata,
1688            )
1689            .with_namespace(Some("local"))
1690            .with_timestamp(Some(ts()))
1691        );
1692    }
1693
1694    #[tokio::test]
1695    async fn user_ip_set() {
1696        let config = parse_config(
1697            r#"
1698            [[metrics]]
1699            type = "set"
1700            field = "user_ip"
1701            name = "unique_user_ip"
1702            "#,
1703        );
1704
1705        let event = create_event("user_ip", "1.2.3.4");
1706        let mut metadata =
1707            event
1708                .metadata()
1709                .clone()
1710                .with_origin_metadata(DatadogMetricOriginMetadata::new(
1711                    None,
1712                    None,
1713                    Some(ORIGIN_SERVICE_VALUE),
1714                ));
1715        // definitions aren't valid for metrics yet, it's just set to the default (anything).
1716        metadata.set_schema_definition(&Arc::new(Definition::any()));
1717        set_test_source_metadata(&mut metadata);
1718
1719        let metric = do_transform(config, event).await.unwrap();
1720
1721        assert_eq!(
1722            metric.into_metric(),
1723            Metric::new_with_metadata(
1724                "unique_user_ip",
1725                MetricKind::Incremental,
1726                MetricValue::Set {
1727                    values: vec!["1.2.3.4".into()].into_iter().collect()
1728                },
1729                metadata,
1730            )
1731            .with_timestamp(Some(ts()))
1732        );
1733    }
1734
1735    #[tokio::test]
1736    async fn response_time_histogram() {
1737        let config = parse_config(
1738            r#"
1739            [[metrics]]
1740            type = "histogram"
1741            field = "response_time"
1742            "#,
1743        );
1744
1745        let event = create_event("response_time", "2.5");
1746        let mut metadata =
1747            event
1748                .metadata()
1749                .clone()
1750                .with_origin_metadata(DatadogMetricOriginMetadata::new(
1751                    None,
1752                    None,
1753                    Some(ORIGIN_SERVICE_VALUE),
1754                ));
1755
1756        // definitions aren't valid for metrics yet, it's just set to the default (anything).
1757        metadata.set_schema_definition(&Arc::new(Definition::any()));
1758        set_test_source_metadata(&mut metadata);
1759
1760        let metric = do_transform(config, event).await.unwrap();
1761
1762        assert_eq!(
1763            metric.into_metric(),
1764            Metric::new_with_metadata(
1765                "response_time",
1766                MetricKind::Incremental,
1767                MetricValue::Distribution {
1768                    samples: vector_lib::samples![2.5 => 1],
1769                    statistic: StatisticKind::Histogram
1770                },
1771                metadata
1772            )
1773            .with_timestamp(Some(ts()))
1774        );
1775    }
1776
1777    #[tokio::test]
1778    async fn response_time_summary() {
1779        let config = parse_config(
1780            r#"
1781            [[metrics]]
1782            type = "summary"
1783            field = "response_time"
1784            "#,
1785        );
1786
1787        let event = create_event("response_time", "2.5");
1788        let mut metadata =
1789            event
1790                .metadata()
1791                .clone()
1792                .with_origin_metadata(DatadogMetricOriginMetadata::new(
1793                    None,
1794                    None,
1795                    Some(ORIGIN_SERVICE_VALUE),
1796                ));
1797
1798        // definitions aren't valid for metrics yet, it's just set to the default (anything).
1799        metadata.set_schema_definition(&Arc::new(Definition::any()));
1800        set_test_source_metadata(&mut metadata);
1801
1802        let metric = do_transform(config, event).await.unwrap();
1803
1804        assert_eq!(
1805            metric.into_metric(),
1806            Metric::new_with_metadata(
1807                "response_time",
1808                MetricKind::Incremental,
1809                MetricValue::Distribution {
1810                    samples: vector_lib::samples![2.5 => 1],
1811                    statistic: StatisticKind::Summary
1812                },
1813                metadata
1814            )
1815            .with_timestamp(Some(ts()))
1816        );
1817    }
1818
1819    //  Metric Metadata Tests
1820    //
1821    fn create_log_event(json_str: &str) -> Event {
1822        create_log_event_with_namespace(json_str, Some(TEST_NAMESPACE))
1823    }
1824
1825    fn create_log_event_with_namespace(json_str: &str, namespace: Option<&str>) -> Event {
1826        let mut log_value: Value =
1827            serde_json::from_str(json_str).expect("JSON was not well-formatted");
1828        log_value.insert(vrl::path!("timestamp"), ts());
1829
1830        if let Some(namespace) = namespace {
1831            log_value.insert(vrl::path!("namespace"), namespace);
1832        }
1833
1834        let mut metadata = EventMetadata::default();
1835        set_test_source_metadata(&mut metadata);
1836
1837        Event::Log(LogEvent::from_parts(log_value, metadata.clone()))
1838    }
1839
1840    #[tokio::test]
1841    async fn transform_gauge() {
1842        let config = LogToMetricConfig {
1843            metrics: None,
1844            all_metrics: Some(true),
1845        };
1846
1847        let json_str = r#"{
1848          "gauge": {
1849            "value": 990.0
1850          },
1851          "kind": "absolute",
1852          "name": "test.transform.gauge",
1853          "tags": {
1854            "env": "test_env",
1855            "host": "localhost"
1856          }
1857        }"#;
1858        let log = create_log_event(json_str);
1859        let metric = do_transform(config, log.clone()).await.unwrap();
1860        assert_eq!(
1861            *metric.as_metric(),
1862            Metric::new_with_metadata(
1863                "test.transform.gauge",
1864                MetricKind::Absolute,
1865                MetricValue::Gauge { value: 990.0 },
1866                metric.metadata().clone(),
1867            )
1868            .with_namespace(Some(TEST_NAMESPACE))
1869            .with_tags(Some(metric_tags!(
1870                "env" => "test_env",
1871                "host" => "localhost",
1872            )))
1873            .with_timestamp(Some(ts()))
1874        );
1875    }
1876
1877    #[tokio::test]
1878    async fn transform_histogram() {
1879        let config = LogToMetricConfig {
1880            metrics: None,
1881            all_metrics: Some(true),
1882        };
1883
1884        let json_str = r#"{
1885          "aggregated_histogram": {
1886            "sum": 18.0,
1887            "count": 5,
1888            "buckets": [
1889              {
1890                "upper_limit": 1.0,
1891                "count": 1
1892              },
1893              {
1894                "upper_limit": 2.0,
1895                "count": 2
1896              },
1897              {
1898                "upper_limit": 5.0,
1899                "count": 1
1900              },
1901              {
1902                "upper_limit": 10.0,
1903                "count": 1
1904              }
1905            ]
1906          },
1907          "kind": "absolute",
1908          "name": "test.transform.histogram",
1909          "tags": {
1910            "env": "test_env",
1911            "host": "localhost"
1912          }
1913        }"#;
1914        let log = create_log_event(json_str);
1915        let metric = do_transform(config, log.clone()).await.unwrap();
1916        assert_eq!(
1917            *metric.as_metric(),
1918            Metric::new_with_metadata(
1919                "test.transform.histogram",
1920                MetricKind::Absolute,
1921                MetricValue::AggregatedHistogram {
1922                    count: 5,
1923                    sum: 18.0,
1924                    buckets: vec![
1925                        Bucket {
1926                            upper_limit: 1.0,
1927                            count: 1,
1928                        },
1929                        Bucket {
1930                            upper_limit: 2.0,
1931                            count: 2,
1932                        },
1933                        Bucket {
1934                            upper_limit: 5.0,
1935                            count: 1,
1936                        },
1937                        Bucket {
1938                            upper_limit: 10.0,
1939                            count: 1,
1940                        },
1941                    ],
1942                },
1943                metric.metadata().clone(),
1944            )
1945            .with_namespace(Some(TEST_NAMESPACE))
1946            .with_tags(Some(metric_tags!(
1947                "env" => "test_env",
1948                "host" => "localhost",
1949            )))
1950            .with_timestamp(Some(ts()))
1951        );
1952    }
1953
1954    #[tokio::test]
1955    async fn transform_distribution_histogram() {
1956        let config = LogToMetricConfig {
1957            metrics: None,
1958            all_metrics: Some(true),
1959        };
1960
1961        let json_str = r#"{
1962          "distribution": {
1963            "samples": [
1964              {
1965                "value": 1.0,
1966                "rate": 1
1967              },
1968              {
1969                "value": 2.0,
1970                "rate": 2
1971              }
1972            ],
1973            "statistic": "histogram"
1974          },
1975          "kind": "absolute",
1976          "name": "test.transform.distribution_histogram",
1977          "tags": {
1978            "env": "test_env",
1979            "host": "localhost"
1980          }
1981        }"#;
1982        let log = create_log_event(json_str);
1983        let metric = do_transform(config, log.clone()).await.unwrap();
1984        assert_eq!(
1985            *metric.as_metric(),
1986            Metric::new_with_metadata(
1987                "test.transform.distribution_histogram",
1988                MetricKind::Absolute,
1989                MetricValue::Distribution {
1990                    samples: vec![
1991                        Sample {
1992                            value: 1.0,
1993                            rate: 1
1994                        },
1995                        Sample {
1996                            value: 2.0,
1997                            rate: 2
1998                        },
1999                    ],
2000                    statistic: StatisticKind::Histogram,
2001                },
2002                metric.metadata().clone(),
2003            )
2004            .with_namespace(Some(TEST_NAMESPACE))
2005            .with_tags(Some(metric_tags!(
2006                "env" => "test_env",
2007                "host" => "localhost",
2008            )))
2009            .with_timestamp(Some(ts()))
2010        );
2011    }
2012
2013    #[tokio::test]
2014    async fn transform_distribution_summary() {
2015        let config = LogToMetricConfig {
2016            metrics: None,
2017            all_metrics: Some(true),
2018        };
2019
2020        let json_str = r#"{
2021          "distribution": {
2022            "samples": [
2023              {
2024                "value": 1.0,
2025                "rate": 1
2026              },
2027              {
2028                "value": 2.0,
2029                "rate": 2
2030              }
2031            ],
2032            "statistic": "summary"
2033          },
2034          "kind": "absolute",
2035          "name": "test.transform.distribution_summary",
2036          "tags": {
2037            "env": "test_env",
2038            "host": "localhost"
2039          }
2040        }"#;
2041        let log = create_log_event(json_str);
2042        let metric = do_transform(config, log.clone()).await.unwrap();
2043        assert_eq!(
2044            *metric.as_metric(),
2045            Metric::new_with_metadata(
2046                "test.transform.distribution_summary",
2047                MetricKind::Absolute,
2048                MetricValue::Distribution {
2049                    samples: vec![
2050                        Sample {
2051                            value: 1.0,
2052                            rate: 1
2053                        },
2054                        Sample {
2055                            value: 2.0,
2056                            rate: 2
2057                        },
2058                    ],
2059                    statistic: StatisticKind::Summary,
2060                },
2061                metric.metadata().clone(),
2062            )
2063            .with_namespace(Some(TEST_NAMESPACE))
2064            .with_tags(Some(metric_tags!(
2065                "env" => "test_env",
2066                "host" => "localhost",
2067            )))
2068            .with_timestamp(Some(ts()))
2069        );
2070    }
2071
2072    #[tokio::test]
2073    async fn transform_summary() {
2074        let config = LogToMetricConfig {
2075            metrics: None,
2076            all_metrics: Some(true),
2077        };
2078
2079        let json_str = r#"{
2080          "aggregated_summary": {
2081            "sum": 100.0,
2082            "count": 7,
2083            "quantiles": [
2084              {
2085                "quantile": 0.05,
2086                "value": 10.0
2087              },
2088              {
2089                "quantile": 0.95,
2090                "value": 25.0
2091              }
2092            ]
2093          },
2094          "kind": "absolute",
2095          "name": "test.transform.histogram",
2096          "tags": {
2097            "env": "test_env",
2098            "host": "localhost"
2099          }
2100        }"#;
2101        let log = create_log_event(json_str);
2102        let metric = do_transform(config, log.clone()).await.unwrap();
2103        assert_eq!(
2104            *metric.as_metric(),
2105            Metric::new_with_metadata(
2106                "test.transform.histogram",
2107                MetricKind::Absolute,
2108                MetricValue::AggregatedSummary {
2109                    quantiles: vec![
2110                        Quantile {
2111                            quantile: 0.05,
2112                            value: 10.0,
2113                        },
2114                        Quantile {
2115                            quantile: 0.95,
2116                            value: 25.0,
2117                        },
2118                    ],
2119                    count: 7,
2120                    sum: 100.0,
2121                },
2122                metric.metadata().clone(),
2123            )
2124            .with_namespace(Some(TEST_NAMESPACE))
2125            .with_tags(Some(metric_tags!(
2126                "env" => "test_env",
2127                "host" => "localhost",
2128            )))
2129            .with_timestamp(Some(ts()))
2130        );
2131    }
2132
2133    #[tokio::test]
2134    async fn transform_counter() {
2135        let config = LogToMetricConfig {
2136            metrics: None,
2137            all_metrics: Some(true),
2138        };
2139
2140        let json_str = r#"{
2141          "counter": {
2142            "value": 10.0
2143          },
2144          "kind": "incremental",
2145          "name": "test.transform.counter",
2146          "tags": {
2147            "env": "test_env",
2148            "host": "localhost"
2149          }
2150        }"#;
2151        let log = create_log_event(json_str);
2152        let metric = do_transform(config, log.clone()).await.unwrap();
2153        assert_eq!(
2154            *metric.as_metric(),
2155            Metric::new_with_metadata(
2156                "test.transform.counter",
2157                MetricKind::Incremental,
2158                MetricValue::Counter { value: 10.0 },
2159                metric.metadata().clone(),
2160            )
2161            .with_namespace(Some(TEST_NAMESPACE))
2162            .with_tags(Some(metric_tags!(
2163                "env" => "test_env",
2164                "host" => "localhost",
2165            )))
2166            .with_timestamp(Some(ts()))
2167        );
2168    }
2169
2170    #[tokio::test]
2171    async fn transform_set() {
2172        let config = LogToMetricConfig {
2173            metrics: None,
2174            all_metrics: Some(true),
2175        };
2176
2177        let json_str = r#"{
2178          "set": {
2179            "values": ["990.0", "1234"]
2180          },
2181          "kind": "incremental",
2182          "name": "test.transform.set",
2183          "tags": {
2184            "env": "test_env",
2185            "host": "localhost"
2186          }
2187        }"#;
2188        let log = create_log_event(json_str);
2189        let metric = do_transform(config, log.clone()).await.unwrap();
2190        assert_eq!(
2191            *metric.as_metric(),
2192            Metric::new_with_metadata(
2193                "test.transform.set",
2194                MetricKind::Incremental,
2195                MetricValue::Set {
2196                    values: vec!["990.0".into(), "1234".into()].into_iter().collect()
2197                },
2198                metric.metadata().clone(),
2199            )
2200            .with_namespace(Some(TEST_NAMESPACE))
2201            .with_tags(Some(metric_tags!(
2202                "env" => "test_env",
2203                "host" => "localhost",
2204            )))
2205            .with_timestamp(Some(ts()))
2206        );
2207    }
2208
2209    #[tokio::test]
2210    async fn transform_all_metrics_optional_namespace() {
2211        let config = LogToMetricConfig {
2212            metrics: None,
2213            all_metrics: Some(true),
2214        };
2215
2216        let json_str = r#"{
2217          "counter": {
2218            "value": 10.0
2219          },
2220          "kind": "incremental",
2221          "name": "test.transform.counter",
2222          "tags": {
2223            "env": "test_env",
2224            "host": "localhost"
2225          }
2226        }"#;
2227        let log = create_log_event_with_namespace(json_str, None);
2228        let metric = do_transform(config, log.clone()).await.unwrap();
2229        assert_eq!(
2230            *metric.as_metric(),
2231            Metric::new_with_metadata(
2232                "test.transform.counter",
2233                MetricKind::Incremental,
2234                MetricValue::Counter { value: 10.0 },
2235                metric.metadata().clone(),
2236            )
2237            .with_tags(Some(metric_tags!(
2238                "env" => "test_env",
2239                "host" => "localhost",
2240            )))
2241            .with_timestamp(Some(ts()))
2242        );
2243    }
2244}