1use bytes::{BufMut, BytesMut};
2use chrono::{DateTime, SecondsFormat, SubsecRound, Utc};
3use lookup::lookup_v2::ConfigTargetPath;
4use serde_json;
5use std::borrow::Cow;
6use std::collections::BTreeMap;
7use std::str::FromStr;
8use strum::{EnumString, FromRepr, VariantNames};
9use tokio_util::codec::Encoder;
10use tracing::debug;
11use vector_config::configurable_component;
12use vector_core::{
13 config::DataType,
14 event::{Event, LogEvent, Value},
15 schema,
16};
17use vrl::event_path;
18use vrl::value::ObjectMap;
19
20#[configurable_component]
22#[derive(Clone, Debug, Default)]
23#[serde(default)]
24pub struct SyslogSerializerConfig {
25 pub syslog: SyslogSerializerOptions,
27}
28
29impl SyslogSerializerConfig {
30 pub fn build(&self) -> SyslogSerializer {
32 SyslogSerializer::new(self)
33 }
34
35 pub fn input_type(&self) -> DataType {
37 DataType::Log
38 }
39
40 pub fn schema_requirement(&self) -> schema::Requirement {
42 schema::Requirement::empty()
43 }
44}
45
46#[configurable_component]
48#[derive(Clone, Debug, Default)]
49#[serde(default, deny_unknown_fields)]
50pub struct SyslogSerializerOptions {
51 rfc: SyslogRFC,
53 facility: Option<ConfigTargetPath>,
55 severity: Option<ConfigTargetPath>,
57 app_name: Option<ConfigTargetPath>,
62 proc_id: Option<ConfigTargetPath>,
64 msg_id: Option<ConfigTargetPath>,
66}
67
68#[derive(Debug, Clone)]
70pub struct SyslogSerializer {
71 config: SyslogSerializerConfig,
72}
73
74impl SyslogSerializer {
75 pub fn new(conf: &SyslogSerializerConfig) -> Self {
77 Self {
78 config: conf.clone(),
79 }
80 }
81}
82
83impl Encoder<Event> for SyslogSerializer {
84 type Error = vector_common::Error;
85
86 fn encode(&mut self, event: Event, buffer: &mut BytesMut) -> Result<(), Self::Error> {
87 if let Event::Log(log_event) = event {
88 let syslog_message = ConfigDecanter::new(&log_event).decant_config(&self.config.syslog);
89 let encoded = syslog_message.encode(&self.config.syslog.rfc);
90 buffer.put_slice(encoded.as_bytes());
91 }
92
93 Ok(())
94 }
95}
96
97struct ConfigDecanter<'a> {
98 log: &'a LogEvent,
99}
100
101impl<'a> ConfigDecanter<'a> {
102 fn new(log: &'a LogEvent) -> Self {
103 Self { log }
104 }
105
106 fn decant_config(&self, config: &SyslogSerializerOptions) -> SyslogMessage {
107 let mut app_name = self
108 .get_value(&config.app_name) .unwrap_or_else(|| {
110 self.log
112 .get_by_meaning("service")
113 .map(|v| v.to_string_lossy().to_string())
114 .unwrap_or_else(|| "vector".to_owned())
116 });
117 let mut proc_id = self.get_value(&config.proc_id);
118 let mut msg_id = self.get_value(&config.msg_id);
119
120 match config.rfc {
121 SyslogRFC::Rfc3164 => {
122 app_name = sanitize_to_ascii(&app_name).into_owned();
124 if let Some(pid) = &mut proc_id {
125 *pid = sanitize_to_ascii(pid).into_owned();
126 }
127 }
128 SyslogRFC::Rfc5424 => {
129 truncate_chars(&mut app_name, 48);
131 if let Some(pid) = &mut proc_id {
132 truncate_chars(pid, 128);
133 }
134 if let Some(mid) = &mut msg_id {
135 truncate_chars(mid, 32);
136 }
137 }
138 }
139
140 SyslogMessage {
141 pri: Pri {
142 facility: self.get_facility(config),
143 severity: self.get_severity(config),
144 },
145 timestamp: self.get_timestamp(),
146 hostname: self.log.get_host().map(|v| v.to_string_lossy().to_string()),
147 tag: Tag {
148 app_name,
149 proc_id,
150 msg_id,
151 },
152 structured_data: self.get_structured_data(),
153 message: self.get_payload(),
154 }
155 }
156
157 fn get_value(&self, path: &Option<ConfigTargetPath>) -> Option<String> {
158 path.as_ref()
159 .and_then(|p| self.log.get(p).cloned())
160 .map(|v| v.to_string_lossy().to_string())
161 }
162
163 fn get_structured_data(&self) -> Option<StructuredData> {
164 self.log
165 .get(event_path!("structured_data"))
166 .and_then(|v| v.clone().into_object())
167 .map(StructuredData::from)
168 }
169
170 fn get_timestamp(&self) -> DateTime<Utc> {
171 if let Some(Value::Timestamp(timestamp)) = self.log.get_timestamp() {
172 return *timestamp;
173 }
174 Utc::now()
175 }
176
177 fn get_payload(&self) -> String {
178 self.log
179 .get_message()
180 .map(|v| v.to_string_lossy().to_string())
181 .unwrap_or_default()
182 }
183
184 fn get_facility(&self, config: &SyslogSerializerOptions) -> Facility {
185 config.facility.as_ref().map_or(Facility::User, |path| {
186 self.get_syslog_code(path, Facility::from_repr, Facility::User)
187 })
188 }
189
190 fn get_severity(&self, config: &SyslogSerializerOptions) -> Severity {
191 config
192 .severity
193 .as_ref()
194 .map_or(Severity::Informational, |path| {
195 self.get_syslog_code(path, Severity::from_repr, Severity::Informational)
196 })
197 }
198
199 fn get_syslog_code<T>(
200 &self,
201 path: &ConfigTargetPath,
202 from_repr_fn: fn(usize) -> Option<T>,
203 default_value: T,
204 ) -> T
205 where
206 T: Copy + FromStr,
207 {
208 if let Some(value) = self.log.get(path).cloned() {
209 let s = value.to_string_lossy();
210 if let Ok(val_from_name) = s.to_ascii_lowercase().parse::<T>() {
211 return val_from_name;
212 }
213 if let Value::Integer(n) = value
214 && let Some(val_from_num) = from_repr_fn(n as usize)
215 {
216 return val_from_num;
217 }
218 }
219 default_value
220 }
221}
222
223const NIL_VALUE: &str = "-";
224const SYSLOG_V1: &str = "1";
225const RFC3164_TAG_MAX_LENGTH: usize = 32;
226const SD_ID_MAX_LENGTH: usize = 32;
227
228#[inline]
230fn sanitize_with<F>(s: &str, is_valid: F) -> Cow<'_, str>
231where
232 F: Fn(char) -> bool,
233{
234 match s.char_indices().find(|(_, c)| !is_valid(*c)) {
235 None => Cow::Borrowed(s), Some((first_invalid_idx, _)) => {
237 let mut result = String::with_capacity(s.len());
238 let (valid_prefix, remainder) = s.split_at(first_invalid_idx);
239 result.push_str(valid_prefix);
240 for c in remainder.chars() {
241 result.push(if is_valid(c) { c } else { '_' });
242 }
243
244 Cow::Owned(result)
245 }
246 }
247}
248
249#[inline]
253fn sanitize_to_ascii(s: &str) -> Cow<'_, str> {
254 sanitize_with(s, |c| (' '..='~').contains(&c))
255}
256
257#[inline]
262fn sanitize_name(name: &str) -> Cow<'_, str> {
263 sanitize_with(name, |c| {
264 c.is_ascii_graphic() && !matches!(c, '=' | ']' | '"')
265 })
266}
267
268fn escape_sd_value(s: &str) -> Cow<'_, str> {
270 let needs_escaping = s.chars().any(|c| matches!(c, '\\' | '"' | ']'));
271
272 if !needs_escaping {
273 return Cow::Borrowed(s);
274 }
275
276 let mut result = String::with_capacity(s.len() + 10);
277 for ch in s.chars() {
278 match ch {
279 '\\' => result.push_str("\\\\"),
280 '"' => result.push_str("\\\""),
281 ']' => result.push_str("\\]"),
282 _ => result.push(ch),
283 }
284 }
285
286 Cow::Owned(result)
287}
288
289fn truncate_chars(s: &mut String, max_chars: usize) {
293 if let Some((byte_idx, _)) = s.char_indices().nth(max_chars) {
294 s.truncate(byte_idx);
295 }
296}
297
298#[configurable_component]
300#[derive(PartialEq, Clone, Debug, Default)]
301#[serde(rename_all = "snake_case")]
302pub enum SyslogRFC {
303 Rfc3164,
305 #[default]
307 Rfc5424,
308}
309
310#[derive(Default, Debug)]
311struct SyslogMessage {
312 pri: Pri,
313 timestamp: DateTime<Utc>,
314 hostname: Option<String>,
315 tag: Tag,
316 structured_data: Option<StructuredData>,
317 message: String,
318}
319
320impl SyslogMessage {
321 fn encode(&self, rfc: &SyslogRFC) -> String {
322 let mut result = String::with_capacity(256);
323
324 result.push_str(&self.pri.encode().to_string());
325
326 if *rfc == SyslogRFC::Rfc5424 {
327 result.push_str(SYSLOG_V1);
328 result.push(' ');
329 }
330
331 match rfc {
332 SyslogRFC::Rfc3164 => {
333 result.push_str(&format!("{} ", self.timestamp.format("%b %e %H:%M:%S")));
334 }
335 SyslogRFC::Rfc5424 => {
336 result.push_str(
337 &self
338 .timestamp
339 .round_subsecs(6)
340 .to_rfc3339_opts(SecondsFormat::Micros, true),
341 );
342 result.push(' ');
343 }
344 }
345
346 result.push_str(self.hostname.as_deref().unwrap_or(NIL_VALUE));
347 result.push(' ');
348
349 match rfc {
350 SyslogRFC::Rfc3164 => result.push_str(&self.tag.encode_rfc_3164()),
351 SyslogRFC::Rfc5424 => result.push_str(&self.tag.encode_rfc_5424()),
352 }
353 result.push(' ');
354
355 if *rfc == SyslogRFC::Rfc3164 {
356 if let Some(sd) = &self.structured_data
358 && !sd.elements.is_empty()
359 {
360 debug!(
361 "Structured data present but ignored - RFC 3164 does not support structured data. Consider using RFC 5424 instead."
362 );
363 }
364 } else {
365 if let Some(sd) = &self.structured_data {
366 result.push_str(&sd.encode());
367 } else {
368 result.push_str(NIL_VALUE);
369 }
370 if !self.message.is_empty() {
371 result.push(' ');
372 }
373 }
374
375 if !self.message.is_empty() {
376 if *rfc == SyslogRFC::Rfc3164 {
377 result.push_str(&Self::sanitize_rfc3164_message(&self.message));
378 } else {
379 result.push_str(&self.message);
380 }
381 }
382
383 result
384 }
385
386 fn sanitize_rfc3164_message(message: &str) -> String {
387 message
388 .chars()
389 .map(|ch| if (' '..='~').contains(&ch) { ch } else { ' ' })
390 .collect()
391 }
392}
393
394#[derive(Default, Debug)]
395struct Tag {
396 app_name: String,
397 proc_id: Option<String>,
398 msg_id: Option<String>,
399}
400
401impl Tag {
402 fn encode_rfc_3164(&self) -> String {
403 let mut tag = if let Some(proc_id) = self.proc_id.as_deref() {
404 format!("{}[{}]:", self.app_name, proc_id)
405 } else {
406 format!("{}:", self.app_name)
407 };
408 if tag.chars().count() > RFC3164_TAG_MAX_LENGTH {
409 truncate_chars(&mut tag, RFC3164_TAG_MAX_LENGTH);
410 if !tag.ends_with(':') {
411 tag.pop();
412 tag.push(':');
413 }
414 }
415 tag
416 }
417
418 fn encode_rfc_5424(&self) -> String {
419 let proc_id_str = self.proc_id.as_deref().unwrap_or(NIL_VALUE);
420 let msg_id_str = self.msg_id.as_deref().unwrap_or(NIL_VALUE);
421 format!("{} {} {}", self.app_name, proc_id_str, msg_id_str)
422 }
423}
424
425type StructuredDataMap = BTreeMap<String, BTreeMap<String, String>>;
426#[derive(Debug, Default)]
427struct StructuredData {
428 elements: StructuredDataMap,
429}
430
431impl StructuredData {
432 fn encode(&self) -> String {
433 if self.elements.is_empty() {
434 NIL_VALUE.to_string()
435 } else {
436 self.elements
437 .iter()
438 .fold(String::new(), |mut acc, (sd_id, sd_params)| {
439 acc.push_str(&format!("[{sd_id}"));
440 for (key, value) in sd_params {
441 let esc_val = escape_sd_value(value);
442 acc.push_str(&format!(" {key}=\"{esc_val}\""));
443 }
444 acc.push(']');
445 acc
446 })
447 }
448 }
449}
450
451impl From<ObjectMap> for StructuredData {
452 fn from(fields: ObjectMap) -> Self {
453 let elements = fields
454 .into_iter()
455 .map(|(sd_id, value)| {
456 let sd_id_str: String = sd_id.into();
457 let sanitized_id = sanitize_name(&sd_id_str);
458
459 let final_id = if sanitized_id.chars().count() > SD_ID_MAX_LENGTH {
460 sanitized_id.chars().take(SD_ID_MAX_LENGTH).collect()
461 } else {
462 sanitized_id.into_owned()
463 };
464
465 let sd_params = match value {
466 Value::Object(obj) => {
467 let mut map = BTreeMap::new();
468 flatten_object(obj, String::new(), &mut map);
469 map
470 }
471 scalar => {
472 let mut map = BTreeMap::new();
473 map.insert("value".to_string(), scalar.to_string_lossy().to_string());
474 map
475 }
476 };
477 (final_id, sd_params)
478 })
479 .collect();
480 Self { elements }
481 }
482}
483
484fn flatten_object(obj: ObjectMap, prefix: String, result: &mut BTreeMap<String, String>) {
486 for (key, value) in obj {
487 let key_str: String = key.into();
488
489 let sanitized_key = sanitize_name(&key_str);
490
491 let mut full_key = prefix.clone();
492 if !full_key.is_empty() {
493 full_key.push('.');
494 }
495 full_key.push_str(&sanitized_key);
496
497 match value {
498 Value::Object(nested) => {
499 flatten_object(nested, full_key, result);
500 }
501 Value::Array(arr) => {
502 if let Ok(json) = serde_json::to_string(&arr) {
503 result.insert(full_key, json);
504 } else {
505 result.insert(full_key, format!("{:?}", arr));
506 }
507 }
508 scalar => {
509 result.insert(full_key, scalar.to_string_lossy().to_string());
510 }
511 }
512 }
513}
514
515#[derive(Default, Debug)]
516struct Pri {
517 facility: Facility,
518 severity: Severity,
519}
520
521impl Pri {
522 fn encode(&self) -> String {
525 let pri_val = (self.facility as u8 * 8) + self.severity as u8;
526 format!("<{pri_val}>")
527 }
528}
529
530#[derive(Default, Debug, EnumString, FromRepr, VariantNames, Copy, Clone, PartialEq, Eq)]
532#[strum(serialize_all = "kebab-case")]
533#[configurable_component]
534pub enum Facility {
535 Kern = 0,
537 #[default]
539 User = 1,
540 Mail = 2,
542 Daemon = 3,
544 Auth = 4,
546 Syslog = 5,
548 Lpr = 6,
550 News = 7,
552 Uucp = 8,
554 Cron = 9,
556 Authpriv = 10,
558 Ftp = 11,
560 Ntp = 12,
562 Security = 13,
564 Console = 14,
566 SolarisCron = 15,
568 Local0 = 16,
570 Local1 = 17,
572 Local2 = 18,
574 Local3 = 19,
576 Local4 = 20,
578 Local5 = 21,
580 Local6 = 22,
582 Local7 = 23,
584}
585
586#[derive(Default, Debug, EnumString, FromRepr, VariantNames, Copy, Clone, PartialEq, Eq)]
588#[strum(serialize_all = "kebab-case")]
589#[configurable_component]
590pub enum Severity {
591 #[strum(serialize = "emergency", serialize = "emerg", serialize = "panic")]
593 Emergency = 0,
594 Alert = 1,
596 #[strum(serialize = "critical", serialize = "crit")]
598 Critical = 2,
599 #[strum(serialize = "error", serialize = "err")]
601 Error = 3,
602 #[strum(serialize = "warning", serialize = "warn")]
604 Warning = 4,
605 Notice = 5,
607 #[default]
609 #[strum(serialize = "informational", serialize = "info")]
610 Informational = 6,
611 Debug = 7,
613}
614
615#[cfg(test)]
616mod tests {
617 use super::*;
618 use bytes::BytesMut;
619 use chrono::NaiveDate;
620 use std::sync::Arc;
621 use vector_core::config::LogNamespace;
622 use vector_core::event::Event::Metric;
623 use vector_core::event::{Event, MetricKind, MetricValue, StatisticKind};
624 use vrl::path::parse_target_path;
625 use vrl::prelude::Kind;
626 use vrl::{btreemap, event_path, value};
627
628 fn run_encode(config: SyslogSerializerConfig, event: Event) -> String {
629 let mut serializer = SyslogSerializer::new(&config);
630 let mut buffer = BytesMut::new();
631 serializer.encode(event, &mut buffer).unwrap();
632 String::from_utf8(buffer.to_vec()).unwrap()
633 }
634
635 fn create_simple_log() -> LogEvent {
636 let mut log = LogEvent::from("original message");
637 log.insert(
638 event_path!("timestamp"),
639 NaiveDate::from_ymd_opt(2025, 8, 28)
640 .unwrap()
641 .and_hms_micro_opt(18, 30, 00, 123456)
642 .unwrap()
643 .and_local_timezone(Utc)
644 .unwrap(),
645 );
646 log.insert(event_path!("host"), "test-host.com");
647 log
648 }
649
650 fn create_test_log() -> LogEvent {
651 let mut log = create_simple_log();
652 log.insert(event_path!("app"), "my-app");
653 log.insert(event_path!("pid"), "12345");
654 log.insert(event_path!("mid"), "req-abc-789");
655 log.insert(event_path!("fac"), "daemon"); log.insert(event_path!("sev"), Value::from(2u8)); log.insert(
658 event_path!("structured_data"),
659 value!({"metrics": {"retries": 3}}),
660 );
661 log
662 }
663
664 #[test]
665 fn test_rfc5424_defaults() {
666 let config = toml::from_str::<SyslogSerializerConfig>(
667 r#"
668 [syslog]
669 rfc = "rfc5424"
670 "#,
671 )
672 .unwrap();
673 let log = create_simple_log();
674 let output = run_encode(config, Event::Log(log));
675 let expected =
676 "<14>1 2025-08-28T18:30:00.123456Z test-host.com vector - - - original message";
677 assert_eq!(output, expected);
678 }
679
680 #[test]
681 fn test_rfc5424_all_fields() {
682 let config = toml::from_str::<SyslogSerializerConfig>(
683 r#"
684 [syslog]
685 app_name = ".app"
686 proc_id = ".pid"
687 msg_id = ".mid"
688 facility = ".fac"
689 severity = ".sev"
690 "#,
691 )
692 .unwrap();
693 let log = create_test_log();
694 let output = run_encode(config, Event::Log(log));
695 let expected = "<26>1 2025-08-28T18:30:00.123456Z test-host.com my-app 12345 req-abc-789 [metrics retries=\"3\"] original message";
696 assert_eq!(output, expected);
697 }
698
699 #[test]
700 fn test_rfc3164_all_fields() {
701 let config = toml::from_str::<SyslogSerializerConfig>(
702 r#"
703 [syslog]
704 rfc = "rfc3164"
705 facility = ".fac"
706 severity = ".sev"
707 app_name = ".app"
708 proc_id = ".pid"
709 "#,
710 )
711 .unwrap();
712 let log = create_test_log();
713 let output = run_encode(config, Event::Log(log));
714 let expected = "<26>Aug 28 18:30:00 test-host.com my-app[12345]: original message";
716 assert_eq!(output, expected);
717 }
718
719 #[test]
720 fn test_parsing_logic() {
721 let mut log = LogEvent::from("test message");
722 let config_fac =
723 toml::from_str::<SyslogSerializerOptions>(r#"facility = ".syslog_facility""#).unwrap();
724 let config_sev =
725 toml::from_str::<SyslogSerializerOptions>(r#"severity = ".syslog_severity""#).unwrap();
726 log.insert(event_path!("syslog_facility"), "daemon");
728 log.insert(event_path!("syslog_severity"), "critical");
729 let decanter = ConfigDecanter::new(&log);
730 let facility = decanter.get_facility(&config_fac);
731 let severity = decanter.get_severity(&config_sev);
732 assert_eq!(facility, Facility::Daemon);
733 assert_eq!(severity, Severity::Critical);
734
735 log.insert(event_path!("syslog_facility"), "DAEMON");
737 log.insert(event_path!("syslog_severity"), "CRITICAL");
738 let decanter = ConfigDecanter::new(&log);
739 let facility = decanter.get_facility(&config_fac);
740 let severity = decanter.get_severity(&config_sev);
741 assert_eq!(facility, Facility::Daemon);
742 assert_eq!(severity, Severity::Critical);
743
744 log.insert(event_path!("syslog_facility"), Value::from(3u8));
746 log.insert(event_path!("syslog_severity"), Value::from(2u8));
747 let decanter = ConfigDecanter::new(&log);
748 let facility = decanter.get_facility(&config_fac);
749 let severity = decanter.get_severity(&config_sev);
750 assert_eq!(facility, Facility::Daemon);
751 assert_eq!(severity, Severity::Critical);
752
753 log.insert(event_path!("syslog_severity"), "crit");
755 let decanter = ConfigDecanter::new(&log);
756 assert_eq!(decanter.get_severity(&config_sev), Severity::Critical);
757
758 log.insert(event_path!("syslog_severity"), "emerg");
759 let decanter = ConfigDecanter::new(&log);
760 assert_eq!(decanter.get_severity(&config_sev), Severity::Emergency);
761
762 log.insert(event_path!("syslog_severity"), "err");
763 let decanter = ConfigDecanter::new(&log);
764 assert_eq!(decanter.get_severity(&config_sev), Severity::Error);
765
766 log.insert(event_path!("syslog_severity"), "info");
767 let decanter = ConfigDecanter::new(&log);
768 assert_eq!(decanter.get_severity(&config_sev), Severity::Informational);
769
770 log.insert(event_path!("syslog_severity"), "warn");
771 let decanter = ConfigDecanter::new(&log);
772 assert_eq!(decanter.get_severity(&config_sev), Severity::Warning);
773
774 log.insert(event_path!("syslog_severity"), "panic");
775 let decanter = ConfigDecanter::new(&log);
776 assert_eq!(decanter.get_severity(&config_sev), Severity::Emergency);
777
778 log.insert(event_path!("syslog_severity"), "CRIT");
780 let decanter = ConfigDecanter::new(&log);
781 assert_eq!(decanter.get_severity(&config_sev), Severity::Critical);
782
783 log.insert(event_path!("syslog_severity"), "EMERG");
784 let decanter = ConfigDecanter::new(&log);
785 assert_eq!(decanter.get_severity(&config_sev), Severity::Emergency);
786
787 let empty_config =
789 toml::from_str::<SyslogSerializerOptions>(r#"facility = ".missing_field""#).unwrap();
790 let default_facility = decanter.get_facility(&empty_config);
791 let default_severity = decanter.get_severity(&empty_config);
792 assert_eq!(default_facility, Facility::User);
793 assert_eq!(default_severity, Severity::Informational);
794 }
795
796 #[test]
797 fn test_rfc3164_sanitization() {
798 let config = toml::from_str::<SyslogSerializerConfig>(
799 r#"
800 [syslog]
801 rfc = "rfc3164"
802 "#,
803 )
804 .unwrap();
805
806 let mut log = create_simple_log();
807 log.insert(
808 event_path!("message"),
809 "A\nB\tC, Привіт D, E\u{0007}F", );
811
812 let output = run_encode(config, Event::Log(log));
813 let expected_message = "A B C, D, E F";
814 assert!(output.ends_with(expected_message));
815 }
816
817 #[test]
818 fn test_rfc5424_field_truncation() {
819 let long_string = "vector".repeat(50);
820
821 let mut log = create_simple_log();
822 log.insert(event_path!("long_app_name"), long_string.clone());
823 log.insert(event_path!("long_proc_id"), long_string.clone());
824 log.insert(event_path!("long_msg_id"), long_string.clone());
825
826 let config = toml::from_str::<SyslogSerializerConfig>(
827 r#"
828 [syslog]
829 rfc = "rfc5424"
830 app_name = ".long_app_name"
831 proc_id = ".long_proc_id"
832 msg_id = ".long_msg_id"
833 "#,
834 )
835 .unwrap();
836
837 let decanter = ConfigDecanter::new(&log);
838 let message = decanter.decant_config(&config.syslog);
839
840 assert_eq!(message.tag.app_name.len(), 48);
841 assert_eq!(message.tag.proc_id.unwrap().len(), 128);
842 assert_eq!(message.tag.msg_id.unwrap().len(), 32);
843 }
844
845 #[test]
846 fn test_rfc3164_tag_truncation() {
847 let config = toml::from_str::<SyslogSerializerConfig>(
848 r#"
849 [syslog]
850 rfc = "rfc3164"
851 facility = "user"
852 severity = "notice"
853 app_name = ".app_name"
854 proc_id = ".proc_id"
855 "#,
856 )
857 .unwrap();
858
859 let mut log = create_simple_log();
860 log.insert(
861 event_path!("app_name"),
862 "this-is-a-very-very-long-application-name",
863 );
864 log.insert(event_path!("proc_id"), "1234567890");
865
866 let output = run_encode(config, Event::Log(log));
867 let expected_tag = "this-is-a-very-very-long-applic:";
868 assert!(output.contains(expected_tag));
869 }
870
871 #[test]
872 fn test_rfc5424_missing_fields() {
873 let config = toml::from_str::<SyslogSerializerConfig>(
874 r#"
875 [syslog]
876 rfc = "rfc5424"
877 app_name = ".app" # configured path, but not in log
878 proc_id = ".pid" # configured path, but not in log
879 msg_id = ".mid" # configured path, but not in log
880 "#,
881 )
882 .unwrap();
883
884 let log = create_simple_log();
885 let output = run_encode(config, Event::Log(log));
886
887 let expected =
888 "<14>1 2025-08-28T18:30:00.123456Z test-host.com vector - - - original message";
889 assert_eq!(output, expected);
890 }
891
892 #[test]
893 fn test_invalid_parsing_fallback() {
894 let config = toml::from_str::<SyslogSerializerConfig>(
895 r#"
896 [syslog]
897 rfc = "rfc5424"
898 facility = ".fac"
899 severity = ".sev"
900 "#,
901 )
902 .unwrap();
903
904 let mut log = create_simple_log();
905
906 log.insert(event_path!("fac"), "");
907 log.insert(event_path!("sev"), "invalid_severity_name");
908
909 let output = run_encode(config, Event::Log(log));
910
911 let expected_pri = "<14>";
912 assert!(output.starts_with(expected_pri));
913
914 let expected_suffix = "vector - - - original message";
915 assert!(output.ends_with(expected_suffix));
916 }
917
918 #[test]
919 fn test_rfc5424_empty_message_and_sd() {
920 let config = toml::from_str::<SyslogSerializerConfig>(
921 r#"
922 [syslog]
923 rfc = "rfc5424"
924 app_name = ".app"
925 proc_id = ".pid"
926 msg_id = ".mid"
927 "#,
928 )
929 .unwrap();
930
931 let mut log = create_simple_log();
932 log.insert(event_path!("message"), "");
933 log.insert(event_path!("structured_data"), value!({}));
934
935 let output = run_encode(config, Event::Log(log));
936 let expected = "<14>1 2025-08-28T18:30:00.123456Z test-host.com vector - - -";
937 assert_eq!(output, expected);
938 }
939
940 #[test]
941 fn test_non_log_event_filtering() {
942 let config = toml::from_str::<SyslogSerializerConfig>(
943 r#"
944 [syslog]
945 rfc = "rfc5424"
946 "#,
947 )
948 .unwrap();
949
950 let metric_event = Metric(vector_core::event::Metric::new(
951 "metric1",
952 MetricKind::Incremental,
953 MetricValue::Distribution {
954 samples: vector_core::samples![10.0 => 1],
955 statistic: StatisticKind::Histogram,
956 },
957 ));
958
959 let mut serializer = SyslogSerializer::new(&config);
960 let mut buffer = BytesMut::new();
961
962 let result = serializer.encode(metric_event, &mut buffer);
963
964 assert!(result.is_ok());
965 assert!(buffer.is_empty());
966 }
967
968 #[test]
969 fn test_minimal_event() {
970 let config = toml::from_str::<SyslogSerializerConfig>(
971 r#"
972 [syslog]
973 "#,
974 )
975 .unwrap();
976 let log = LogEvent::from("");
977
978 let output = run_encode(config, Event::Log(log));
979 let expected_suffix = "vector - - -";
980 assert!(output.starts_with("<14>1"));
981 assert!(output.ends_with(expected_suffix));
982 }
983
984 #[test]
985 fn test_app_name_meaning_fallback() {
986 let config = toml::from_str::<SyslogSerializerConfig>(
987 r#"
988 [syslog]
989 rfc = "rfc5424"
990 severity = ".sev"
991 app_name = ".nonexistent"
992 "#,
993 )
994 .unwrap();
995
996 let mut log = LogEvent::default();
997 log.insert(event_path!("syslog", "service"), "meaning-app");
998
999 let schema = schema::Definition::new_with_default_metadata(
1000 Kind::object(btreemap! {
1001 "syslog" => Kind::object(btreemap! {
1002 "service" => Kind::bytes(),
1003 })
1004 }),
1005 [LogNamespace::Vector],
1006 );
1007 let schema = schema.with_meaning(parse_target_path("syslog.service").unwrap(), "service");
1008 let mut event = Event::from(log);
1009 event
1010 .metadata_mut()
1011 .set_schema_definition(&Arc::new(schema));
1012
1013 let output = run_encode(config, event);
1014 assert!(output.contains("meaning-app - -"));
1015 }
1016
1017 #[test]
1018 fn test_structured_data_with_scalars() {
1019 let config = toml::from_str::<SyslogSerializerConfig>(
1020 r#"
1021 [syslog]
1022 rfc = "rfc5424"
1023 "#,
1024 )
1025 .unwrap();
1026
1027 let mut log = create_simple_log();
1028 log.insert(
1029 event_path!("structured_data"),
1030 value!({"simple_string": "hello", "simple_number": 42}),
1031 );
1032
1033 let output = run_encode(config, Event::Log(log));
1034 assert!(output.contains(r#"[simple_number value="42"]"#));
1035 assert!(output.contains(r#"[simple_string value="hello"]"#));
1036 }
1037
1038 #[test]
1039 fn test_structured_data_with_nested_objects() {
1040 let config = toml::from_str::<SyslogSerializerConfig>(
1041 r#"
1042 [syslog]
1043 rfc = "rfc5424"
1044 "#,
1045 )
1046 .unwrap();
1047
1048 let mut log = create_simple_log();
1049 log.insert(
1050 event_path!("structured_data"),
1051 value!({
1052 "meta": {
1053 "request": {
1054 "id": "abc-123",
1055 "method": "GET"
1056 },
1057 "user": "bob"
1058 }
1059 }),
1060 );
1061
1062 let output = run_encode(config, Event::Log(log));
1063 assert!(output.contains(r#"[meta request.id="abc-123" request.method="GET" user="bob"]"#));
1064 }
1065
1066 #[test]
1067 fn test_structured_data_with_arrays() {
1068 let config = toml::from_str::<SyslogSerializerConfig>(
1069 r#"
1070 [syslog]
1071 rfc = "rfc5424"
1072 "#,
1073 )
1074 .unwrap();
1075
1076 let mut log = create_simple_log();
1077 log.insert(
1078 event_path!("structured_data"),
1079 value!({
1080 "data": {
1081 "tags": ["tag1", "tag2", "tag3"]
1082 }
1083 }),
1084 );
1085
1086 let output = run_encode(config, Event::Log(log));
1087 assert!(output.contains(r#"[data tags="[\"tag1\",\"tag2\",\"tag3\"\]"]"#));
1089 }
1090
1091 #[test]
1092 fn test_structured_data_complex_nested() {
1093 let config = toml::from_str::<SyslogSerializerConfig>(
1094 r#"
1095 [syslog]
1096 rfc = "rfc5424"
1097 "#,
1098 )
1099 .unwrap();
1100
1101 let mut log = create_simple_log();
1102 log.insert(
1103 event_path!("structured_data"),
1104 value!({
1105 "tracking": {
1106 "session": {
1107 "user": {
1108 "id": "123",
1109 "name": "alice"
1110 },
1111 "duration_ms": 5000
1112 }
1113 }
1114 }),
1115 );
1116
1117 let output = run_encode(config, Event::Log(log));
1118 assert!(output.contains(r#"session.duration_ms="5000""#));
1119 assert!(output.contains(r#"session.user.id="123""#));
1120 assert!(output.contains(r#"session.user.name="alice""#));
1121 }
1122
1123 #[test]
1124 fn test_structured_data_sanitization() {
1125 let config = toml::from_str::<SyslogSerializerConfig>(
1126 r#"
1127 [syslog]
1128 rfc = "rfc5424"
1129 "#,
1130 )
1131 .unwrap();
1132
1133 let mut log = create_simple_log();
1134 log.insert(
1135 event_path!("structured_data"),
1136 value!({
1137 "my id": { "user=name": "alice", "foo]bar": "value1", "has\"quote": "value2" }
1142 }),
1143 );
1144
1145 let output = run_encode(config, Event::Log(log));
1146 assert!(output.contains(r#"[my_id"#));
1148 assert!(output.contains(r#"foo_bar="value1""#));
1149 assert!(output.contains(r#"has_quote="value2""#));
1150 assert!(output.contains(r#"user_name="alice""#));
1151 }
1152
1153 #[test]
1154 fn test_structured_data_sd_id_length_limit() {
1155 let config = toml::from_str::<SyslogSerializerConfig>(
1156 r#"
1157 [syslog]
1158 rfc = "rfc5424"
1159 "#,
1160 )
1161 .unwrap();
1162
1163 let mut log = create_simple_log();
1164 log.insert(
1165 event_path!("structured_data"),
1166 value!({
1167 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": {
1168 "key": "value"
1169 }
1170 }),
1171 );
1172
1173 let output = run_encode(config, Event::Log(log));
1174 let expected_id = "a".repeat(32);
1175 assert!(output.contains(&format!("[{}", expected_id)));
1176 assert!(!output.contains(&format!("[{}", "a".repeat(50))));
1177 }
1178
1179 #[test]
1180 fn test_utf8_safe_truncation() {
1181 let config = toml::from_str::<SyslogSerializerConfig>(
1182 r#"
1183 [syslog]
1184 rfc = "rfc5424"
1185 app_name = ".app"
1186 proc_id = ".proc"
1187 msg_id = ".msg"
1188 "#,
1189 )
1190 .unwrap();
1191
1192 let mut log = create_simple_log();
1193 log.insert(
1195 event_path!("app"),
1196 "app_😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀",
1197 );
1198 log.insert(
1199 event_path!("proc"),
1200 "процес_😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀",
1201 );
1202 log.insert(event_path!("msg"), "довге_повідомлення ");
1203
1204 log.insert(
1205 event_path!("structured_data"),
1206 value!({
1207 "_😀_дуже_довге_значення_більше_тридцати_двух_символів": {
1208 "_😀_": "value"
1209 }
1210 }),
1211 );
1212 let output = run_encode(config, Event::Log(log));
1213 assert!(output.starts_with("<14>1"));
1214 assert!(output.contains("app_"));
1215
1216 let expected_sd_id: String = "_".repeat(32);
1217 assert!(output.contains(&format!("[{}", expected_sd_id)));
1218 }
1219
1220 #[test]
1221 fn test_rfc3164_ascii_sanitization() {
1222 let config = toml::from_str::<SyslogSerializerConfig>(
1223 r#"
1224 [syslog]
1225 rfc = "rfc3164"
1226 app_name = ".app"
1227 proc_id = ".proc"
1228 "#,
1229 )
1230 .unwrap();
1231
1232 let mut log = create_simple_log();
1233 log.insert(event_path!("app"), "my_app_😀_тест");
1235 log.insert(event_path!("proc"), "процес_123");
1236
1237 let output = run_encode(config, Event::Log(log));
1238
1239 assert!(output.starts_with("<14>"));
1240 assert!(output.contains("my_app_____"));
1241 assert!(output.contains("[_______123]:"));
1242
1243 assert!(!output.contains("😀"));
1244 assert!(!output.contains("тест"));
1245 assert!(!output.contains("процес"));
1246 }
1247}