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 parts.push(parse_literal(&src[last_end..all.start()])?);
555 }
556
557 let path = cap[1].trim().to_owned();
558
559 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 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}