Skip to main content

vector/transforms/
remap.rs

1// Derivative's Debug impl generates `let _ = field.fmt(f)` which triggers this lint.
2#![allow(clippy::let_underscore_must_use)]
3
4use std::{
5    collections::{BTreeMap, HashMap},
6    fs::File,
7    io::{self, Read},
8    path::PathBuf,
9    sync::Mutex,
10};
11
12use snafu::{ResultExt, Snafu};
13use vector_lib::{
14    TimeZone,
15    codecs::MetricTagValues,
16    compile_vrl,
17    config::LogNamespace,
18    configurable::configurable_component,
19    enrichment::TableRegistry,
20    lookup::{PathPrefix, metadata_path, owned_value_path},
21    schema::Definition,
22};
23use vector_vrl_functions::set_semantic_meaning::MeaningList;
24use vector_vrl_metrics::MetricsStorage;
25use vrl::{
26    compiler::{
27        CompileConfig, ExpressionError, Program, TypeState, VrlRuntime,
28        runtime::{Runtime, Terminate},
29        state::ExternalEnv,
30    },
31    diagnostic::{DiagnosticMessage, Note},
32    path,
33    path::ValuePath,
34    value::{Kind, Value},
35};
36
37use crate::{
38    Result,
39    config::{
40        ComponentKey, DataType, Input, OutputId, TransformConfig, TransformContext,
41        TransformOutput, log_schema,
42    },
43    event::{Event, TargetEvents, VrlTarget},
44    format_vrl_diagnostics,
45    internal_events::{RemapMappingAbort, RemapMappingError},
46    schema,
47    transforms::{SyncTransform, Transform, TransformOutputsBuf},
48};
49
50const DROPPED: &str = "dropped";
51type CacheKey = (TableRegistry, schema::Definition);
52type CacheValue = (Program, String, MeaningList);
53
54/// Configuration for the `remap` transform.
55#[configurable_component(transform(
56    "remap",
57    "Modify your observability data as it passes through your topology using Vector Remap Language (VRL)."
58))]
59#[derive(Derivative)]
60#[serde(deny_unknown_fields)]
61#[derivative(Default, Debug)]
62pub struct RemapConfig {
63    /// The [Vector Remap Language][vrl] (VRL) program to execute for each event.
64    ///
65    /// Required if `file` is missing.
66    ///
67    /// [vrl]: https://vector.dev/docs/reference/vrl
68    #[configurable(metadata(
69        docs::examples = ". = parse_json!(.message)\n.new_field = \"new value\"\n.status = to_int!(.status)\n.duration = parse_duration!(.duration, \"s\")\n.new_name = del(.old_name)",
70        docs::syntax_override = "remap_program"
71    ))]
72    pub source: Option<String>,
73
74    /// File path to the [Vector Remap Language][vrl] (VRL) program to execute for each event.
75    ///
76    /// If a relative path is provided, its root is the current working directory.
77    ///
78    /// Required if `source` is missing.
79    ///
80    /// [vrl]: https://vector.dev/docs/reference/vrl
81    #[configurable(metadata(docs::examples = "./my/program.vrl"))]
82    pub file: Option<PathBuf>,
83
84    /// File paths to the [Vector Remap Language][vrl] (VRL) programs to execute for each event.
85    ///
86    /// If a relative path is provided, its root is the current working directory.
87    ///
88    /// Required if `source` or `file` are missing.
89    ///
90    /// [vrl]: https://vector.dev/docs/reference/vrl
91    #[configurable(metadata(docs::examples = "['./my/program.vrl', './my/program2.vrl']"))]
92    pub files: Option<Vec<PathBuf>>,
93
94    /// When set to `single`, metric tag values are exposed as single strings, the
95    /// same as they were before this config option. Tags with multiple values show the last assigned value, and null values
96    /// are ignored.
97    ///
98    /// When set to `full`, all metric tags are exposed as arrays of either string or null
99    /// values.
100    #[serde(default)]
101    pub metric_tag_values: MetricTagValues,
102
103    /// The name of the timezone to apply to timestamp conversions that do not contain an explicit
104    /// time zone.
105    ///
106    /// This overrides the [global `timezone`][global_timezone] option. The time zone name may be
107    /// any name in the [TZ database][tz_database], or `local` to indicate system local time.
108    ///
109    /// [global_timezone]: https://vector.dev/docs/reference/configuration//global-options#timezone
110    /// [tz_database]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
111    #[serde(default)]
112    #[configurable(metadata(docs::advanced))]
113    pub timezone: Option<TimeZone>,
114
115    /// Drops any event that encounters an error during processing.
116    ///
117    /// Normally, if a VRL program encounters an error when processing an event, the original,
118    /// unmodified event is sent downstream. In some cases, you may not want to send the event
119    /// any further, such as if certain transformation or enrichment is strictly required. Setting
120    /// `drop_on_error` to `true` allows you to ensure these events do not get processed any
121    /// further.
122    ///
123    /// Additionally, dropped events can potentially be diverted to a specially named output for
124    /// further logging and analysis by setting `reroute_dropped`.
125    #[serde(default = "crate::serde::default_false")]
126    #[configurable(metadata(docs::human_name = "Drop Event on Error"))]
127    pub drop_on_error: bool,
128
129    /// Drops any event that is manually aborted during processing.
130    ///
131    /// If a VRL program is manually aborted (using [`abort`][vrl_docs_abort]) when
132    /// processing an event, this option controls whether the original, unmodified event is sent
133    /// downstream without any modifications or if it is dropped.
134    ///
135    /// Additionally, dropped events can potentially be diverted to a specially-named output for
136    /// further logging and analysis by setting `reroute_dropped`.
137    ///
138    /// [vrl_docs_abort]: https://vector.dev/docs/reference/vrl/expressions/#abort
139    #[serde(default = "crate::serde::default_true")]
140    #[configurable(metadata(docs::human_name = "Drop Event on Abort"))]
141    pub drop_on_abort: bool,
142
143    /// Reroutes dropped events to a named output instead of halting processing on them.
144    ///
145    /// When using `drop_on_error` or `drop_on_abort`, events that are "dropped" are processed no
146    /// further. In some cases, it may be desirable to keep the events around for further analysis,
147    /// debugging, or retrying.
148    ///
149    /// In these cases, `reroute_dropped` can be set to `true` which forwards the original event
150    /// to a specially-named output, `dropped`. The original event is annotated with additional
151    /// fields describing why the event was dropped.
152    #[serde(default = "crate::serde::default_false")]
153    #[configurable(metadata(docs::human_name = "Reroute Dropped Events"))]
154    pub reroute_dropped: bool,
155
156    #[configurable(derived, metadata(docs::hidden))]
157    #[serde(default)]
158    pub runtime: VrlRuntime,
159
160    #[configurable(derived, metadata(docs::hidden))]
161    #[serde(skip)]
162    #[derivative(Debug = "ignore")]
163    /// Cache can't be `BTreeMap` or `HashMap` because of `TableRegistry`, which doesn't allow us to inspect tables inside it.
164    /// And even if we allowed the inspection, the tables can be huge, resulting in a long comparison or hash computation
165    /// while using `Vec` allows us to use just a shallow comparison
166    pub cache: Mutex<Vec<(CacheKey, std::result::Result<CacheValue, String>)>>,
167}
168
169impl Clone for RemapConfig {
170    fn clone(&self) -> Self {
171        Self {
172            source: self.source.clone(),
173            file: self.file.clone(),
174            files: self.files.clone(),
175            metric_tag_values: self.metric_tag_values,
176            timezone: self.timezone,
177            drop_on_error: self.drop_on_error,
178            drop_on_abort: self.drop_on_abort,
179            reroute_dropped: self.reroute_dropped,
180            runtime: self.runtime,
181            cache: Mutex::new(Default::default()),
182        }
183    }
184}
185
186impl RemapConfig {
187    fn compile_vrl_program(
188        &self,
189        enrichment_tables: TableRegistry,
190        metrics_storage: MetricsStorage,
191        merged_schema_definition: schema::Definition,
192    ) -> Result<(Program, String, MeaningList)> {
193        if let Some((_, res)) = self
194            .cache
195            .lock()
196            .expect("Data poisoned")
197            .iter()
198            .find(|v| v.0.0 == enrichment_tables && v.0.1 == merged_schema_definition)
199        {
200            return res.clone().map_err(Into::into);
201        }
202
203        let source = match (&self.source, &self.file, &self.files) {
204            (Some(source), None, None) => source.to_owned(),
205            (None, Some(path), None) => Self::read_file(path)?,
206            (None, None, Some(paths)) => {
207                let mut combined_source = String::new();
208                for path in paths {
209                    let content = Self::read_file(path)?;
210                    combined_source.push_str(&content);
211                    combined_source.push('\n');
212                }
213                combined_source
214            }
215            _ => return Err(Box::new(BuildError::SourceAndOrFileOrFiles)),
216        };
217
218        let state = TypeState {
219            local: Default::default(),
220            external: ExternalEnv::new_with_kind(
221                merged_schema_definition.event_kind().clone(),
222                merged_schema_definition.metadata_kind().clone(),
223            ),
224        };
225        let mut config = CompileConfig::default();
226
227        config.set_custom(enrichment_tables.clone());
228        config.set_custom(metrics_storage);
229        config.set_custom(MeaningList::default());
230
231        let res = compile_vrl(&source, &vector_vrl_functions::all(), &state, config)
232            .map_err(|diagnostics| format_vrl_diagnostics(&source, diagnostics))
233            .map(|result| {
234                (
235                    result.program,
236                    format_vrl_diagnostics(&source, result.warnings),
237                    result.config.get_custom::<MeaningList>().unwrap().clone(),
238                )
239            });
240
241        self.cache
242            .lock()
243            .expect("Data poisoned")
244            .push(((enrichment_tables, merged_schema_definition), res.clone()));
245
246        res.map_err(Into::into)
247    }
248
249    fn read_file(path: &PathBuf) -> Result<String> {
250        let mut buffer = String::new();
251        File::open(path)
252            .with_context(|_| FileOpenFailedSnafu { path })?
253            .read_to_string(&mut buffer)
254            .with_context(|_| FileReadFailedSnafu { path })?;
255        Ok(buffer)
256    }
257}
258
259impl_generate_config_from_default!(RemapConfig);
260
261#[async_trait::async_trait]
262#[typetag::serde(name = "remap")]
263impl TransformConfig for RemapConfig {
264    async fn build(&self, context: &TransformContext) -> Result<Transform> {
265        let (transform, warnings) = match self.runtime {
266            VrlRuntime::Ast => {
267                let (remap, warnings) = Remap::new_ast(self.clone(), context)?;
268                (Transform::synchronous(remap), warnings)
269            }
270        };
271
272        // TODO: We could improve on this by adding support for non-fatal error
273        // messages in the topology. This would make the topology responsible
274        // for printing warnings (including potentially emitting metrics),
275        // instead of individual transforms.
276        if !warnings.is_empty() {
277            warn!(message = "VRL compilation warning.", %warnings);
278        }
279
280        Ok(transform)
281    }
282
283    fn validate_env(&self, context: &TransformContext) -> std::result::Result<(), Vec<String>> {
284        self.compile_vrl_program(
285            context.enrichment_tables.clone(),
286            context.metrics_storage.clone(),
287            context.merged_schema_definition.clone(),
288        )
289        .map(|_| ())
290        .map_err(|e| vec![e.to_string()])
291    }
292
293    fn input(&self) -> Input {
294        Input::all()
295    }
296
297    fn outputs(
298        &self,
299        context: &TransformContext,
300        input_definitions: &[(OutputId, schema::Definition)],
301    ) -> Vec<TransformOutput> {
302        let merged_definition: Definition = input_definitions
303            .iter()
304            .map(|(_output, definition)| definition.clone())
305            .reduce(Definition::merge)
306            .unwrap_or_else(Definition::any);
307
308        // We need to compile the VRL program in order to know the schema definition output of this
309        // transform. We ignore any compilation errors, as those are caught by the transform build
310        // step.
311        let compiled = self
312            .compile_vrl_program(
313                context.enrichment_tables.clone(),
314                context.metrics_storage.clone(),
315                merged_definition,
316            )
317            .map(|(program, _, meaning_list)| (program.final_type_info().state, meaning_list.0))
318            .map_err(|_| ());
319
320        let mut dropped_definitions = HashMap::new();
321        let mut default_definitions = HashMap::new();
322
323        for (output_id, input_definition) in input_definitions {
324            let default_definition = compiled
325                .clone()
326                .map(|(state, meaning)| {
327                    let mut new_type_def = Definition::new(
328                        state.external.target_kind().clone(),
329                        state.external.metadata_kind().clone(),
330                        input_definition.log_namespaces().clone(),
331                    );
332
333                    for (id, path) in input_definition.meanings() {
334                        // Attempt to copy over the meanings from the input definition.
335                        // The function will fail if the meaning that now points to a field that no longer exists,
336                        // this is fine since we will no longer want that meaning in the output definition.
337                        new_type_def.try_with_meaning(path.clone(), id).ok();
338                    }
339
340                    // Apply any semantic meanings set in the VRL program
341                    for (id, path) in meaning {
342                        // currently only event paths are supported
343                        new_type_def = new_type_def.with_meaning(path, &id);
344                    }
345                    new_type_def
346                })
347                .unwrap_or_else(|_| {
348                    Definition::new_with_default_metadata(
349                        // The program failed to compile, so it can "never" return a value
350                        Kind::never(),
351                        input_definition.log_namespaces().clone(),
352                    )
353                });
354
355            // When a message is dropped and re-routed, we keep the original event, but also annotate
356            // it with additional metadata.
357            let dropped_definition = Definition::combine_log_namespaces(
358                input_definition.log_namespaces(),
359                input_definition.clone().with_event_field(
360                    log_schema().metadata_key().expect("valid metadata key"),
361                    Kind::object(BTreeMap::from([
362                        ("reason".into(), Kind::bytes()),
363                        ("message".into(), Kind::bytes()),
364                        ("component_id".into(), Kind::bytes()),
365                        ("component_type".into(), Kind::bytes()),
366                        ("component_kind".into(), Kind::bytes()),
367                    ])),
368                    Some("metadata"),
369                ),
370                input_definition
371                    .clone()
372                    .with_metadata_field(&owned_value_path!("reason"), Kind::bytes(), None)
373                    .with_metadata_field(&owned_value_path!("message"), Kind::bytes(), None)
374                    .with_metadata_field(&owned_value_path!("component_id"), Kind::bytes(), None)
375                    .with_metadata_field(&owned_value_path!("component_type"), Kind::bytes(), None)
376                    .with_metadata_field(&owned_value_path!("component_kind"), Kind::bytes(), None),
377            );
378
379            default_definitions.insert(
380                output_id.clone(),
381                VrlTarget::modify_schema_definition_for_into_events(default_definition),
382            );
383            dropped_definitions.insert(
384                output_id.clone(),
385                VrlTarget::modify_schema_definition_for_into_events(dropped_definition),
386            );
387        }
388
389        let default_output = TransformOutput::new(DataType::all_bits(), default_definitions);
390
391        if self.reroute_dropped {
392            vec![
393                default_output,
394                TransformOutput::new(DataType::all_bits(), dropped_definitions).with_port(DROPPED),
395            ]
396        } else {
397            vec![default_output]
398        }
399    }
400
401    fn enable_concurrency(&self) -> bool {
402        true
403    }
404
405    fn files_to_watch(&self) -> Vec<&PathBuf> {
406        self.file
407            .iter()
408            .chain(self.files.iter().flatten())
409            .collect()
410    }
411}
412
413#[derive(Debug, Clone)]
414pub struct Remap<Runner>
415where
416    Runner: VrlRunner,
417{
418    component_key: Option<ComponentKey>,
419    program: Program,
420    timezone: TimeZone,
421    drop_on_error: bool,
422    drop_on_abort: bool,
423    reroute_dropped: bool,
424    runner: Runner,
425    metric_tag_values: MetricTagValues,
426}
427
428pub trait VrlRunner {
429    fn run(
430        &mut self,
431        target: &mut VrlTarget,
432        program: &Program,
433        timezone: &TimeZone,
434    ) -> std::result::Result<Value, Terminate>;
435}
436
437#[derive(Debug)]
438pub struct AstRunner {
439    pub runtime: Runtime,
440}
441
442impl Clone for AstRunner {
443    fn clone(&self) -> Self {
444        Self {
445            runtime: Runtime::default(),
446        }
447    }
448}
449
450impl VrlRunner for AstRunner {
451    fn run(
452        &mut self,
453        target: &mut VrlTarget,
454        program: &Program,
455        timezone: &TimeZone,
456    ) -> std::result::Result<Value, Terminate> {
457        let result = self.runtime.resolve(target, program, timezone);
458        self.runtime.clear();
459        result
460    }
461}
462
463impl Remap<AstRunner> {
464    pub fn new_ast(
465        config: RemapConfig,
466        context: &TransformContext,
467    ) -> crate::Result<(Self, String)> {
468        let (program, warnings, _) = config.compile_vrl_program(
469            context.enrichment_tables.clone(),
470            context.metrics_storage.clone(),
471            context.merged_schema_definition.clone(),
472        )?;
473
474        let runtime = Runtime::default();
475        let runner = AstRunner { runtime };
476
477        Self::new(config, context, program, runner).map(|remap| (remap, warnings))
478    }
479}
480
481impl<Runner> Remap<Runner>
482where
483    Runner: VrlRunner,
484{
485    fn new(
486        config: RemapConfig,
487        context: &TransformContext,
488        program: Program,
489        runner: Runner,
490    ) -> crate::Result<Self> {
491        Ok(Remap {
492            component_key: context.key.clone(),
493            program,
494            timezone: config
495                .timezone
496                .unwrap_or_else(|| context.globals.timezone()),
497            drop_on_error: config.drop_on_error,
498            drop_on_abort: config.drop_on_abort,
499            reroute_dropped: config.reroute_dropped,
500            runner,
501            metric_tag_values: config.metric_tag_values,
502        })
503    }
504
505    #[cfg(test)]
506    const fn runner(&self) -> &Runner {
507        &self.runner
508    }
509
510    fn dropped_data(&self, reason: &str, error: ExpressionError) -> serde_json::Value {
511        let message = error
512            .notes()
513            .iter()
514            .rfind(|note| matches!(note, Note::UserErrorMessage(_)))
515            .map(|note| note.to_string())
516            .unwrap_or_else(|| error.to_string());
517        serde_json::json!({
518                "reason": reason,
519                "message": message,
520                "component_id": self.component_key,
521                "component_type": "remap",
522                "component_kind": "transform",
523        })
524    }
525
526    fn annotate_dropped(&self, event: &mut Event, reason: &str, error: ExpressionError) {
527        match event {
528            Event::Log(log) => match log.namespace() {
529                LogNamespace::Legacy => {
530                    if let Some(metadata_key) = log_schema().metadata_key() {
531                        log.insert(
532                            (PathPrefix::Event, metadata_key.concat(path!("dropped"))),
533                            self.dropped_data(reason, error),
534                        );
535                    }
536                }
537                LogNamespace::Vector => {
538                    log.insert(
539                        metadata_path!("vector", "dropped"),
540                        self.dropped_data(reason, error),
541                    );
542                }
543            },
544            Event::Metric(metric) => {
545                if let Some(metadata_key) = log_schema().metadata_key() {
546                    metric.replace_tag(format!("{metadata_key}.dropped.reason"), reason.into());
547                    metric.replace_tag(
548                        format!("{metadata_key}.dropped.component_id"),
549                        self.component_key
550                            .as_ref()
551                            .map(ToString::to_string)
552                            .unwrap_or_default(),
553                    );
554                    metric.replace_tag(
555                        format!("{metadata_key}.dropped.component_type"),
556                        "remap".into(),
557                    );
558                    metric.replace_tag(
559                        format!("{metadata_key}.dropped.component_kind"),
560                        "transform".into(),
561                    );
562                }
563            }
564            Event::Trace(trace) => {
565                trace.maybe_insert(log_schema().metadata_key_target_path(), || {
566                    self.dropped_data(reason, error).into()
567                });
568            }
569        }
570    }
571
572    fn run_vrl(&mut self, target: &mut VrlTarget) -> std::result::Result<Value, Terminate> {
573        self.runner.run(target, &self.program, &self.timezone)
574    }
575}
576
577impl<Runner> SyncTransform for Remap<Runner>
578where
579    Runner: VrlRunner + Clone + Send + Sync,
580{
581    fn transform(&mut self, event: Event, output: &mut TransformOutputsBuf) {
582        // If a program can fail or abort at runtime and we know that we will still need to forward
583        // the event in that case (either to the main output or `dropped`, depending on the
584        // config), we need to clone the original event and keep it around, to allow us to discard
585        // any mutations made to the event while the VRL program runs, before it failed or aborted.
586        //
587        // The `drop_on_{error, abort}` transform config allows operators to remove events from the
588        // main output if they're failed or aborted, in which case we can skip the cloning, since
589        // any mutations made by VRL will be ignored regardless. If they have configured
590        // `reroute_dropped`, however, we still need to do the clone to ensure that we can forward
591        // the event to the `dropped` output.
592        let forward_on_error = !self.drop_on_error || self.reroute_dropped;
593        let forward_on_abort = !self.drop_on_abort || self.reroute_dropped;
594        let original_event = if (self.program.info().fallible && forward_on_error)
595            || (self.program.info().abortable && forward_on_abort)
596        {
597            Some(event.clone())
598        } else {
599            None
600        };
601
602        let log_namespace = event
603            .maybe_as_log()
604            .map(|log| log.namespace())
605            .unwrap_or(LogNamespace::Legacy);
606
607        let mut target = VrlTarget::new(
608            event,
609            self.program.info(),
610            match self.metric_tag_values {
611                MetricTagValues::Single => false,
612                MetricTagValues::Full => true,
613            },
614        );
615        let result = self.run_vrl(&mut target);
616
617        match result {
618            Ok(_) => match target.into_events(log_namespace) {
619                TargetEvents::One(event) => push_default(event, output),
620                TargetEvents::Logs(events) => events.for_each(|event| push_default(event, output)),
621                TargetEvents::Traces(events) => {
622                    events.for_each(|event| push_default(event, output))
623                }
624            },
625            Err(reason) => {
626                let (reason, error, drop) = match reason {
627                    Terminate::Abort(error) => {
628                        if !self.reroute_dropped {
629                            emit!(RemapMappingAbort {
630                                event_dropped: self.drop_on_abort,
631                            });
632                        }
633                        ("abort", error, self.drop_on_abort)
634                    }
635                    Terminate::Error(error) => {
636                        if !self.reroute_dropped {
637                            emit!(RemapMappingError {
638                                error: error.to_string(),
639                                event_dropped: self.drop_on_error,
640                            });
641                        }
642                        ("error", error, self.drop_on_error)
643                    }
644                };
645
646                if !drop {
647                    let event = original_event.expect("event will be set");
648
649                    push_default(event, output);
650                } else if self.reroute_dropped {
651                    let mut event = original_event.expect("event will be set");
652
653                    self.annotate_dropped(&mut event, reason, error);
654                    push_dropped(event, output);
655                }
656            }
657        }
658    }
659}
660
661#[inline]
662fn push_default(event: Event, output: &mut TransformOutputsBuf) {
663    output.push(None, event)
664}
665
666#[inline]
667fn push_dropped(event: Event, output: &mut TransformOutputsBuf) {
668    output.push(Some(DROPPED), event);
669}
670
671#[derive(Debug, Snafu)]
672pub enum BuildError {
673    #[snafu(display("must provide exactly one of `source` or `file` or `files` configuration"))]
674    SourceAndOrFileOrFiles,
675
676    #[snafu(display("Could not open vrl program {:?}: {}", path, source))]
677    FileOpenFailed { path: PathBuf, source: io::Error },
678    #[snafu(display("Could not read vrl program {:?}: {}", path, source))]
679    FileReadFailed { path: PathBuf, source: io::Error },
680}
681
682#[cfg(test)]
683mod tests {
684    use std::{
685        collections::{HashMap, HashSet},
686        sync::Arc,
687    };
688
689    use chrono::DateTime;
690    use indoc::{formatdoc, indoc};
691    use tokio::sync::mpsc;
692    use tokio_stream::wrappers::ReceiverStream;
693    use vector_lib::{config::GlobalOptions, event::EventMetadata, metric_tags};
694    use vrl::{btreemap, event_path, value::kind::Collection};
695
696    use super::*;
697    use crate::{
698        config::{ConfigBuilder, build_unit_tests},
699        event::{
700            LogEvent, Metric, Value,
701            metric::{MetricKind, MetricValue},
702        },
703        metrics::Controller,
704        schema,
705        test_util::components::{
706            COMPONENT_MULTIPLE_OUTPUTS_TESTS, assert_transform_compliance, init_test,
707        },
708        transforms::{OutputBuffer, test::create_topology},
709    };
710
711    fn test_default_schema_definition() -> schema::Definition {
712        schema::Definition::empty_legacy_namespace().with_event_field(
713            &owned_value_path!("a default field"),
714            Kind::integer().or_bytes(),
715            Some("default"),
716        )
717    }
718
719    fn test_dropped_schema_definition() -> schema::Definition {
720        schema::Definition::empty_legacy_namespace().with_event_field(
721            &owned_value_path!("a dropped field"),
722            Kind::boolean().or_null(),
723            Some("dropped"),
724        )
725    }
726
727    fn remap(config: RemapConfig) -> Result<Remap<AstRunner>> {
728        let schema_definitions = HashMap::from([
729            (
730                None,
731                [("source".into(), test_default_schema_definition())].into(),
732            ),
733            (
734                Some(DROPPED.to_owned()),
735                [("source".into(), test_dropped_schema_definition())].into(),
736            ),
737        ]);
738
739        Remap::new_ast(config, &TransformContext::new_test(schema_definitions))
740            .map(|(remap, _)| remap)
741    }
742
743    #[test]
744    fn generate_config() {
745        crate::test_util::test_generate_config::<RemapConfig>();
746    }
747
748    #[test]
749    fn config_missing_source_and_file() {
750        let config = RemapConfig {
751            source: None,
752            file: None,
753            ..Default::default()
754        };
755
756        let err = remap(config).unwrap_err().to_string();
757        assert_eq!(
758            &err,
759            "must provide exactly one of `source` or `file` or `files` configuration"
760        )
761    }
762
763    #[test]
764    fn config_both_source_and_file() {
765        let config = RemapConfig {
766            source: Some("".to_owned()),
767            file: Some("".into()),
768            ..Default::default()
769        };
770
771        let err = remap(config).unwrap_err().to_string();
772        assert_eq!(
773            &err,
774            "must provide exactly one of `source` or `file` or `files` configuration"
775        )
776    }
777
778    fn get_field_string(event: &Event, field: &str) -> String {
779        event
780            .as_log()
781            .get(&vrl::path::parse_target_path(field).unwrap())
782            .unwrap()
783            .to_string_lossy()
784            .into_owned()
785    }
786
787    #[test]
788    fn check_remap_doesnt_share_state_between_events() {
789        let conf = RemapConfig {
790            source: Some(".foo = .sentinel".to_string()),
791            file: None,
792            drop_on_error: true,
793            drop_on_abort: false,
794            ..Default::default()
795        };
796        let mut tform = remap(conf).unwrap();
797        assert!(tform.runner().runtime.is_empty());
798
799        let event1 = {
800            let mut event1 = LogEvent::from("event1");
801            event1.insert(vrl::event_path!("sentinel"), "bar");
802            Event::from(event1)
803        };
804        let result1 = transform_one(&mut tform, event1).unwrap();
805        assert_eq!(get_field_string(&result1, "message"), "event1");
806        assert_eq!(get_field_string(&result1, "foo"), "bar");
807        assert!(tform.runner().runtime.is_empty());
808
809        let event2 = {
810            let event2 = LogEvent::from("event2");
811            Event::from(event2)
812        };
813        let result2 = transform_one(&mut tform, event2).unwrap();
814        assert_eq!(get_field_string(&result2, "message"), "event2");
815        assert_eq!(result2.as_log().get(event_path!("foo")), Some(&Value::Null));
816        assert!(tform.runner().runtime.is_empty());
817    }
818
819    #[test]
820    fn remap_return_raw_string_vector_namespace() {
821        let initial_definition = Definition::default_for_namespace(&[LogNamespace::Vector].into());
822
823        let event = {
824            let mut metadata = EventMetadata::default()
825                .with_schema_definition(&Arc::new(initial_definition.clone()));
826            // the Vector metadata field is required for an event to correctly detect the namespace at runtime
827            metadata
828                .value_mut()
829                .insert(&owned_value_path!("vector"), BTreeMap::new());
830
831            let mut event = LogEvent::new_with_metadata(metadata);
832            event.insert(event_path!("copy_from"), "buz");
833            Event::from(event)
834        };
835
836        let conf = RemapConfig {
837            source: Some(r#"  . = "root string";"#.to_string()),
838            file: None,
839            drop_on_error: true,
840            drop_on_abort: false,
841            ..Default::default()
842        };
843        let mut tform = remap(conf.clone()).unwrap();
844        let result = transform_one(&mut tform, event).unwrap();
845        assert_eq!(get_field_string(&result, "."), "root string");
846
847        let mut outputs = conf.outputs(
848            &Default::default(),
849            &[(OutputId::dummy(), initial_definition)],
850        );
851
852        assert_eq!(outputs.len(), 1);
853        let output = outputs.pop().unwrap();
854        assert_eq!(output.port, None);
855        let actual_schema_def = output.schema_definitions(true)[&OutputId::dummy()].clone();
856        let expected_schema =
857            Definition::new(Kind::bytes(), Kind::any_object(), [LogNamespace::Vector]);
858        assert_eq!(actual_schema_def, expected_schema);
859    }
860
861    #[test]
862    fn check_remap_adds() {
863        let event = {
864            let mut event = LogEvent::from("augment me");
865            event.insert(event_path!("copy_from"), "buz");
866            Event::from(event)
867        };
868
869        let conf = RemapConfig {
870            source: Some(
871                r#"  .foo = "bar"
872  .bar = "baz"
873  .copy = .copy_from
874"#
875                .to_string(),
876            ),
877            file: None,
878            drop_on_error: true,
879            drop_on_abort: false,
880            ..Default::default()
881        };
882        let mut tform = remap(conf).unwrap();
883        let result = transform_one(&mut tform, event).unwrap();
884        assert_eq!(get_field_string(&result, "message"), "augment me");
885        assert_eq!(get_field_string(&result, "copy_from"), "buz");
886        assert_eq!(get_field_string(&result, "foo"), "bar");
887        assert_eq!(get_field_string(&result, "bar"), "baz");
888        assert_eq!(get_field_string(&result, "copy"), "buz");
889    }
890
891    #[test]
892    fn check_remap_emits_multiple() {
893        let event = {
894            let mut event = LogEvent::from("augment me");
895            event.insert(
896                event_path!("events"),
897                vec![btreemap!("message" => "foo"), btreemap!("message" => "bar")],
898            );
899            Event::from(event)
900        };
901
902        let conf = RemapConfig {
903            source: Some(
904                indoc! {r"
905                . = .events
906            "}
907                .to_owned(),
908            ),
909            file: None,
910            drop_on_error: true,
911            drop_on_abort: false,
912            ..Default::default()
913        };
914        let mut tform = remap(conf).unwrap();
915
916        let out = collect_outputs(&mut tform, event);
917        assert_eq!(2, out.primary.len());
918        let mut result = out.primary.into_events();
919
920        let r = result.next().unwrap();
921        assert_eq!(get_field_string(&r, "message"), "foo");
922        let r = result.next().unwrap();
923        assert_eq!(get_field_string(&r, "message"), "bar");
924    }
925
926    #[test]
927    fn check_remap_error() {
928        let event = {
929            let mut event = Event::Log(LogEvent::from("augment me"));
930            event.as_mut_log().insert(event_path!("bar"), "is a string");
931            event
932        };
933
934        let conf = RemapConfig {
935            source: Some(formatdoc! {r#"
936                .foo = "foo"
937                .not_an_int = int!(.bar)
938                .baz = 12
939            "#}),
940            file: None,
941            drop_on_error: false,
942            drop_on_abort: false,
943            ..Default::default()
944        };
945        let mut tform = remap(conf).unwrap();
946
947        let event = transform_one(&mut tform, event).unwrap();
948
949        assert_eq!(
950            event.as_log().get(event_path!("bar")),
951            Some(&Value::from("is a string"))
952        );
953        assert!(event.as_log().get(event_path!("foo")).is_none());
954        assert!(event.as_log().get(event_path!("baz")).is_none());
955    }
956
957    #[test]
958    fn check_remap_error_drop() {
959        let event = {
960            let mut event = Event::Log(LogEvent::from("augment me"));
961            event.as_mut_log().insert(event_path!("bar"), "is a string");
962            event
963        };
964
965        let conf = RemapConfig {
966            source: Some(formatdoc! {r#"
967                .foo = "foo"
968                .not_an_int = int!(.bar)
969                .baz = 12
970            "#}),
971            file: None,
972            drop_on_error: true,
973            drop_on_abort: false,
974            ..Default::default()
975        };
976        let mut tform = remap(conf).unwrap();
977
978        assert!(transform_one(&mut tform, event).is_none())
979    }
980
981    #[test]
982    fn check_remap_error_infallible() {
983        let event = {
984            let mut event = Event::Log(LogEvent::from("augment me"));
985            event.as_mut_log().insert(event_path!("bar"), "is a string");
986            event
987        };
988
989        let conf = RemapConfig {
990            source: Some(formatdoc! {r#"
991                .foo = "foo"
992                .baz = 12
993            "#}),
994            file: None,
995            drop_on_error: false,
996            drop_on_abort: false,
997            ..Default::default()
998        };
999        let mut tform = remap(conf).unwrap();
1000
1001        let event = transform_one(&mut tform, event).unwrap();
1002
1003        assert_eq!(
1004            event.as_log().get(event_path!("foo")),
1005            Some(&Value::from("foo"))
1006        );
1007        assert_eq!(
1008            event.as_log().get(event_path!("bar")),
1009            Some(&Value::from("is a string"))
1010        );
1011        assert_eq!(
1012            event.as_log().get(event_path!("baz")),
1013            Some(&Value::from(12))
1014        );
1015    }
1016
1017    #[test]
1018    fn check_remap_abort() {
1019        let event = {
1020            let mut event = Event::Log(LogEvent::from("augment me"));
1021            event.as_mut_log().insert(event_path!("bar"), "is a string");
1022            event
1023        };
1024
1025        let conf = RemapConfig {
1026            source: Some(formatdoc! {r#"
1027                .foo = "foo"
1028                abort
1029                .baz = 12
1030            "#}),
1031            file: None,
1032            drop_on_error: false,
1033            drop_on_abort: false,
1034            ..Default::default()
1035        };
1036        let mut tform = remap(conf).unwrap();
1037
1038        let event = transform_one(&mut tform, event).unwrap();
1039
1040        assert_eq!(
1041            event.as_log().get(event_path!("bar")),
1042            Some(&Value::from("is a string"))
1043        );
1044        assert!(event.as_log().get(event_path!("foo")).is_none());
1045        assert!(event.as_log().get(event_path!("baz")).is_none());
1046    }
1047
1048    #[test]
1049    fn check_remap_abort_drop() {
1050        let event = {
1051            let mut event = Event::Log(LogEvent::from("augment me"));
1052            event.as_mut_log().insert(event_path!("bar"), "is a string");
1053            event
1054        };
1055
1056        let conf = RemapConfig {
1057            source: Some(formatdoc! {r#"
1058                .foo = "foo"
1059                abort
1060                .baz = 12
1061            "#}),
1062            file: None,
1063            drop_on_error: false,
1064            drop_on_abort: true,
1065            ..Default::default()
1066        };
1067        let mut tform = remap(conf).unwrap();
1068
1069        assert!(transform_one(&mut tform, event).is_none())
1070    }
1071
1072    #[test]
1073    fn check_remap_metric() {
1074        let metric = Event::Metric(Metric::new(
1075            "counter",
1076            MetricKind::Absolute,
1077            MetricValue::Counter { value: 1.0 },
1078        ));
1079        let metadata = metric.metadata().clone();
1080
1081        let conf = RemapConfig {
1082            source: Some(
1083                r#".tags.host = "zoobub"
1084                       .name = "zork"
1085                       .namespace = "zerk"
1086                       .kind = "incremental""#
1087                    .to_string(),
1088            ),
1089            file: None,
1090            drop_on_error: true,
1091            drop_on_abort: false,
1092            ..Default::default()
1093        };
1094        let mut tform = remap(conf).unwrap();
1095
1096        let result = transform_one(&mut tform, metric).unwrap();
1097        assert_eq!(
1098            result,
1099            Event::Metric(
1100                Metric::new_with_metadata(
1101                    "zork",
1102                    MetricKind::Incremental,
1103                    MetricValue::Counter { value: 1.0 },
1104                    // The schema definition is set in the topology, which isn't used in this test. Setting the definition
1105                    // to the actual value to skip the assertion here
1106                    metadata
1107                )
1108                .with_namespace(Some("zerk"))
1109                .with_tags(Some(metric_tags! {
1110                    "host" => "zoobub",
1111                }))
1112            )
1113        );
1114    }
1115
1116    #[test]
1117    fn remap_timezone_fallback() {
1118        let error = Event::from_json_value(
1119            serde_json::json!({"timestamp": "2022-12-27 00:00:00"}),
1120            LogNamespace::Legacy,
1121        )
1122        .unwrap();
1123        let conf = RemapConfig {
1124            source: Some(formatdoc! {r#"
1125                .timestamp = parse_timestamp!(.timestamp, format: "%Y-%m-%d %H:%M:%S")
1126            "#}),
1127            drop_on_error: true,
1128            drop_on_abort: true,
1129            reroute_dropped: true,
1130            ..Default::default()
1131        };
1132        let context = TransformContext {
1133            key: Some(ComponentKey::from("remapper")),
1134            globals: GlobalOptions {
1135                timezone: Some(TimeZone::parse("America/Los_Angeles").unwrap()),
1136                ..Default::default()
1137            },
1138            ..Default::default()
1139        };
1140        let mut tform = Remap::new_ast(conf, &context).unwrap().0;
1141
1142        let output = transform_one_fallible(&mut tform, error).unwrap();
1143        let log = output.as_log();
1144        assert_eq!(
1145            log["timestamp"],
1146            DateTime::<chrono::Utc>::from(
1147                DateTime::parse_from_rfc3339("2022-12-27T00:00:00-08:00").unwrap()
1148            )
1149            .into()
1150        );
1151    }
1152
1153    #[test]
1154    fn remap_timezone_override() {
1155        let error = Event::from_json_value(
1156            serde_json::json!({"timestamp": "2022-12-27 00:00:00"}),
1157            LogNamespace::Legacy,
1158        )
1159        .unwrap();
1160        let conf = RemapConfig {
1161            source: Some(formatdoc! {r#"
1162                .timestamp = parse_timestamp!(.timestamp, format: "%Y-%m-%d %H:%M:%S")
1163            "#}),
1164            drop_on_error: true,
1165            drop_on_abort: true,
1166            reroute_dropped: true,
1167            timezone: Some(TimeZone::parse("America/Los_Angeles").unwrap()),
1168            ..Default::default()
1169        };
1170        let context = TransformContext {
1171            key: Some(ComponentKey::from("remapper")),
1172            globals: GlobalOptions {
1173                timezone: Some(TimeZone::parse("Etc/UTC").unwrap()),
1174                ..Default::default()
1175            },
1176            ..Default::default()
1177        };
1178        let mut tform = Remap::new_ast(conf, &context).unwrap().0;
1179
1180        let output = transform_one_fallible(&mut tform, error).unwrap();
1181        let log = output.as_log();
1182        assert_eq!(
1183            log["timestamp"],
1184            DateTime::<chrono::Utc>::from(
1185                DateTime::parse_from_rfc3339("2022-12-27T00:00:00-08:00").unwrap()
1186            )
1187            .into()
1188        );
1189    }
1190
1191    #[test]
1192    fn check_remap_branching() {
1193        let happy =
1194            Event::from_json_value(serde_json::json!({"hello": "world"}), LogNamespace::Legacy)
1195                .unwrap();
1196        let abort = Event::from_json_value(
1197            serde_json::json!({"hello": "goodbye"}),
1198            LogNamespace::Legacy,
1199        )
1200        .unwrap();
1201        let error =
1202            Event::from_json_value(serde_json::json!({"hello": 42}), LogNamespace::Legacy).unwrap();
1203
1204        let happy_metric = {
1205            let mut metric = Metric::new(
1206                "counter",
1207                MetricKind::Absolute,
1208                MetricValue::Counter { value: 1.0 },
1209            );
1210            metric.replace_tag("hello".into(), "world".into());
1211            Event::Metric(metric)
1212        };
1213
1214        let abort_metric = {
1215            let mut metric = Metric::new(
1216                "counter",
1217                MetricKind::Absolute,
1218                MetricValue::Counter { value: 1.0 },
1219            );
1220            metric.replace_tag("hello".into(), "goodbye".into());
1221            Event::Metric(metric)
1222        };
1223
1224        let error_metric = {
1225            let mut metric = Metric::new(
1226                "counter",
1227                MetricKind::Absolute,
1228                MetricValue::Counter { value: 1.0 },
1229            );
1230            metric.replace_tag("not_hello".into(), "oops".into());
1231            Event::Metric(metric)
1232        };
1233
1234        let conf = RemapConfig {
1235            source: Some(formatdoc! {r#"
1236                if exists(.tags) {{
1237                    # metrics
1238                    .tags.foo = "bar"
1239                    if string!(.tags.hello) == "goodbye" {{
1240                      abort
1241                    }}
1242                }} else {{
1243                    # logs
1244                    .foo = "bar"
1245                    if string(.hello) == "goodbye" {{
1246                      abort
1247                    }}
1248                }}
1249            "#}),
1250            drop_on_error: true,
1251            drop_on_abort: true,
1252            reroute_dropped: true,
1253            ..Default::default()
1254        };
1255        let schema_definitions = HashMap::from([
1256            (
1257                None,
1258                [("source".into(), test_default_schema_definition())].into(),
1259            ),
1260            (
1261                Some(DROPPED.to_owned()),
1262                [("source".into(), test_dropped_schema_definition())].into(),
1263            ),
1264        ]);
1265        let context = TransformContext {
1266            key: Some(ComponentKey::from("remapper")),
1267            schema_definitions,
1268            merged_schema_definition: schema::Definition::new_with_default_metadata(
1269                Kind::any_object(),
1270                [LogNamespace::Legacy],
1271            )
1272            .with_event_field(&owned_value_path!("hello"), Kind::bytes(), None),
1273            ..Default::default()
1274        };
1275        let mut tform = Remap::new_ast(conf, &context).unwrap().0;
1276
1277        let output = transform_one_fallible(&mut tform, happy).unwrap();
1278        let log = output.as_log();
1279        assert_eq!(log["hello"], "world".into());
1280        assert_eq!(log["foo"], "bar".into());
1281        assert!(!log.contains(event_path!("metadata")));
1282
1283        let output = transform_one_fallible(&mut tform, abort).unwrap_err();
1284        let log = output.as_log();
1285        assert_eq!(log["hello"], "goodbye".into());
1286        assert!(!log.contains(event_path!("foo")));
1287        assert_eq!(
1288            log["metadata"],
1289            serde_json::json!({
1290                "dropped": {
1291                    "reason": "abort",
1292                    "message": "aborted",
1293                    "component_id": "remapper",
1294                    "component_type": "remap",
1295                    "component_kind": "transform",
1296                }
1297            })
1298            .try_into()
1299            .unwrap()
1300        );
1301
1302        let output = transform_one_fallible(&mut tform, error).unwrap_err();
1303        let log = output.as_log();
1304        assert_eq!(log["hello"], 42.into());
1305        assert!(!log.contains(event_path!("foo")));
1306        assert_eq!(
1307            log["metadata"],
1308            serde_json::json!({
1309                "dropped": {
1310                    "reason": "error",
1311                    "message": "function call error for \"string\" at (160:174): expected string, got integer",
1312                    "component_id": "remapper",
1313                    "component_type": "remap",
1314                    "component_kind": "transform",
1315                }
1316            })
1317            .try_into()
1318            .unwrap()
1319        );
1320
1321        let output = transform_one_fallible(&mut tform, happy_metric).unwrap();
1322        similar_asserts::assert_eq!(
1323            output,
1324            Event::Metric(
1325                Metric::new_with_metadata(
1326                    "counter",
1327                    MetricKind::Absolute,
1328                    MetricValue::Counter { value: 1.0 },
1329                    // The schema definition is set in the topology, which isn't used in this test. Setting the definition
1330                    // to the actual value to skip the assertion here
1331                    EventMetadata::default()
1332                        .with_schema_definition(output.metadata().schema_definition()),
1333                )
1334                .with_tags(Some(metric_tags! {
1335                    "hello" => "world",
1336                    "foo" => "bar",
1337                }))
1338            )
1339        );
1340
1341        let output = transform_one_fallible(&mut tform, abort_metric).unwrap_err();
1342        similar_asserts::assert_eq!(
1343            output,
1344            Event::Metric(
1345                Metric::new_with_metadata(
1346                    "counter",
1347                    MetricKind::Absolute,
1348                    MetricValue::Counter { value: 1.0 },
1349                    // The schema definition is set in the topology, which isn't used in this test. Setting the definition
1350                    // to the actual value to skip the assertion here
1351                    EventMetadata::default()
1352                        .with_schema_definition(output.metadata().schema_definition()),
1353                )
1354                .with_tags(Some(metric_tags! {
1355                    "hello" => "goodbye",
1356                    "metadata.dropped.reason" => "abort",
1357                    "metadata.dropped.component_id" => "remapper",
1358                    "metadata.dropped.component_type" => "remap",
1359                    "metadata.dropped.component_kind" => "transform",
1360                }))
1361            )
1362        );
1363
1364        let output = transform_one_fallible(&mut tform, error_metric).unwrap_err();
1365        similar_asserts::assert_eq!(
1366            output,
1367            Event::Metric(
1368                Metric::new_with_metadata(
1369                    "counter",
1370                    MetricKind::Absolute,
1371                    MetricValue::Counter { value: 1.0 },
1372                    // The schema definition is set in the topology, which isn't used in this test. Setting the definition
1373                    // to the actual value to skip the assertion here
1374                    EventMetadata::default()
1375                        .with_schema_definition(output.metadata().schema_definition()),
1376                )
1377                .with_tags(Some(metric_tags! {
1378                    "not_hello" => "oops",
1379                    "metadata.dropped.reason" => "error",
1380                    "metadata.dropped.component_id" => "remapper",
1381                    "metadata.dropped.component_type" => "remap",
1382                    "metadata.dropped.component_kind" => "transform",
1383                }))
1384            )
1385        );
1386    }
1387
1388    #[test]
1389    fn check_remap_branching_assert_with_message() {
1390        let error_trigger_assert_custom_message =
1391            Event::from_json_value(serde_json::json!({"hello": 42}), LogNamespace::Legacy).unwrap();
1392        let error_trigger_default_assert_message =
1393            Event::from_json_value(serde_json::json!({"hello": 0}), LogNamespace::Legacy).unwrap();
1394        let conf = RemapConfig {
1395            source: Some(formatdoc! {r#"
1396                assert_eq!(.hello, 0, "custom message here")
1397                assert_eq!(.hello, 1)
1398            "#}),
1399            drop_on_error: true,
1400            drop_on_abort: true,
1401            reroute_dropped: true,
1402            ..Default::default()
1403        };
1404        let context = TransformContext {
1405            key: Some(ComponentKey::from("remapper")),
1406            ..Default::default()
1407        };
1408        let mut tform = Remap::new_ast(conf, &context).unwrap().0;
1409
1410        let output =
1411            transform_one_fallible(&mut tform, error_trigger_assert_custom_message).unwrap_err();
1412        let log = output.as_log();
1413        assert_eq!(log["hello"], 42.into());
1414        assert!(!log.contains(event_path!("foo")));
1415        assert_eq!(
1416            log["metadata"],
1417            serde_json::json!({
1418                "dropped": {
1419                    "reason": "error",
1420                    "message": "custom message here",
1421                    "component_id": "remapper",
1422                    "component_type": "remap",
1423                    "component_kind": "transform",
1424                }
1425            })
1426            .try_into()
1427            .unwrap()
1428        );
1429
1430        let output =
1431            transform_one_fallible(&mut tform, error_trigger_default_assert_message).unwrap_err();
1432        let log = output.as_log();
1433        assert_eq!(log["hello"], 0.into());
1434        assert!(!log.contains(event_path!("foo")));
1435        assert_eq!(
1436            log["metadata"],
1437            serde_json::json!({
1438                "dropped": {
1439                    "reason": "error",
1440                    "message": "function call error for \"assert_eq\" at (45:66): assertion failed: 0 == 1",
1441                    "component_id": "remapper",
1442                    "component_type": "remap",
1443                    "component_kind": "transform",
1444                }
1445            })
1446            .try_into()
1447            .unwrap()
1448        );
1449    }
1450
1451    #[test]
1452    fn check_remap_branching_abort_with_message() {
1453        let error =
1454            Event::from_json_value(serde_json::json!({"hello": 42}), LogNamespace::Legacy).unwrap();
1455        let conf = RemapConfig {
1456            source: Some(formatdoc! {r#"
1457                abort "custom message here"
1458            "#}),
1459            drop_on_error: true,
1460            drop_on_abort: true,
1461            reroute_dropped: true,
1462            ..Default::default()
1463        };
1464        let context = TransformContext {
1465            key: Some(ComponentKey::from("remapper")),
1466            ..Default::default()
1467        };
1468        let mut tform = Remap::new_ast(conf, &context).unwrap().0;
1469
1470        let output = transform_one_fallible(&mut tform, error).unwrap_err();
1471        let log = output.as_log();
1472        assert_eq!(log["hello"], 42.into());
1473        assert!(!log.contains(event_path!("foo")));
1474        assert_eq!(
1475            log["metadata"],
1476            serde_json::json!({
1477                "dropped": {
1478                    "reason": "abort",
1479                    "message": "custom message here",
1480                    "component_id": "remapper",
1481                    "component_type": "remap",
1482                    "component_kind": "transform",
1483                }
1484            })
1485            .try_into()
1486            .unwrap()
1487        );
1488    }
1489
1490    #[test]
1491    fn check_remap_branching_disabled() {
1492        let happy =
1493            Event::from_json_value(serde_json::json!({"hello": "world"}), LogNamespace::Legacy)
1494                .unwrap();
1495        let abort = Event::from_json_value(
1496            serde_json::json!({"hello": "goodbye"}),
1497            LogNamespace::Legacy,
1498        )
1499        .unwrap();
1500        let error =
1501            Event::from_json_value(serde_json::json!({"hello": 42}), LogNamespace::Legacy).unwrap();
1502
1503        let conf = RemapConfig {
1504            source: Some(formatdoc! {r#"
1505                if exists(.tags) {{
1506                    # metrics
1507                    .tags.foo = "bar"
1508                    if string!(.tags.hello) == "goodbye" {{
1509                      abort
1510                    }}
1511                }} else {{
1512                    # logs
1513                    .foo = "bar"
1514                    if string!(.hello) == "goodbye" {{
1515                      abort
1516                    }}
1517                }}
1518            "#}),
1519            drop_on_error: true,
1520            drop_on_abort: true,
1521            reroute_dropped: false,
1522            ..Default::default()
1523        };
1524
1525        let schema_definition = schema::Definition::new_with_default_metadata(
1526            Kind::any_object(),
1527            [LogNamespace::Legacy],
1528        )
1529        .with_event_field(&owned_value_path!("foo"), Kind::any(), None)
1530        .with_event_field(&owned_value_path!("tags"), Kind::any(), None);
1531
1532        assert_eq!(
1533            conf.outputs(
1534                &Default::default(),
1535                &[(
1536                    "test".into(),
1537                    schema::Definition::new_with_default_metadata(
1538                        Kind::any_object(),
1539                        [LogNamespace::Legacy]
1540                    )
1541                )],
1542            ),
1543            vec![TransformOutput::new(
1544                DataType::all_bits(),
1545                [("test".into(), schema_definition)].into()
1546            )]
1547        );
1548
1549        let context = TransformContext {
1550            key: Some(ComponentKey::from("remapper")),
1551            ..Default::default()
1552        };
1553        let mut tform = Remap::new_ast(conf, &context).unwrap().0;
1554
1555        let output = transform_one_fallible(&mut tform, happy).unwrap();
1556        let log = output.as_log();
1557        assert_eq!(log["hello"], "world".into());
1558        assert_eq!(log["foo"], "bar".into());
1559        assert!(!log.contains(event_path!("metadata")));
1560
1561        let out = collect_outputs(&mut tform, abort);
1562        assert!(out.primary.is_empty());
1563        assert!(out.named[DROPPED].is_empty());
1564
1565        let out = collect_outputs(&mut tform, error);
1566        assert!(out.primary.is_empty());
1567        assert!(out.named[DROPPED].is_empty());
1568    }
1569
1570    #[tokio::test]
1571    async fn check_remap_branching_metrics_with_output() {
1572        init_test();
1573
1574        let config: ConfigBuilder = serde_yaml::from_str(indoc! {"
1575            transforms:
1576              foo:
1577                inputs: []
1578                type: remap
1579                drop_on_abort: true
1580                reroute_dropped: true
1581                source: abort
1582            tests:
1583              - name: metric output
1584                input:
1585                  insert_at: foo
1586                  value: none
1587                outputs:
1588                  - extract_from: foo.dropped
1589                    conditions:
1590                      - type: vrl
1591                        source: \"true\"
1592        "})
1593        .unwrap();
1594
1595        let mut tests = build_unit_tests(config).await.unwrap();
1596        assert!(tests.remove(0).run().await.errors.is_empty());
1597        // Check that metrics were emitted with output tag
1598        COMPONENT_MULTIPLE_OUTPUTS_TESTS.assert(&["output"]);
1599    }
1600
1601    struct CollectedOutput {
1602        primary: OutputBuffer,
1603        named: HashMap<String, OutputBuffer>,
1604    }
1605
1606    fn collect_outputs(ft: &mut dyn SyncTransform, event: Event) -> CollectedOutput {
1607        let mut outputs = TransformOutputsBuf::new_with_capacity(
1608            vec![
1609                TransformOutput::new(DataType::all_bits(), HashMap::new()),
1610                TransformOutput::new(DataType::all_bits(), HashMap::new()).with_port(DROPPED),
1611            ],
1612            1,
1613        );
1614
1615        ft.transform(event, &mut outputs);
1616
1617        CollectedOutput {
1618            primary: outputs.take_primary(),
1619            named: outputs.take_all_named(),
1620        }
1621    }
1622
1623    fn transform_one(ft: &mut dyn SyncTransform, event: Event) -> Option<Event> {
1624        let out = collect_outputs(ft, event);
1625        assert_eq!(0, out.named.values().map(|v| v.len()).sum::<usize>());
1626        assert!(out.primary.len() <= 1);
1627        out.primary.into_events().next()
1628    }
1629
1630    fn transform_one_fallible(
1631        ft: &mut dyn SyncTransform,
1632        event: Event,
1633    ) -> std::result::Result<Event, Event> {
1634        let mut outputs = TransformOutputsBuf::new_with_capacity(
1635            vec![
1636                TransformOutput::new(DataType::all_bits(), HashMap::new()),
1637                TransformOutput::new(DataType::all_bits(), HashMap::new()).with_port(DROPPED),
1638            ],
1639            1,
1640        );
1641
1642        ft.transform(event, &mut outputs);
1643
1644        let mut buf = outputs.drain().collect::<Vec<_>>();
1645        let mut err_buf = outputs.drain_named(DROPPED).collect::<Vec<_>>();
1646
1647        assert!(buf.len() < 2);
1648        assert!(err_buf.len() < 2);
1649        match (buf.pop(), err_buf.pop()) {
1650            (Some(good), None) => Ok(good),
1651            (None, Some(bad)) => Err(bad),
1652            (a, b) => panic!("expected output xor error output, got {a:?} and {b:?}"),
1653        }
1654    }
1655
1656    #[tokio::test]
1657    async fn emits_internal_events() {
1658        assert_transform_compliance(async move {
1659            let config = RemapConfig {
1660                source: Some("abort".to_owned()),
1661                drop_on_abort: true,
1662                ..Default::default()
1663            };
1664
1665            let (tx, rx) = mpsc::channel(1);
1666            let (topology, mut out) = create_topology(ReceiverStream::new(rx), config).await;
1667
1668            let log = LogEvent::from("hello world");
1669            tx.send(log.into()).await.unwrap();
1670
1671            drop(tx);
1672            topology.stop().await;
1673            assert_eq!(out.recv().await, None);
1674        })
1675        .await
1676    }
1677
1678    #[test]
1679    fn test_combined_transforms_simple() {
1680        // Make sure that when getting the definitions from one transform and
1681        // passing them to another the correct definition is still produced.
1682
1683        // Transform 1 sets a simple value.
1684        let transform1 = RemapConfig {
1685            source: Some(r#".thing = "potato""#.to_string()),
1686            ..Default::default()
1687        };
1688
1689        let transform2 = RemapConfig {
1690            source: Some(".thang = .thing".to_string()),
1691            ..Default::default()
1692        };
1693
1694        let outputs1 = transform1.outputs(
1695            &Default::default(),
1696            &[("in".into(), schema::Definition::default_legacy_namespace())],
1697        );
1698
1699        assert_eq!(
1700            vec![TransformOutput::new(
1701                DataType::all_bits(),
1702                // The `never` definition should have been passed on to the end.
1703                [(
1704                    "in".into(),
1705                    Definition::default_legacy_namespace().with_event_field(
1706                        &owned_value_path!("thing"),
1707                        Kind::bytes(),
1708                        None
1709                    ),
1710                )]
1711                .into()
1712            )],
1713            outputs1
1714        );
1715
1716        let outputs2 = transform2.outputs(
1717            &Default::default(),
1718            &[(
1719                "in1".into(),
1720                outputs1[0].schema_definitions(true)[&"in".into()].clone(),
1721            )],
1722        );
1723
1724        assert_eq!(
1725            vec![TransformOutput::new(
1726                DataType::all_bits(),
1727                [(
1728                    "in1".into(),
1729                    Definition::default_legacy_namespace()
1730                        .with_event_field(&owned_value_path!("thing"), Kind::bytes(), None)
1731                        .with_event_field(&owned_value_path!("thang"), Kind::bytes(), None),
1732                )]
1733                .into(),
1734            )],
1735            outputs2
1736        );
1737    }
1738
1739    #[test]
1740    fn test_combined_transforms_unnest() {
1741        // Make sure that when getting the definitions from one transform and
1742        // passing them to another the correct definition is still produced.
1743
1744        // Transform 1 sets a simple value.
1745        let transform1 = RemapConfig {
1746            source: Some(
1747                indoc! {
1748                r#"
1749                .thing = [{"cabbage": 32}, {"parsnips": 45}]
1750                . = unnest(.thing)
1751                "#
1752                }
1753                .to_string(),
1754            ),
1755            ..Default::default()
1756        };
1757
1758        let transform2 = RemapConfig {
1759            source: Some(r#".thang = .thing.cabbage || "beetroot""#.to_string()),
1760            ..Default::default()
1761        };
1762
1763        let outputs1 = transform1.outputs(
1764            &Default::default(),
1765            &[(
1766                "in".into(),
1767                schema::Definition::new_with_default_metadata(
1768                    Kind::any_object(),
1769                    [LogNamespace::Legacy],
1770                ),
1771            )],
1772        );
1773
1774        assert_eq!(
1775            vec![TransformOutput::new(
1776                DataType::all_bits(),
1777                [(
1778                    "in".into(),
1779                    Definition::new_with_default_metadata(
1780                        Kind::any_object(),
1781                        [LogNamespace::Legacy]
1782                    )
1783                    .with_event_field(
1784                        &owned_value_path!("thing"),
1785                        Kind::object(Collection::from(BTreeMap::from([
1786                            ("cabbage".into(), Kind::integer().or_undefined(),),
1787                            ("parsnips".into(), Kind::integer().or_undefined(),)
1788                        ]))),
1789                        None
1790                    ),
1791                )]
1792                .into(),
1793            )],
1794            outputs1
1795        );
1796
1797        let outputs2 = transform2.outputs(
1798            &Default::default(),
1799            &[(
1800                "in1".into(),
1801                outputs1[0].schema_definitions(true)[&"in".into()].clone(),
1802            )],
1803        );
1804
1805        assert_eq!(
1806            vec![TransformOutput::new(
1807                DataType::all_bits(),
1808                [(
1809                    "in1".into(),
1810                    Definition::default_legacy_namespace()
1811                        .with_event_field(
1812                            &owned_value_path!("thing"),
1813                            Kind::object(Collection::from(BTreeMap::from([
1814                                ("cabbage".into(), Kind::integer().or_undefined(),),
1815                                ("parsnips".into(), Kind::integer().or_undefined(),)
1816                            ]))),
1817                            None
1818                        )
1819                        .with_event_field(
1820                            &owned_value_path!("thang"),
1821                            Kind::integer().or_null(),
1822                            None
1823                        ),
1824                )]
1825                .into(),
1826            )],
1827            outputs2
1828        );
1829    }
1830
1831    #[test]
1832    fn test_transform_abort() {
1833        // An abort should not change the typedef.
1834
1835        let transform1 = RemapConfig {
1836            source: Some(r"abort".to_string()),
1837            ..Default::default()
1838        };
1839
1840        let outputs1 = transform1.outputs(
1841            &Default::default(),
1842            &[(
1843                "in".into(),
1844                schema::Definition::new_with_default_metadata(
1845                    Kind::any_object(),
1846                    [LogNamespace::Legacy],
1847                ),
1848            )],
1849        );
1850
1851        assert_eq!(
1852            vec![TransformOutput::new(
1853                DataType::all_bits(),
1854                [(
1855                    "in".into(),
1856                    Definition::new_with_default_metadata(
1857                        Kind::any_object(),
1858                        [LogNamespace::Legacy]
1859                    ),
1860                )]
1861                .into(),
1862            )],
1863            outputs1
1864        );
1865    }
1866
1867    #[test]
1868    fn test_error_outputs() {
1869        // Even if we fail to compile the VRL it should still output
1870        // the correct ports. This may change if we separate the
1871        // `outputs` function into one returning outputs and a separate
1872        // returning schema definitions.
1873        let transform1 = RemapConfig {
1874            // This enrichment table does not exist.
1875            source: Some(r#". |= get_enrichment_table_record("carrot", {"id": .id})"#.to_string()),
1876            reroute_dropped: true,
1877            ..Default::default()
1878        };
1879
1880        let outputs1 = transform1.outputs(
1881            &Default::default(),
1882            &[(
1883                "in".into(),
1884                schema::Definition::new_with_default_metadata(
1885                    Kind::any_object(),
1886                    [LogNamespace::Legacy],
1887                ),
1888            )],
1889        );
1890
1891        assert_eq!(
1892            HashSet::from([None, Some("dropped".to_string())]),
1893            outputs1
1894                .into_iter()
1895                .map(|output| output.port)
1896                .collect::<HashSet<_>>()
1897        );
1898    }
1899
1900    #[test]
1901    fn test_non_object_events() {
1902        let transform1 = RemapConfig {
1903            // This enrichment table does not exist.
1904            source: Some(r#". = "fish" "#.to_string()),
1905            ..Default::default()
1906        };
1907
1908        let outputs1 = transform1.outputs(
1909            &Default::default(),
1910            &[(
1911                "in".into(),
1912                schema::Definition::new_with_default_metadata(
1913                    Kind::any_object(),
1914                    [LogNamespace::Legacy],
1915                ),
1916            )],
1917        );
1918
1919        let wanted = schema::Definition::new_with_default_metadata(
1920            Kind::object(Collection::from_unknown(Kind::undefined())),
1921            [LogNamespace::Legacy],
1922        )
1923        .with_event_field(&owned_value_path!("message"), Kind::bytes(), None);
1924
1925        assert_eq!(
1926            HashMap::from([(OutputId::from("in"), wanted)]),
1927            outputs1[0].schema_definitions(true),
1928        );
1929    }
1930
1931    #[test]
1932    fn test_array_and_non_object_events() {
1933        let transform1 = RemapConfig {
1934            source: Some(
1935                indoc! {r#"
1936                    if .lizard == true {
1937                        .thing = [{"cabbage": 42}];
1938                        . = unnest(.thing)
1939                    } else {
1940                      . = "fish"
1941                    }
1942                    "#}
1943                .to_string(),
1944            ),
1945            ..Default::default()
1946        };
1947
1948        let outputs1 = transform1.outputs(
1949            &Default::default(),
1950            &[(
1951                "in".into(),
1952                schema::Definition::new_with_default_metadata(
1953                    Kind::any_object(),
1954                    [LogNamespace::Legacy],
1955                ),
1956            )],
1957        );
1958
1959        let wanted = schema::Definition::new_with_default_metadata(
1960            Kind::any_object(),
1961            [LogNamespace::Legacy],
1962        )
1963        .with_event_field(&owned_value_path!("message"), Kind::any(), None)
1964        .with_event_field(
1965            &owned_value_path!("thing"),
1966            Kind::object(Collection::from(BTreeMap::from([(
1967                "cabbage".into(),
1968                Kind::integer(),
1969            )])))
1970            .or_undefined(),
1971            None,
1972        );
1973
1974        assert_eq!(
1975            HashMap::from([(OutputId::from("in"), wanted)]),
1976            outputs1[0].schema_definitions(true),
1977        );
1978    }
1979
1980    #[test]
1981    fn check_remap_array_vector_namespace() {
1982        let event = {
1983            let mut event = LogEvent::from("input");
1984            // mark the event as a "Vector" namespaced log
1985            event
1986                .metadata_mut()
1987                .value_mut()
1988                .insert(vrl::path!("vector"), BTreeMap::new());
1989            Event::from(event)
1990        };
1991
1992        let conf = RemapConfig {
1993            source: Some(
1994                r". = [null]
1995"
1996                .to_string(),
1997            ),
1998            file: None,
1999            drop_on_error: true,
2000            drop_on_abort: false,
2001            ..Default::default()
2002        };
2003        let mut tform = remap(conf.clone()).unwrap();
2004        let result = transform_one(&mut tform, event).unwrap();
2005
2006        // Legacy namespace nests this under "message", Vector should set it as the root
2007        assert_eq!(result.as_log().get(event_path!()), Some(&Value::Null));
2008
2009        let outputs1 = conf.outputs(
2010            &Default::default(),
2011            &[(
2012                "in".into(),
2013                schema::Definition::new_with_default_metadata(
2014                    Kind::any_object(),
2015                    [LogNamespace::Vector],
2016                ),
2017            )],
2018        );
2019
2020        let wanted =
2021            schema::Definition::new_with_default_metadata(Kind::null(), [LogNamespace::Vector]);
2022
2023        assert_eq!(
2024            HashMap::from([(OutputId::from("in"), wanted)]),
2025            outputs1[0].schema_definitions(true),
2026        );
2027    }
2028
2029    fn assert_no_metrics(source: String) {
2030        vector_lib::metrics::init_test();
2031
2032        let config = RemapConfig {
2033            source: Some(source),
2034            drop_on_error: true,
2035            drop_on_abort: true,
2036            reroute_dropped: true,
2037            ..Default::default()
2038        };
2039        let mut ast_runner = remap(config).unwrap();
2040        let input_event =
2041            Event::from_json_value(serde_json::json!({"a": 42}), LogNamespace::Vector).unwrap();
2042        let dropped_event = transform_one_fallible(&mut ast_runner, input_event).unwrap_err();
2043        let dropped_log = dropped_event.as_log();
2044        assert_eq!(dropped_log.get(event_path!("a")), Some(&Value::from(42)));
2045
2046        let controller = Controller::get().expect("no controller");
2047        let metrics = controller
2048            .capture_metrics()
2049            .into_iter()
2050            .map(|metric| (metric.name().to_string(), metric))
2051            .collect::<BTreeMap<String, Metric>>();
2052        assert_eq!(metrics.get("component_discarded_events_total"), None);
2053        assert_eq!(metrics.get("component_errors_total"), None);
2054    }
2055    #[test]
2056    fn do_not_emit_metrics_when_dropped() {
2057        assert_no_metrics("abort".to_string());
2058    }
2059
2060    #[test]
2061    fn do_not_emit_metrics_when_errored() {
2062        assert_no_metrics("parse_key_value!(.message)".to_string());
2063    }
2064}