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#[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 pub metrics: Option<Vec<MetricConfig>>,
48
49 pub all_metrics: Option<bool>,
77}
78
79#[configurable_component]
81#[derive(Clone, Debug)]
82pub struct CounterConfig {
83 #[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#[configurable_component]
99#[derive(Clone, Debug)]
100pub struct MetricConfig {
101 pub field: Template,
103
104 pub name: Option<Template>,
108
109 pub namespace: Option<Template>,
111
112 #[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#[configurable_component]
128#[derive(Clone, Debug)]
129#[serde(untagged)]
130pub enum TagConfig {
131 Plain(Option<Template>),
133
134 Multi(Vec<Option<Template>>),
136}
137
138#[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 Counter(CounterConfig),
146
147 Histogram,
149
150 Gauge,
152
153 Set,
155
156 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 vec![TransformOutput::new(DataType::Metric, HashMap::new())]
220 }
221
222 fn enable_concurrency(&self) -> bool {
223 true
224 }
225}
226
227#[configurable_component]
229#[derive(Clone, Debug)]
230pub enum TransformParseErrorKind {
231 FloatError,
233 IntError,
235 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 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 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 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 return;
945 }
946 }
947 }
948 }
949
950 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}