diff --git a/Cargo.lock b/Cargo.lock index 42694f43..45c92e12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -717,7 +717,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hl" -version = "0.24.2" +version = "0.25.0" dependencies = [ "atoi", "bincode", diff --git a/Cargo.toml b/Cargo.toml index eca5129f..deef8729 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ categories = ["command-line-utilities"] description = "Utility for viewing json-formatted log files." keywords = ["cli", "human", "log"] name = "hl" -version = "0.24.2" +version = "0.25.0" edition = "2021" build = "build.rs" diff --git a/README.md b/README.md index cbeb7824..c0732287 100644 --- a/README.md +++ b/README.md @@ -424,6 +424,7 @@ Options: --list-themes List available themes and exit -s, --sort Sort messages chronologically -F, --follow Follow input streams and sort messages chronologically during time frame set by --sync-interval-ms option + --tail Number of last messages to preload from each file in --follow mode [default: 10] --sync-interval-ms Synchronization interval for live streaming mode enabled by --follow option [default: 100] -o, --output Output file --dump-index Dump index metadata and exit diff --git a/src/app.rs b/src/app.rs index b4a5c5a3..d2e07b7c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -66,6 +66,7 @@ pub struct Options { pub input_info: Option, pub dump_index: bool, pub app_dirs: Option, + pub tail: u64, } impl Options { @@ -396,7 +397,7 @@ impl App { if let InputReference::File(filename) = &input_ref { meta = Some(fs::metadata(filename)?); } - let mut input = Some(input_ref.open()?); + let mut input = Some(input_ref.open_tail(self.options.tail)?); let is_file = |meta: &Option| meta.as_ref().map(|m|m.is_file()).unwrap_or(false); let process = |input: &mut Option, is_file: bool| { if let Some(input) = input { diff --git a/src/input.rs b/src/input.rs index 46e8af55..e17960da 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,4 +1,5 @@ // std imports +use std::cmp::min; use std::convert::TryInto; use std::fs::File; use std::io::{self, stdin, BufReader, Read, Seek, SeekFrom}; @@ -61,12 +62,56 @@ impl InputReference { self.hold()?.open() } + pub fn open_tail(&self, n: u64) -> io::Result { + match self { + Self::Stdin => self.open(), + Self::File(path) => { + let mut file = File::open(path) + .map_err(|e| io::Error::new(e.kind(), format!("failed to open {}: {}", self.description(), e)))?; + Self::seek_tail(&mut file, n).ok(); + Ok(Input::new(self.clone(), Box::new(file))) + } + } + } + pub fn description(&self) -> String { match self { Self::Stdin => "".into(), Self::File(filename) => format!("file '{}'", Color::Yellow.paint(filename.to_string_lossy())), } } + + fn seek_tail(file: &mut File, lines: u64) -> io::Result<()> { + const BUF_SIZE: usize = 64 * 1024; + let mut scratch = [0; BUF_SIZE]; + let mut count: u64 = 0; + let mut prev_pos = file.seek(SeekFrom::End(0))?; + let mut pos = prev_pos; + while pos > 0 { + pos -= min(BUF_SIZE as u64, pos); + pos = file.seek(SeekFrom::Start(pos))?; + if pos == prev_pos { + break; + } + let bn = min(BUF_SIZE, (prev_pos - pos) as usize); + let buf = scratch[..bn].as_mut(); + + file.read_exact(buf)?; + + for i in (0..bn).rev() { + if buf[i] == b'\n' { + if count == lines { + file.seek(SeekFrom::Start(pos + i as u64 + 1))?; + return Ok(()); + } + count += 1; + } + } + + prev_pos = pos; + } + Ok(()) + } } // --- diff --git a/src/main.rs b/src/main.rs index 12bd2809..ca0b71ed 100644 --- a/src/main.rs +++ b/src/main.rs @@ -182,6 +182,10 @@ struct Opt { #[arg(long, short = 'F')] follow: bool, + /// Number of last messages to preload from each file in --follow mode. + #[arg(long, default_value = "10")] + tail: u64, + /// Synchronization interval for live streaming mode enabled by --follow option. #[arg(long, default_value = "100")] sync_interval_ms: u64, @@ -402,6 +406,7 @@ fn run() -> Result<()> { }, dump_index: opt.dump_index, app_dirs: Some(app_dirs), + tail: opt.tail, }); // Configure input.