1use crate::model::log::{LogEvent, Severity};
35use crate::model::metric::MetricEvent;
36use crate::{EncoderError, SondaError};
37
38use super::Encoder;
39
40const FACILITY_USER: u8 = 1;
42
43const SYSLOG_VERSION: u8 = 1;
45
46const NILVALUE: &str = "-";
48
49fn severity_to_syslog(severity: Severity) -> u8 {
61 match severity {
62 Severity::Fatal => 0, Severity::Error => 3, Severity::Warn => 4, Severity::Info => 6, Severity::Debug => 7, Severity::Trace => 7, }
69}
70
71pub struct Syslog {
79 hostname: String,
81 app_name: String,
83}
84
85impl Syslog {
86 pub fn new(hostname: Option<String>, app_name: Option<String>) -> Self {
93 Self {
94 hostname: hostname.unwrap_or_else(|| "sonda".to_string()),
95 app_name: app_name.unwrap_or_else(|| "sonda".to_string()),
96 }
97 }
98}
99
100impl Default for Syslog {
101 fn default() -> Self {
102 Self::new(None, None)
103 }
104}
105
106impl Encoder for Syslog {
107 fn encode_metric(
109 &self,
110 _event: &MetricEvent,
111 _buf: &mut Vec<u8>,
112 ) -> Result<(), crate::SondaError> {
113 Err(SondaError::Encoder(EncoderError::NotSupported(
114 "metric encoding not supported by syslog encoder".into(),
115 )))
116 }
117
118 fn encode_log(&self, event: &LogEvent, buf: &mut Vec<u8>) -> Result<(), SondaError> {
125 use std::io::Write;
126
127 let syslog_severity = severity_to_syslog(event.severity);
128 let priority = FACILITY_USER * 8 + syslog_severity;
129
130 write!(buf, "<{priority}>{version} ", version = SYSLOG_VERSION)
132 .expect("write to Vec<u8> is infallible");
133
134 super::format_rfc3339_millis(event.timestamp, buf)?;
136
137 write!(
139 buf,
140 " {hostname} {app_name} {procid} {msgid} ",
141 hostname = self.hostname,
142 app_name = self.app_name,
143 procid = NILVALUE,
144 msgid = NILVALUE,
145 )
146 .expect("write to Vec<u8> is infallible");
147
148 if event.labels.is_empty() {
151 buf.extend_from_slice(NILVALUE.as_bytes());
152 } else {
153 buf.extend_from_slice(b"[sonda");
154 for (k, v) in event.labels.iter() {
155 buf.push(b' ');
156 buf.extend_from_slice(k.as_bytes());
157 buf.extend_from_slice(b"=\"");
158 for ch in v.bytes() {
160 match ch {
161 b'\\' => buf.extend_from_slice(b"\\\\"),
162 b']' => buf.extend_from_slice(b"\\]"),
163 b'"' => buf.extend_from_slice(b"\\\""),
164 _ => buf.push(ch),
165 }
166 }
167 buf.push(b'"');
168 }
169 buf.push(b']');
170 }
171
172 buf.push(b' ');
174 buf.extend_from_slice(event.message.as_bytes());
175 buf.push(b'\n');
176
177 Ok(())
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use std::collections::BTreeMap;
184 use std::time::{Duration, UNIX_EPOCH};
185
186 use crate::model::log::{LogEvent, Severity};
187
188 use super::*;
189
190 fn make_log_event(
192 severity: Severity,
193 message: &str,
194 fields: &[(&str, &str)],
195 ts: std::time::SystemTime,
196 ) -> LogEvent {
197 let mut map = BTreeMap::new();
198 for (k, v) in fields {
199 map.insert(k.to_string(), v.to_string());
200 }
201 LogEvent::with_timestamp(
202 ts,
203 severity,
204 message.to_string(),
205 crate::model::metric::Labels::default(),
206 map,
207 )
208 }
209
210 #[test]
215 fn encode_metric_returns_not_supported_error() {
216 use crate::model::metric::{Labels, MetricEvent};
217 let labels = Labels::from_pairs(&[]).unwrap();
218 let event =
219 MetricEvent::with_timestamp("cpu".to_string(), 1.0, labels, UNIX_EPOCH).unwrap();
220 let encoder = Syslog::default();
221 let mut buf = Vec::new();
222 let result = encoder.encode_metric(&event, &mut buf);
223 assert!(
224 result.is_err(),
225 "syslog encoder must return error for encode_metric"
226 );
227 let msg = result.unwrap_err().to_string();
228 assert!(
229 msg.contains("metric encoding not supported"),
230 "error message must mention 'metric encoding not supported', got: {msg}"
231 );
232 }
233
234 #[test]
235 fn encode_metric_does_not_write_to_buffer() {
236 use crate::model::metric::{Labels, MetricEvent};
237 let labels = Labels::from_pairs(&[]).unwrap();
238 let event = MetricEvent::with_timestamp("up".to_string(), 1.0, labels, UNIX_EPOCH).unwrap();
239 let encoder = Syslog::default();
240 let mut buf = Vec::new();
241 let _ = encoder.encode_metric(&event, &mut buf);
242 assert!(
243 buf.is_empty(),
244 "buffer must remain empty when encode_metric returns error"
245 );
246 }
247
248 #[test]
253 fn encode_log_produces_line_ending_with_newline() {
254 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
255 let event = make_log_event(Severity::Info, "hello", &[], ts);
256 let encoder = Syslog::default();
257 let mut buf = Vec::new();
258 encoder.encode_log(&event, &mut buf).unwrap();
259 assert_eq!(
260 *buf.last().unwrap(),
261 b'\n',
262 "syslog line must end with newline"
263 );
264 }
265
266 #[test]
267 fn encode_log_starts_with_priority_marker() {
268 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
269 let event = make_log_event(Severity::Info, "hello", &[], ts);
270 let encoder = Syslog::default();
271 let mut buf = Vec::new();
272 encoder.encode_log(&event, &mut buf).unwrap();
273 let line = String::from_utf8(buf).unwrap();
274 assert!(
275 line.starts_with('<'),
276 "syslog line must start with '<': {line}"
277 );
278 }
279
280 #[test]
281 fn encode_log_contains_version_one() {
282 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
283 let event = make_log_event(Severity::Info, "test", &[], ts);
284 let encoder = Syslog::default();
285 let mut buf = Vec::new();
286 encoder.encode_log(&event, &mut buf).unwrap();
287 let line = String::from_utf8(buf).unwrap();
288 let after_priority = line.find('>').unwrap();
290 let version_token: &str = line[after_priority + 1..]
291 .split_whitespace()
292 .next()
293 .unwrap();
294 assert_eq!(version_token, "1", "RFC 5424 version must be 1");
295 }
296
297 #[test]
298 fn encode_log_contains_hostname_in_output() {
299 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
300 let event = make_log_event(Severity::Info, "hello", &[], ts);
301 let encoder = Syslog::new(Some("myhost".to_string()), None);
302 let mut buf = Vec::new();
303 encoder.encode_log(&event, &mut buf).unwrap();
304 let line = String::from_utf8(buf).unwrap();
305 assert!(
306 line.contains("myhost"),
307 "syslog line must contain hostname 'myhost': {line}"
308 );
309 }
310
311 #[test]
312 fn encode_log_contains_app_name_in_output() {
313 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
314 let event = make_log_event(Severity::Info, "hello", &[], ts);
315 let encoder = Syslog::new(None, Some("myapp".to_string()));
316 let mut buf = Vec::new();
317 encoder.encode_log(&event, &mut buf).unwrap();
318 let line = String::from_utf8(buf).unwrap();
319 assert!(
320 line.contains("myapp"),
321 "syslog line must contain app-name 'myapp': {line}"
322 );
323 }
324
325 #[test]
326 fn encode_log_default_hostname_and_app_name_are_sonda() {
327 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
328 let event = make_log_event(Severity::Info, "hello", &[], ts);
329 let encoder = Syslog::default();
330 let mut buf = Vec::new();
331 encoder.encode_log(&event, &mut buf).unwrap();
332 let line = String::from_utf8(buf).unwrap();
333 assert!(
335 line.contains("sonda sonda"),
336 "default hostname and app_name must both be 'sonda': {line}"
337 );
338 }
339
340 #[test]
341 fn encode_log_contains_message_in_output() {
342 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
343 let event = make_log_event(Severity::Info, "request completed", &[], ts);
344 let encoder = Syslog::default();
345 let mut buf = Vec::new();
346 encoder.encode_log(&event, &mut buf).unwrap();
347 let line = String::from_utf8(buf).unwrap();
348 assert!(
349 line.contains("request completed"),
350 "syslog line must contain the message: {line}"
351 );
352 }
353
354 fn extract_priority(buf: &[u8]) -> u8 {
360 let line = std::str::from_utf8(buf).unwrap();
361 let end = line.find('>').expect("syslog line must contain '>'");
362 line[1..end]
363 .parse::<u8>()
364 .expect("priority must be a number")
365 }
366
367 #[test]
368 fn priority_for_trace_is_facility_user_plus_debug_syslog_severity() {
369 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
371 let event = make_log_event(Severity::Trace, "trace msg", &[], ts);
372 let encoder = Syslog::default();
373 let mut buf = Vec::new();
374 encoder.encode_log(&event, &mut buf).unwrap();
375 let priority = extract_priority(&buf);
376 assert_eq!(
377 priority, 15,
378 "Trace priority must be 15 (facility=1, severity=7)"
379 );
380 }
381
382 #[test]
383 fn priority_for_debug_is_facility_user_plus_debug_syslog_severity() {
384 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
386 let event = make_log_event(Severity::Debug, "debug msg", &[], ts);
387 let encoder = Syslog::default();
388 let mut buf = Vec::new();
389 encoder.encode_log(&event, &mut buf).unwrap();
390 let priority = extract_priority(&buf);
391 assert_eq!(
392 priority, 15,
393 "Debug priority must be 15 (facility=1, severity=7)"
394 );
395 }
396
397 #[test]
398 fn priority_for_info_is_facility_user_plus_informational_syslog_severity() {
399 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
401 let event = make_log_event(Severity::Info, "info msg", &[], ts);
402 let encoder = Syslog::default();
403 let mut buf = Vec::new();
404 encoder.encode_log(&event, &mut buf).unwrap();
405 let priority = extract_priority(&buf);
406 assert_eq!(
407 priority, 14,
408 "Info priority must be 14 (facility=1, severity=6)"
409 );
410 }
411
412 #[test]
413 fn priority_for_warn_is_facility_user_plus_warning_syslog_severity() {
414 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
416 let event = make_log_event(Severity::Warn, "warn msg", &[], ts);
417 let encoder = Syslog::default();
418 let mut buf = Vec::new();
419 encoder.encode_log(&event, &mut buf).unwrap();
420 let priority = extract_priority(&buf);
421 assert_eq!(
422 priority, 12,
423 "Warn priority must be 12 (facility=1, severity=4)"
424 );
425 }
426
427 #[test]
428 fn priority_for_error_is_facility_user_plus_error_syslog_severity() {
429 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
431 let event = make_log_event(Severity::Error, "error msg", &[], ts);
432 let encoder = Syslog::default();
433 let mut buf = Vec::new();
434 encoder.encode_log(&event, &mut buf).unwrap();
435 let priority = extract_priority(&buf);
436 assert_eq!(
437 priority, 11,
438 "Error priority must be 11 (facility=1, severity=3)"
439 );
440 }
441
442 #[test]
443 fn priority_for_fatal_is_facility_user_plus_emergency_syslog_severity() {
444 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
446 let event = make_log_event(Severity::Fatal, "fatal msg", &[], ts);
447 let encoder = Syslog::default();
448 let mut buf = Vec::new();
449 encoder.encode_log(&event, &mut buf).unwrap();
450 let priority = extract_priority(&buf);
451 assert_eq!(
452 priority, 8,
453 "Fatal priority must be 8 (facility=1, severity=0)"
454 );
455 }
456
457 #[test]
462 fn encode_log_contains_nil_values_for_procid_msgid_and_sd() {
463 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
464 let event = make_log_event(Severity::Info, "hello", &[], ts);
465 let encoder = Syslog::default();
466 let mut buf = Vec::new();
467 encoder.encode_log(&event, &mut buf).unwrap();
468 let line = String::from_utf8(buf).unwrap();
469 assert!(
471 line.contains("- - -"),
472 "syslog line must contain '- - -' (PROCID MSGID SD): {line}"
473 );
474 }
475
476 #[test]
481 fn encode_log_timestamp_is_rfc3339_with_millisecond_precision() {
482 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
484 let event = make_log_event(Severity::Info, "hello", &[], ts);
485 let encoder = Syslog::default();
486 let mut buf = Vec::new();
487 encoder.encode_log(&event, &mut buf).unwrap();
488 let line = String::from_utf8(buf).unwrap();
489 assert!(
490 line.contains("2026-03-20T12:00:00.000Z"),
491 "syslog line must contain RFC 3339 timestamp: {line}"
492 );
493 }
494
495 #[test]
500 fn encode_log_message_with_spaces_is_included_verbatim() {
501 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
502 let event = make_log_event(
503 Severity::Info,
504 "Request from 10.0.0.1 to /api/v2/metrics",
505 &[],
506 ts,
507 );
508 let encoder = Syslog::default();
509 let mut buf = Vec::new();
510 encoder.encode_log(&event, &mut buf).unwrap();
511 let line = String::from_utf8(buf).unwrap();
512 assert!(
513 line.contains("Request from 10.0.0.1 to /api/v2/metrics"),
514 "message with spaces must be preserved: {line}"
515 );
516 }
517
518 #[test]
519 fn encode_log_message_with_unicode_characters() {
520 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
521 let event = make_log_event(Severity::Warn, "Ошибка: сервер недоступен", &[], ts);
522 let encoder = Syslog::default();
523 let mut buf = Vec::new();
524 encoder.encode_log(&event, &mut buf).unwrap();
525 let line = String::from_utf8(buf).unwrap();
526 assert!(
527 line.contains("Ошибка: сервер недоступен"),
528 "unicode message must be preserved: {line}"
529 );
530 }
531
532 #[test]
533 fn encode_log_message_with_angle_brackets() {
534 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
535 let event = make_log_event(Severity::Error, "value <nil> detected", &[], ts);
536 let encoder = Syslog::default();
537 let mut buf = Vec::new();
538 encoder.encode_log(&event, &mut buf).unwrap();
539 let line = String::from_utf8(buf).unwrap();
540 assert!(
541 line.contains("value <nil> detected"),
542 "message with angle brackets must be preserved: {line}"
543 );
544 }
545
546 #[test]
551 fn regression_anchor_info_severity_exact_output() {
552 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
555 let event = make_log_event(Severity::Info, "Request from 10.0.0.1", &[], ts);
556 let encoder = Syslog::new(Some("sonda".to_string()), Some("sonda".to_string()));
557 let mut buf = Vec::new();
558 encoder.encode_log(&event, &mut buf).unwrap();
559 let output = String::from_utf8(buf).unwrap();
560 assert_eq!(
561 output,
562 "<14>1 2026-03-20T12:00:00.000Z sonda sonda - - - Request from 10.0.0.1\n"
563 );
564 }
565
566 #[test]
567 fn regression_anchor_error_severity_exact_output() {
568 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
570 let event = make_log_event(Severity::Error, "connection refused", &[], ts);
571 let encoder = Syslog::new(Some("web01".to_string()), Some("nginx".to_string()));
572 let mut buf = Vec::new();
573 encoder.encode_log(&event, &mut buf).unwrap();
574 let output = String::from_utf8(buf).unwrap();
575 assert_eq!(
576 output,
577 "<11>1 2026-03-20T12:00:00.000Z web01 nginx - - - connection refused\n"
578 );
579 }
580
581 #[test]
582 fn regression_anchor_fatal_severity_exact_output() {
583 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
585 let event = make_log_event(Severity::Fatal, "system crash", &[], ts);
586 let encoder = Syslog::default();
587 let mut buf = Vec::new();
588 encoder.encode_log(&event, &mut buf).unwrap();
589 let output = String::from_utf8(buf).unwrap();
590 assert_eq!(
591 output,
592 "<8>1 2026-03-20T12:00:00.000Z sonda sonda - - - system crash\n"
593 );
594 }
595
596 fn make_log_event_with_labels(
602 severity: Severity,
603 message: &str,
604 labels: &[(&str, &str)],
605 fields: &[(&str, &str)],
606 ts: std::time::SystemTime,
607 ) -> LogEvent {
608 let mut field_map = BTreeMap::new();
609 for (k, v) in fields {
610 field_map.insert(k.to_string(), v.to_string());
611 }
612 let label_set = crate::model::metric::Labels::from_pairs(labels).unwrap();
613 LogEvent::with_timestamp(ts, severity, message.to_string(), label_set, field_map)
614 }
615
616 #[test]
617 fn encode_log_with_labels_includes_structured_data() {
618 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
619 let event = make_log_event_with_labels(
620 Severity::Info,
621 "labeled event",
622 &[("device", "wlan0")],
623 &[],
624 ts,
625 );
626 let encoder = Syslog::default();
627 let mut buf = Vec::new();
628 encoder.encode_log(&event, &mut buf).unwrap();
629 let line = String::from_utf8(buf).unwrap();
630 assert!(
631 line.contains("[sonda device=\"wlan0\"]"),
632 "syslog line must contain structured data [sonda device=\"wlan0\"]: {line}"
633 );
634 }
635
636 #[test]
637 fn encode_log_with_multiple_labels_includes_all_in_structured_data() {
638 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
639 let event = make_log_event_with_labels(
640 Severity::Info,
641 "multi-label event",
642 &[("device", "wlan0"), ("hostname", "router_01")],
643 &[],
644 ts,
645 );
646 let encoder = Syslog::default();
647 let mut buf = Vec::new();
648 encoder.encode_log(&event, &mut buf).unwrap();
649 let line = String::from_utf8(buf).unwrap();
650 assert!(
652 line.contains("[sonda device=\"wlan0\" hostname=\"router_01\"]"),
653 "syslog line must contain sorted labels in structured data: {line}"
654 );
655 }
656
657 #[test]
658 fn encode_log_without_labels_uses_nil_structured_data() {
659 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
660 let event = make_log_event(Severity::Info, "no labels", &[], ts);
661 let encoder = Syslog::default();
662 let mut buf = Vec::new();
663 encoder.encode_log(&event, &mut buf).unwrap();
664 let line = String::from_utf8(buf).unwrap();
665 assert!(
667 line.contains("- - -"),
668 "syslog line without labels must use nil SD (- - -): {line}"
669 );
670 assert!(
671 !line.contains("[sonda"),
672 "syslog line without labels must not contain [sonda: {line}"
673 );
674 }
675
676 #[test]
677 fn encode_log_with_labels_escapes_backslash_in_value() {
678 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
679 let event = make_log_event_with_labels(
680 Severity::Info,
681 "escape test",
682 &[("path", "C:\\Users\\admin")],
683 &[],
684 ts,
685 );
686 let encoder = Syslog::default();
687 let mut buf = Vec::new();
688 encoder.encode_log(&event, &mut buf).unwrap();
689 let line = String::from_utf8(buf).unwrap();
690 assert!(
692 line.contains("path=\"C:\\\\Users\\\\admin\""),
693 "backslashes in label values must be escaped: {line}"
694 );
695 }
696
697 #[test]
698 fn encode_log_with_labels_escapes_closing_bracket_in_value() {
699 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
700 let event = make_log_event_with_labels(
701 Severity::Info,
702 "bracket test",
703 &[("tag", "foo]bar")],
704 &[],
705 ts,
706 );
707 let encoder = Syslog::default();
708 let mut buf = Vec::new();
709 encoder.encode_log(&event, &mut buf).unwrap();
710 let line = String::from_utf8(buf).unwrap();
711 assert!(
713 line.contains("tag=\"foo\\]bar\""),
714 "closing bracket in label value must be escaped: {line}"
715 );
716 }
717
718 #[test]
719 fn encode_log_with_labels_escapes_double_quote_in_value() {
720 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
721 let event = make_log_event_with_labels(
722 Severity::Info,
723 "quote test",
724 &[("desc", "it said \"hello\"")],
725 &[],
726 ts,
727 );
728 let encoder = Syslog::default();
729 let mut buf = Vec::new();
730 encoder.encode_log(&event, &mut buf).unwrap();
731 let line = String::from_utf8(buf).unwrap();
732 assert!(
734 line.contains("desc=\"it said \\\"hello\\\"\""),
735 "double quotes in label value must be escaped: {line}"
736 );
737 }
738
739 #[test]
740 fn encode_log_with_labels_escapes_all_special_characters_combined() {
741 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
742 let event = make_log_event_with_labels(
743 Severity::Info,
744 "combined escape",
745 &[("mixed", "a\\b]c\"d")],
746 &[],
747 ts,
748 );
749 let encoder = Syslog::default();
750 let mut buf = Vec::new();
751 encoder.encode_log(&event, &mut buf).unwrap();
752 let line = String::from_utf8(buf).unwrap();
753 assert!(
755 line.contains("mixed=\"a\\\\b\\]c\\\"d\""),
756 "all special characters must be escaped: {line}"
757 );
758 }
759
760 #[test]
761 fn regression_anchor_info_severity_with_labels_exact_output() {
762 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
763 let event = make_log_event_with_labels(
764 Severity::Info,
765 "Request from 10.0.0.1",
766 &[("device", "wlan0"), ("hostname", "router_01")],
767 &[],
768 ts,
769 );
770 let encoder = Syslog::new(Some("sonda".to_string()), Some("sonda".to_string()));
771 let mut buf = Vec::new();
772 encoder.encode_log(&event, &mut buf).unwrap();
773 let output = String::from_utf8(buf).unwrap();
774 assert_eq!(
775 output,
776 "<14>1 2026-03-20T12:00:00.000Z sonda sonda - - [sonda device=\"wlan0\" hostname=\"router_01\"] Request from 10.0.0.1\n"
777 );
778 }
779
780 #[test]
785 fn syslog_encoder_is_send_and_sync() {
786 fn assert_send_sync<T: Send + Sync>() {}
787 assert_send_sync::<Syslog>();
788 }
789
790 #[cfg(feature = "config")]
795 #[test]
796 fn encoder_config_syslog_deserializes_without_optional_fields() {
797 use crate::encoder::{create_encoder, EncoderConfig};
798 let yaml = "type: syslog";
799 let config: EncoderConfig = serde_yaml_ng::from_str(yaml).unwrap();
800 assert!(
801 matches!(
802 config,
803 EncoderConfig::Syslog {
804 hostname: None,
805 app_name: None
806 }
807 ),
808 "syslog config without optional fields should have None for hostname and app_name"
809 );
810 let _enc = create_encoder(&config).unwrap();
812 }
813
814 #[cfg(feature = "config")]
815 #[test]
816 fn encoder_config_syslog_deserializes_with_hostname() {
817 use crate::encoder::EncoderConfig;
818 let yaml = "type: syslog\nhostname: myhost";
819 let config: EncoderConfig = serde_yaml_ng::from_str(yaml).unwrap();
820 assert!(matches!(
821 config,
822 EncoderConfig::Syslog {
823 hostname: Some(ref h),
824 app_name: None,
825 } if h == "myhost"
826 ));
827 }
828
829 #[cfg(feature = "config")]
830 #[test]
831 fn encoder_config_syslog_deserializes_with_both_hostname_and_app_name() {
832 use crate::encoder::EncoderConfig;
833 let yaml = "type: syslog\nhostname: prod-01\napp_name: api-server";
834 let config: EncoderConfig = serde_yaml_ng::from_str(yaml).unwrap();
835 assert!(matches!(
836 config,
837 EncoderConfig::Syslog {
838 hostname: Some(ref h),
839 app_name: Some(ref a),
840 } if h == "prod-01" && a == "api-server"
841 ));
842 }
843
844 #[test]
845 fn create_encoder_syslog_via_factory_encodes_log_event() {
846 use crate::encoder::{create_encoder, EncoderConfig};
847 let config = EncoderConfig::Syslog {
848 hostname: Some("testhost".to_string()),
849 app_name: Some("testapp".to_string()),
850 };
851 let encoder = create_encoder(&config).unwrap();
852 let ts = UNIX_EPOCH + Duration::from_millis(1_774_008_000_000);
853 let event = make_log_event(Severity::Info, "factory test", &[], ts);
854 let mut buf = Vec::new();
855 encoder.encode_log(&event, &mut buf).unwrap();
856 let output = String::from_utf8(buf).unwrap();
857 assert!(
858 output.contains("testhost"),
859 "factory-created encoder must use configured hostname"
860 );
861 assert!(
862 output.contains("testapp"),
863 "factory-created encoder must use configured app_name"
864 );
865 assert!(
866 output.contains("factory test"),
867 "factory-created encoder must include the message"
868 );
869 }
870}