use std::io::{self, Write};
use bytes::{Buf, BytesMut};
use logpatterns::*;
use miette::{IntoDiagnostic, Result};
pub mod config;
pub mod defaults;
pub mod errors;
pub mod json_profiler;
pub mod logpatterns;
pub mod memory_budget;
pub mod metrics;
pub mod multiplexed;
pub mod readers;
#[cfg(test)]
mod tests;
use clap::Parser;
use clap::builder::Styles;
use clap::builder::styling::AnsiColor;
pub use errors::TaleError;
pub use memory_budget::{MemoryBudget, MemoryPressure};
pub use readers::strategies::{IsStrategy, Strategy};
pub use readers::{ChunkedFileReader, FileProcessor};
#[derive(Debug, Clone, Parser, Default)]
#[clap(name="tale", version, styles = v3_styles(), max_term_width = 100)]
pub struct Args {
#[arg(short, long)]
pub follow: bool,
#[arg(short = 'F', long)]
pub sticky: bool,
#[arg(short, long, group = "units")]
pub blocks: Option<i64>,
#[arg(short = 'c', long, group = "units")]
pub bytes: Option<i64>,
#[arg(short = 'n', long, group = "units")]
pub offset: Option<i64>,
#[arg(short, long)]
pub verbose: bool,
#[arg(short, long, conflicts_with = "verbose")]
pub quiet: bool,
#[arg(short, long)]
pub timestamps: bool,
#[arg(long, default_value = "250")]
pub window: u64,
#[arg(long)]
pub chunked: bool,
#[arg(long, conflicts_with = "chunked")]
pub no_chunked: bool,
#[arg(short, long)]
pub adaptive: bool,
#[arg(short, long)]
pub max_memory: Option<usize>,
#[cfg(debug_assertions)]
#[arg(long, hide = true)]
pub conservative: bool,
#[cfg(debug_assertions)]
#[arg(short = 's', long)]
pub chunk_strategy: Option<Strategy>,
#[cfg(debug_assertions)]
#[arg(long, hide = true)]
pub profile_json: bool,
#[arg(allow_hyphen_values = true)]
pub args: Vec<String>,
}
fn v3_styles() -> Styles {
Styles::styled()
.header(AnsiColor::Yellow.on_default())
.usage(AnsiColor::Green.on_default())
.literal(AnsiColor::Green.on_default())
.placeholder(AnsiColor::Green.on_default())
}
#[inline]
pub fn process_line(line: &str, buffer: &mut BytesMut, outlock: &mut io::StdoutLock<'_>) -> Result<()> {
match serde_json::from_str::<Printable<'_>>(line) {
Ok(message) => {
#[cfg(debug_assertions)]
json_profiler::record_variant(&message);
message.write(buffer);
outlock.write_all(buffer.chunk()).into_diagnostic()?;
outlock.write_all(&[0x0a; 1]).into_diagnostic()?; buffer.clear();
}
Err(_) => {
#[cfg(debug_assertions)]
json_profiler::record_parse_error();
outlock.write_all(line.as_bytes()).into_diagnostic()?;
outlock.write_all(b"\n").into_diagnostic()?;
}
}
Ok(())
}
#[inline]
pub fn strip_line_ending(line: &mut String) {
if line.ends_with('\n') {
line.pop();
if line.ends_with('\r') {
line.pop();
}
}
}
#[cfg(test)]
mod cli_tests {
use super::*;
#[test]
fn verify_cli() {
use clap::CommandFactory;
Args::command().debug_assert();
}
#[test]
fn offset_unit_args() {
use crate::Args;
use crate::config::{ConfigOpts, OffsetUnit};
let args = Args {
bytes: Some(100),
args: vec!["test.log".to_string()],
..Default::default()
};
let config = ConfigOpts::new(&args).expect("Config should be valid for test");
assert!(matches!(config.offset_unit, OffsetUnit::Bytes));
assert_eq!(config.offset, 100);
let args = Args {
blocks: Some(2),
args: vec!["test.log".to_string()],
..Default::default()
};
let config = ConfigOpts::new(&args).expect("Config should be valid for test");
assert!(matches!(config.offset_unit, OffsetUnit::Blocks));
assert_eq!(config.offset, 2);
let args = Args {
offset: Some(5),
args: vec!["test.log".to_string()],
..Default::default()
};
let config = ConfigOpts::new(&args).expect("Config should be valid for test");
assert!(matches!(config.offset_unit, OffsetUnit::Lines));
assert_eq!(config.offset, 5);
}
#[test]
fn can_run_cli_and_emit_help() {
let output = std::process::Command::new("cargo")
.args(["run", "--", "--help"])
.output()
.expect("failed to execute");
assert!(output.status.success());
}
}