Skip to main content

sozu_command_lib/logging/
logs.rs

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  /// Side-channel mirror of [`InnerLogger::colored`]. Read by
26  /// [`is_logger_colored`] without borrowing the [`LOGGER`] cell, so macros
27  /// like `log_context!` can consult it while the logger is already held
28  /// mutably by the enclosing `error!`/`info!`/... emission.
29  static LOGGER_COLORED: Cell<bool> = const { Cell::new(false) };
30}
31
32/// Returns `true` when the thread-local [`Logger`] is configured to emit colored
33/// output. Safe to call from inside an `error!`/`info!`/… argument list — the
34/// flag mirrors [`Logger::is_colored`] (i.e. [`InnerLogger::colored`]) into a
35/// dedicated [`Cell`] at [`Logger::init`] time so we never re-borrow the main
36/// logger cell (which is already held mutably while the outer log macro is
37/// formatting its arguments). The mirror is the single source of truth for
38/// hot-path callers; `Logger::is_colored` is the single source of truth for
39/// the rest of the API. They go through the same `InnerLogger::colored`
40/// field, so they cannot diverge by construction.
41pub fn is_logger_colored() -> bool {
42    LOGGER_COLORED.with(|c| c.get())
43}
44
45/// ANSI palette used by every `log_context!` / `log_*_context!` macro in the
46/// proxy stack. Returns a 5-tuple `(open, reset, grey, gray, white)` where:
47///
48/// - `open`   = `\x1b[1;97m` (bold bright-white) — protocol label (`MUX`,
49///   `MUX-H2`, `SOCKET`, `RUSTLS`, `PIPE`, …).
50/// - `reset`  = `\x1b[0m` — ANSI reset.
51/// - `grey`   = `\x1b[37m` (light grey) — `Session` keyword.
52/// - `gray`   = `\x1b[90m` (bright black) — attribute keys (`peer=`,
53///   `local=`, `cluster=`, …).
54/// - `white`  = `\x1b[97m` (bright white) — attribute values.
55///
56/// When [`is_logger_colored`] returns `false` every slot is an empty string,
57/// so macros can unconditionally inline the tokens into `format!` without
58/// leaking escape codes into plain-text log sinks.
59///
60/// Callers that only need the label pair can destructure with wildcards:
61/// `let (open, reset, _, _, _) = ansi_palette();`.
62pub 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
76// TODO: check if this error is critical:
77//     could not register compat logger: SetLoggerError(())
78// The CompatLogger may need a variable that tells wether it has been initiated already
79pub 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    /// inherited from the config's `log_target`, used to revive the backend
110    log_target: String,
111    pub colored: bool,
112    /// target of the access logs
113    access_backend: Option<LoggerBackend>,
114    /// used to revive the backend for access logs
115    access_logs_target: Option<String>,
116    /// how to format the access logs
117    access_format: AccessLogFormat,
118    access_colored: bool,
119    buffer: LoggerBuffer,
120}
121
122pub struct Logger {
123    inner: InnerLogger,
124    /// is displayed in each log, for instance "MAIN" or worker_id
125    tag: String,
126    /// the pid of the current process (main or worker)
127    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                // Mirror into the side-channel cell via the canonical
197                // accessor so `is_logger_colored()` can be called from
198                // inside log-macro argument formatters without re-borrowing
199                // the main `LOGGER` cell (it is held mutably here and by
200                // every subsequent `error!`/`info!`/...). Going through
201                // `Logger::is_colored` (instead of the `Deref`'d
202                // `logger.colored`) makes the single source of truth
203                // explicit at the call site.
204                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                // SAFETY: `libc::getpid` takes no input pointers, never
218                // fails, and returns a value type. No invariant beyond
219                // "FFI signature matches libc".
220                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    /// Returns `true` when the logger emits colored output on its main backend.
241    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        // `Stdout` is a `LineWriter`, `File` a `MultiLineWriter`: both already
280        // collapse a record's fragments, so they take `args` directly.
281        LoggerBackend::Stdout(stdout) => {
282            let _ = stdout.write_fmt(args);
283            Ok(())
284        }
285        // `Tcp` is the one backend with NO buffering of its own, so
286        // `write_fmt` sent every fragment of the formatter as its own syscall:
287        // measured on a loopback sink, one 360-byte access-log line cost 81.7
288        // `send(2)` calls, 822 of the 1634 in a 20-request run being a single
289        // byte. Formatting into the reused buffer first makes it one call, and
290        // allocates nothing per record.
291        //
292        // Unlike the datagram arms below, a stream may accept FEWER bytes than
293        // offered, so the record needs `write_all`: discarding the count here
294        // would silently truncate a log line.
295        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        // Datagram sinks must hand the kernel one contiguous record: there is no
300        // incremental `write_str` that yields a single datagram. This is why the
301        // buffer exists at all -- it replaced the per-record `String` these two
302        // arms used to allocate.
303        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
308/// Frame one protobuf access log into `buffer`: the prost length delimiter,
309/// the record, then two zero bytes. The decoder of a `tcp://` stream relies on
310/// that framing to find where each record starts.
311fn 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]); // add two empty bytes after each protobuf access log
326    Ok(())
327}
328
329/// Hand one complete record to a stream sink.
330///
331/// A stream may accept FEWER bytes than offered: discarding the count of a
332/// bare `write` would truncate the record and desynchronise the framing of
333/// every record after it, so the record goes through `write_all`, as the
334/// ASCII `Tcp` arm of [`log_arguments`] does.
335fn write_stream_record<W: Write>(stream: &mut W, bytes: &[u8]) -> Result<(), IoError> {
336    stream.write_all(bytes)
337}
338
339impl InnerLogger {
340    /// Drain the buffered records of both the main and the access-log backend.
341    ///
342    /// Called once, on a worker's way out: nothing on the per-record path
343    /// flushes, which is what lets a `file://` target batch many records into
344    /// one `write(2)`. Errors are printed rather than logged, because logging
345    /// from here would re-borrow the [`LOGGER`] cell this runs under.
346    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    /// write an access log to the proper logging target
367    ///
368    /// Protobuf access logs are written with a prost length delimiter before, and 2 empty bytes after
369    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        // Search for the longest match, the vector is assumed to be pre-sorted.
450        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        // Search for the longest match, the vector is assumed to be pre-sorted.
461        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    /// Write out whatever this backend still holds in user space.
486    ///
487    /// Only `File` (a [`MultiLineWriter`] that drains whole lines once its
488    /// buffer fills) and `Stdout` (a line-buffered `Stdout`) keep bytes back;
489    /// every other backend already sent each record in its own syscall, so
490    /// there is nothing left to write. No `fsync`: this empties the process
491    /// buffer, it does not make the kernel's page cache durable.
492    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    /// The "error" level.
505    ///
506    /// Designates very serious errors.
507    Error = 1, // This way these line up with the discriminants for LogLevelFilter below
508    /// The "warn" level.
509    ///
510    /// Designates hazardous situations.
511    Warn,
512    /// The "info" level.
513    ///
514    /// Designates useful information.
515    Info,
516    /// The "debug" level.
517    ///
518    /// Designates lower priority information.
519    Debug,
520    /// The "trace" level.
521    ///
522    /// Designates very low priority, often extremely verbose, information.
523    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    /// Returns the most verbose logging level.
576    #[inline]
577    pub fn max() -> LogLevel {
578        LogLevel::Trace
579    }
580
581    /// Converts the `LogLevel` to the equivalent `LogLevelFilter`.
582    #[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    /// Returns the most verbose logging level filter.
658    #[inline]
659    pub fn max() -> LogLevelFilter {
660        LogLevelFilter::Trace
661    }
662
663    /// Converts `self` to the equivalent `LogLevel`.
664    ///
665    /// Returns `None` if `self` is `LogLevelFilter::Off`.
666    #[inline]
667    pub fn to_log_level(self) -> Option<LogLevel> {
668        LogLevel::from_usize(self as usize)
669    }
670}
671
672/// Metadata about a log message.
673#[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                        // if the single argument is a log-level string or number,
715                        // treat that as a global fallback
716                        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
750/// start the logger with all logs and access logs on stdout
751pub 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
759/// start the logger from config (takes RUST_LOG into account)
760pub 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
772/// start the logger, after:
773///
774/// - determining logging backends
775/// - taking RUST_LOG into account
776pub 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
796/// defaults to stdout if the log target is unparseable
797fn 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            }; // logger dropped here
1015
1016            if !success {
1017                // recording this metric may borrow the logger, so we have
1018                // to perform this action after the block above
1019                $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]
1035/// dynamically chose between info_access and error_access
1036macro_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/// log a failure concerning an HTTP or TCP request
1048#[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/// log the success of an HTTP or TCP request
1056#[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/// log an error with Sōzu's custom log stack
1064#[macro_export]
1065macro_rules! error {
1066    ($format:expr $(, $args:expr)* $(,)?) => {
1067        $crate::_log!($crate::logging::LogLevel::Error, $format $(, $args)*)
1068    };
1069}
1070
1071/// log a warning with Sōzu’s custom log stack
1072#[macro_export]
1073macro_rules! warn {
1074    ($format:expr $(, $args:expr)* $(,)?) => {
1075        $crate::_log!($crate::logging::LogLevel::Warn, $format $(, $args)*)
1076    };
1077}
1078
1079/// log an info with Sōzu’s custom log stack
1080#[macro_export]
1081macro_rules! info {
1082    ($format:expr $(, $args:expr)* $(,)?) => {
1083        $crate::_log!($crate::logging::LogLevel::Info, $format $(, $args)*)
1084    };
1085}
1086
1087/// log a debug with Sōzu’s custom log stack
1088#[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        // Dead-code arm silences `unused_variables` warnings on
1094        // `--no-default-features` builds where the emit arm is stripped.
1095        // Must gate on the SAME set of features as the emit arm above, or
1096        // else both arms compile in and double-move the caller's arguments
1097        // (observed on `release + logs-debug` builds where the emit arm
1098        // fires and the no-op arm also fires, each consuming `$args` once).
1099        #[cfg(not(any(debug_assertions, feature = "logs-debug", feature = "logs-trace")))]
1100        if false {$( let _ = $args; )*}
1101    }};
1102}
1103
1104/// log a trace with Sōzu’s custom log stack
1105#[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/// write a log with a "FIXME" prefix on an info level
1116#[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    /// Drain the thread's [`LOGGER`]. A no-op when that cell is already
1166    /// borrowed, which only happens if `flush` is reached from inside a log
1167    /// call on the same thread.
1168    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/// start a logger used in test environment
1178#[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
1197/// yields (Rfc3339Time, unix_epoch)
1198pub 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    //! The property the `tcp://` access-log backend depends on: a record is
1209    //! handed to the sink in ONE call, whatever its formatter fragment count.
1210    //!
1211    //! To SEE THESE RED (regression proof), restore the pre-fix `Tcp` arm of
1212    //! [`super::log_arguments`] — `LoggerBackend::Tcp(socket) =>
1213    //! socket.write_fmt(args)` — and change the two `buffer.fmt(...)` calls
1214    //! below to `sink.write_fmt(...)`, which is what that arm does. Both
1215    //! one-call expectations then fail with the fragment count in the message
1216    //! (`39`, not `1`), and `a_short_write_does_not_truncate_a_record` reports a
1217    //! truncated record. 39 is a FLOOR for the live record: this fixture omits
1218    //! the tag map and the OpenTelemetry field, and the proxy was measured at 82
1219    //! fragments per record.
1220    //!
1221    //! `short_writes_keep_consecutive_protobuf_records_framed` covers the
1222    //! protobuf `Tcp` arm of [`super::InnerLogger::log_access`], which goes
1223    //! through [`super::write_stream_record`]. To see it red, make that helper
1224    //! `stream.write(bytes).map(|_| ())`, the pre-fix arm: the first record is
1225    //! truncated after `limit` bytes and decoding reports `BufferUnderflow`.
1226    //!
1227    //! `cargo test` cannot count the `write(2)` syscalls the real
1228    //! `LoggerBackend::Tcp(TcpStream)` arm performs: the variant holds a
1229    //! concrete `TcpStream`, so no counting sink can be substituted, and the
1230    //! only in-process syscall counter (`/proc/self/io`'s `syscw`) is
1231    //! process-wide and would race every other test in the binary. That arm's
1232    //! syscall count is proven outside the suite, by tracing the running proxy
1233    //! (81.7 `send(2)` per access-log line before, 1 after; see the
1234    //! `perf(logging)` CHANGELOG entry).
1235    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    /// Counts the calls a record arrives in, and records every chunk.
1253    #[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    /// A stream sink that accepts at most `limit` bytes per call, the way a
1271    /// `TcpStream` with a full send buffer does.
1272    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    /// The runtime field values of an access-log record.
1291    ///
1292    /// These must come from a NON-literal source. `format_args!` flattens
1293    /// literal arguments into its template at compile time
1294    /// (rustc's `flatten_format_args`), so a fixture built from literals
1295    /// collapses to a single static piece and would measure nothing: the
1296    /// fragment count this test exists to pin only appears for runtime values,
1297    /// which is what `InnerLogger::log_access` actually passes.
1298    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    /// The live `AccessLogFormat::Ascii` template of `InnerLogger::log_access`,
1348    /// prompt included: 20 runtime arguments, so 20 formatter fragments plus the
1349    /// literal pieces between them.
1350    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        // A datagram sink is all-or-nothing, so the `Unix`/`Udp` arms may discard
1432        // the returned count. A stream may accept fewer bytes than offered, which
1433        // is why the `Tcp` arm wraps `write_all` rather than `write`.
1434        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    /// A protobuf access log whose `tag` and `bytes_in` tell records apart.
1462    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        // Issue #1830: the protobuf `Tcp` arm of `InnerLogger::log_access` used
1475        // a bare `write` and discarded its count, so a stream that accepted a
1476        // prefix truncated the record, and the next record's bytes were read as
1477        // the remainder of the first.
1478        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    /// Build the exact record type that [`InnerLogger::log_access`] converts
1563    /// and frames before selecting the concrete `LoggerBackend::Tcp` arm.
1564    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        // Contract (sozu-proxy/sozu#1830): a successful protobuf TCP access-log
1656        // write covers the complete frame. Two successful records must remain
1657        // independently decodable, including the two zero-byte separators.
1658        // The refuting observation is `true` from `log_access` after the real
1659        // socket accepted only a prefix and shifted the following frame.
1660        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        // A blocking production TcpStream may return a successful prefix when
1666        // its send timeout fires after making progress. Shrinking SO_SNDBUF and
1667        // withholding reads during each call makes that boundary deterministic.
1668        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            // Surfacing the short write is an accepted implementation of the
1699            // contract: the caller can revive the backend and no truncated
1700            // record was reported as delivered.
1701            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}