1use std::{
2 cell::{Cell, RefCell},
3 cmp, env,
4 fmt::Arguments,
5 fs::{File, OpenOptions},
6 io::{Error as IoError, ErrorKind as IoErrorKind, Stdout, Write, stdout},
7 net::{SocketAddr, TcpStream, UdpSocket},
8 ops::{Deref, DerefMut},
9 path::Path,
10 str::FromStr,
11};
12
13use mio::net::UnixDatagram;
14use prost::{Message, encoding::encoded_len_varint};
15
16use crate::{
17 config::{Config, DEFAULT_LOG_TARGET},
18 logging::{LogAddress, LogDuration, LogError, LogMessage, RequestRecord},
19 proto::command::{ProtobufAccessLog, ProtobufAccessLogFormat},
20 writer::MultiLineWriter,
21};
22
23thread_local! {
24 pub static LOGGER: RefCell<Logger> = RefCell::new(Logger::new());
25 static LOGGER_COLORED: Cell<bool> = const { Cell::new(false) };
30}
31
32pub fn is_logger_colored() -> bool {
42 LOGGER_COLORED.with(|c| c.get())
43}
44
45pub fn ansi_palette() -> (
63 &'static str,
64 &'static str,
65 &'static str,
66 &'static str,
67 &'static str,
68) {
69 if is_logger_colored() {
70 ("\x1b[1;97m", "\x1b[0m", "\x1b[37m", "\x1b[90m", "\x1b[97m")
71 } else {
72 ("", "", "", "", "")
73 }
74}
75
76pub static COMPAT_LOGGER: CompatLogger = CompatLogger;
80
81#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
82#[serde(deny_unknown_fields, rename_all = "lowercase")]
83pub enum AccessLogFormat {
84 Ascii,
85 Protobuf,
86}
87
88impl From<&ProtobufAccessLogFormat> for AccessLogFormat {
89 fn from(value: &ProtobufAccessLogFormat) -> Self {
90 match value {
91 ProtobufAccessLogFormat::Ascii => Self::Ascii,
92 ProtobufAccessLogFormat::Protobuf => Self::Protobuf,
93 }
94 }
95}
96
97impl From<&Option<AccessLogFormat>> for ProtobufAccessLogFormat {
98 fn from(value: &Option<AccessLogFormat>) -> Self {
99 match value {
100 Some(AccessLogFormat::Ascii) | None => Self::Ascii,
101 Some(AccessLogFormat::Protobuf) => Self::Protobuf,
102 }
103 }
104}
105
106pub struct InnerLogger {
107 directives: Vec<LogDirective>,
108 backend: LoggerBackend,
109 log_target: String,
111 pub colored: bool,
112 access_backend: Option<LoggerBackend>,
114 access_logs_target: Option<String>,
116 access_format: AccessLogFormat,
118 access_colored: bool,
119 buffer: LoggerBuffer,
120}
121
122pub struct Logger {
123 inner: InnerLogger,
124 tag: String,
126 pid: i32,
128 initialized: bool,
129}
130
131impl std::ops::Deref for Logger {
132 type Target = InnerLogger;
133 fn deref(&self) -> &Self::Target {
134 &self.inner
135 }
136}
137impl std::ops::DerefMut for Logger {
138 fn deref_mut(&mut self) -> &mut Self::Target {
139 &mut self.inner
140 }
141}
142
143impl Default for Logger {
144 fn default() -> Self {
145 Self {
146 inner: InnerLogger {
147 directives: vec![LogDirective {
148 name: None,
149 level: LogLevelFilter::Error,
150 }],
151 backend: LoggerBackend::Stdout(stdout()),
152 log_target: DEFAULT_LOG_TARGET.to_string(),
153 colored: false,
154 access_backend: None,
155 access_logs_target: None,
156 access_format: AccessLogFormat::Ascii,
157 access_colored: false,
158 buffer: LoggerBuffer(Vec::with_capacity(4096)),
159 },
160 tag: "UNINITIALIZED".to_string(),
161 pid: 0,
162 initialized: false,
163 }
164 }
165}
166
167impl Logger {
168 pub fn new() -> Self {
169 Self::default()
170 }
171
172 pub fn init(
173 tag: String,
174 spec: &str,
175 log_target: &str,
176 colored: bool,
177 access_logs_target: Option<&str>,
178 access_format: Option<AccessLogFormat>,
179 access_colored: Option<bool>,
180 ) -> Result<(), LogError> {
181 println!("Logs will be sent to {log_target}");
182 let backend = target_or_default(log_target);
183
184 println!("Access logs will be sent to {access_logs_target:?}");
185 let access_backend = access_logs_target.map(target_to_backend).transpose()?;
186
187 let (directives, _errors) = parse_logging_spec(spec);
188 LOGGER.with(|logger| {
189 let mut logger = logger.borrow_mut();
190 if !logger.initialized {
191 logger.set_directives(directives);
192 logger.colored = match backend {
193 LoggerBackend::Stdout(_) => colored,
194 _ => false,
195 };
196 LOGGER_COLORED.with(|c| c.set(logger.is_colored()));
205 logger.access_colored = match (&access_backend, &backend) {
206 (Some(LoggerBackend::Stdout(_)), _) | (None, LoggerBackend::Stdout(_)) => {
207 access_colored.unwrap_or(colored)
208 }
209 _ => false,
210 };
211 logger.backend = backend;
212 logger.log_target = log_target.to_owned();
213 logger.access_backend = access_backend;
214 logger.access_logs_target = access_logs_target.map(ToOwned::to_owned);
215 logger.access_format = access_format.unwrap_or(AccessLogFormat::Ascii);
216 logger.tag = tag;
217 logger.pid = unsafe { libc::getpid() };
221 logger.initialized = true;
222
223 let _ = log::set_logger(&COMPAT_LOGGER)
224 .map_err(|e| println!("could not register compat logger: {e:?}"));
225
226 log::set_max_level(log::LevelFilter::Info);
227 }
228 });
229 Ok(())
230 }
231
232 pub fn set_directives(&mut self, directives: Vec<LogDirective>) {
233 self.directives = directives;
234 }
235
236 pub fn split(&mut self) -> (i32, &str, &mut InnerLogger) {
237 (self.pid, &self.tag, &mut self.inner)
238 }
239
240 pub fn is_colored(&self) -> bool {
242 self.inner.colored
243 }
244}
245
246struct LoggerBuffer(Vec<u8>);
247
248impl Deref for LoggerBuffer {
249 type Target = Vec<u8>;
250 fn deref(&self) -> &Self::Target {
251 &self.0
252 }
253}
254impl DerefMut for LoggerBuffer {
255 fn deref_mut(&mut self) -> &mut Self::Target {
256 &mut self.0
257 }
258}
259
260impl LoggerBuffer {
261 fn fmt<F: FnOnce(&[u8]) -> Result<usize, IoError>>(
262 &mut self,
263 args: Arguments,
264 flush: F,
265 ) -> Result<(), IoError> {
266 self.clear();
267 self.write_fmt(args)?;
268 flush(self.as_slice())?;
269 Ok(())
270 }
271}
272
273fn log_arguments(
274 args: Arguments,
275 backend: &mut LoggerBackend,
276 buffer: &mut LoggerBuffer,
277) -> Result<(), IoError> {
278 match backend {
279 LoggerBackend::Stdout(stdout) => {
282 let _ = stdout.write_fmt(args);
283 Ok(())
284 }
285 LoggerBackend::Tcp(socket) => {
296 buffer.fmt(args, |bytes| socket.write_all(bytes).map(|()| bytes.len()))
297 }
298 LoggerBackend::File(file) => file.write_fmt(args),
299 LoggerBackend::Unix(socket) => buffer.fmt(args, |bytes| socket.send(bytes)),
304 LoggerBackend::Udp(sock, addr) => buffer.fmt(args, |b| sock.send_to(b, *addr)),
305 }
306}
307
308fn encode_protobuf_access_log(
312 binary_log: &ProtobufAccessLog,
313 buffer: &mut LoggerBuffer,
314) -> Result<(), IoError> {
315 let log_length = binary_log.encoded_len();
316 let total_length = log_length + encoded_len_varint(log_length as u64);
317 buffer.clear();
318 let current_capacity = buffer.capacity();
319 if current_capacity < total_length {
320 buffer.reserve(total_length - current_capacity);
321 }
322 binary_log
323 .encode_length_delimited(&mut buffer.0)
324 .map_err(|e| IoError::new(IoErrorKind::InvalidData, e))?;
325 buffer.extend_from_slice(&[0, 0]); Ok(())
327}
328
329fn write_stream_record<W: Write>(stream: &mut W, bytes: &[u8]) -> Result<(), IoError> {
336 stream.write_all(bytes)
337}
338
339impl InnerLogger {
340 pub fn flush(&mut self) {
347 if let Err(e) = self.backend.flush() {
348 println!("Could not flush logs to {}: {e:?}", self.backend.as_ref());
349 }
350 if let Some(access_backend) = self.access_backend.as_mut()
351 && let Err(e) = access_backend.flush()
352 {
353 println!(
354 "Could not flush access logs to {}: {e:?}",
355 access_backend.as_ref()
356 );
357 }
358 }
359
360 pub fn log(&mut self, args: Arguments) {
361 if let Err(e) = log_arguments(args, &mut self.backend, &mut self.buffer) {
362 println!("Could not write log to {}: {e:?}", self.backend.as_ref());
363 }
364 }
365
366 pub fn log_access(&mut self, log: RequestRecord) -> bool {
370 let backend = self.access_backend.as_mut().unwrap_or(&mut self.backend);
371
372 let io_result = match self.access_format {
373 AccessLogFormat::Protobuf => {
374 let binary_log = log.into_binary_access_log();
375 if let Err(e) = encode_protobuf_access_log(&binary_log, &mut self.buffer) {
376 Err(e)
377 } else {
378 let bytes = &self.buffer;
379 match backend {
380 LoggerBackend::Stdout(stdout) => {
381 let _ = stdout.write(bytes);
382 return true;
383 }
384 LoggerBackend::Tcp(socket) => {
385 write_stream_record(socket, bytes).map(|()| bytes.len())
386 }
387 LoggerBackend::File(file) => file.write(bytes),
388 LoggerBackend::Unix(socket) => socket.send(bytes),
389 LoggerBackend::Udp(socket, address) => socket.send_to(bytes, *address),
390 }
391 .map(|_| ())
392 }
393 }
394 AccessLogFormat::Ascii => crate::_prompt_log! {
395 logger: |args| log_arguments(args, backend, &mut self.buffer),
396 is_access: true,
397 condition: self.access_colored,
398 prompt: [
399 log.now,
400 log.precise_time,
401 log.pid,
402 log.level,
403 log.tag,
404 ],
405 standard: {
406 formats: ["{} {} {} {}/{}/{}/{}/{} {} {} [{}] {:?} {} {}{}\n"],
407 args: [
408 log.context,
409 LogAddress(log.session_address),
410 LogAddress(log.backend_address),
411 LogDuration(Some(log.request_time)),
412 LogDuration(Some(log.service_time)),
413 LogDuration(log.response_time),
414 LogDuration(log.client_rtt),
415 LogDuration(log.server_rtt),
416 log.bytes_in,
417 log.bytes_out,
418 log.full_tags(),
419 log.otel,
420 log.protocol,
421 log.endpoint,
422 LogMessage(log.message),
423 ]
424 },
425 colored: {
426 formats: ["\x1b[;1m{}\x1b[m {} {} {}/{}/{}/{}/{} {} {} \x1b[2m[{}] {:?} \x1b[;1m{} {:#}\x1b[m{}\n"],
427 args: @,
428 }
429 },
430 };
431
432 if let Err(e) = io_result {
433 println!("Could not write access log to {}: {e:?}", backend.as_ref());
434 println!(
435 "Trying to revive the backend of access logs to {:?}, or defaulting to {}",
436 self.access_logs_target, self.log_target
437 );
438 let log_target = self.access_logs_target.as_ref().unwrap_or(&self.log_target);
439 if let Err(err) = backend.revive(log_target) {
440 eprintln!("could not revive logger backend: {err}");
441 }
442 false
443 } else {
444 true
445 }
446 }
447
448 pub fn enabled(&self, meta: Metadata) -> bool {
449 for directive in self.directives.iter().rev() {
451 match &directive.name {
452 Some(name) if !meta.target.starts_with(name) => {}
453 Some(_) | None => return meta.level <= directive.level,
454 }
455 }
456 false
457 }
458
459 fn compat_enabled(&self, meta: &log::Metadata) -> bool {
460 for directive in self.directives.iter().rev() {
462 match &directive.name {
463 Some(name) if !meta.target().starts_with(name) => {}
464 Some(_) | None => return LogLevel::from(meta.level()) <= directive.level,
465 }
466 }
467 false
468 }
469}
470
471pub enum LoggerBackend {
472 Stdout(Stdout),
473 Unix(UnixDatagram),
474 Udp(UdpSocket, SocketAddr),
475 Tcp(TcpStream),
476 File(crate::writer::MultiLineWriter<File>),
477}
478
479impl LoggerBackend {
480 fn revive(&mut self, log_target: &str) -> Result<(), LogError> {
481 *self = target_to_backend(log_target)?;
482 Ok(())
483 }
484
485 fn flush(&mut self) -> Result<(), IoError> {
493 match self {
494 LoggerBackend::File(file) => file.flush(),
495 LoggerBackend::Stdout(stdout) => stdout.flush(),
496 _ => Ok(()),
497 }
498 }
499}
500
501#[repr(usize)]
502#[derive(Clone, Copy, Eq, Debug)]
503pub enum LogLevel {
504 Error = 1, Warn,
512 Info,
516 Debug,
520 Trace,
524}
525
526static LOG_LEVEL_NAMES: [&str; 6] = ["OFF", "ERROR", "WARN", "INFO", "DEBUG", "TRACE"];
527
528impl PartialEq for LogLevel {
529 #[inline]
530 fn eq(&self, other: &LogLevel) -> bool {
531 *self as usize == *other as usize
532 }
533}
534
535impl PartialEq<LogLevelFilter> for LogLevel {
536 #[inline]
537 fn eq(&self, other: &LogLevelFilter) -> bool {
538 *self as usize == *other as usize
539 }
540}
541
542impl PartialOrd for LogLevel {
543 #[inline]
544 fn partial_cmp(&self, other: &LogLevel) -> Option<cmp::Ordering> {
545 Some(self.cmp(other))
546 }
547}
548
549impl PartialOrd<LogLevelFilter> for LogLevel {
550 #[inline]
551 fn partial_cmp(&self, other: &LogLevelFilter) -> Option<cmp::Ordering> {
552 Some((*self as usize).cmp(&(*other as usize)))
553 }
554}
555
556impl Ord for LogLevel {
557 #[inline]
558 fn cmp(&self, other: &LogLevel) -> cmp::Ordering {
559 (*self as usize).cmp(&(*other as usize))
560 }
561}
562
563impl LogLevel {
564 fn from_usize(u: usize) -> Option<LogLevel> {
565 match u {
566 1 => Some(LogLevel::Error),
567 2 => Some(LogLevel::Warn),
568 3 => Some(LogLevel::Info),
569 4 => Some(LogLevel::Debug),
570 5 => Some(LogLevel::Trace),
571 _ => None,
572 }
573 }
574
575 #[inline]
577 pub fn max() -> LogLevel {
578 LogLevel::Trace
579 }
580
581 #[inline]
583 pub fn to_log_level_filter(self) -> LogLevelFilter {
584 LogLevelFilter::from_usize(self as usize).unwrap()
585 }
586}
587
588#[repr(usize)]
589#[derive(Clone, Copy, Eq, Debug)]
590pub enum LogLevelFilter {
591 Off,
592 Error,
593 Warn,
594 Info,
595 Debug,
596 Trace,
597}
598
599impl PartialEq for LogLevelFilter {
600 #[inline]
601 fn eq(&self, other: &LogLevelFilter) -> bool {
602 *self as usize == *other as usize
603 }
604}
605
606impl PartialEq<LogLevel> for LogLevelFilter {
607 #[inline]
608 fn eq(&self, other: &LogLevel) -> bool {
609 other.eq(self)
610 }
611}
612
613impl PartialOrd for LogLevelFilter {
614 #[inline]
615 fn partial_cmp(&self, other: &LogLevelFilter) -> Option<cmp::Ordering> {
616 Some(self.cmp(other))
617 }
618}
619
620impl PartialOrd<LogLevel> for LogLevelFilter {
621 #[inline]
622 fn partial_cmp(&self, other: &LogLevel) -> Option<cmp::Ordering> {
623 other.partial_cmp(self).map(|x| x.reverse())
624 }
625}
626
627impl Ord for LogLevelFilter {
628 #[inline]
629 fn cmp(&self, other: &LogLevelFilter) -> cmp::Ordering {
630 (*self as usize).cmp(&(*other as usize))
631 }
632}
633
634impl FromStr for LogLevelFilter {
635 type Err = ();
636 fn from_str(level: &str) -> Result<LogLevelFilter, ()> {
637 LOG_LEVEL_NAMES
638 .iter()
639 .position(|&name| name.eq_ignore_ascii_case(level))
640 .map(|p| LogLevelFilter::from_usize(p).unwrap())
641 .ok_or(())
642 }
643}
644
645impl LogLevelFilter {
646 fn from_usize(u: usize) -> Option<LogLevelFilter> {
647 match u {
648 0 => Some(LogLevelFilter::Off),
649 1 => Some(LogLevelFilter::Error),
650 2 => Some(LogLevelFilter::Warn),
651 3 => Some(LogLevelFilter::Info),
652 4 => Some(LogLevelFilter::Debug),
653 5 => Some(LogLevelFilter::Trace),
654 _ => None,
655 }
656 }
657 #[inline]
659 pub fn max() -> LogLevelFilter {
660 LogLevelFilter::Trace
661 }
662
663 #[inline]
667 pub fn to_log_level(self) -> Option<LogLevel> {
668 LogLevel::from_usize(self as usize)
669 }
670}
671
672#[derive(Debug)]
674pub struct Metadata {
675 pub level: LogLevel,
676 pub target: &'static str,
677}
678
679#[derive(Debug)]
680pub struct LogDirective {
681 name: Option<String>,
682 level: LogLevelFilter,
683}
684
685#[derive(thiserror::Error, Debug)]
686pub enum LogSpecParseError {
687 #[error("Too many '/'s: {0}")]
688 TooManySlashes(String),
689 #[error("Too many '='s: {0}")]
690 TooManyEquals(String),
691 #[error("Invalid log level: {0}")]
692 InvalidLogLevel(String),
693}
694
695pub fn parse_logging_spec(spec: &str) -> (Vec<LogDirective>, Vec<LogSpecParseError>) {
696 let mut dirs = Vec::new();
697 let mut errors = Vec::new();
698
699 let mut parts = spec.split('/');
700 let mods = parts.next();
701 let _ = parts.next();
702 if parts.next().is_some() {
703 errors.push(LogSpecParseError::TooManySlashes(spec.to_string()));
704 }
705 if let Some(m) = mods {
706 for s in m.split(',') {
707 if s.is_empty() {
708 continue;
709 }
710 let mut parts = s.split('=');
711 let (log_level, name) =
712 match (parts.next(), parts.next().map(|s| s.trim()), parts.next()) {
713 (Some(part0), None, None) => {
714 match part0.parse() {
717 Ok(num) => (num, None),
718 Err(_) => {
719 errors.push(LogSpecParseError::InvalidLogLevel(s.to_string()));
720 (LogLevelFilter::max(), None)
721 }
722 }
723 }
724 (Some(part0), Some(""), None) => (LogLevelFilter::max(), Some(part0)),
725 (Some(part0), Some(part1), None) => match part1.parse() {
726 Ok(num) => (num, Some(part0)),
727 Err(_) => {
728 errors.push(LogSpecParseError::InvalidLogLevel(s.to_string()));
729 continue;
730 }
731 },
732 _ => {
733 errors.push(LogSpecParseError::TooManyEquals(s.to_string()));
734 continue;
735 }
736 };
737 dirs.push(LogDirective {
738 name: name.map(|s| s.to_string()),
739 level: log_level,
740 });
741 }
742 }
743
744 for error in &errors {
745 println!("{error:?}");
746 }
747 (dirs, errors)
748}
749
750pub fn setup_default_logging(
752 log_colored: bool,
753 log_level: &str,
754 tag: &str,
755) -> Result<(), LogError> {
756 setup_logging("stdout", log_colored, None, None, None, log_level, tag)
757}
758
759pub fn setup_logging_with_config(config: &Config, tag: &str) -> Result<(), LogError> {
761 setup_logging(
762 &config.log_target,
763 config.log_colored,
764 config.access_logs_target.as_deref(),
765 config.access_logs_format.clone(),
766 config.access_logs_colored,
767 &config.log_level,
768 tag,
769 )
770}
771
772pub fn setup_logging(
777 log_target: &str,
778 log_colored: bool,
779 access_logs_target: Option<&str>,
780 access_logs_format: Option<AccessLogFormat>,
781 access_logs_colored: Option<bool>,
782 log_level: &str,
783 tag: &str,
784) -> Result<(), LogError> {
785 Logger::init(
786 tag.to_string(),
787 env::var("RUST_LOG").as_deref().unwrap_or(log_level),
788 log_target,
789 log_colored,
790 access_logs_target,
791 access_logs_format,
792 access_logs_colored,
793 )
794}
795
796fn target_or_default(target: &str) -> LoggerBackend {
798 match target_to_backend(target) {
799 Ok(backend) => backend,
800 Err(target_error) => {
801 eprintln!("{target_error}, defaulting to stdout");
802 LoggerBackend::Stdout(stdout())
803 }
804 }
805}
806
807pub fn target_to_backend(target: &str) -> Result<LoggerBackend, LogError> {
808 if target == "stdout" {
809 return Ok(LoggerBackend::Stdout(stdout()));
810 }
811
812 if let Some(addr) = target.strip_prefix("udp://") {
813 let address = addr
814 .parse::<SocketAddr>()
815 .map_err(|e| LogError::InvalidSocketAddress(target.to_owned(), e))?;
816
817 let socket = UdpSocket::bind(("0.0.0.0", 0)).map_err(LogError::UdpBind)?;
818
819 return Ok(LoggerBackend::Udp(socket, address));
820 }
821
822 if let Some(addr) = target.strip_prefix("tcp://") {
823 let tcp_stream =
824 TcpStream::connect(addr).map_err(|e| LogError::TcpConnect(target.to_owned(), e))?;
825
826 return Ok(LoggerBackend::Tcp(tcp_stream));
827 }
828
829 if let Some(addr) = target.strip_prefix("unix://") {
830 let socket = UnixDatagram::unbound().map_err(LogError::CreateUnixSocket)?;
831
832 socket
833 .connect(addr)
834 .map_err(|e| LogError::ConnectToUnixSocket(target.to_owned(), e))?;
835
836 return Ok(LoggerBackend::Unix(socket));
837 }
838
839 if let Some(addr) = target.strip_prefix("file://") {
840 let path = Path::new(addr);
841 let file = OpenOptions::new()
842 .create(true)
843 .append(true)
844 .open(path)
845 .map_err(|e| LogError::OpenFile(target.to_owned(), e))?;
846
847 return Ok(LoggerBackend::File(MultiLineWriter::new(file)));
848 }
849
850 Err(LogError::InvalidLogTarget(
851 target.to_owned(),
852 "Log target is not parseable",
853 ))
854}
855
856#[macro_export]
857macro_rules! _prompt_log {
858 {
859 logger: $logger:expr,
860 is_access: $access:expr,
861 condition: $cond:expr,
862 prompt: [$($p:tt)*],
863 standard: {$($std:tt)*}$(,)?
864 } => {
865 $crate::_prompt_log!{
866 logger: $logger,
867 is_access: $access,
868 condition: $cond,
869 prompt: [$($p)*],
870 standard: {$($std)*},
871 colored: {$($std)*},
872 }
873 };
874 {
875 logger: $logger:expr,
876 is_access: $access:expr,
877 condition: $cond:expr,
878 prompt: [$($p:tt)*],
879 standard: {
880 formats: [$($std_fmt:tt)*],
881 args: [$($std_args:expr),*$(,)?]$(,)?
882 },
883 colored: {
884 formats: [$($col_fmt:tt)*],
885 args: @$(,)?
886 }$(,)?
887 } => {
888 $crate::_prompt_log!{
889 logger: $logger,
890 is_access: $access,
891 condition: $cond,
892 prompt: [$($p)*],
893 standard: {
894 formats: [$($std_fmt)*],
895 args: [$($std_args),*],
896 },
897 colored: {
898 formats: [$($col_fmt)*],
899 args: [$($std_args),*],
900 },
901 }
902 };
903 {
904 logger: $logger:expr,
905 is_access: $access:expr,
906 condition: $cond:expr,
907 prompt: [$now:expr, $precise_time:expr, $pid:expr, $lvl:expr, $tag:expr$(,)?],
908 standard: {
909 formats: [$($std_fmt:tt)*],
910 args: [$($std_args:expr),*$(,)?]$(,)?
911 },
912 colored: {
913 formats: [$($col_fmt:tt)*],
914 args: [$($col_args:expr),*$(,)?]$(,)?
915 }$(,)?
916 } => {
917 if $cond {
918 $crate::_prompt_log!(@bind [$logger, concat!("{} \x1b[2m{} \x1b[;2;1m{} {} \x1b[0;1m{}\x1b[m\t", $($col_fmt)*)] [$now, $precise_time, $pid, $lvl.as_str($access, true), $tag] $($col_args),*)
919 } else {
920 $crate::_prompt_log!(@bind [$logger, concat!("{} {} {} {} {}\t", $($std_fmt)*)] [$now, $precise_time, $pid, $lvl.as_str($access, false), $tag] $($std_args),*)
921 }
922 };
923 (@bind [$logger:expr, $fmt:expr] [$($bindings:expr),*] $arg:expr $(, $args:expr)*) => {{
924 let binding = &$arg;
925 $crate::_prompt_log!(@bind [$logger, $fmt] [$($bindings),* , binding] $($args),*)
926 }};
927 (@bind [$logger:expr, $fmt:expr] [$($bindings:expr),*]) => {
928 $logger(format_args!($fmt, $($bindings),*))
929 };
930}
931
932#[derive(Clone, Copy, Debug)]
933pub struct LogLineCachedState(u8);
934const LOG_LINE_ENABLED: u8 = 1 << 7;
935
936impl Default for LogLineCachedState {
937 fn default() -> Self {
938 Self::new()
939 }
940}
941
942impl LogLineCachedState {
943 pub const fn new() -> Self {
944 Self(0)
945 }
946 #[inline(always)]
947 pub fn version(&self) -> u8 {
948 self.0 & !LOG_LINE_ENABLED
949 }
950 #[inline(always)]
951 pub fn enabled(&self) -> bool {
952 self.0 & LOG_LINE_ENABLED != 0
953 }
954 #[inline(always)]
955 pub fn set(&mut self, version: u8, enabled: bool) {
956 self.0 = version;
957 if enabled {
958 self.0 |= LOG_LINE_ENABLED
959 }
960 }
961}
962
963#[macro_export]
964macro_rules! _log_enabled {
965 ($logger:expr, $lvl:expr) => {{
966 let logger = $logger.borrow_mut();
967 if !logger.enabled($crate::logging::Metadata {
968 level: $lvl,
969 target: module_path!(),
970 }) {
971 return;
972 }
973 logger
974 }};
975}
976
977#[macro_export]
978macro_rules! _log {
979 ($lvl:expr, $format:expr $(, $args:expr)*) => {{
980 $crate::logging::LOGGER.with(|logger| {
981 let mut logger = $crate::_log_enabled!(logger, $lvl);
982 let (pid, tag, inner) = logger.split();
983 let (now, precise_time) = $crate::logging::now();
984
985 $crate::_prompt_log!{
986 logger: |args| inner.log(args),
987 is_access: false,
988 condition: inner.colored,
989 prompt: [now, precise_time, pid, $lvl, tag],
990 standard: {
991 formats: [$format, '\n'],
992 args: [$($args),*]
993 }
994 };
995 })
996 }};
997}
998
999#[macro_export]
1000macro_rules! _log_access {
1001 ($lvl:expr, $on_failure:block, $($request_record_fields:tt)*) => {{
1002 $crate::logging::LOGGER.with(|logger| {
1003 let success = {
1004 let mut logger = $crate::_log_enabled!(logger, $lvl);
1005 let (pid, tag, inner) = logger.split();
1006 let (now, precise_time) = $crate::logging::now();
1007
1008 inner.log_access(
1009 $crate::_structured_access_log!(
1010 [$crate::logging::RequestRecord]
1011 pid, tag, now, precise_time, level: $lvl, $($request_record_fields)*
1012 )
1013 )
1014 }; if !success {
1017 $on_failure
1020 }
1021 });
1022 }};
1023}
1024
1025#[macro_export]
1026macro_rules! _structured_access_log {
1027 ([$($struct_name:tt)+] $($fields:tt)*) => {{
1028 $($struct_name)+ {$(
1029 $fields
1030 )*}
1031 }};
1032}
1033
1034#[macro_export]
1035macro_rules! log_access {
1037 ($error:expr, on_failure: $on_failure:block, $($request_record_fields:tt)*) => {
1038 let lvl = if $error {
1039 $crate::logging::LogLevel::Error
1040 } else {
1041 $crate::logging::LogLevel::Info
1042 };
1043 _log_access!(lvl, $on_failure, $($request_record_fields)*);
1044 };
1045}
1046
1047#[macro_export]
1049macro_rules! error_access {
1050 (on_failure: $on_failure:block, $($request_record_fields:tt)*) => {
1051 $crate::_log_access!($crate::logging::LogLevel::Error, $on_failure, $($request_record_fields)*);
1052 };
1053}
1054
1055#[macro_export]
1057macro_rules! info_access {
1058 (on_failure: $on_failure:block, $($request_record_fields:tt)*) => {
1059 $crate::_log_access!($crate::logging::LogLevel::Info, $on_failure, $($request_record_fields)*);
1060 };
1061}
1062
1063#[macro_export]
1065macro_rules! error {
1066 ($format:expr $(, $args:expr)* $(,)?) => {
1067 $crate::_log!($crate::logging::LogLevel::Error, $format $(, $args)*)
1068 };
1069}
1070
1071#[macro_export]
1073macro_rules! warn {
1074 ($format:expr $(, $args:expr)* $(,)?) => {
1075 $crate::_log!($crate::logging::LogLevel::Warn, $format $(, $args)*)
1076 };
1077}
1078
1079#[macro_export]
1081macro_rules! info {
1082 ($format:expr $(, $args:expr)* $(,)?) => {
1083 $crate::_log!($crate::logging::LogLevel::Info, $format $(, $args)*)
1084 };
1085}
1086
1087#[macro_export]
1089macro_rules! debug {
1090 ($format:expr $(, $args:expr)* $(,)?) => {{
1091 #[cfg(any(debug_assertions, feature = "logs-debug", feature = "logs-trace"))]
1092 $crate::_log!($crate::logging::LogLevel::Debug, concat!("{}\t", $format), module_path!() $(, $args)*);
1093 #[cfg(not(any(debug_assertions, feature = "logs-debug", feature = "logs-trace")))]
1100 if false {$( let _ = $args; )*}
1101 }};
1102}
1103
1104#[macro_export]
1106macro_rules! trace {
1107 ($format:expr $(, $args:expr)* $(,)?) => {{
1108 #[cfg(any(debug_assertions, feature = "logs-trace"))]
1109 $crate::_log!($crate::logging::LogLevel::Trace, concat!("{}\t", $format), module_path!() $(, $args)*);
1110 #[cfg(not(any(debug_assertions, feature = "logs-trace")))]
1111 if false {$( let _ = $args; )*}
1112 }};
1113}
1114
1115#[macro_export]
1117macro_rules! fixme {
1118 ($(, $args:expr)* $(,)?) => {
1119 $crate::_log!($crate::logging::LogLevel::Info, "FIXME: {}:{} in {}: {}", file!(), line!(), module_path!() $(, $args)*)
1120 };
1121}
1122
1123pub struct CompatLogger;
1124
1125impl From<log::Level> for LogLevel {
1126 fn from(lvl: log::Level) -> Self {
1127 match lvl {
1128 log::Level::Error => LogLevel::Error,
1129 log::Level::Warn => LogLevel::Warn,
1130 log::Level::Info => LogLevel::Info,
1131 log::Level::Debug => LogLevel::Debug,
1132 log::Level::Trace => LogLevel::Trace,
1133 }
1134 }
1135}
1136
1137impl log::Log for CompatLogger {
1138 fn enabled(&self, _: &log::Metadata) -> bool {
1139 true
1140 }
1141
1142 fn log(&self, record: &log::Record) {
1143 LOGGER.with(|logger| {
1144 let mut logger = logger.borrow_mut();
1145 if !logger.compat_enabled(record.metadata()) {
1146 return;
1147 }
1148 let (pid, tag, inner) = logger.split();
1149 let (now, precise_time) = now();
1150 crate::_prompt_log! {
1151 logger: |args| inner.log(args),
1152 is_access: false,
1153 condition: inner.colored,
1154 prompt: [
1155 now, precise_time, pid, LogLevel::from(record.level()), tag
1156 ],
1157 standard: {
1158 formats: ["{}\n"],
1159 args: [record.args()]
1160 }
1161 };
1162 })
1163 }
1164
1165 fn flush(&self) {
1169 LOGGER.with(|logger| {
1170 if let Ok(mut logger) = logger.try_borrow_mut() {
1171 logger.flush();
1172 }
1173 })
1174 }
1175}
1176
1177#[macro_export]
1179macro_rules! setup_test_logger {
1180 () => {
1181 let _ = $crate::logging::Logger::init(
1182 module_path!().to_string(),
1183 "error",
1184 sozu_command_lib::config::DEFAULT_LOG_TARGET,
1185 false,
1186 None,
1187 None,
1188 None,
1189 );
1190 };
1191}
1192
1193pub struct Rfc3339Time {
1194 pub inner: ::time::OffsetDateTime,
1195}
1196
1197pub fn now() -> (Rfc3339Time, i128) {
1199 let t = time::OffsetDateTime::now_utc();
1200 (
1201 Rfc3339Time { inner: t },
1202 (t - time::OffsetDateTime::UNIX_EPOCH).whole_nanoseconds(),
1203 )
1204}
1205
1206#[cfg(test)]
1207mod tests {
1208 use super::{
1236 AccessLogFormat, InnerLogger, LogLevel, LoggerBackend, LoggerBuffer,
1237 encode_protobuf_access_log, now, write_stream_record,
1238 };
1239 use crate::{
1240 logging::{EndpointRecord, LogContext, RequestRecord},
1241 proto::command::ProtobufAccessLog,
1242 };
1243 use nix::sys::socket::{setsockopt, sockopt::SndBuf};
1244 use prost::Message;
1245 use rusty_ulid::Ulid;
1246 use std::{
1247 io::{Error as IoError, ErrorKind, Read, Write},
1248 net::{Shutdown, TcpListener},
1249 time::Duration,
1250 };
1251
1252 #[derive(Default)]
1254 struct CountingSink {
1255 calls: usize,
1256 bytes: Vec<u8>,
1257 }
1258
1259 impl Write for CountingSink {
1260 fn write(&mut self, buf: &[u8]) -> Result<usize, IoError> {
1261 self.calls += 1;
1262 self.bytes.extend_from_slice(buf);
1263 Ok(buf.len())
1264 }
1265 fn flush(&mut self) -> Result<(), IoError> {
1266 Ok(())
1267 }
1268 }
1269
1270 struct ShortWriteSink {
1273 limit: usize,
1274 calls: usize,
1275 bytes: Vec<u8>,
1276 }
1277
1278 impl Write for ShortWriteSink {
1279 fn write(&mut self, buf: &[u8]) -> Result<usize, IoError> {
1280 self.calls += 1;
1281 let n = buf.len().min(self.limit);
1282 self.bytes.extend_from_slice(&buf[..n]);
1283 Ok(n)
1284 }
1285 fn flush(&mut self) -> Result<(), IoError> {
1286 Ok(())
1287 }
1288 }
1289
1290 struct Fields {
1299 timestamp: &'static str,
1300 nanos: u64,
1301 pid: i32,
1302 level: &'static str,
1303 tag: &'static str,
1304 context: &'static str,
1305 session: &'static str,
1306 backend: &'static str,
1307 request_time: &'static str,
1308 service_time: &'static str,
1309 response_time: &'static str,
1310 client_rtt: &'static str,
1311 server_rtt: &'static str,
1312 bytes_in: u64,
1313 bytes_out: u64,
1314 protocol: &'static str,
1315 endpoint: &'static str,
1316 request: &'static str,
1317 status: u16,
1318 message: &'static str,
1319 }
1320
1321 #[inline(never)]
1322 fn fields() -> Fields {
1323 Fields {
1324 timestamp: "2026-09-26T08:30:17.644122Z",
1325 nanos: 1_790_411_417_644_122_623,
1326 pid: 990_961,
1327 level: "INFO-ACCESS",
1328 tag: "WRK-00",
1329 context: "01M3EDCA4D8KTDBGT0C64M50R1 PyBackend py",
1330 session: "127.0.0.1:54610",
1331 backend: "127.0.0.1:21080",
1332 request_time: "1102\u{3bc}s",
1333 service_time: "415\u{3bc}s",
1334 response_time: "661\u{3bc}s",
1335 client_rtt: "18\u{3bc}s",
1336 server_rtt: "34\u{3bc}s",
1337 bytes_in: 91,
1338 bytes_out: 253,
1339 protocol: "http",
1340 endpoint: "lolcatho.st:21081",
1341 request: "GET /index.html",
1342 status: 200,
1343 message: " | H1::Complete",
1344 }
1345 }
1346
1347 macro_rules! access_log_shaped {
1351 ($sink:expr, $buffer:expr, $f:expr) => {
1352 $buffer.fmt(
1353 format_args!(
1354 "{} {} {} {} {}\t[{}] {} {} {}/{}/{}/{}/{} {} {} {} {} {} {}{}\n",
1355 $f.timestamp,
1356 $f.nanos,
1357 $f.pid,
1358 $f.level,
1359 $f.tag,
1360 $f.context,
1361 $f.session,
1362 $f.backend,
1363 $f.request_time,
1364 $f.service_time,
1365 $f.response_time,
1366 $f.client_rtt,
1367 $f.server_rtt,
1368 $f.bytes_in,
1369 $f.bytes_out,
1370 $f.protocol,
1371 $f.endpoint,
1372 $f.request,
1373 $f.status,
1374 $f.message,
1375 ),
1376 |bytes| $sink.write(bytes),
1377 )
1378 };
1379 }
1380
1381 #[test]
1382 fn a_fragmented_record_reaches_the_sink_in_exactly_one_call() {
1383 let mut buffer = LoggerBuffer(Vec::with_capacity(4096));
1384 let mut sink = CountingSink::default();
1385
1386 let f = fields();
1387 access_log_shaped!(sink, buffer, f).expect("the record is written");
1388
1389 assert_eq!(
1390 sink.calls, 1,
1391 "an access-log record must reach a stream sink in ONE call, not one \
1392 per formatter fragment -- the `tcp://` backend has no buffering of \
1393 its own, so a call here is a syscall there"
1394 );
1395 assert!(
1396 sink.bytes.ends_with(b" | H1::Complete\n"),
1397 "the single call must carry the COMPLETE record, terminator included"
1398 );
1399 assert!(
1400 sink.bytes.starts_with(b"2026-09-26T08:30:17.644122Z "),
1401 "...and start at the beginning of the record"
1402 );
1403 }
1404
1405 #[test]
1406 fn the_buffer_is_reused_so_a_record_costs_no_allocation() {
1407 let mut buffer = LoggerBuffer(Vec::with_capacity(4096));
1408 let capacity_before = buffer.capacity();
1409
1410 let mut first = CountingSink::default();
1411 let f = fields();
1412 access_log_shaped!(first, buffer, f).expect("first record written");
1413 let mut second = CountingSink::default();
1414 access_log_shaped!(second, buffer, f).expect("second record written");
1415
1416 assert_eq!(
1417 buffer.capacity(),
1418 capacity_before,
1419 "a record must not grow the buffer: the zero-allocation property of \
1420 this path is what makes buffering acceptable on the hot path"
1421 );
1422 assert_eq!(second.calls, 1, "the second record is still a single call");
1423 assert_eq!(
1424 first.bytes, second.bytes,
1425 "`fmt` clears the buffer, so a record must not carry the previous one's tail"
1426 );
1427 }
1428
1429 #[test]
1430 fn a_short_write_does_not_truncate_a_record() {
1431 let mut buffer = LoggerBuffer(Vec::with_capacity(4096));
1435 let mut sink = ShortWriteSink {
1436 limit: 7,
1437 calls: 0,
1438 bytes: Vec::new(),
1439 };
1440
1441 let written = buffer
1442 .fmt(
1443 format_args!("{} {} {}\n", "first", "second", "third"),
1444 |bytes| sink.write_all(bytes).map(|()| bytes.len()),
1445 )
1446 .map(|()| sink.bytes.clone());
1447
1448 let bytes = written.expect("the record is written despite short writes");
1449 assert_eq!(
1450 bytes, b"first second third\n",
1451 "`write_all` must keep going until the whole record is consumed; \
1452 a bare `write` would have stopped after {} bytes",
1453 sink.limit
1454 );
1455 assert!(
1456 sink.calls > 1,
1457 "the fixture must actually exercise the short-write path"
1458 );
1459 }
1460
1461 fn protobuf_record(tag: &str, bytes_in: u64) -> ProtobufAccessLog {
1463 ProtobufAccessLog {
1464 message: Some("an error message long enough to need several short writes".into()),
1465 protocol: "HTTP".into(),
1466 tag: tag.into(),
1467 bytes_in,
1468 ..Default::default()
1469 }
1470 }
1471
1472 #[test]
1473 fn short_writes_keep_consecutive_protobuf_records_framed() {
1474 let mut buffer = LoggerBuffer(Vec::with_capacity(4096));
1479 let mut sink = ShortWriteSink {
1480 limit: 7,
1481 calls: 0,
1482 bytes: Vec::new(),
1483 };
1484 let records = [
1485 protobuf_record("WRK-00", 91),
1486 protobuf_record("WRK-01", 253),
1487 ];
1488
1489 for record in &records {
1490 encode_protobuf_access_log(record, &mut buffer).expect("the record is framed");
1491 assert!(
1492 buffer.len() > sink.limit,
1493 "the fixture must actually exercise the short-write path"
1494 );
1495 write_stream_record(&mut sink, &buffer)
1496 .expect("the record is written despite short writes");
1497 }
1498
1499 let mut stream = sink.bytes.as_slice();
1500 for expected in &records {
1501 let decoded = ProtobufAccessLog::decode_length_delimited(&mut stream)
1502 .expect("every record decodes from the stream, in order");
1503 assert_eq!(
1504 &decoded, expected,
1505 "a short write must not truncate a record or shift the next one"
1506 );
1507 assert_eq!(
1508 stream.get(..2),
1509 Some(&[0u8, 0][..]),
1510 "each record is followed by its two zero bytes"
1511 );
1512 stream = &stream[2..];
1513 }
1514 assert!(stream.is_empty(), "nothing follows the last record");
1515 }
1516
1517 #[test]
1518 fn a_stream_record_error_is_propagated() {
1519 struct FailingSink;
1520 impl Write for FailingSink {
1521 fn write(&mut self, _: &[u8]) -> Result<usize, IoError> {
1522 Err(IoError::new(ErrorKind::BrokenPipe, "peer went away"))
1523 }
1524 fn flush(&mut self) -> Result<(), IoError> {
1525 Ok(())
1526 }
1527 }
1528
1529 let error = write_stream_record(&mut FailingSink, b"a protobuf record")
1530 .expect_err("a dead sink must surface, so `log_access` can revive the backend");
1531 assert_eq!(error.kind(), ErrorKind::BrokenPipe);
1532 }
1533
1534 #[test]
1535 fn a_sink_error_is_propagated() {
1536 struct FailingSink;
1537 impl Write for FailingSink {
1538 fn write(&mut self, _: &[u8]) -> Result<usize, IoError> {
1539 Err(IoError::new(ErrorKind::BrokenPipe, "peer went away"))
1540 }
1541 fn flush(&mut self) -> Result<(), IoError> {
1542 Ok(())
1543 }
1544 }
1545
1546 let mut buffer = LoggerBuffer(Vec::with_capacity(4096));
1547 let mut sink = FailingSink;
1548 let error = buffer
1549 .fmt(format_args!("a {} record\n", "failing"), |bytes| {
1550 sink.write_all(bytes).map(|()| bytes.len())
1551 })
1552 .expect_err("a dead sink must surface, so `log_access` can revive the backend");
1553
1554 assert_eq!(
1555 error.kind(),
1556 ErrorKind::BrokenPipe,
1557 "the sink's error kind must survive: `log_access` reports it and \
1558 calls `LoggerBackend::revive`"
1559 );
1560 }
1561
1562 fn protobuf_access_record<'a>(message: &'a str, tag: &'a str, id: u128) -> RequestRecord<'a> {
1565 let (now, precise_time) = now();
1566 RequestRecord {
1567 message: Some(message),
1568 context: LogContext {
1569 session_id: Ulid::from(id),
1570 request_id: Some(Ulid::from(id + 1)),
1571 cluster_id: Some("short-write-cluster"),
1572 backend_id: Some("short-write-backend"),
1573 },
1574 session_address: None,
1575 backend_address: None,
1576 protocol: "tcp",
1577 endpoint: EndpointRecord::Tcp,
1578 tags: None,
1579 client_rtt: None,
1580 server_rtt: None,
1581 user_agent: None,
1582 x_request_id: None,
1583 tls_version: None,
1584 tls_cipher: None,
1585 tls_sni: None,
1586 tls_alpn: None,
1587 xff_chain: None,
1588 service_time: Duration::ZERO,
1589 response_time: None,
1590 request_time: Duration::ZERO,
1591 start_time_ns: None,
1592 bytes_in: id as usize,
1593 bytes_out: (id + 1) as usize,
1594 otel: None,
1595 pid: 1,
1596 tag,
1597 level: LogLevel::Info,
1598 now,
1599 precise_time,
1600 }
1601 }
1602
1603 fn encode_protobuf_frame(record: RequestRecord<'_>) -> Vec<u8> {
1604 let protobuf = record.into_binary_access_log();
1605 let mut frame = Vec::new();
1606 protobuf
1607 .encode_length_delimited(&mut frame)
1608 .expect("the protobuf access-log fixture must encode");
1609 frame.extend_from_slice(&[0, 0]);
1610 frame
1611 }
1612
1613 fn drain_until_idle(peer: &mut std::net::TcpStream, captured: &mut Vec<u8>) -> usize {
1614 let before = captured.len();
1615 let mut bytes = [0u8; 64 * 1024];
1616 loop {
1617 match peer.read(&mut bytes) {
1618 Ok(0) => break,
1619 Ok(read) => captured.extend_from_slice(&bytes[..read]),
1620 Err(error)
1621 if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut) =>
1622 {
1623 break;
1624 }
1625 Err(error) => panic!("could not drain the loopback peer: {error}"),
1626 }
1627 }
1628 captured.len() - before
1629 }
1630
1631 fn decode_protobuf_tags(mut stream: &[u8]) -> Result<Vec<String>, String> {
1632 let mut tags = Vec::new();
1633 for record_index in 0..2 {
1634 let record = ProtobufAccessLog::decode_length_delimited(&mut stream)
1635 .map_err(|error| format!("record {record_index} did not decode: {error}"))?;
1636 if !stream.starts_with(&[0, 0]) {
1637 return Err(format!(
1638 "record {record_index} was not followed by its two zero bytes"
1639 ));
1640 }
1641 stream = &stream[2..];
1642 tags.push(record.tag);
1643 }
1644 if !stream.is_empty() {
1645 return Err(format!(
1646 "{} unframed bytes follow the records",
1647 stream.len()
1648 ));
1649 }
1650 Ok(tags)
1651 }
1652
1653 #[test]
1654 fn protobuf_tcp_short_write_completes_or_surfaces_error() {
1655 let listener = TcpListener::bind("127.0.0.1:0").expect("bind the loopback log sink");
1661 let address = listener.local_addr().expect("read the log sink address");
1662 let sender = std::net::TcpStream::connect(address).expect("connect the log backend");
1663 let (mut peer, _) = listener.accept().expect("accept the log backend");
1664
1665 setsockopt(&sender, SndBuf, &4096usize).expect("shrink the sender queue");
1669 sender
1670 .set_write_timeout(Some(Duration::from_millis(250)))
1671 .expect("bound a blocked log write");
1672 peer.set_read_timeout(Some(Duration::from_millis(250)))
1673 .expect("bound capture reads");
1674
1675 let log_target = format!("tcp://{address}");
1676 let mut logger = InnerLogger {
1677 directives: Vec::new(),
1678 backend: LoggerBackend::Tcp(sender),
1679 log_target,
1680 colored: false,
1681 access_backend: None,
1682 access_logs_target: None,
1683 access_format: AccessLogFormat::Protobuf,
1684 access_colored: false,
1685 buffer: LoggerBuffer(Vec::with_capacity(4096)),
1686 };
1687
1688 let first_message = "A".repeat(1024 * 1024);
1689 let second_message = "B".repeat(1024 * 1024);
1690 let first_expected =
1691 encode_protobuf_frame(protobuf_access_record(&first_message, "WRK-SHORT-00", 10));
1692 let second_expected =
1693 encode_protobuf_frame(protobuf_access_record(&second_message, "WRK-SHORT-01", 20));
1694
1695 let first_ok =
1696 logger.log_access(protobuf_access_record(&first_message, "WRK-SHORT-00", 10));
1697 if !first_ok {
1698 return;
1702 }
1703
1704 let mut captured = Vec::new();
1705 let first_written = drain_until_idle(&mut peer, &mut captured);
1706 assert!(
1707 first_written > 0 && first_written < first_expected.len(),
1708 "the harness must induce a real short write: captured {first_written} of {} bytes",
1709 first_expected.len()
1710 );
1711
1712 let second_ok =
1713 logger.log_access(protobuf_access_record(&second_message, "WRK-SHORT-01", 20));
1714 let second_written = drain_until_idle(&mut peer, &mut captured);
1715
1716 if let LoggerBackend::Tcp(sender) = &logger.backend {
1717 sender
1718 .shutdown(Shutdown::Write)
1719 .expect("half-close the owned log socket");
1720 }
1721 let _ = drain_until_idle(&mut peer, &mut captured);
1722
1723 let mut expected = first_expected;
1724 expected.extend_from_slice(&second_expected);
1725 let decoded = decode_protobuf_tags(&captured);
1726 assert!(
1727 second_ok
1728 && second_written > 0
1729 && captured == expected
1730 && decoded == Ok(vec!["WRK-SHORT-00".to_owned(), "WRK-SHORT-01".to_owned()]),
1731 "two successful protobuf TCP writes must preserve progress and framing: \
1732 first_ok={first_ok}, second_ok={second_ok}, first_written={first_written}, \
1733 second_written={second_written}, captured={}, expected={}, decoded={decoded:?}",
1734 captured.len(),
1735 expected.len(),
1736 );
1737 }
1738}