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