codecs/encoding/format/
syslog.rs

1use bytes::{BufMut, BytesMut};
2use chrono::{DateTime, SecondsFormat, SubsecRound, Utc};
3use lookup::lookup_v2::ConfigTargetPath;
4use std::collections::HashMap;
5use std::fmt::Write;
6use std::str::FromStr;
7use strum::{EnumString, FromRepr, VariantNames};
8use tokio_util::codec::Encoder;
9use vector_config::configurable_component;
10use vector_core::{
11    config::DataType,
12    event::{Event, LogEvent, Value},
13    schema,
14};
15use vrl::value::ObjectMap;
16
17/// Config used to build a `SyslogSerializer`.
18#[configurable_component]
19#[derive(Clone, Debug, Default)]
20#[serde(default)]
21pub struct SyslogSerializerConfig {
22    /// Options for the Syslog serializer.
23    pub syslog: SyslogSerializerOptions,
24}
25
26impl SyslogSerializerConfig {
27    /// Build the `SyslogSerializer` from this configuration.
28    pub fn build(&self) -> SyslogSerializer {
29        SyslogSerializer::new(self)
30    }
31
32    /// The data type of events that are accepted by `SyslogSerializer`.
33    pub fn input_type(&self) -> DataType {
34        DataType::Log
35    }
36
37    /// The schema required by the serializer.
38    pub fn schema_requirement(&self) -> schema::Requirement {
39        schema::Requirement::empty()
40    }
41}
42
43/// Syslog serializer options.
44#[configurable_component]
45#[derive(Clone, Debug, Default)]
46#[serde(default, deny_unknown_fields)]
47pub struct SyslogSerializerOptions {
48    /// RFC to use for formatting.
49    rfc: SyslogRFC,
50    /// Path to a field in the event to use for the facility. Defaults to "user".
51    facility: Option<ConfigTargetPath>,
52    /// Path to a field in the event to use for the severity. Defaults to "informational".
53    severity: Option<ConfigTargetPath>,
54    /// Path to a field in the event to use for the app name.
55    ///
56    /// If not provided, the encoder checks for a semantic "service" field.
57    /// If that is also missing, it defaults to "vector".
58    app_name: Option<ConfigTargetPath>,
59    /// Path to a field in the event to use for the proc ID.
60    proc_id: Option<ConfigTargetPath>,
61    /// Path to a field in the event to use for the msg ID.
62    msg_id: Option<ConfigTargetPath>,
63}
64
65/// Serializer that converts an `Event` to bytes using the Syslog format.
66#[derive(Debug, Clone)]
67pub struct SyslogSerializer {
68    config: SyslogSerializerConfig,
69}
70
71impl SyslogSerializer {
72    /// Creates a new `SyslogSerializer`.
73    pub fn new(conf: &SyslogSerializerConfig) -> Self {
74        Self {
75            config: conf.clone(),
76        }
77    }
78}
79
80impl Encoder<Event> for SyslogSerializer {
81    type Error = vector_common::Error;
82
83    fn encode(&mut self, event: Event, buffer: &mut BytesMut) -> Result<(), Self::Error> {
84        if let Event::Log(log_event) = event {
85            let syslog_message = ConfigDecanter::new(&log_event).decant_config(&self.config.syslog);
86            let vec = syslog_message
87                .encode(&self.config.syslog.rfc)
88                .as_bytes()
89                .to_vec();
90            buffer.put_slice(&vec);
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        if config.rfc == SyslogRFC::Rfc5424 {
120            if app_name.len() > 48 {
121                app_name.truncate(48);
122            }
123            if let Some(pid) = &mut proc_id
124                && pid.len() > 128
125            {
126                pid.truncate(128);
127            }
128            if let Some(mid) = &mut msg_id
129                && mid.len() > 32
130            {
131                mid.truncate(32);
132            }
133        }
134
135        SyslogMessage {
136            pri: Pri {
137                facility: self.get_facility(config),
138                severity: self.get_severity(config),
139            },
140            timestamp: self.get_timestamp(),
141            hostname: self.log.get_host().map(|v| v.to_string_lossy().to_string()),
142            tag: Tag {
143                app_name,
144                proc_id,
145                msg_id,
146            },
147            structured_data: self.get_structured_data(),
148            message: self.get_payload(),
149        }
150    }
151
152    fn get_value(&self, path: &Option<ConfigTargetPath>) -> Option<String> {
153        path.as_ref()
154            .and_then(|p| self.log.get(p).cloned())
155            .map(|v| v.to_string_lossy().to_string())
156    }
157
158    fn get_structured_data(&self) -> Option<StructuredData> {
159        self.log
160            .get("structured_data")
161            .and_then(|v| v.clone().into_object())
162            .map(StructuredData::from)
163    }
164
165    fn get_timestamp(&self) -> DateTime<Utc> {
166        if let Some(Value::Timestamp(timestamp)) = self.log.get_timestamp() {
167            return *timestamp;
168        }
169        Utc::now()
170    }
171
172    fn get_payload(&self) -> String {
173        self.log
174            .get_message()
175            .map(|v| v.to_string_lossy().to_string())
176            .unwrap_or_default()
177    }
178
179    fn get_facility(&self, config: &SyslogSerializerOptions) -> Facility {
180        config.facility.as_ref().map_or(Facility::User, |path| {
181            self.get_syslog_code(path, Facility::from_repr, Facility::User)
182        })
183    }
184
185    fn get_severity(&self, config: &SyslogSerializerOptions) -> Severity {
186        config
187            .severity
188            .as_ref()
189            .map_or(Severity::Informational, |path| {
190                self.get_syslog_code(path, Severity::from_repr, Severity::Informational)
191            })
192    }
193
194    fn get_syslog_code<T>(
195        &self,
196        path: &ConfigTargetPath,
197        from_repr_fn: fn(usize) -> Option<T>,
198        default_value: T,
199    ) -> T
200    where
201        T: Copy + FromStr,
202    {
203        if let Some(value) = self.log.get(path).cloned() {
204            let s = value.to_string_lossy();
205            if let Ok(val_from_name) = s.to_ascii_lowercase().parse::<T>() {
206                return val_from_name;
207            }
208            if let Value::Integer(n) = value
209                && let Some(val_from_num) = from_repr_fn(n as usize)
210            {
211                return val_from_num;
212            }
213        }
214        default_value
215    }
216}
217
218const NIL_VALUE: &str = "-";
219const SYSLOG_V1: &str = "1";
220const RFC3164_TAG_MAX_LENGTH: usize = 32;
221
222/// The syslog RFC standard to use for formatting.
223#[configurable_component]
224#[derive(PartialEq, Clone, Debug, Default)]
225#[serde(rename_all = "snake_case")]
226pub enum SyslogRFC {
227    /// The legacy RFC3164 syslog format.
228    Rfc3164,
229    /// The modern RFC5424 syslog format.
230    #[default]
231    Rfc5424,
232}
233
234#[derive(Default, Debug)]
235struct SyslogMessage {
236    pri: Pri,
237    timestamp: DateTime<Utc>,
238    hostname: Option<String>,
239    tag: Tag,
240    structured_data: Option<StructuredData>,
241    message: String,
242}
243
244impl SyslogMessage {
245    fn encode(&self, rfc: &SyslogRFC) -> String {
246        let pri_header = self.pri.encode();
247
248        let mut parts = Vec::new();
249
250        let timestamp_str = match rfc {
251            SyslogRFC::Rfc3164 => self.timestamp.format("%b %e %H:%M:%S").to_string(),
252            SyslogRFC::Rfc5424 => self
253                .timestamp
254                .round_subsecs(6)
255                .to_rfc3339_opts(SecondsFormat::Micros, true),
256        };
257        parts.push(timestamp_str);
258        parts.push(self.hostname.as_deref().unwrap_or(NIL_VALUE).to_string());
259
260        let tag_str = match rfc {
261            SyslogRFC::Rfc3164 => self.tag.encode_rfc_3164(),
262            SyslogRFC::Rfc5424 => self.tag.encode_rfc_5424(),
263        };
264        parts.push(tag_str);
265
266        let mut message_part = self.message.clone();
267        if *rfc == SyslogRFC::Rfc3164 {
268            message_part = Self::sanitize_rfc3164_message(&message_part);
269        }
270
271        if let Some(sd) = &self.structured_data {
272            let sd_string = sd.encode();
273            if *rfc == SyslogRFC::Rfc3164 {
274                if !sd.elements.is_empty() {
275                    if !message_part.is_empty() {
276                        message_part = format!("{sd_string} {message_part}");
277                    } else {
278                        message_part = sd_string;
279                    }
280                }
281            } else {
282                parts.push(sd_string);
283            }
284        } else if *rfc == SyslogRFC::Rfc5424 {
285            parts.push(NIL_VALUE.to_string());
286        }
287
288        if !message_part.is_empty() {
289            parts.push(message_part);
290        }
291
292        let main_message = parts.join(" ");
293
294        if *rfc == SyslogRFC::Rfc5424 {
295            format!("{pri_header}{SYSLOG_V1} {main_message}")
296        } else {
297            format!("{pri_header}{main_message}")
298        }
299    }
300
301    fn sanitize_rfc3164_message(message: &str) -> String {
302        message
303            .chars()
304            .map(|ch| if (' '..='~').contains(&ch) { ch } else { ' ' })
305            .collect()
306    }
307}
308
309#[derive(Default, Debug)]
310struct Tag {
311    app_name: String,
312    proc_id: Option<String>,
313    msg_id: Option<String>,
314}
315
316impl Tag {
317    fn encode_rfc_3164(&self) -> String {
318        let mut tag = if let Some(proc_id) = self.proc_id.as_deref() {
319            format!("{}[{}]:", self.app_name, proc_id)
320        } else {
321            format!("{}:", self.app_name)
322        };
323        if tag.len() > RFC3164_TAG_MAX_LENGTH {
324            tag.truncate(RFC3164_TAG_MAX_LENGTH);
325            if !tag.ends_with(':') {
326                tag.pop();
327                tag.push(':');
328            }
329        }
330        tag
331    }
332
333    fn encode_rfc_5424(&self) -> String {
334        let proc_id_str = self.proc_id.as_deref().unwrap_or(NIL_VALUE);
335        let msg_id_str = self.msg_id.as_deref().unwrap_or(NIL_VALUE);
336        format!("{} {} {}", self.app_name, proc_id_str, msg_id_str)
337    }
338}
339
340type StructuredDataMap = HashMap<String, HashMap<String, String>>;
341#[derive(Debug, Default)]
342struct StructuredData {
343    elements: StructuredDataMap,
344}
345
346impl StructuredData {
347    fn encode(&self) -> String {
348        if self.elements.is_empty() {
349            NIL_VALUE.to_string()
350        } else {
351            self.elements
352                .iter()
353                .fold(String::new(), |mut acc, (sd_id, sd_params)| {
354                    let _ = write!(acc, "[{sd_id}");
355                    for (key, value) in sd_params {
356                        let esc_val = Self::escape_sd(value);
357                        let _ = write!(acc, " {key}=\"{esc_val}\"");
358                    }
359                    let _ = write!(acc, "]");
360                    acc
361                })
362        }
363    }
364
365    fn escape_sd(s: &str) -> String {
366        s.replace('\\', "\\\\")
367            .replace('"', "\\\"")
368            .replace(']', "\\]")
369    }
370}
371
372impl From<ObjectMap> for StructuredData {
373    fn from(fields: ObjectMap) -> Self {
374        let elements = fields
375            .into_iter()
376            .flat_map(|(sd_id, value)| {
377                let sd_params = value
378                    .into_object()?
379                    .into_iter()
380                    .map(|(k, v)| (k.into(), v.to_string_lossy().to_string()))
381                    .collect();
382                Some((sd_id.into(), sd_params))
383            })
384            .collect();
385        Self { elements }
386    }
387}
388
389#[derive(Default, Debug)]
390struct Pri {
391    facility: Facility,
392    severity: Severity,
393}
394
395impl Pri {
396    // The last paragraph describes how to compose the enums into `PRIVAL`:
397    // https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1
398    fn encode(&self) -> String {
399        let pri_val = (self.facility as u8 * 8) + self.severity as u8;
400        format!("<{pri_val}>")
401    }
402}
403
404/// Syslog facility
405#[derive(Default, Debug, EnumString, FromRepr, VariantNames, Copy, Clone, PartialEq, Eq)]
406#[strum(serialize_all = "kebab-case")]
407#[configurable_component]
408pub enum Facility {
409    /// Kern
410    Kern = 0,
411    /// User
412    #[default]
413    User = 1,
414    /// Mail
415    Mail = 2,
416    /// Daemon
417    Daemon = 3,
418    /// Auth
419    Auth = 4,
420    /// Syslog
421    Syslog = 5,
422    /// Lpr
423    Lpr = 6,
424    /// News
425    News = 7,
426    /// Uucp
427    Uucp = 8,
428    /// Cron
429    Cron = 9,
430    /// Authpriv
431    Authpriv = 10,
432    /// Ftp
433    Ftp = 11,
434    /// Ntp
435    Ntp = 12,
436    /// Security
437    Security = 13,
438    /// Console
439    Console = 14,
440    /// SolarisCron
441    SolarisCron = 15,
442    /// Local0
443    Local0 = 16,
444    /// Local1
445    Local1 = 17,
446    /// Local2
447    Local2 = 18,
448    /// Local3
449    Local3 = 19,
450    /// Local4
451    Local4 = 20,
452    /// Local5
453    Local5 = 21,
454    /// Local6
455    Local6 = 22,
456    /// Local7
457    Local7 = 23,
458}
459
460/// Syslog severity
461#[derive(Default, Debug, EnumString, FromRepr, VariantNames, Copy, Clone, PartialEq, Eq)]
462#[strum(serialize_all = "kebab-case")]
463#[configurable_component]
464pub enum Severity {
465    /// Emergency
466    Emergency = 0,
467    /// Alert
468    Alert = 1,
469    /// Critical
470    Critical = 2,
471    /// Error
472    Error = 3,
473    /// Warning
474    Warning = 4,
475    /// Notice
476    Notice = 5,
477    /// Informational
478    #[default]
479    Informational = 6,
480    /// Debug
481    Debug = 7,
482}
483
484#[cfg(test)]
485mod tests {
486    use super::*;
487    use bytes::BytesMut;
488    use chrono::NaiveDate;
489    use std::sync::Arc;
490    use vector_core::config::LogNamespace;
491    use vector_core::event::Event::Metric;
492    use vector_core::event::{Event, MetricKind, MetricValue, StatisticKind};
493    use vrl::path::parse_target_path;
494    use vrl::prelude::Kind;
495    use vrl::{btreemap, event_path, value};
496
497    fn run_encode(config: SyslogSerializerConfig, event: Event) -> String {
498        let mut serializer = SyslogSerializer::new(&config);
499        let mut buffer = BytesMut::new();
500        serializer.encode(event, &mut buffer).unwrap();
501        String::from_utf8(buffer.to_vec()).unwrap()
502    }
503
504    fn create_simple_log() -> LogEvent {
505        let mut log = LogEvent::from("original message");
506        log.insert(
507            event_path!("timestamp"),
508            NaiveDate::from_ymd_opt(2025, 8, 28)
509                .unwrap()
510                .and_hms_micro_opt(18, 30, 00, 123456)
511                .unwrap()
512                .and_local_timezone(Utc)
513                .unwrap(),
514        );
515        log.insert(event_path!("host"), "test-host.com");
516        log
517    }
518
519    fn create_test_log() -> LogEvent {
520        let mut log = create_simple_log();
521        log.insert(event_path!("app"), "my-app");
522        log.insert(event_path!("pid"), "12345");
523        log.insert(event_path!("mid"), "req-abc-789");
524        log.insert(event_path!("fac"), "daemon"); //3
525        log.insert(event_path!("sev"), Value::from(2u8)); // Critical
526        log.insert(
527            event_path!("structured_data"),
528            value!({"metrics": {"retries": 3}}),
529        );
530        log
531    }
532
533    #[test]
534    fn test_rfc5424_defaults() {
535        let config = toml::from_str::<SyslogSerializerConfig>(
536            r#"
537            [syslog]
538            rfc = "rfc5424"
539        "#,
540        )
541        .unwrap();
542        let log = create_simple_log();
543        let output = run_encode(config, Event::Log(log));
544        let expected =
545            "<14>1 2025-08-28T18:30:00.123456Z test-host.com vector - - - original message";
546        assert_eq!(output, expected);
547    }
548
549    #[test]
550    fn test_rfc5424_all_fields() {
551        let config = toml::from_str::<SyslogSerializerConfig>(
552            r#"
553            [syslog]
554            app_name = ".app"
555            proc_id = ".pid"
556            msg_id = ".mid"
557            facility = ".fac"
558            severity = ".sev"
559        "#,
560        )
561        .unwrap();
562        let log = create_test_log();
563        let output = run_encode(config, Event::Log(log));
564        let expected = "<26>1 2025-08-28T18:30:00.123456Z test-host.com my-app 12345 req-abc-789 [metrics retries=\"3\"] original message";
565        assert_eq!(output, expected);
566    }
567
568    #[test]
569    fn test_rfc3164_all_fields() {
570        let config = toml::from_str::<SyslogSerializerConfig>(
571            r#"
572            [syslog]
573            rfc = "rfc3164"
574            facility = ".fac"
575            severity = ".sev"
576            app_name = ".app"
577            proc_id = ".pid"
578        "#,
579        )
580        .unwrap();
581        let log = create_test_log();
582        let output = run_encode(config, Event::Log(log));
583        let expected = "<26>Aug 28 18:30:00 test-host.com my-app[12345]: [metrics retries=\"3\"] original message";
584        assert_eq!(output, expected);
585    }
586
587    #[test]
588    fn test_parsing_logic() {
589        let mut log = LogEvent::from("test message");
590        let config_fac =
591            toml::from_str::<SyslogSerializerOptions>(r#"facility = ".syslog_facility""#).unwrap();
592        let config_sev =
593            toml::from_str::<SyslogSerializerOptions>(r#"severity = ".syslog_severity""#).unwrap();
594        //check lowercase and digit
595        log.insert(event_path!("syslog_facility"), "daemon");
596        log.insert(event_path!("syslog_severity"), "critical");
597        let decanter = ConfigDecanter::new(&log);
598        let facility = decanter.get_facility(&config_fac);
599        let severity = decanter.get_severity(&config_sev);
600        assert_eq!(facility, Facility::Daemon);
601        assert_eq!(severity, Severity::Critical);
602
603        //check uppercase
604        log.insert(event_path!("syslog_facility"), "DAEMON");
605        log.insert(event_path!("syslog_severity"), "CRITICAL");
606        let decanter = ConfigDecanter::new(&log);
607        let facility = decanter.get_facility(&config_fac);
608        let severity = decanter.get_severity(&config_sev);
609        assert_eq!(facility, Facility::Daemon);
610        assert_eq!(severity, Severity::Critical);
611
612        //check digit
613        log.insert(event_path!("syslog_facility"), Value::from(3u8));
614        log.insert(event_path!("syslog_severity"), Value::from(2u8));
615        let decanter = ConfigDecanter::new(&log);
616        let facility = decanter.get_facility(&config_fac);
617        let severity = decanter.get_severity(&config_sev);
618        assert_eq!(facility, Facility::Daemon);
619        assert_eq!(severity, Severity::Critical);
620
621        //check defaults with empty config
622        let empty_config =
623            toml::from_str::<SyslogSerializerOptions>(r#"facility = ".missing_field""#).unwrap();
624        let default_facility = decanter.get_facility(&empty_config);
625        let default_severity = decanter.get_severity(&empty_config);
626        assert_eq!(default_facility, Facility::User);
627        assert_eq!(default_severity, Severity::Informational);
628    }
629
630    #[test]
631    fn test_rfc3164_sanitization() {
632        let config = toml::from_str::<SyslogSerializerConfig>(
633            r#"
634        [syslog]
635        rfc = "rfc3164"
636    "#,
637        )
638        .unwrap();
639
640        let mut log = create_simple_log();
641        log.insert(
642            event_path!("message"),
643            "A\nB\tC, Привіт D, E\u{0007}F", //newline, tab, unicode
644        );
645
646        let output = run_encode(config, Event::Log(log));
647        let expected_message = "A B C,        D, E F";
648        assert!(output.ends_with(expected_message));
649    }
650
651    #[test]
652    fn test_rfc5424_field_truncation() {
653        let long_string = "vector".repeat(50);
654
655        let mut log = create_simple_log();
656        log.insert(event_path!("long_app_name"), long_string.clone());
657        log.insert(event_path!("long_proc_id"), long_string.clone());
658        log.insert(event_path!("long_msg_id"), long_string.clone());
659
660        let config = toml::from_str::<SyslogSerializerConfig>(
661            r#"
662        [syslog]
663        rfc = "rfc5424"
664        app_name = ".long_app_name"
665        proc_id = ".long_proc_id"
666        msg_id = ".long_msg_id"
667    "#,
668        )
669        .unwrap();
670
671        let decanter = ConfigDecanter::new(&log);
672        let message = decanter.decant_config(&config.syslog);
673
674        assert_eq!(message.tag.app_name.len(), 48);
675        assert_eq!(message.tag.proc_id.unwrap().len(), 128);
676        assert_eq!(message.tag.msg_id.unwrap().len(), 32);
677    }
678
679    #[test]
680    fn test_rfc3164_tag_truncation() {
681        let config = toml::from_str::<SyslogSerializerConfig>(
682            r#"
683        [syslog]
684        rfc = "rfc3164"
685        facility = "user"
686        severity = "notice"
687        app_name = ".app_name"
688        proc_id = ".proc_id"
689    "#,
690        )
691        .unwrap();
692
693        let mut log = create_simple_log();
694        log.insert(
695            event_path!("app_name"),
696            "this-is-a-very-very-long-application-name",
697        );
698        log.insert(event_path!("proc_id"), "1234567890");
699
700        let output = run_encode(config, Event::Log(log));
701        let expected_tag = "this-is-a-very-very-long-applic:";
702        assert!(output.contains(expected_tag));
703    }
704
705    #[test]
706    fn test_rfc5424_missing_fields() {
707        let config = toml::from_str::<SyslogSerializerConfig>(
708            r#"
709        [syslog]
710        rfc = "rfc5424"
711        app_name = ".app"  # configured path, but not in log
712        proc_id = ".pid"   # configured path, but not in log
713        msg_id = ".mid"    # configured path, but not in log
714    "#,
715        )
716        .unwrap();
717
718        let log = create_simple_log();
719        let output = run_encode(config, Event::Log(log));
720
721        let expected =
722            "<14>1 2025-08-28T18:30:00.123456Z test-host.com vector - - - original message";
723        assert_eq!(output, expected);
724    }
725
726    #[test]
727    fn test_invalid_parsing_fallback() {
728        let config = toml::from_str::<SyslogSerializerConfig>(
729            r#"
730        [syslog]
731        rfc = "rfc5424"
732        facility = ".fac"
733        severity = ".sev"
734    "#,
735        )
736        .unwrap();
737
738        let mut log = create_simple_log();
739
740        log.insert(event_path!("fac"), "");
741        log.insert(event_path!("sev"), "invalid_severity_name");
742
743        let output = run_encode(config, Event::Log(log));
744
745        let expected_pri = "<14>";
746        assert!(output.starts_with(expected_pri));
747
748        let expected_suffix = "vector - - - original message";
749        assert!(output.ends_with(expected_suffix));
750    }
751
752    #[test]
753    fn test_rfc5424_empty_message_and_sd() {
754        let config = toml::from_str::<SyslogSerializerConfig>(
755            r#"
756        [syslog]
757        rfc = "rfc5424"
758        app_name = ".app"
759        proc_id = ".pid"
760        msg_id = ".mid"
761    "#,
762        )
763        .unwrap();
764
765        let mut log = create_simple_log();
766        log.insert(event_path!("message"), "");
767        log.insert(event_path!("structured_data"), value!({}));
768
769        let output = run_encode(config, Event::Log(log));
770        let expected = "<14>1 2025-08-28T18:30:00.123456Z test-host.com vector - - -";
771        assert_eq!(output, expected);
772    }
773
774    #[test]
775    fn test_non_log_event_filtering() {
776        let config = toml::from_str::<SyslogSerializerConfig>(
777            r#"
778        [syslog]
779        rfc = "rfc5424"
780    "#,
781        )
782        .unwrap();
783
784        let metric_event = Metric(vector_core::event::Metric::new(
785            "metric1",
786            MetricKind::Incremental,
787            MetricValue::Distribution {
788                samples: vector_core::samples![10.0 => 1],
789                statistic: StatisticKind::Histogram,
790            },
791        ));
792
793        let mut serializer = SyslogSerializer::new(&config);
794        let mut buffer = BytesMut::new();
795
796        let result = serializer.encode(metric_event, &mut buffer);
797
798        assert!(result.is_ok());
799        assert!(buffer.is_empty());
800    }
801
802    #[test]
803    fn test_minimal_event() {
804        let config = toml::from_str::<SyslogSerializerConfig>(
805            r#"
806        [syslog]
807    "#,
808        )
809        .unwrap();
810        let log = LogEvent::from("");
811
812        let output = run_encode(config, Event::Log(log));
813        let expected_suffix = "vector - - -";
814        assert!(output.starts_with("<14>1"));
815        assert!(output.ends_with(expected_suffix));
816    }
817
818    #[test]
819    fn test_app_name_meaning_fallback() {
820        let config = toml::from_str::<SyslogSerializerConfig>(
821            r#"
822        [syslog]
823        rfc = "rfc5424"
824        severity = ".sev"
825        app_name = ".nonexistent"
826    "#,
827        )
828        .unwrap();
829
830        let mut log = LogEvent::default();
831        log.insert("syslog.service", "meaning-app");
832
833        let schema = schema::Definition::new_with_default_metadata(
834            Kind::object(btreemap! {
835                "syslog" => Kind::object(btreemap! {
836                    "service" => Kind::bytes(),
837                })
838            }),
839            [LogNamespace::Vector],
840        );
841        let schema = schema.with_meaning(parse_target_path("syslog.service").unwrap(), "service");
842        let mut event = Event::from(log);
843        event
844            .metadata_mut()
845            .set_schema_definition(&Arc::new(schema));
846
847        let output = run_encode(config, event);
848        assert!(output.contains("meaning-app - -"));
849    }
850}