util_zfmtutil_zfmt is a utility crate that implements a client-server logging system using the zfmt structured logging library over Pigweed Maize IPC channels.
It allows userspace processes (clients) to log structured events efficiently by sending them as serialized binary payloads over IPC. The logging server (logmgr) buffers these events in memory and handles rendering them to text (e.g., for UART output) or exposing them to other readers (e.g., USB).
The logging system uses a deferred-rendering, client-server architecture:
zfmt macros (wrapped by util_zfmt macros) to serialize log arguments into a compact binary format at the log site. This binary payload is sent over an IPC channel to the log server.LogBuffer).logmgr prints them to UART, or when a USB manager reads them). This keeps the client-side logging latency extremely low.NOTE: Future work will enable binary log streams with client-side decoding.
sequenceDiagram participant Client participant LogServer (logmgr) participant UART/USB Client->>LogServer (logmgr): IPC: OPCODE_LOG_WRITE (serialized event) Note over LogServer (logmgr): Inject timestamp & seq Note over LogServer (logmgr): Push to LogBuffer (binary) LogServer (logmgr)-->>Client: IPC: Response (success) Note over LogServer (logmgr): service_uart_tx (or USB read) LogServer (logmgr)->>LogServer (logmgr): Read binary from LogBuffer LogServer (logmgr)->>LogServer (logmgr): render_event (binary -> text) LogServer (logmgr)->>UART/USB: Transmit text
IpcLogger: Implements the zfmt::Logger trait. It wraps an IpcChannel (conventionally channel 0 in multi-process apps) and sends serialized log packets using OPCODE_LOG_WRITE.info!, warn!, and error! are provided for convenience. They use a global IpcLogger instance bound to channel 0.LogBuffer<const N: usize>: A circular byte buffer that stores variable-length serialized zfmt frames. If the buffer is full, it automatically evicts the oldest frames to make room. It is designed to support cursors for multiple independent readers.LogServer<const N: usize>: Wraps LogBuffer and processes incoming IPC requests. It handles:EventHeader of incoming logs.usbmgr) using client-provided cursors.render Module: Provides render_event which parses the binary format (detecting StreamStart, EventHeader, and DebugMessage tags) and formats the output into a zfmt::FixedBuf as a human-readable ASCII string.The log server listens for requests with the following opcodes:
OPCODE_LOG_WRITE ("WLOG"):[OPCODE (4B) | Serialized Event Payload]OPCODE_LOG_READ ("RLOG"):[OPCODE (4B) | Cursor (8B)][Status (4B) | Actual Cursor Used (8B) | Log Frame ]OPCODE_CLEAR_NOTIFIER ("CLRN"):[OPCODE (4B)]clock-earlgrey: Enables querying the hardware timer on the Earlgrey platform to inject real timestamps. If disabled, timestamps default to 0.use util_zfmt::{info, warn}; use zfmt::Zfmt; #[derive(Zfmt)] #[zfmt(format = "Hello, user {}! Status code: {}")] struct UserLoginEvent<'a> { username: &'a str, status: u32, } fn handle_login(username: &str) { // ... info!(UserLoginEvent { username, status: 0 }); }
use util_zfmt::{LogServer, render::render_event, FixedBuf}; fn print_logs<const N: usize>(server: &LogServer<N>, cursor: &mut u64) { let actual_cursor = if *cursor < server.buffer.read { server.buffer.read } else { *cursor }; *cursor = actual_cursor; if let Some((_tag, s1, s2)) = server.buffer.next_frame_slice(actual_cursor) { let mut temp_frame = [0u8; 260]; let frame_len = s1.len() + s2.len(); // Reconstruct contiguous frame if it wrapped around temp_frame[..s1.len()].copy_from_slice(s1); temp_frame[s1.len()..frame_len].copy_from_slice(s2); let mut buf = FixedBuf::<512>::new(); if let Some(consumed) = render_event(&temp_frame[..frame_len], &mut buf) { // Print the rendered ASCII string print_to_uart(buf.as_slice()); print_to_uart(b"\r\n"); *cursor += consumed as u64; } else { // Skip corrupted frame *cursor += frame_len as u64; } } }