Skip to main content

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            #[expect(
555                clippy::string_slice,
556                reason = "indices come from regex match positions, always char boundaries"
557            )]
558            parts.push(parse_literal(&src[last_end..all.start()])?);
559        }
560
561        let path = cap[1].trim().to_owned();
562
563        // This checks the syntax, but doesn't yet store it for use later
564        // see: https://github.com/vectordotdev/vector/issues/14864
565        if parse_target_path(&path).is_err() {
566            return Err(TemplateParseError::InvalidPathSyntax { path });
567        }
568
569        parts.push(Part::Reference(path));
570        last_end = all.end();
571    }
572    if src.len() > last_end {
573        #[expect(
574            clippy::string_slice,
575            reason = "last_end comes from a regex match end position, always a char boundary"
576        )]
577        parts.push(parse_literal(&src[last_end..])?);
578    }
579
580    Ok(parts)
581}
582
583fn render_metric_field<'a>(key: &str, metric: &'a Metric) -> Option<&'a str> {
584    match key {
585        "name" => Some(metric.name()),
586        "namespace" => metric.namespace(),
587        _ if let Some(tag_key) = key.strip_prefix("tags.") => {
588            metric.tags().and_then(|tags| tags.get(tag_key))
589        }
590        _ => None,
591    }
592}
593
594fn render_timestamp(
595    items: &ParsedStrftime,
596    event: EventRef<'_>,
597    tz_offset: Option<FixedOffset>,
598) -> String {
599    let timestamp = match event {
600        EventRef::Log(log) => log.get_timestamp().and_then(Value::as_timestamp).copied(),
601        EventRef::Metric(metric) => metric.timestamp(),
602        EventRef::Trace(trace) => {
603            log_schema()
604                .timestamp_key_target_path()
605                .and_then(|timestamp_key| {
606                    trace
607                        .get(timestamp_key)
608                        .and_then(Value::as_timestamp)
609                        .copied()
610                })
611        }
612    }
613    .unwrap_or_else(Utc::now);
614
615    match tz_offset {
616        Some(offset) => timestamp
617            .with_timezone(&offset)
618            .format_with_items(items.as_items())
619            .to_string(),
620        None => timestamp
621            .with_timezone(&chrono::Utc)
622            .format_with_items(items.as_items())
623            .to_string(),
624    }
625}
626
627#[cfg(test)]
628mod tests {
629    use chrono::{Offset, TimeZone, Utc};
630    use chrono_tz::Tz;
631    use vector_lib::{
632        config::LogNamespace,
633        lookup::{PathPrefix, metadata_path},
634        metric_tags,
635    };
636    use vrl::event_path;
637
638    use super::*;
639    use crate::event::{Event, LogEvent, MetricKind, MetricValue};
640
641    #[test]
642    fn get_fields() {
643        let f1 = Template::try_from("{{ foo }}")
644            .unwrap()
645            .get_fields()
646            .unwrap();
647        let f2 = Template::try_from("{{ foo }}-{{ bar }}")
648            .unwrap()
649            .get_fields()
650            .unwrap();
651        let f3 = Template::try_from("nofield").unwrap().get_fields();
652        let f4 = Template::try_from("%F").unwrap().get_fields();
653        let f5 = UnsignedIntTemplate::try_from("{{ foo }}-{{ bar }}")
654            .unwrap()
655            .get_fields()
656            .unwrap();
657        let f6 = UnsignedIntTemplate::from(123u64).get_fields();
658        let f7 = UnsignedIntTemplate::try_from("%s").unwrap().get_fields();
659
660        assert_eq!(f1, vec!["foo"]);
661        assert_eq!(f2, vec!["foo", "bar"]);
662        assert_eq!(f3, None);
663        assert_eq!(f4, None);
664        assert_eq!(f5, vec!["foo", "bar"]);
665        assert_eq!(f6, None);
666        assert_eq!(f7, None);
667    }
668
669    #[test]
670    fn is_dynamic() {
671        assert!(Template::try_from("/kube-demo/%F").unwrap().is_dynamic());
672        assert!(!Template::try_from("/kube-demo/echo").unwrap().is_dynamic());
673        assert!(
674            Template::try_from("/kube-demo/{{ foo }}")
675                .unwrap()
676                .is_dynamic()
677        );
678        assert!(
679            Template::try_from("/kube-demo/{{ foo }}/%F")
680                .unwrap()
681                .is_dynamic()
682        );
683    }
684
685    #[test]
686    fn render_log_static() {
687        let event = Event::Log(LogEvent::from("hello world"));
688        let template = Template::try_from("foo").unwrap();
689
690        assert_eq!(Ok(Bytes::from("foo")), template.render(&event))
691    }
692
693    #[test]
694    fn render_log_unsigned_number() {
695        let event = Event::Log(LogEvent::from("hello world"));
696        let template = UnsignedIntTemplate::from(123);
697
698        assert_eq!(Ok(123), template.render(&event))
699    }
700
701    #[test]
702    fn render_log_unsigned_number_dynamic() {
703        let mut event = Event::Log(LogEvent::from("hello world"));
704        event.as_mut_log().insert("foo", 123);
705
706        let template = UnsignedIntTemplate::try_from("{{ foo }}").unwrap();
707        assert_eq!(Ok(123), template.render(&event))
708    }
709
710    #[test]
711    fn render_log_dynamic() {
712        let mut event = Event::Log(LogEvent::from("hello world"));
713        event.as_mut_log().insert("log_stream", "stream");
714        let template = Template::try_from("{{log_stream}}").unwrap();
715
716        assert_eq!(Ok(Bytes::from("stream")), template.render(&event))
717    }
718
719    #[test]
720    fn render_log_metadata() {
721        let mut event = Event::Log(LogEvent::from("hello world"));
722        event
723            .as_mut_log()
724            .insert(metadata_path!("metadata_key"), "metadata_value");
725        let template = Template::try_from("{{%metadata_key}}").unwrap();
726
727        assert_eq!(Ok(Bytes::from("metadata_value")), template.render(&event))
728    }
729
730    #[test]
731    fn render_log_dynamic_with_prefix() {
732        let mut event = Event::Log(LogEvent::from("hello world"));
733        event.as_mut_log().insert("log_stream", "stream");
734        let template = Template::try_from("abcd-{{log_stream}}").unwrap();
735
736        assert_eq!(Ok(Bytes::from("abcd-stream")), template.render(&event))
737    }
738
739    #[test]
740    fn render_log_dynamic_with_postfix() {
741        let mut event = Event::Log(LogEvent::from("hello world"));
742        event.as_mut_log().insert("log_stream", "stream");
743        let template = Template::try_from("{{log_stream}}-abcd").unwrap();
744
745        assert_eq!(Ok(Bytes::from("stream-abcd")), template.render(&event))
746    }
747
748    #[test]
749    fn render_log_dynamic_missing_key() {
750        let event = Event::Log(LogEvent::from("hello world"));
751        let template = Template::try_from("{{log_stream}}-{{foo}}").unwrap();
752
753        assert_eq!(
754            Err(TemplateRenderingError::MissingKeys {
755                missing_keys: vec!["log_stream".to_string(), "foo".to_string()]
756            }),
757            template.render(&event)
758        );
759    }
760
761    #[test]
762    fn render_log_dynamic_multiple_keys() {
763        let mut event = Event::Log(LogEvent::from("hello world"));
764        event.as_mut_log().insert("foo", "bar");
765        event.as_mut_log().insert("baz", "quux");
766        let template = Template::try_from("stream-{{foo}}-{{baz}}.log").unwrap();
767
768        assert_eq!(
769            Ok(Bytes::from("stream-bar-quux.log")),
770            template.render(&event)
771        )
772    }
773
774    #[test]
775    fn render_log_dynamic_weird_junk() {
776        let mut event = Event::Log(LogEvent::from("hello world"));
777        event.as_mut_log().insert("foo", "bar");
778        event.as_mut_log().insert("baz", "quux");
779        let template = Template::try_from(r"{stream}{\{{}}}-{{foo}}-{{baz}}.log").unwrap();
780
781        assert_eq!(
782            Ok(Bytes::from(r"{stream}{\{{}}}-bar-quux.log")),
783            template.render(&event)
784        )
785    }
786
787    #[test]
788    fn render_log_timestamp_strftime_style() {
789        let ts = Utc
790            .with_ymd_and_hms(2001, 2, 3, 4, 5, 6)
791            .single()
792            .expect("invalid timestamp");
793
794        let mut event = Event::Log(LogEvent::from("hello world"));
795        event
796            .as_mut_log()
797            .insert(log_schema().timestamp_key_target_path().unwrap(), ts);
798
799        let template = Template::try_from("abcd-%F").unwrap();
800
801        assert_eq!(Ok(Bytes::from("abcd-2001-02-03")), template.render(&event))
802    }
803
804    #[test]
805    fn render_log_timestamp_strftime_style_namespace() {
806        let ts = Utc
807            .with_ymd_and_hms(2001, 2, 3, 4, 5, 6)
808            .single()
809            .expect("invalid timestamp");
810
811        let mut event = Event::Log(LogEvent::from("hello world"));
812        event.as_mut_log().insert("@timestamp", ts);
813        // use Vector namespace instead of legacy
814        LogNamespace::Vector.insert_vector_metadata(event.as_mut_log(), Some("foo"), "foo", "bar");
815        let new_schema = event
816            .as_mut_log()
817            .metadata()
818            .schema_definition()
819            .as_ref()
820            .clone()
821            .with_meaning(parse_target_path("@timestamp").unwrap(), "timestamp");
822        event
823            .as_mut_log()
824            .metadata_mut()
825            .set_schema_definition(&std::sync::Arc::new(new_schema));
826
827        let template = Template::try_from("abcd-%F").unwrap();
828
829        assert_eq!(Ok(Bytes::from("abcd-2001-02-03")), template.render(&event))
830    }
831
832    #[test]
833    fn render_log_timestamp_multiple_strftime_style() {
834        let ts = Utc
835            .with_ymd_and_hms(2001, 2, 3, 4, 5, 6)
836            .single()
837            .expect("invalid timestamp");
838
839        let mut event = Event::Log(LogEvent::from("hello world"));
840        event
841            .as_mut_log()
842            .insert(log_schema().timestamp_key_target_path().unwrap(), ts);
843
844        let template = Template::try_from("abcd-%F_%T").unwrap();
845
846        assert_eq!(
847            Ok(Bytes::from("abcd-2001-02-03_04:05:06")),
848            template.render(&event)
849        )
850    }
851
852    #[test]
853    fn render_log_dynamic_with_strftime() {
854        let ts = Utc
855            .with_ymd_and_hms(2001, 2, 3, 4, 5, 6)
856            .single()
857            .expect("invalid timestamp");
858
859        let mut event = Event::Log(LogEvent::from("hello world"));
860        event.as_mut_log().insert("foo", "butts");
861        event.as_mut_log().insert(
862            (PathPrefix::Event, log_schema().timestamp_key().unwrap()),
863            ts,
864        );
865
866        let template = Template::try_from("{{ foo }}-%F_%T").unwrap();
867
868        assert_eq!(
869            Ok(Bytes::from("butts-2001-02-03_04:05:06")),
870            template.render(&event)
871        )
872    }
873
874    #[test]
875    fn render_log_dynamic_with_nested_strftime() {
876        let ts = Utc
877            .with_ymd_and_hms(2001, 2, 3, 4, 5, 6)
878            .single()
879            .expect("invalid timestamp");
880
881        let mut event = Event::Log(LogEvent::from("hello world"));
882        event.as_mut_log().insert("format", "%F");
883        event.as_mut_log().insert(
884            (PathPrefix::Event, log_schema().timestamp_key().unwrap()),
885            ts,
886        );
887
888        let template = Template::try_from("nested {{ format }} %T").unwrap();
889
890        assert_eq!(
891            Ok(Bytes::from("nested %F 04:05:06")),
892            template.render(&event)
893        )
894    }
895
896    #[test]
897    fn render_log_dynamic_with_reverse_nested_strftime() {
898        let ts = Utc
899            .with_ymd_and_hms(2001, 2, 3, 4, 5, 6)
900            .single()
901            .expect("invalid timestamp");
902
903        let mut event = Event::Log(LogEvent::from("hello world"));
904        event.as_mut_log().insert("\"%F\"", "foo");
905        event.as_mut_log().insert(
906            (PathPrefix::Event, log_schema().timestamp_key().unwrap()),
907            ts,
908        );
909
910        let template = Template::try_from("nested {{ \"%F\" }} %T").unwrap();
911
912        assert_eq!(
913            Ok(Bytes::from("nested foo 04:05:06")),
914            template.render(&event)
915        )
916    }
917
918    #[test]
919    fn render_metric_timestamp() {
920        let template = Template::try_from("timestamp %F %T").unwrap();
921
922        assert_eq!(
923            Ok(Bytes::from("timestamp 2002-03-04 05:06:07")),
924            template.render(&sample_metric())
925        );
926    }
927
928    #[test]
929    fn render_metric_with_tags() {
930        let template = Template::try_from("name={{name}} component={{tags.component}}").unwrap();
931        let metric = sample_metric().with_tags(Some(metric_tags!(
932            "test" => "true",
933            "component" => "template",
934        )));
935        assert_eq!(
936            Ok(Bytes::from("name=a-counter component=template")),
937            template.render(&metric)
938        );
939    }
940
941    #[test]
942    fn render_metric_without_tags() {
943        let template = Template::try_from("name={{name}} component={{tags.component}}").unwrap();
944        assert_eq!(
945            Err(TemplateRenderingError::MissingKeys {
946                missing_keys: vec!["tags.component".into()]
947            }),
948            template.render(&sample_metric())
949        );
950    }
951
952    #[test]
953    fn render_metric_with_namespace() {
954        let template = Template::try_from("namespace={{namespace}} name={{name}}").unwrap();
955        let metric = sample_metric().with_namespace(Some("vector-test"));
956        assert_eq!(
957            Ok(Bytes::from("namespace=vector-test name=a-counter")),
958            template.render(&metric)
959        );
960    }
961
962    #[test]
963    fn render_metric_without_namespace() {
964        let template = Template::try_from("namespace={{namespace}} name={{name}}").unwrap();
965        let metric = sample_metric();
966        assert_eq!(
967            Err(TemplateRenderingError::MissingKeys {
968                missing_keys: vec!["namespace".into()]
969            }),
970            template.render(&metric)
971        );
972    }
973
974    #[test]
975    fn render_log_with_timezone() {
976        let ts = Utc.with_ymd_and_hms(2001, 2, 3, 4, 5, 6).unwrap();
977
978        let template = Template::try_from("vector-%Y-%m-%d-%H.log").unwrap();
979        let mut event = Event::Log(LogEvent::from("hello world"));
980        event.as_mut_log().insert(
981            (PathPrefix::Event, log_schema().timestamp_key().unwrap()),
982            ts,
983        );
984
985        let tz: Tz = "Asia/Singapore".parse().unwrap();
986        let offset = Some(Utc::now().with_timezone(&tz).offset().fix());
987        assert_eq!(
988            Ok(Bytes::from("vector-2001-02-03-12.log")),
989            template.with_tz_offset(offset).render(&event)
990        );
991    }
992
993    #[test]
994    fn render_log_unsigned_int_with_timezone() {
995        let ts = Utc.with_ymd_and_hms(2001, 2, 3, 4, 5, 6).unwrap();
996
997        let template = UnsignedIntTemplate::try_from("%Y%m%d%H").unwrap();
998        let mut event = Event::Log(LogEvent::from("hello world"));
999        event.as_mut_log().insert(event_path!("timestamp"), ts);
1000
1001        let tz: Tz = "Asia/Singapore".parse().unwrap();
1002        let offset = Some(Utc::now().with_timezone(&tz).offset().fix());
1003
1004        assert_eq!(
1005            Ok(2001020312),
1006            template.with_tz_offset(offset).render(&event)
1007        );
1008    }
1009
1010    fn sample_metric() -> Metric {
1011        Metric::new(
1012            "a-counter",
1013            MetricKind::Absolute,
1014            MetricValue::Counter { value: 1.1 },
1015        )
1016        .with_timestamp(Some(
1017            Utc.with_ymd_and_hms(2002, 3, 4, 5, 6, 7)
1018                .single()
1019                .expect("invalid timestamp"),
1020        ))
1021    }
1022
1023    #[test]
1024    fn strftime_error() {
1025        assert_eq!(
1026            Template::try_from("%E").unwrap_err(),
1027            TemplateParseError::StrftimeError
1028        );
1029    }
1030
1031    #[test]
1032    fn strftime_non_int_result() {
1033        let template = UnsignedIntTemplate::try_from("a-%s").unwrap();
1034        let ts = Utc.with_ymd_and_hms(2001, 2, 3, 4, 5, 6).unwrap();
1035
1036        let mut event = Event::Log(LogEvent::from("hello world"));
1037        event.as_mut_log().insert(event_path!("timestamp"), ts);
1038
1039        assert_eq!(
1040            Err(TemplateRenderingError::NotNumeric {
1041                input: "a-981173106".to_owned()
1042            }),
1043            template.render(&event)
1044        );
1045    }
1046}