Add live log buffering support and endpoint; enhance log display functionality.

This commit is contained in:
Kai Börnert
2026-04-27 15:04:05 +02:00
parent 3fa8077b81
commit f0c9ed4e7f
9 changed files with 274 additions and 66 deletions

View File

@@ -2,42 +2,84 @@ use alloc::string::String;
use alloc::vec::Vec;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::blocking_mutex::Mutex as BlockingMutex;
use embassy_sync::mutex::Mutex;
use log::{error, LevelFilter, Log, Metadata, Record};
use log::{LevelFilter, Log, Metadata, Record};
const MAX_LIVE_LOG_ENTRIES: usize = 64;
struct LiveLogBuffer {
entries: Vec<(u64, String)>,
next_seq: u64,
}
impl LiveLogBuffer {
const fn new() -> Self {
Self {
entries: Vec::new(),
next_seq: 0,
}
}
fn push(&mut self, text: String) {
if self.entries.len() >= MAX_LIVE_LOG_ENTRIES {
self.entries.remove(0);
}
self.entries.push((self.next_seq, text));
self.next_seq += 1;
}
fn get_after(&self, after: Option<u64>) -> (Vec<(u64, String)>, bool, u64) {
let next_seq = self.next_seq;
match after {
None => (self.entries.clone(), false, next_seq),
Some(after_seq) => {
let result: Vec<_> = self.entries
.iter()
.filter(|(seq, _)| *seq > after_seq)
.cloned()
.collect();
// Dropped if there are entries that should exist (seq > after_seq) but
// the oldest retained entry has a higher seq than after_seq + 1.
let dropped = if next_seq > after_seq.saturating_add(1) {
if let Some((oldest_seq, _)) = self.entries.first() {
*oldest_seq > after_seq.saturating_add(1)
} else {
// Buffer empty but entries were written — all dropped
true
}
} else {
false
};
(result, dropped, next_seq)
}
}
}
}
pub struct InterceptorLogger {
// Async mutex for start/stop capture from async context
async_capture: Mutex<CriticalSectionRawMutex, ()>,
// Blocking mutex for the actual data to be used in sync log()
sync_capture: BlockingMutex<CriticalSectionRawMutex, core::cell::RefCell<Option<Vec<String>>>>,
live_log: BlockingMutex<CriticalSectionRawMutex, core::cell::RefCell<LiveLogBuffer>>,
}
impl InterceptorLogger {
pub const fn new() -> Self {
Self {
async_capture: Mutex::new(()),
sync_capture: BlockingMutex::new(core::cell::RefCell::new(None)),
live_log: BlockingMutex::new(core::cell::RefCell::new(LiveLogBuffer::new())),
}
}
pub async fn start_capture(&self) {
let _guard = self.async_capture.lock().await;
self.sync_capture.lock(|capture| {
*capture.borrow_mut() = Some(Vec::new());
});
}
pub async fn stop_capture(&self) -> Option<Vec<String>> {
let _guard = self.async_capture.lock().await;
self.sync_capture
.lock(|capture| capture.borrow_mut().take())
/// Returns (entries_after, dropped, next_seq).
/// Pass `after = None` to retrieve the entire current buffer.
/// Pass `after = Some(seq)` to retrieve only entries with seq > that value.
pub fn get_live_logs(&self, after: Option<u64>) -> (Vec<(u64, String)>, bool, u64) {
self.live_log.lock(|buf| buf.borrow().get_after(after))
}
pub fn init(&'static self) {
match log::set_logger(self).map(|()| log::set_max_level(LevelFilter::Info)) {
Ok(()) => {}
Err(e) => {
error!("Logger already set: {}", e);
Err(_e) => {
esp_println::println!("ERROR: Logger already set");
}
}
}
@@ -50,16 +92,14 @@ impl Log for InterceptorLogger {
fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
let message = alloc::format!("{}", record.args());
let message = alloc::format!("{}: {}", record.level(), record.args());
// Print to serial using esp_println
esp_println::println!("{}: {}", record.level(), message);
// Print to serial
esp_println::println!("{}", message);
// Capture if active
self.sync_capture.lock(|capture| {
if let Some(ref mut buffer) = *capture.borrow_mut() {
buffer.push(alloc::format!("{}: {}", record.level(), message));
}
// Store in live log ring buffer
self.live_log.lock(|buf| {
buf.borrow_mut().push(message);
});
}
}