1use 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#[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#[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#[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 let reserve_size = parts
115 .iter()
116 .map(|part| match part {
117 Part::Literal(lit) => lit.len(),
118 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
148impl ConfigurableString for Template {}
150
151impl Template {
152 pub const fn with_tz_offset(mut self, tz_offset: Option<FixedOffset>) -> Self {
154 self.tz_offset = tz_offset;
155 self
156 }
157 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 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 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)] pub fn get_ref(&self) -> &str {
235 &self.src
236 }
237
238 pub const fn is_empty(&self) -> bool {
240 self.src.is_empty()
241 }
242
243 pub const fn is_dynamic(&self) -> bool {
245 !self.is_static
246 }
247}
248
249#[derive(Clone, Debug, Eq, Hash, PartialEq)]
251#[configurable_component]
252#[serde(untagged)]
253enum UnsignedIntTemplateSource {
254 Number(u64),
256 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#[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 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 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 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#[derive(Clone, Debug, Eq, Hash, PartialEq)]
468enum Part {
469 Literal(String),
471 Strftime(ParsedStrftime),
473 Reference(String),
475}
476
477#[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 Item::Space(space) => Item::OwnedSpace(space.into()),
488 Item::Literal(lit) => Item::OwnedLiteral(lit.into()),
489 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
547fn 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 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 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}