Skip to main content

codecs/encoding/format/
syslog.rs

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/// Config used to build a `SyslogSerializer`.
21#[configurable_component]
22#[derive(Clone, Debug, Default)]
23#[serde(default)]
24pub struct SyslogSerializerConfig {
25    /// Options for the Syslog serializer.
26    pub syslog: SyslogSerializerOptions,
27}
28
29impl SyslogSerializerConfig {
30    /// Build the `SyslogSerializer` from this configuration.
31    pub fn build(&self) -> SyslogSerializer {
32        SyslogSerializer::new(self)
33    }
34
35    /// The data type of events that are accepted by `SyslogSerializer`.
36    pub fn input_type(&self) -> DataType {
37        DataType::Log
38    }
39
40    /// The schema required by the serializer.
41    pub fn schema_requirement(&self) -> schema::Requirement {
42        schema::Requirement::empty()
43    }
44}
45
46/// Syslog serializer options.
47#[configurable_component]
48#[derive(Clone, Debug, Default)]
49#[serde(default, deny_unknown_fields)]
50pub struct SyslogSerializerOptions {
51    /// RFC to use for formatting.
52    rfc: SyslogRFC,
53    /// Path to a field in the event to use for the facility. Defaults to "user".
54    facility: Option<ConfigTargetPath>,
55    /// Path to a field in the event to use for the severity. Defaults to "informational".
56    severity: Option<ConfigTargetPath>,
57    /// Path to a field in the event to use for the app name.
58    ///
59    /// If not provided, the encoder checks for a semantic "service" field.
60    /// If that is also missing, it defaults to "vector".
61    app_name: Option<ConfigTargetPath>,
62    /// Path to a field in the event to use for the proc ID.
63    proc_id: Option<ConfigTargetPath>,
64    /// Path to a field in the event to use for the msg ID.
65    msg_id: Option<ConfigTargetPath>,
66}
67
68/// Serializer that converts an `Event` to bytes using the Syslog format.
69#[derive(Debug, Clone)]
70pub struct SyslogSerializer {
71    config: SyslogSerializerConfig,
72}
73
74impl SyslogSerializer {
75    /// Creates a new `SyslogSerializer`.
76    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) // P1: Configured path
109            .unwrap_or_else(|| {
110                // P2: Semantic Fallback: Check for the field designated as "service" in the schema
111                self.log
112                    .get_by_meaning("service")
113                    .map(|v| v.to_string_lossy().to_string())
114                    // P3: Hardcoded default
115                    .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                // RFC 3164: TAG field (app_name and proc_id) must be ASCII printable
123                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 to character limits (not byte limits to avoid UTF-8 panics)
130                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/// Replaces invalid characters with '_'
229#[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), // All valid, zero allocation
236        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/// Sanitize a string to ASCII printable characters (space to tilde, ASCII 32-126)
250/// Used for RFC 3164 TAG field (app_name and proc_id)
251/// Invalid characters are replaced with '_'
252#[inline]
253fn sanitize_to_ascii(s: &str) -> Cow<'_, str> {
254    sanitize_with(s, |c| (' '..='~').contains(&c))
255}
256
257/// Sanitize SD-ID or PARAM-NAME according to RFC 5424
258/// Per RFC 5424, these NAMES must only contain printable ASCII (33-126)
259/// excluding '=', ' ', ']', '"'
260/// Invalid characters are replaced with '_'
261#[inline]
262fn sanitize_name(name: &str) -> Cow<'_, str> {
263    sanitize_with(name, |c| {
264        c.is_ascii_graphic() && !matches!(c, '=' | ']' | '"')
265    })
266}
267
268/// Escape PARAM-VALUE according to RFC 5424
269fn 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
289/// Safely truncate a string to a maximum number of characters (not bytes!)
290/// This avoids panics when truncating at a multi-byte UTF-8 character boundary
291/// Optimized to iterate only through necessary characters (not the entire string)
292fn 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/// The syslog RFC standard to use for formatting.
299#[configurable_component]
300#[derive(PartialEq, Clone, Debug, Default)]
301#[serde(rename_all = "snake_case")]
302pub enum SyslogRFC {
303    /// The legacy RFC3164 syslog format.
304    Rfc3164,
305    /// The modern RFC5424 syslog format.
306    #[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            // RFC 3164 does not support structured data
357            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
484/// Helper function to flatten nested objects with dot notation
485fn 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    // The last paragraph describes how to compose the enums into `PRIVAL`:
523    // https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1
524    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/// Syslog facility
531#[derive(Default, Debug, EnumString, FromRepr, VariantNames, Copy, Clone, PartialEq, Eq)]
532#[strum(serialize_all = "kebab-case")]
533#[configurable_component]
534pub enum Facility {
535    /// Kern
536    Kern = 0,
537    /// User
538    #[default]
539    User = 1,
540    /// Mail
541    Mail = 2,
542    /// Daemon
543    Daemon = 3,
544    /// Auth
545    Auth = 4,
546    /// Syslog
547    Syslog = 5,
548    /// Lpr
549    Lpr = 6,
550    /// News
551    News = 7,
552    /// Uucp
553    Uucp = 8,
554    /// Cron
555    Cron = 9,
556    /// Authpriv
557    Authpriv = 10,
558    /// Ftp
559    Ftp = 11,
560    /// Ntp
561    Ntp = 12,
562    /// Security
563    Security = 13,
564    /// Console
565    Console = 14,
566    /// SolarisCron
567    SolarisCron = 15,
568    /// Local0
569    Local0 = 16,
570    /// Local1
571    Local1 = 17,
572    /// Local2
573    Local2 = 18,
574    /// Local3
575    Local3 = 19,
576    /// Local4
577    Local4 = 20,
578    /// Local5
579    Local5 = 21,
580    /// Local6
581    Local6 = 22,
582    /// Local7
583    Local7 = 23,
584}
585
586/// Syslog severity
587#[derive(Default, Debug, EnumString, FromRepr, VariantNames, Copy, Clone, PartialEq, Eq)]
588#[strum(serialize_all = "kebab-case")]
589#[configurable_component]
590pub enum Severity {
591    /// Emergency
592    #[strum(serialize = "emergency", serialize = "emerg", serialize = "panic")]
593    Emergency = 0,
594    /// Alert
595    Alert = 1,
596    /// Critical
597    #[strum(serialize = "critical", serialize = "crit")]
598    Critical = 2,
599    /// Error
600    #[strum(serialize = "error", serialize = "err")]
601    Error = 3,
602    /// Warning
603    #[strum(serialize = "warning", serialize = "warn")]
604    Warning = 4,
605    /// Notice
606    Notice = 5,
607    /// Informational
608    #[default]
609    #[strum(serialize = "informational", serialize = "info")]
610    Informational = 6,
611    /// Debug
612    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"); //3
656        log.insert(event_path!("sev"), Value::from(2u8)); // Critical
657        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        // RFC 3164 does not support structured data, so it's ignored
715        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        //check lowercase and digit
727        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        //check uppercase
736        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        //check digit
745        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        //check short-form severity aliases
754        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        //check uppercase short-form aliases
779        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        //check defaults with empty config
788        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", //newline, tab, unicode
810        );
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        // Arrays should be JSON-encoded and escaped
1088        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": {  // SD-ID with space - should be sanitized to my_id
1138                    "user=name": "alice",  // PARAM-NAME with = - should be sanitized to user_name
1139                    "foo]bar": "value1",   // PARAM-NAME with ] - should be sanitized to foo_bar
1140                    "has\"quote": "value2" // PARAM-NAME with " - should be sanitized to has_quote
1141                }
1142            }),
1143        );
1144
1145        let output = run_encode(config, Event::Log(log));
1146        // All invalid characters should be replaced with _
1147        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        // Create fields with UTF-8 characters (emoji, Cyrillic, etc.) each emoji is 4 bytes
1194        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        // Use non-ASCII characters in app_name and proc_id
1234        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}