use std::path::PathBuf;
#[derive(Debug, Clone, Default)]
pub struct ConfigOpts {
pub tailing: bool,
pub sticky: bool,
pub offset: i64,
pub offset_unit: OffsetUnit,
pub show_time: bool,
pub batch_window_ms: u64,
pub mode: InputMode,
pub force_chunked: bool,
pub disable_chunked: bool,
pub conservative: bool,
pub no_file_names: bool,
pub all_file_names: bool,
pub adaptive: bool,
pub strategy: Option<Strategy>,
pub max_memory: Option<usize>,
#[cfg(debug_assertions)]
pub profile_json: bool,
}
#[derive(Debug, Clone, Default, Copy)]
pub enum OffsetUnit {
#[default]
Lines,
Blocks,
Bytes,
}
#[derive(Debug, Clone, Default)]
pub enum InputMode {
#[default]
Stdin,
SingleFile { path: PathBuf },
MultiFile { paths: Vec<PathBuf> },
}
#[cfg(not(test))]
mod runtime {
use std::sync::OnceLock;
use super::ConfigOpts;
pub static CONFIG: OnceLock<ConfigOpts> = OnceLock::new();
pub fn config() -> &'static ConfigOpts {
CONFIG
.get()
.expect("programmer error: tried to access configuration before it was set")
}
pub fn set(input: ConfigOpts) -> Result<(), Box<ConfigOpts>> {
match CONFIG.set(input) {
Ok(_) => Ok(()),
Err(e) => Err(Box::new(e)),
}
}
}
#[cfg(test)]
mod runtime {
use std::cell::RefCell;
use super::ConfigOpts;
thread_local! {
pub static TEST_CONFIG: RefCell<Option<ConfigOpts>> = const { RefCell::new(None) };
}
pub fn config() -> ConfigOpts {
TEST_CONFIG.with(|cfg| cfg.borrow().as_ref().cloned().unwrap_or_else(ConfigOpts::default))
}
pub fn set(input: ConfigOpts) -> Result<(), Box<ConfigOpts>> {
TEST_CONFIG.with(|cfg| {
*cfg.borrow_mut() = Some(input);
});
Ok(())
}
pub fn update<F>(f: F)
where
F: FnOnce(&mut ConfigOpts),
{
TEST_CONFIG.with(|cfg| {
let mut borrowed = cfg.borrow_mut();
if borrowed.is_none() {
*borrowed = Some(ConfigOpts::default());
}
if let Some(ref mut config) = borrowed.as_mut() {
f(config);
}
});
}
pub fn with_config<F, R>(new_config: ConfigOpts, f: F) -> R
where
F: FnOnce() -> R,
{
let old_config = TEST_CONFIG.with(|cfg| cfg.borrow().clone());
TEST_CONFIG.with(|cfg| {
*cfg.borrow_mut() = Some(new_config);
});
let result = f();
TEST_CONFIG.with(|cfg| {
*cfg.borrow_mut() = old_config;
});
result
}
}
use miette::Result;
pub use runtime::{config, set};
#[cfg(test)]
pub use runtime::{update, with_config};
use crate::defaults::{SystemDefaults, get_system_config};
use crate::errors::TaleError;
use crate::readers::{AdaptiveStrategy, ConservativeStrategy, StaticStrategy, Strategy};
pub fn tailing() -> bool {
#[cfg(not(test))]
return config().tailing;
#[cfg(test)]
return config().tailing;
}
pub fn sticky() -> bool {
#[cfg(not(test))]
return config().sticky;
#[cfg(test)]
return config().sticky;
}
pub fn offset() -> i64 {
#[cfg(not(test))]
return config().offset;
#[cfg(test)]
return config().offset;
}
pub fn offset_unit() -> OffsetUnit {
#[cfg(not(test))]
return config().offset_unit;
#[cfg(test)]
return config().offset_unit;
}
pub fn show_time() -> bool {
#[cfg(not(test))]
return config().show_time;
#[cfg(test)]
return config().show_time;
}
pub fn batch_window_ms() -> u64 {
#[cfg(not(test))]
return config().batch_window_ms;
#[cfg(test)]
return config().batch_window_ms;
}
pub fn force_chunked() -> bool {
#[cfg(not(test))]
return config().force_chunked;
#[cfg(test)]
return config().force_chunked;
}
pub fn disable_chunked() -> bool {
#[cfg(not(test))]
return config().disable_chunked;
#[cfg(test)]
return config().disable_chunked;
}
pub fn conservative() -> bool {
#[cfg(not(test))]
return config().conservative;
#[cfg(test)]
return config().conservative;
}
pub fn mode() -> InputMode {
#[cfg(not(test))]
return config().mode.clone();
#[cfg(test)]
return config().mode;
}
fn unescape_glob_pattern(pattern: &str) -> String {
let mut result = String::new();
let mut chars = pattern.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\\' {
if let Some(&next_ch) = chars.peek() {
if matches!(next_ch, '*' | '?' | '[' | ']' | '{' | '}') {
chars.next();
result.push(next_ch);
} else {
result.push(ch);
}
} else {
result.push(ch);
}
} else {
result.push(ch);
}
}
result
}
fn is_glob(maybe: &str) -> bool {
if maybe.contains('?') || maybe.contains('*') || maybe.contains('[') || maybe.contains('{') {
return true;
}
maybe.contains("\\*") || maybe.contains("\\?") || maybe.contains("\\[") || maybe.contains("\\{")
}
fn expand_globs(args: &[String]) -> Result<Vec<PathBuf>, TaleError> {
let mut all_paths = Vec::new();
for candidate in args {
if is_glob(candidate.as_str()) {
let unescaped_pattern = unescape_glob_pattern(candidate);
let pattern = glob::glob(&unescaped_pattern)?;
for fpath in pattern.flatten() {
if fpath.is_file() {
all_paths.push(fpath);
}
}
} else {
let fpath = PathBuf::from(candidate);
if fpath.exists() && fpath.is_file() {
all_paths.push(fpath);
}
}
}
all_paths.sort();
Ok(all_paths)
}
fn handle_possible_paths(args: &[String]) -> Result<Vec<PathBuf>, TaleError> {
match expand_globs(args) {
Ok(paths) => {
if paths.is_empty() {
let patterns: Vec<String> = args.iter().map(|s| format!("'{}'", s)).collect();
Err(TaleError::from(Box::new(crate::errors::FileError::NotFound {
path: PathBuf::from(patterns.join(", ")),
similar_files: vec![
"Check if the glob pattern is correct".to_string(),
"Verify the files exist in the specified directory".to_string(),
"Try using an absolute path".to_string(),
],
})))
} else {
Ok(paths)
}
}
Err(e) => {
Err(e)
}
}
}
impl ConfigOpts {
pub fn new(args: &crate::Args) -> Result<Self> {
let system_config = get_system_config();
let (mode, maybe_offset) = match args.args.len() {
0 => (InputMode::Stdin, None),
1 => {
let only = &args.args[0];
if (only.starts_with('-') || only.starts_with('+'))
&& only.len() > 1
&& let Ok(offset) = only.parse::<i64>()
{
(InputMode::Stdin, Some(offset))
} else {
if is_glob(only) {
let paths = handle_possible_paths(vec![only.clone()].as_slice())?;
(InputMode::MultiFile { paths }, None)
} else {
(
InputMode::SingleFile {
path: PathBuf::from(only),
},
None,
)
}
}
}
2 => {
let (first, second) = (&args.args[0], &args.args[1]);
if let Ok(offset) = first.parse::<i64>() {
(
InputMode::SingleFile {
path: PathBuf::from(second),
},
Some(offset),
)
} else {
let paths = handle_possible_paths(args.args.as_slice())?;
(InputMode::MultiFile { paths }, None)
}
}
_ => {
let paths = handle_possible_paths(args.args.as_slice())?;
(InputMode::MultiFile { paths }, None)
}
};
let (offset, offset_unit) = if let Some(blocks) = args.blocks {
(blocks, OffsetUnit::Blocks)
} else if let Some(bytes) = args.bytes {
(bytes, OffsetUnit::Bytes)
} else if let Some(lines) = args.offset {
(lines, OffsetUnit::Lines)
} else if let Some(offset) = maybe_offset {
(offset, OffsetUnit::Lines)
} else {
(0, OffsetUnit::Lines)
};
let max_memory = args.max_memory.unwrap_or_else(|| {
let system_percentage = system_config.memory_percentage;
if let Some(memory_stats) = memory_stats::memory_stats() {
let system_memory = memory_stats.physical_mem;
let calculated = (system_memory as f64 * system_percentage / 100.0) as usize;
calculated.clamp(SystemDefaults::MIN_MEMORY_BUDGET, SystemDefaults::MAX_MEMORY_BUDGET)
} else {
system_config.max_memory_mb * 1024 * 1024
}
});
#[cfg(debug_assertions)]
let stratarg = args.chunk_strategy.clone();
#[cfg(not(debug_assertions))]
let stratarg = None;
let strategy = stratarg.or_else(|| match system_config.strategy {
"static" => Some(Strategy::Static(StaticStrategy::default())),
"adaptive" => Some(Strategy::Adaptive(AdaptiveStrategy::default())),
"conservative" => Some(Strategy::Conservative(ConservativeStrategy::default())),
_ => Some(Strategy::Conservative(ConservativeStrategy::default())),
});
let force_chunked = if args.chunked {
true
} else if args.no_chunked {
false
} else {
system_config.force_chunked
};
Ok(Self {
tailing: args.follow || args.sticky,
sticky: args.sticky,
offset,
offset_unit,
show_time: args.timestamps,
batch_window_ms: args.window,
mode,
force_chunked,
disable_chunked: args.no_chunked,
no_file_names: args.quiet,
all_file_names: args.verbose,
adaptive: args.adaptive,
strategy,
max_memory: Some(max_memory),
#[cfg(debug_assertions)]
conservative: args.conservative,
#[cfg(not(debug_assertions))]
conservative: false,
#[cfg(debug_assertions)]
profile_json: args.profile_json,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn complicated_args() {
let args = crate::Args {
timestamps: true,
follow: true,
sticky: false,
blocks: None,
bytes: Some(-5),
offset: None,
verbose: false,
quiet: false,
window: 250,
chunked: false,
no_chunked: false,
args: vec!["-4".to_string()],
adaptive: false,
chunk_strategy: None,
max_memory: Some(10_000_000_000),
conservative: false,
#[cfg(debug_assertions)]
profile_json: false,
};
let config = ConfigOpts::new(&args).expect("Config should be valid for test");
assert_eq!(config.offset, -5);
assert!(matches!(config.mode, InputMode::Stdin));
}
#[test]
fn glob_expansions() {
let fixture_glob = "./fixtures/*.log".to_string();
let results = expand_globs(&[fixture_glob]).expect("this list of paths should expand successfully");
assert_eq!(results.len(), 8); assert_eq!(
results.as_slice(),
vec![
PathBuf::from("fixtures/ascii_colors.log"),
PathBuf::from("fixtures/garbage_prefix.log"),
PathBuf::from("fixtures/java_stacktrace.log"),
PathBuf::from("fixtures/just_loglines.log"),
PathBuf::from("fixtures/log4j.log"),
PathBuf::from("fixtures/mixed_json_types.log"),
PathBuf::from("fixtures/mixed_text_json.log"),
PathBuf::from("fixtures/windows_line_endings.log")
]
);
}
#[test]
fn can_unescape_glob_pattern() {
assert_eq!(unescape_glob_pattern("\\*.log"), "*.log");
assert_eq!(unescape_glob_pattern("test\\?.txt"), "test?.txt");
assert_eq!(unescape_glob_pattern("\\[abc\\]"), "[abc]");
assert_eq!(unescape_glob_pattern("\\*.log\\?"), "*.log?");
assert_eq!(unescape_glob_pattern("test\\*file\\?.log"), "test*file?.log");
assert_eq!(unescape_glob_pattern("file\\name.txt"), "file\\name.txt");
assert_eq!(unescape_glob_pattern("path\\to\\file"), "path\\to\\file");
assert_eq!(unescape_glob_pattern("*.log"), "*.log");
assert_eq!(unescape_glob_pattern("test?.txt"), "test?.txt");
assert_eq!(unescape_glob_pattern(""), "");
assert_eq!(unescape_glob_pattern("\\"), "\\");
assert_eq!(unescape_glob_pattern("file\\"), "file\\");
}
#[test]
fn is_glob_with_escaped_patterns_works() {
assert!(is_glob("\\*.log"));
assert!(is_glob("test\\?.txt"));
assert!(is_glob("\\[abc]"));
assert!(is_glob("*.log"));
assert!(is_glob("test?.txt"));
assert!(is_glob("[abc]"));
assert!(!is_glob("file.log"));
assert!(!is_glob("test.txt"));
assert!(!is_glob("path/to/file"));
assert!(!is_glob("file\\name.txt"));
assert!(!is_glob("path\\to\\file"));
}
#[test]
fn can_expand_escaped_globs() {
let escaped_fixture_glob = ".\\*/fixtures/\\*.log".to_string();
let normal_fixture_glob = "./fixtures/*.log".to_string();
if let (Ok(escaped_results), Ok(normal_results)) = (
expand_globs(&[escaped_fixture_glob]),
expand_globs(&[normal_fixture_glob]),
) {
assert_eq!(escaped_results, normal_results);
}
}
#[test]
fn can_modify_config() {
let initial_config = ConfigOpts {
tailing: false,
sticky: false,
offset: 10,
offset_unit: OffsetUnit::Lines,
show_time: false,
batch_window_ms: 250,
mode: InputMode::Stdin,
force_chunked: false,
disable_chunked: false,
..Default::default()
};
with_config(initial_config.clone(), || {
update(|cfg| {
cfg.tailing = true;
cfg.offset = 20;
cfg.show_time = true;
});
assert!(tailing());
assert_eq!(offset(), 20);
assert!(show_time());
});
}
#[test]
fn can_test_with_config() {
let original_config = ConfigOpts::default();
set(original_config.clone()).expect("should set config");
let original_offset = offset();
let original_tailing = tailing();
let result = with_config(
ConfigOpts {
tailing: true,
sticky: false,
offset: 42,
offset_unit: OffsetUnit::Bytes,
show_time: true,
batch_window_ms: 500,
mode: InputMode::Stdin,
force_chunked: true,
disable_chunked: false,
..Default::default()
},
|| {
assert_eq!(offset(), 42);
assert!(tailing());
assert_eq!(batch_window_ms(), 500);
assert!(force_chunked());
"test_successful"
},
);
assert_eq!(offset(), original_offset);
assert_eq!(tailing(), original_tailing);
assert_eq!(result, "test_successful");
}
#[test]
fn concurrent_access_to_test_config() {
use std::thread;
use std::time::Duration;
let handles: Vec<_> = (0..3)
.map(|i| {
thread::spawn(move || {
let config = ConfigOpts {
offset: i * 10,
tailing: i % 2 == 0,
show_time: i % 2 == 1,
..ConfigOpts::default()
};
set(config).expect("should set config");
thread::sleep(Duration::from_millis(10));
assert_eq!(offset(), i * 10);
assert_eq!(tailing(), i % 2 == 0);
assert_eq!(show_time(), i % 2 == 1);
i
})
})
.collect();
let results: Vec<_> = handles
.into_iter()
.map(|h| h.join().expect("test results should always be ok"))
.collect();
assert_eq!(results, vec![0, 1, 2]);
}
#[test]
fn config_accessors_work() {
let test_config = ConfigOpts {
tailing: true,
sticky: true,
offset: -100,
offset_unit: OffsetUnit::Blocks,
show_time: true,
batch_window_ms: 1000,
mode: InputMode::SingleFile {
path: PathBuf::from("test.log"),
},
force_chunked: true,
disable_chunked: false,
..Default::default()
};
with_config(test_config, || {
assert!(tailing());
assert!(sticky());
assert_eq!(offset(), -100);
assert!(matches!(offset_unit(), OffsetUnit::Blocks));
assert!(show_time());
assert_eq!(batch_window_ms(), 1000);
assert!(matches!(mode(), InputMode::SingleFile { .. }));
assert!(force_chunked());
assert!(!disable_chunked());
});
}
}