| Crates.io | lazylog-framework |
| lib.rs | lazylog-framework |
| version | 0.3.4 |
| created_at | 2025-11-14 10:44:29.810022+00 |
| updated_at | 2025-11-14 10:52:58.87976+00 |
| description | A framework for building terminal-based log viewers with vim-like navigation |
| homepage | https://github.com/tr-nc/lazylog |
| repository | https://github.com/tr-nc/lazylog |
| max_upload_size | |
| id | 1932572 |
| size | 218,665 |
A powerful, extensible framework for building terminal-based log viewers with vim-like navigation and real-time monitoring capabilities.
/ searchy[dependencies]
lazylog-framework = "0.3"
use lazylog_framework::{LogProvider, LogParser, LogItem, start_with_provider};
use anyhow::Result;
use std::sync::Arc;
// 1. implement LogProvider for your log source
struct MyLogProvider;
impl LogProvider for MyLogProvider {
fn start(&mut self) -> Result<()> {
// setup resources (open files, connect to streams, etc.)
Ok(())
}
fn stop(&mut self) -> Result<()> {
// cleanup resources
Ok(())
}
fn poll_logs(&mut self) -> Result<Vec<String>> {
// return raw log strings since last poll (non-blocking)
Ok(vec!["2025-01-15 10:30:00 INFO Application started".to_string()])
}
}
// 2. implement LogParser to format your logs
struct MyLogParser;
impl LogParser for MyLogParser {
fn parse(&self, raw_log: &str) -> Option<LogItem> {
// parse raw string into LogItem
Some(LogItem::new(raw_log.to_string(), raw_log.to_string()))
}
fn format_preview(&self, item: &LogItem, detail_level: u8) -> String {
// format log for display at given detail level
match detail_level {
0 => item.content.clone(),
_ => format!("[{}] {}", item.time, item.content),
}
}
fn get_searchable_text(&self, item: &LogItem, _detail_level: u8) -> String {
// return text that should be searchable
item.content.clone()
}
}
// 3. run the application
fn main() -> Result<()> {
use ratatui::{Terminal, backend::CrosstermBackend};
use std::io;
let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?;
let provider = MyLogProvider;
let parser = Arc::new(MyLogParser);
lazylog_framework::start_with_provider(&mut terminal, provider, parser)?;
Ok(())
}
The framework uses a two-trait system that separates log acquisition from parsing:
┌──────────────┐ poll_logs() ┌─────────────┐
│ LogProvider │ ─────────────────> │ Vec<String> │ (raw logs)
└──────────────┘ └──────┬──────┘
│
│ parse()
│
┌──────────────┐ format_preview() ┌──────▼─────┐
│ LogParser │ <───────────────── │ LogItem │
└──────────────┘ └────────────┘
│ │
└──────────────┬──────────────────┘
│
↓
┌──────────────────┐
│ Ring Buffer │
│ (16K capacity) │
└─────────┬────────┘
│
↓
┌──────────────────┐
│ lazylog-framework│
│ - Rendering │
│ - Navigation │
│ - Filtering │
│ - UI management │
└─────────┬────────┘
│
↓
┌──────────┐
│ Terminal │
└──────────┘
Provides raw log data from any source (files, network, APIs, etc.):
pub trait LogProvider: Send {
/// initialize resources (called once at startup)
fn start(&mut self) -> Result<()>;
/// cleanup resources (called once at shutdown)
fn stop(&mut self) -> Result<()>;
/// poll for new logs (non-blocking, returns raw strings)
fn poll_logs(&mut self) -> Result<Vec<String>>;
}
Key points:
poll_logs() must be non-blocking - return empty vec if no logs availableLogItemsParses raw strings and formats them for display:
pub trait LogParser: Send + Sync {
/// parse raw log string into structured LogItem (return None to filter)
fn parse(&self, raw_log: &str) -> Option<LogItem>;
/// format log for display at given detail level (0-4)
fn format_preview(&self, item: &LogItem, detail_level: u8) -> String;
/// extract searchable text for filtering
fn get_searchable_text(&self, item: &LogItem, detail_level: u8) -> String;
/// format for clipboard (optional, has default)
fn make_yank_content(&self, item: &LogItem) -> String { /* default impl */ }
/// max detail level supported (optional, default: 4)
fn max_detail_level(&self) -> u8 { 4 }
}
Structured representation of a log entry:
pub struct LogItem {
pub id: Uuid, // auto-generated unique ID
pub time: String, // timestamp (auto-generated or custom)
pub content: String, // parsed log message
pub raw_content: String, // original log line
pub metadata: HashMap<String, String>, // extensible key-value storage
}
Use the builder pattern to add metadata:
let log = LogItem::new(
"Application started".to_string(),
"2025-01-15 10:30:00 INFO main.rs Application started".to_string(),
)
.with_metadata("level", "INFO")
.with_metadata("module", "main")
.with_metadata("severity", "1");
Customize behavior with AppDesc:
use std::time::Duration;
use lazylog_framework::{AppDesc, start_with_desc};
use std::sync::Arc;
let parser = Arc::new(MyParser);
let mut desc = AppDesc::new(parser);
desc.poll_interval = Duration::from_millis(50); // poll every 50ms
desc.ring_buffer_size = 32768; // 32K log capacity
desc.show_debug_logs = true; // show debug panel
start_with_desc(&mut terminal, provider, desc)?;
| Key | Action |
|---|---|
j / k / ↑ / ↓ |
Move to prev/next log |
d |
Jump to bottom (latest log) |
h / l / ← / → |
Horizontal scrolling |
Space |
Make selected log visible in view |
| Mouse scroll | Vertical scrolling |
Shift + Mouse scroll |
Horizontal scrolling |
| Key | Action |
|---|---|
/ |
Enter filter mode |
y |
Copy current log to clipboard |
a |
Copy all displayed logs to clipboard |
c |
Clear all logs |
w |
Toggle text wrapping |
[ |
Decrease detail level |
] |
Increase detail level |
Esc |
Go back / Clear filter |
q |
Quit program |
Ctrl+c |
Quit program |
| Key | Action |
|---|---|
1 / 2 / 3 |
Toggle focus on panel 1/2/3 |
| Key | Action |
|---|---|
? |
Show/hide help popup |
b |
Toggle debug logs visibility |
Control how much information is displayed (0-4):
Your parser defines what each level shows via format_preview(). Common convention:
| Level | Description |
|---|---|
| 0 | Content only (minimal) |
| 1 | Time + content |
| 2 | Time + level + content |
| 3 | Time + level + module + content |
| 4 | All fields (maximum detail) |
Users can adjust levels with +/- keys to progressively reveal more information.
See the examples/ directory for complete working implementations:
Generate dummy logs to demonstrate basic usage:
cargo run --example simple
Tail a log file (like tail -f):
cargo run --example file -- /path/to/logfile.log
# or generate test logs:
while true; do echo "$(date) Test log"; sleep 1; done > /tmp/test.log
cargo run --example file -- /tmp/test.log
Parse JSON logs with metadata and detail levels:
cargo run --example structured
use std::fs::File;
use std::io::{BufRead, BufReader, Seek, SeekFrom};
struct FileProvider {
reader: Option<BufReader<File>>,
path: String,
}
impl LogProvider for FileProvider {
fn start(&mut self) -> Result<()> {
let file = File::open(&self.path)?;
let mut reader = BufReader::new(file);
reader.seek(SeekFrom::End(0))?; // start at end (tail mode)
self.reader = Some(reader);
Ok(())
}
fn stop(&mut self) -> Result<()> {
self.reader = None;
Ok(())
}
fn poll_logs(&mut self) -> Result<Vec<String>> {
let mut logs = Vec::new();
if let Some(reader) = &mut self.reader {
let mut line = String::new();
while reader.read_line(&mut line)? > 0 {
if !line.trim().is_empty() {
logs.push(line.trim().to_string());
}
line.clear();
}
}
Ok(logs)
}
}
struct JsonParser;
impl LogParser for JsonParser {
fn parse(&self, raw_log: &str) -> Option<LogItem> {
let json: serde_json::Value = serde_json::from_str(raw_log).ok()?;
Some(LogItem::new(
json["message"].as_str()?.to_string(),
raw_log.to_string(),
)
.with_metadata("level", json["level"].as_str().unwrap_or("INFO"))
.with_metadata("module", json["module"].as_str().unwrap_or("")))
}
fn format_preview(&self, item: &LogItem, level: u8) -> String {
match level {
0 => item.content.clone(),
1 => format!("[{}] {}", item.time, item.content),
2 => format!("[{}] [{}] {}",
item.time,
item.get_metadata("level").unwrap_or(""),
item.content),
_ => format!("[{}] [{}] [{}] {}",
item.time,
item.get_metadata("level").unwrap_or(""),
item.get_metadata("module").unwrap_or(""),
item.content),
}
}
fn get_searchable_text(&self, item: &LogItem, level: u8) -> String {
if level >= 2 {
format!("{} {} {}",
item.get_metadata("level").unwrap_or(""),
item.get_metadata("module").unwrap_or(""),
item.content)
} else {
item.content.clone()
}
}
}
use std::net::UdpSocket;
struct SyslogProvider {
socket: UdpSocket,
}
impl SyslogProvider {
fn new(addr: &str) -> Result<Self> {
let socket = UdpSocket::bind(addr)?;
socket.set_nonblocking(true)?; // critical for non-blocking poll
Ok(Self { socket })
}
}
impl LogProvider for SyslogProvider {
fn start(&mut self) -> Result<()> { Ok(()) }
fn stop(&mut self) -> Result<()> { Ok(()) }
fn poll_logs(&mut self) -> Result<Vec<String>> {
let mut logs = Vec::new();
let mut buf = [0u8; 65536];
loop {
match self.socket.recv(&mut buf) {
Ok(size) => {
let msg = String::from_utf8_lossy(&buf[..size]);
logs.push(msg.to_string());
}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => break,
Err(e) => return Err(e.into()),
}
}
Ok(logs)
}
}
The framework includes doctests for all public APIs. Run them with:
cargo test --doc
Full API documentation is available at docs.rs/lazylog-framework.
Generate local documentation:
cargo doc --open
MIT OR Apache-2.0
Contributions welcome! Please:
cargo test and cargo clippy passBuilt with: