vector/
template.rs

1//! Functionality for managing template fields used by Vector's sinks.
2use std::{borrow::Cow, convert::TryFrom, fmt, hash::Hash, path::PathBuf, sync::LazyLock};
3
4use bytes::Bytes;
5use chrono::{
6    FixedOffset, Utc,
7    format::{Item, strftime::StrftimeItems},
8};
9use regex::Regex;
10use snafu::Snafu;
11use vector_lib::{
12    configurable::{ConfigurableNumber, ConfigurableString, NumberClass, configurable_component},
13    lookup::lookup_v2::parse_target_path,
14};
15
16use crate::{
17    config::log_schema,
18    event::{EventRef, Metric, Value},
19};
20
21static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\{\{(?P<key>[^\}]+)\}\}").unwrap());
22
23/// Errors raised whilst parsing a Template field.
24#[allow(missing_docs)]
25#[derive(Clone, Debug, Eq, PartialEq, Snafu)]
26pub enum TemplateParseError {
27    #[snafu(display("Invalid strftime item"))]
28    StrftimeError,
29    #[snafu(display(
30        "Invalid field path in template {:?} (see https://vector.dev/docs/reference/configuration/template-syntax/)",
31        path
32    ))]
33    InvalidPathSyntax { path: String },
34    #[snafu(display("Invalid numeric template"))]
35    InvalidNumericTemplate { template: String },
36}
37
38/// Errors raised whilst rendering a Template.
39#[allow(missing_docs)]
40#[derive(Clone, Debug, Eq, PartialEq, Snafu)]
41pub enum TemplateRenderingError {
42    #[snafu(display("Missing fields on event: {:?}", missing_keys))]
43    MissingKeys { missing_keys: Vec<String> },
44    #[snafu(display("Not numeric: {:?}", input))]
45    NotNumeric { input: String },
46}
47
48/// A templated field.
49///
50/// In many cases, components can be configured so that part of the component's functionality can be
51/// customized on a per-event basis. For example, you have a sink that writes events to a file and you want to
52/// specify which file an event should go to by using an event field as part of the
53/// input to the filename used.
54///
55/// By using `Template`, users can specify either fixed strings or templated strings. Templated strings use a common syntax to
56/// refer to fields in an event that is used as the input data when rendering the template. An example of a fixed string
57/// is `my-file.log`. An example of a template string is `my-file-{{key}}.log`, where `{{key}}`
58/// is the key's value when the template is rendered into a string.
59#[configurable_component]
60#[configurable(metadata(docs::templateable))]
61#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
62#[serde(try_from = "String", into = "String")]
63pub struct Template {
64    src: String,
65
66    #[serde(skip)]
67    parts: Vec<Part>,
68
69    #[serde(skip)]
70    is_static: bool,
71
72    #[serde(skip)]
73    reserve_size: usize,
74
75    #[serde(skip)]
76    tz_offset: Option<FixedOffset>,
77}
78
79impl TryFrom<&str> for Template {
80    type Error = TemplateParseError;
81
82    fn try_from(src: &str) -> Result<Self, Self::Error> {
83        Template::try_from(Cow::Borrowed(src))
84    }
85}
86
87impl TryFrom<String> for Template {
88    type Error = TemplateParseError;
89
90    fn try_from(src: String) -> Result<Self, Self::Error> {
91        Template::try_from(Cow::Owned(src))
92    }
93}
94
95impl TryFrom<PathBuf> for Template {
96    type Error = TemplateParseError;
97
98    fn try_from(p: PathBuf) -> Result<Self, Self::Error> {
99        Template::try_from(p.to_string_lossy().into_owned())
100    }
101}
102
103impl TryFrom<Cow<'_, str>> for Template {
104    type Error = TemplateParseError;
105
106    fn try_from(src: Cow<'_, str>) -> Result<Self, Self::Error> {
107        parse_template(&src).map(|parts| {
108            let is_static =
109                parts.is_empty() || (parts.len() == 1 && matches!(parts[0], Part::Literal(..)));
110
111            // Calculate a minimum size to reserve for rendered string. This doesn't have to be
112            // exact, and can't be because of references and time format specifiers. We just want a
113            // better starting number than 0 to avoid the first reallocations if possible.
114            let reserve_size = parts
115                .iter()
116                .map(|part| match part {
117                    Part::Literal(lit) => lit.len(),
118                    // We can't really put a useful number here, assume at least one byte will come
119                    // from the input event.
120                    Part::Reference(_path) => 1,
121                    Part::Strftime(parsed) => parsed.reserve_size(),
122                })
123                .sum();
124
125            Template {
126                parts,
127                src: src.into_owned(),
128                is_static,
129                reserve_size,
130                tz_offset: None,
131            }
132        })
133    }
134}
135
136impl From<Template> for String {
137    fn from(template: Template) -> String {
138        template.src
139    }
140}
141
142impl fmt::Display for Template {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        self.src.fmt(f)
145    }
146}
147
148// This is safe because we literally defer to `String` for the schema of `Template`.
149impl ConfigurableString for Template {}
150
151impl Template {
152    /// set tz offset
153    pub const fn with_tz_offset(mut self, tz_offset: Option<FixedOffset>) -> Self {
154        self.tz_offset = tz_offset;
155        self
156    }
157    /// Renders the given template with data from the event.
158    pub fn render<'a>(
159        &self,
160        event: impl Into<EventRef<'a>>,
161    ) -> Result<Bytes, TemplateRenderingError> {
162        self.render_string(event.into()).map(Into::into)
163    }
164
165    /// Renders the given template with data from the event.
166    pub fn render_string<'a>(
167        &self,
168        event: impl Into<EventRef<'a>>,
169    ) -> Result<String, TemplateRenderingError> {
170        if self.is_static {
171            Ok(self.src.clone())
172        } else {
173            self.render_event(event.into())
174        }
175    }
176
177    fn render_event(&self, event: EventRef<'_>) -> Result<String, TemplateRenderingError> {
178        let mut missing_keys = Vec::new();
179        let mut out = String::with_capacity(self.reserve_size);
180        for part in &self.parts {
181            match part {
182                Part::Literal(lit) => out.push_str(lit),
183                Part::Strftime(items) => {
184                    out.push_str(&render_timestamp(items, event, self.tz_offset))
185                }
186                Part::Reference(key) => {
187                    out.push_str(
188                        &match event {
189                            EventRef::Log(log) => log
190                                .parse_path_and_get_value(key)
191                                .ok()
192                                .and_then(|v| v.map(Value::to_string_lossy)),
193                            EventRef::Metric(metric) => {
194                                render_metric_field(key, metric).map(Cow::Borrowed)
195                            }
196                            EventRef::Trace(trace) => trace
197                                .parse_path_and_get_value(key)
198                                .ok()
199                                .and_then(|v| v.map(Value::to_string_lossy)),
200                        }
201                        .unwrap_or_else(|| {
202                            missing_keys.push(key.to_owned());
203                            Cow::Borrowed("")
204                        }),
205                    );
206                }
207            }
208        }
209        if missing_keys.is_empty() {
210            Ok(out)
211        } else {
212            Err(TemplateRenderingError::MissingKeys { missing_keys })
213        }
214    }
215
216    /// Returns the names of the fields that are rendered in this template.
217    pub fn get_fields(&self) -> Option<Vec<String>> {
218        let parts: Vec<_> = self
219            .parts
220            .iter()
221            .filter_map(|part| {
222                if let Part::Reference(r) = part {
223                    Some(r.to_owned())
224                } else {
225                    None
226                }
227            })
228            .collect();
229        (!parts.is_empty()).then_some(parts)
230    }
231
232    #[allow(clippy::missing_const_for_fn)] // Adding `const` results in https://doc.rust-lang.org/error_codes/E0015.html
233    /// Returns a reference to the template string.
234    pub fn get_ref(&self) -> &str {
235        &self.src
236    }
237
238    /// Returns `true` if this template string has a length of zero, and `false` otherwise.
239    pub const fn is_empty(&self) -> bool {
240        self.src.is_empty()
241    }
242
243    /// A dynamic template string contains sections that depend on the input event or time.
244    pub const fn is_dynamic(&self) -> bool {
245        !self.is_static
246    }
247}
248
249/// The source of a `uint` template. May be a constant numeric value or a template string.
250#[derive(Clone, Debug, Eq, Hash, PartialEq)]
251#[configurable_component]
252#[serde(untagged)]
253enum UnsignedIntTemplateSource {
254    /// A static unsigned number.
255    Number(u64),
256    /// A string, which may be a template.
257    String(String),
258}
259
260impl Default for UnsignedIntTemplateSource {
261    fn default() -> Self {
262        Self::Number(Default::default())
263    }
264}
265
266impl fmt::Display for UnsignedIntTemplateSource {
267    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
268        match self {
269            Self::Number(i) => i.fmt(f),
270            Self::String(s) => s.fmt(f),
271        }
272    }
273}
274
275/// Unsigned integer template.
276#[configurable_component]
277#[configurable(metadata(docs::templateable))]
278#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
279#[serde(
280    try_from = "UnsignedIntTemplateSource",
281    into = "UnsignedIntTemplateSource"
282)]
283pub struct UnsignedIntTemplate {
284    src: UnsignedIntTemplateSource,
285
286    #[serde(skip)]
287    parts: Vec<Part>,
288
289    #[serde(skip)]
290    tz_offset: Option<FixedOffset>,
291}
292
293impl TryFrom<UnsignedIntTemplateSource> for UnsignedIntTemplate {
294    type Error = TemplateParseError;
295
296    fn try_from(src: UnsignedIntTemplateSource) -> Result<Self, Self::Error> {
297        match src {
298            UnsignedIntTemplateSource::Number(num) => Ok(UnsignedIntTemplate {
299                src: UnsignedIntTemplateSource::Number(num),
300                parts: Vec::new(),
301                tz_offset: None,
302            }),
303            UnsignedIntTemplateSource::String(s) => UnsignedIntTemplate::try_from(s),
304        }
305    }
306}
307
308impl From<UnsignedIntTemplate> for UnsignedIntTemplateSource {
309    fn from(template: UnsignedIntTemplate) -> UnsignedIntTemplateSource {
310        template.src
311    }
312}
313
314impl TryFrom<&str> for UnsignedIntTemplate {
315    type Error = TemplateParseError;
316
317    fn try_from(src: &str) -> Result<Self, Self::Error> {
318        UnsignedIntTemplate::try_from(Cow::Borrowed(src))
319    }
320}
321
322impl TryFrom<String> for UnsignedIntTemplate {
323    type Error = TemplateParseError;
324
325    fn try_from(src: String) -> Result<Self, Self::Error> {
326        UnsignedIntTemplate::try_from(Cow::Owned(src))
327    }
328}
329
330impl From<u64> for UnsignedIntTemplate {
331    fn from(num: u64) -> UnsignedIntTemplate {
332        UnsignedIntTemplate {
333            src: UnsignedIntTemplateSource::Number(num),
334            parts: Vec::new(),
335            tz_offset: None,
336        }
337    }
338}
339
340impl TryFrom<Cow<'_, str>> for UnsignedIntTemplate {
341    type Error = TemplateParseError;
342
343    fn try_from(src: Cow<'_, str>) -> Result<Self, Self::Error> {
344        parse_template(&src).and_then(|parts| {
345            let is_static =
346                parts.is_empty() || (parts.len() == 1 && matches!(parts[0], Part::Literal(..)));
347
348            if is_static {
349                match src.parse::<u64>() {
350                    Ok(num) => Ok(UnsignedIntTemplate {
351                        src: UnsignedIntTemplateSource::Number(num),
352                        parts,
353                        tz_offset: None,
354                    }),
355                    Err(_) => Err(TemplateParseError::InvalidNumericTemplate {
356                        template: src.into_owned(),
357                    }),
358                }
359            } else {
360                Ok(UnsignedIntTemplate {
361                    parts,
362                    src: UnsignedIntTemplateSource::String(src.into_owned()),
363                    tz_offset: None,
364                })
365            }
366        })
367    }
368}
369
370impl From<UnsignedIntTemplate> for String {
371    fn from(template: UnsignedIntTemplate) -> String {
372        template.src.to_string()
373    }
374}
375
376impl fmt::Display for UnsignedIntTemplate {
377    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
378        self.src.fmt(f)
379    }
380}
381
382impl ConfigurableString for UnsignedIntTemplate {}
383impl ConfigurableNumber for UnsignedIntTemplate {
384    type Numeric = u64;
385
386    fn class() -> NumberClass {
387        NumberClass::Unsigned
388    }
389}
390
391impl UnsignedIntTemplate {
392    /// Renders the given template with data from the event.
393    pub fn render<'a>(
394        &self,
395        event: impl Into<EventRef<'a>>,
396    ) -> Result<u64, TemplateRenderingError> {
397        match self.src {
398            UnsignedIntTemplateSource::Number(num) => Ok(num),
399            UnsignedIntTemplateSource::String(_) => self.render_event(event.into()),
400        }
401    }
402
403    /// set tz offset
404    pub const fn with_tz_offset(mut self, tz_offset: Option<FixedOffset>) -> Self {
405        self.tz_offset = tz_offset;
406        self
407    }
408
409    fn render_event(&self, event: EventRef<'_>) -> Result<u64, TemplateRenderingError> {
410        let mut missing_keys = Vec::new();
411        let mut out = String::with_capacity(20);
412        for part in &self.parts {
413            match part {
414                Part::Literal(lit) => out.push_str(lit),
415                Part::Reference(key) => {
416                    out.push_str(
417                        &match event {
418                            EventRef::Log(log) => log
419                                .parse_path_and_get_value(key)
420                                .ok()
421                                .and_then(|v| v.map(Value::to_string_lossy)),
422                            EventRef::Metric(metric) => {
423                                render_metric_field(key, metric).map(Cow::Borrowed)
424                            }
425                            EventRef::Trace(trace) => trace
426                                .parse_path_and_get_value(key)
427                                .ok()
428                                .and_then(|v| v.map(Value::to_string_lossy)),
429                        }
430                        .unwrap_or_else(|| {
431                            missing_keys.push(key.to_owned());
432                            Cow::Borrowed("")
433                        }),
434                    );
435                }
436                Part::Strftime(items) => {
437                    out.push_str(&render_timestamp(items, event, self.tz_offset))
438                }
439            }
440        }
441        if missing_keys.is_empty() {
442            out.parse::<u64>()
443                .map_err(|_| TemplateRenderingError::NotNumeric { input: out })
444        } else {
445            Err(TemplateRenderingError::MissingKeys { missing_keys })
446        }
447    }
448
449    /// Returns the names of the fields that are rendered in this template.
450    pub fn get_fields(&self) -> Option<Vec<String>> {
451        let parts: Vec<_> = self
452            .parts
453            .iter()
454            .filter_map(|part| {
455                if let Part::Reference(r) = part {
456                    Some(r.to_owned())
457                } else {
458                    None
459                }
460            })
461            .collect();
462        (!parts.is_empty()).then_some(parts)
463    }
464}
465
466/// One part of the template string after parsing.
467#[derive(Clone, Debug, Eq, Hash, PartialEq)]
468enum Part {
469    /// A literal piece of text to be copied verbatim into the output.
470    Literal(String),
471    /// A literal piece of text containing a time format string.
472    Strftime(ParsedStrftime),
473    /// A reference to the source event, to be copied from the relevant field or tag.
474    Reference(String),
475}
476
477// Wrap the parsed time formatter in order to provide `impl Hash` and some convenience functions.
478#[derive(Clone, Debug, Eq, Hash, PartialEq)]
479struct ParsedStrftime(Box<[Item<'static>]>);
480
481impl ParsedStrftime {
482    fn parse(fmt: &str) -> Result<Self, TemplateParseError> {
483        Ok(Self(
484            StrftimeItems::new(fmt)
485                .map(|item| match item {
486                    // Box the references so they outlive the reference
487                    Item::Space(space) => Item::OwnedSpace(space.into()),
488                    Item::Literal(lit) => Item::OwnedLiteral(lit.into()),
489                    // And copy all the others
490                    Item::Fixed(f) => Item::Fixed(f),
491                    Item::Numeric(num, pad) => Item::Numeric(num, pad),
492                    Item::Error => Item::Error,
493                    Item::OwnedSpace(space) => Item::OwnedSpace(space),
494                    Item::OwnedLiteral(lit) => Item::OwnedLiteral(lit),
495                })
496                .map(|item| {
497                    matches!(item, Item::Error)
498                        .then(|| Err(TemplateParseError::StrftimeError))
499                        .unwrap_or(Ok(item))
500                })
501                .collect::<Result<Vec<_>, _>>()?
502                .into(),
503        ))
504    }
505
506    fn is_dynamic(&self) -> bool {
507        self.0.iter().any(|item| match item {
508            Item::Fixed(_) => true,
509            Item::Numeric(_, _) => true,
510            Item::Error
511            | Item::Space(_)
512            | Item::OwnedSpace(_)
513            | Item::Literal(_)
514            | Item::OwnedLiteral(_) => false,
515        })
516    }
517
518    fn as_items(&self) -> impl Iterator<Item = &Item<'static>> + Clone {
519        self.0.iter()
520    }
521
522    fn reserve_size(&self) -> usize {
523        self.0
524            .iter()
525            .map(|item| match item {
526                Item::Literal(lit) => lit.len(),
527                Item::OwnedLiteral(lit) => lit.len(),
528                Item::Space(space) => space.len(),
529                Item::OwnedSpace(space) => space.len(),
530                Item::Error => 0,
531                Item::Numeric(_, _) => 2,
532                Item::Fixed(_) => 2,
533            })
534            .sum()
535    }
536}
537
538fn parse_literal(src: &str) -> Result<Part, TemplateParseError> {
539    let parsed = ParsedStrftime::parse(src)?;
540    Ok(if parsed.is_dynamic() {
541        Part::Strftime(parsed)
542    } else {
543        Part::Literal(src.to_string())
544    })
545}
546
547// Pre-parse the template string into a series of parts to be filled in at render time.
548fn parse_template(src: &str) -> Result<Vec<Part>, TemplateParseError> {
549    let mut last_end = 0;
550    let mut parts = Vec::new();
551    for cap in RE.captures_iter(src) {
552        let all = cap.get(0).expect("Capture 0 is always defined");
553        if all.start() > last_end {
554            parts.push(parse_literal(&src[last_end..all.start()])?);
555        }
556
557        let path = cap[1].trim().to_owned();
558
559        // This checks the syntax, but doesn't yet store it for use later
560        // see: https://github.com/vectordotdev/vector/issues/14864
561        if parse_target_path(&path).is_err() {
562            return Err(TemplateParseError::InvalidPathSyntax { path });
563        }
564
565        parts.push(Part::Reference(path));
566        last_end = all.end();
567    }
568    if src.len() > last_end {
569        parts.push(parse_literal(&src[last_end..])?);
570    }
571
572    Ok(parts)
573}
574
575fn render_metric_field<'a>(key: &str, metric: &'a Metric) -> Option<&'a str> {
576    match key {
577        "name" => Some(metric.name()),
578        "namespace" => metric.namespace(),
579        _ if key.starts_with("tags.") => metric.tags().and_then(|tags| tags.get(&key[5..])),
580        _ => None,
581    }
582}
583
584fn render_timestamp(
585    items: &ParsedStrftime,
586    event: EventRef<'_>,
587    tz_offset: Option<FixedOffset>,
588) -> String {
589    let timestamp = match event {
590        EventRef::Log(log) => log.get_timestamp().and_then(Value::as_timestamp).copied(),
591        EventRef::Metric(metric) => metric.timestamp(),
592        EventRef::Trace(trace) => {
593            log_schema()
594                .timestamp_key_target_path()
595                .and_then(|timestamp_key| {
596                    trace
597                        .get(timestamp_key)
598                        .and_then(Value::as_timestamp)
599                        .copied()
600                })
601        }
602    }
603    .unwrap_or_else(Utc::now);
604
605    match tz_offset {
606        Some(offset) => timestamp
607            .with_timezone(&offset)
608            .format_with_items(items.as_items())
609            .to_string(),
610        None => timestamp
611            .with_timezone(&chrono::Utc)
612            .format_with_items(items.as_items())
613            .to_string(),
614    }
615}
616
617#[cfg(test)]
618mod tests {
619    use chrono::{Offset, TimeZone, Utc};
620    use chrono_tz::Tz;
621    use vector_lib::{
622        config::LogNamespace,
623        lookup::{PathPrefix, metadata_path},
624        metric_tags,
625    };
626    use vrl::event_path;
627
628    use super::*;
629    use crate::event::{Event, LogEvent, MetricKind, MetricValue};
630
631    #[test]
632    fn get_fields() {
633        let f1 = Template::try_from("{{ foo }}")
634            .unwrap()
635            .get_fields()
636            .unwrap();
637        let f2 = Template::try_from("{{ foo }}-{{ bar }}")
638            .unwrap()
639            .get_fields()
640            .unwrap();
641        let f3 = Template::try_from("nofield").unwrap().get_fields();
642        let f4 = Template::try_from("%F").unwrap().get_fields();
643        let f5 = UnsignedIntTemplate::try_from("{{ foo }}-{{ bar }}")
644            .unwrap()
645            .get_fields()
646            .unwrap();
647        let f6 = UnsignedIntTemplate::from(123u64).get_fields();
648        let f7 = UnsignedIntTemplate::try_from("%s").unwrap().get_fields();
649
650        assert_eq!(f1, vec!["foo"]);
651        assert_eq!(f2, vec!["foo", "bar"]);
652        assert_eq!(f3, None);
653        assert_eq!(f4, None);
654        assert_eq!(f5, vec!["foo", "bar"]);
655        assert_eq!(f6, None);
656        assert_eq!(f7, None);
657    }
658
659    #[test]
660    fn is_dynamic() {
661        assert!(Template::try_from("/kube-demo/%F").unwrap().is_dynamic());
662        assert!(!Template::try_from("/kube-demo/echo").unwrap().is_dynamic());
663        assert!(
664            Template::try_from("/kube-demo/{{ foo }}")
665                .unwrap()
666                .is_dynamic()
667        );
668        assert!(
669            Template::try_from("/kube-demo/{{ foo }}/%F")
670                .unwrap()
671                .is_dynamic()
672        );
673    }
674
675    #[test]
676    fn render_log_static() {
677        let event = Event::Log(LogEvent::from("hello world"));
678        let template = Template::try_from("foo").unwrap();
679
680        assert_eq!(Ok(Bytes::from("foo")), template.render(&event))
681    }
682
683    #[test]
684    fn render_log_unsigned_number() {
685        let event = Event::Log(LogEvent::from("hello world"));
686        let template = UnsignedIntTemplate::from(123);
687
688        assert_eq!(Ok(123), template.render(&event))
689    }
690
691    #[test]
692    fn render_log_unsigned_number_dynamic() {
693        let mut event = Event::Log(LogEvent::from("hello world"));
694        event.as_mut_log().insert("foo", 123);
695
696        let template = UnsignedIntTemplate::try_from("{{ foo }}").unwrap();
697        assert_eq!(Ok(123), template.render(&event))
698    }
699
700    #[test]
701    fn render_log_dynamic() {
702        let mut event = Event::Log(LogEvent::from("hello world"));
703        event.as_mut_log().insert("log_stream", "stream");
704        let template = Template::try_from("{{log_stream}}").unwrap();
705
706        assert_eq!(Ok(Bytes::from("stream")), template.render(&event))
707    }
708
709    #[test]
710    fn render_log_metadata() {
711        let mut event = Event::Log(LogEvent::from("hello world"));
712        event
713            .as_mut_log()
714            .insert(metadata_path!("metadata_key"), "metadata_value");
715        let template = Template::try_from("{{%metadata_key}}").unwrap();
716
717        assert_eq!(Ok(Bytes::from("metadata_value")), template.render(&event))
718    }
719
720    #[test]
721    fn render_log_dynamic_with_prefix() {
722        let mut event = Event::Log(LogEvent::from("hello world"));
723        event.as_mut_log().insert("log_stream", "stream");
724        let template = Template::try_from("abcd-{{log_stream}}").unwrap();
725
726        assert_eq!(Ok(Bytes::from("abcd-stream")), template.render(&event))
727    }
728
729    #[test]
730    fn render_log_dynamic_with_postfix() {
731        let mut event = Event::Log(LogEvent::from("hello world"));
732        event.as_mut_log().insert("log_stream", "stream");
733        let template = Template::try_from("{{log_stream}}-abcd").unwrap();
734
735        assert_eq!(Ok(Bytes::from("stream-abcd")), template.render(&event))
736    }
737
738    #[test]
739    fn render_log_dynamic_missing_key() {
740        let event = Event::Log(LogEvent::from("hello world"));
741        let template = Template::try_from("{{log_stream}}-{{foo}}").unwrap();
742
743        assert_eq!(
744            Err(TemplateRenderingError::MissingKeys {
745                missing_keys: vec!["log_stream".to_string(), "foo".to_string()]
746            }),
747            template.render(&event)
748        );
749    }
750
751    #[test]
752    fn render_log_dynamic_multiple_keys() {
753        let mut event = Event::Log(LogEvent::from("hello world"));
754        event.as_mut_log().insert("foo", "bar");
755        event.as_mut_log().insert("baz", "quux");
756        let template = Template::try_from("stream-{{foo}}-{{baz}}.log").unwrap();
757
758        assert_eq!(
759            Ok(Bytes::from("stream-bar-quux.log")),
760            template.render(&event)
761        )
762    }
763
764    #[test]
765    fn render_log_dynamic_weird_junk() {
766        let mut event = Event::Log(LogEvent::from("hello world"));
767        event.as_mut_log().insert("foo", "bar");
768        event.as_mut_log().insert("baz", "quux");
769        let template = Template::try_from(r"{stream}{\{{}}}-{{foo}}-{{baz}}.log").unwrap();
770
771        assert_eq!(
772            Ok(Bytes::from(r"{stream}{\{{}}}-bar-quux.log")),
773            template.render(&event)
774        )
775    }
776
777    #[test]
778    fn render_log_timestamp_strftime_style() {
779        let ts = Utc
780            .with_ymd_and_hms(2001, 2, 3, 4, 5, 6)
781            .single()
782            .expect("invalid timestamp");
783
784        let mut event = Event::Log(LogEvent::from("hello world"));
785        event
786            .as_mut_log()
787            .insert(log_schema().timestamp_key_target_path().unwrap(), ts);
788
789        let template = Template::try_from("abcd-%F").unwrap();
790
791        assert_eq!(Ok(Bytes::from("abcd-2001-02-03")), template.render(&event))
792    }
793
794    #[test]
795    fn render_log_timestamp_strftime_style_namespace() {
796        let ts = Utc
797            .with_ymd_and_hms(2001, 2, 3, 4, 5, 6)
798            .single()
799            .expect("invalid timestamp");
800
801        let mut event = Event::Log(LogEvent::from("hello world"));
802        event.as_mut_log().insert("@timestamp", ts);
803        // use Vector namespace instead of legacy
804        LogNamespace::Vector.insert_vector_metadata(event.as_mut_log(), Some("foo"), "foo", "bar");
805        let new_schema = event
806            .as_mut_log()
807            .metadata()
808            .schema_definition()
809            .as_ref()
810            .clone()
811            .with_meaning(parse_target_path("@timestamp").unwrap(), "timestamp");
812        event
813            .as_mut_log()
814            .metadata_mut()
815            .set_schema_definition(&std::sync::Arc::new(new_schema));
816
817        let template = Template::try_from("abcd-%F").unwrap();
818
819        assert_eq!(Ok(Bytes::from("abcd-2001-02-03")), template.render(&event))
820    }
821
822    #[test]
823    fn render_log_timestamp_multiple_strftime_style() {
824        let ts = Utc
825            .with_ymd_and_hms(2001, 2, 3, 4, 5, 6)
826            .single()
827            .expect("invalid timestamp");
828
829        let mut event = Event::Log(LogEvent::from("hello world"));
830        event
831            .as_mut_log()
832            .insert(log_schema().timestamp_key_target_path().unwrap(), ts);
833
834        let template = Template::try_from("abcd-%F_%T").unwrap();
835
836        assert_eq!(
837            Ok(Bytes::from("abcd-2001-02-03_04:05:06")),
838            template.render(&event)
839        )
840    }
841
842    #[test]
843    fn render_log_dynamic_with_strftime() {
844        let ts = Utc
845            .with_ymd_and_hms(2001, 2, 3, 4, 5, 6)
846            .single()
847            .expect("invalid timestamp");
848
849        let mut event = Event::Log(LogEvent::from("hello world"));
850        event.as_mut_log().insert("foo", "butts");
851        event.as_mut_log().insert(
852            (PathPrefix::Event, log_schema().timestamp_key().unwrap()),
853            ts,
854        );
855
856        let template = Template::try_from("{{ foo }}-%F_%T").unwrap();
857
858        assert_eq!(
859            Ok(Bytes::from("butts-2001-02-03_04:05:06")),
860            template.render(&event)
861        )
862    }
863
864    #[test]
865    fn render_log_dynamic_with_nested_strftime() {
866        let ts = Utc
867            .with_ymd_and_hms(2001, 2, 3, 4, 5, 6)
868            .single()
869            .expect("invalid timestamp");
870
871        let mut event = Event::Log(LogEvent::from("hello world"));
872        event.as_mut_log().insert("format", "%F");
873        event.as_mut_log().insert(
874            (PathPrefix::Event, log_schema().timestamp_key().unwrap()),
875            ts,
876        );
877
878        let template = Template::try_from("nested {{ format }} %T").unwrap();
879
880        assert_eq!(
881            Ok(Bytes::from("nested %F 04:05:06")),
882            template.render(&event)
883        )
884    }
885
886    #[test]
887    fn render_log_dynamic_with_reverse_nested_strftime() {
888        let ts = Utc
889            .with_ymd_and_hms(2001, 2, 3, 4, 5, 6)
890            .single()
891            .expect("invalid timestamp");
892
893        let mut event = Event::Log(LogEvent::from("hello world"));
894        event.as_mut_log().insert("\"%F\"", "foo");
895        event.as_mut_log().insert(
896            (PathPrefix::Event, log_schema().timestamp_key().unwrap()),
897            ts,
898        );
899
900        let template = Template::try_from("nested {{ \"%F\" }} %T").unwrap();
901
902        assert_eq!(
903            Ok(Bytes::from("nested foo 04:05:06")),
904            template.render(&event)
905        )
906    }
907
908    #[test]
909    fn render_metric_timestamp() {
910        let template = Template::try_from("timestamp %F %T").unwrap();
911
912        assert_eq!(
913            Ok(Bytes::from("timestamp 2002-03-04 05:06:07")),
914            template.render(&sample_metric())
915        );
916    }
917
918    #[test]
919    fn render_metric_with_tags() {
920        let template = Template::try_from("name={{name}} component={{tags.component}}").unwrap();
921        let metric = sample_metric().with_tags(Some(metric_tags!(
922            "test" => "true",
923            "component" => "template",
924        )));
925        assert_eq!(
926            Ok(Bytes::from("name=a-counter component=template")),
927            template.render(&metric)
928        );
929    }
930
931    #[test]
932    fn render_metric_without_tags() {
933        let template = Template::try_from("name={{name}} component={{tags.component}}").unwrap();
934        assert_eq!(
935            Err(TemplateRenderingError::MissingKeys {
936                missing_keys: vec!["tags.component".into()]
937            }),
938            template.render(&sample_metric())
939        );
940    }
941
942    #[test]
943    fn render_metric_with_namespace() {
944        let template = Template::try_from("namespace={{namespace}} name={{name}}").unwrap();
945        let metric = sample_metric().with_namespace(Some("vector-test"));
946        assert_eq!(
947            Ok(Bytes::from("namespace=vector-test name=a-counter")),
948            template.render(&metric)
949        );
950    }
951
952    #[test]
953    fn render_metric_without_namespace() {
954        let template = Template::try_from("namespace={{namespace}} name={{name}}").unwrap();
955        let metric = sample_metric();
956        assert_eq!(
957            Err(TemplateRenderingError::MissingKeys {
958                missing_keys: vec!["namespace".into()]
959            }),
960            template.render(&metric)
961        );
962    }
963
964    #[test]
965    fn render_log_with_timezone() {
966        let ts = Utc.with_ymd_and_hms(2001, 2, 3, 4, 5, 6).unwrap();
967
968        let template = Template::try_from("vector-%Y-%m-%d-%H.log").unwrap();
969        let mut event = Event::Log(LogEvent::from("hello world"));
970        event.as_mut_log().insert(
971            (PathPrefix::Event, log_schema().timestamp_key().unwrap()),
972            ts,
973        );
974
975        let tz: Tz = "Asia/Singapore".parse().unwrap();
976        let offset = Some(Utc::now().with_timezone(&tz).offset().fix());
977        assert_eq!(
978            Ok(Bytes::from("vector-2001-02-03-12.log")),
979            template.with_tz_offset(offset).render(&event)
980        );
981    }
982
983    #[test]
984    fn render_log_unsigned_int_with_timezone() {
985        let ts = Utc.with_ymd_and_hms(2001, 2, 3, 4, 5, 6).unwrap();
986
987        let template = UnsignedIntTemplate::try_from("%Y%m%d%H").unwrap();
988        let mut event = Event::Log(LogEvent::from("hello world"));
989        event.as_mut_log().insert(event_path!("timestamp"), ts);
990
991        let tz: Tz = "Asia/Singapore".parse().unwrap();
992        let offset = Some(Utc::now().with_timezone(&tz).offset().fix());
993
994        assert_eq!(
995            Ok(2001020312),
996            template.with_tz_offset(offset).render(&event)
997        );
998    }
999
1000    fn sample_metric() -> Metric {
1001        Metric::new(
1002            "a-counter",
1003            MetricKind::Absolute,
1004            MetricValue::Counter { value: 1.1 },
1005        )
1006        .with_timestamp(Some(
1007            Utc.with_ymd_and_hms(2002, 3, 4, 5, 6, 7)
1008                .single()
1009                .expect("invalid timestamp"),
1010        ))
1011    }
1012
1013    #[test]
1014    fn strftime_error() {
1015        assert_eq!(
1016            Template::try_from("%E").unwrap_err(),
1017            TemplateParseError::StrftimeError
1018        );
1019    }
1020
1021    #[test]
1022    fn strftime_non_int_result() {
1023        let template = UnsignedIntTemplate::try_from("a-%s").unwrap();
1024        let ts = Utc.with_ymd_and_hms(2001, 2, 3, 4, 5, 6).unwrap();
1025
1026        let mut event = Event::Log(LogEvent::from("hello world"));
1027        event.as_mut_log().insert(event_path!("timestamp"), ts);
1028
1029        assert_eq!(
1030            Err(TemplateRenderingError::NotNumeric {
1031                input: "a-981173106".to_owned()
1032            }),
1033            template.render(&event)
1034        );
1035    }
1036}