1use std::io::{self, Write};
8
9use bytes::{Buf, BytesMut};
10use logpatterns::*;
11use miette::{IntoDiagnostic, Result};
12
13pub mod config;
14pub mod defaults;
15pub mod errors;
16pub mod json_profiler;
17pub mod logpatterns;
18pub mod memory_budget;
19pub mod metrics;
20pub mod multiplexed;
21pub mod readers;
22
23#[cfg(test)]
24mod tests;
25
26use clap::Parser;
28use clap::builder::Styles;
29use clap::builder::styling::AnsiColor;
30pub use errors::TaleError;
31pub use memory_budget::{MemoryBudget, MemoryPressure};
32pub use readers::strategies::{IsStrategy, Strategy};
33pub use readers::{ChunkedFileReader, FileProcessor};
34
35#[derive(Debug, Clone, Parser, Default)]
36#[clap(name="tale", version, styles = v3_styles(), max_term_width = 100)]
37pub struct Args {
50 #[arg(short, long)]
52 pub follow: bool,
53 #[arg(short = 'F', long)]
57 pub sticky: bool,
58 #[arg(short, long, group = "units")]
60 pub blocks: Option<i64>,
61 #[arg(short = 'c', long, group = "units")]
63 pub bytes: Option<i64>,
64 #[arg(short = 'n', long, group = "units")]
66 pub offset: Option<i64>,
67 #[arg(short, long)]
70 pub verbose: bool,
71 #[arg(short, long, conflicts_with = "verbose")]
73 pub quiet: bool,
74
75 #[arg(short, long)]
78 pub timestamps: bool,
79 #[arg(long, default_value = "250")]
81 pub window: u64,
82
83 #[arg(long)]
87 pub chunked: bool,
88 #[arg(long, conflicts_with = "chunked")]
91 pub no_chunked: bool,
92 #[arg(short, long)]
94 pub adaptive: bool,
95 #[arg(short, long)]
97 pub max_memory: Option<usize>,
98 #[cfg(debug_assertions)]
99 #[arg(long, hide = true)]
100 pub conservative: bool,
101 #[cfg(debug_assertions)]
102 #[arg(short = 's', long)]
104 pub chunk_strategy: Option<Strategy>,
105 #[cfg(debug_assertions)]
107 #[arg(long, hide = true)]
108 pub profile_json: bool,
109
110 #[arg(allow_hyphen_values = true)]
112 pub args: Vec<String>,
113}
114
115fn v3_styles() -> Styles {
117 Styles::styled()
118 .header(AnsiColor::Yellow.on_default())
119 .usage(AnsiColor::Green.on_default())
120 .literal(AnsiColor::Green.on_default())
121 .placeholder(AnsiColor::Green.on_default())
122}
123
124#[inline]
126pub fn process_line(line: &str, buffer: &mut BytesMut, outlock: &mut io::StdoutLock<'_>) -> Result<()> {
127 match serde_json::from_str::<Printable<'_>>(line) {
128 Ok(message) => {
129 #[cfg(debug_assertions)]
131 json_profiler::record_variant(&message);
132
133 message.write(buffer);
134 outlock.write_all(buffer.chunk()).into_diagnostic()?;
135 outlock.write_all(&[0x0a; 1]).into_diagnostic()?; buffer.clear();
137 }
138 Err(_) => {
139 #[cfg(debug_assertions)]
141 json_profiler::record_parse_error();
142
143 outlock.write_all(line.as_bytes()).into_diagnostic()?;
144 outlock.write_all(b"\n").into_diagnostic()?;
145 }
146 }
147 Ok(())
148}
149
150#[inline]
153pub fn strip_line_ending(line: &mut String) {
154 if line.ends_with('\n') {
155 line.pop();
156 if line.ends_with('\r') {
157 line.pop();
160 }
161 }
162}
163
164#[cfg(test)]
165mod cli_tests {
166 use super::*;
167
168 #[test]
169 fn verify_cli() {
170 use clap::CommandFactory;
171 Args::command().debug_assert();
172 }
173
174 #[test]
175 fn offset_unit_args() {
176 use crate::Args;
177 use crate::config::{ConfigOpts, OffsetUnit};
178
179 let args = Args {
181 bytes: Some(100),
182 args: vec!["test.log".to_string()],
183 ..Default::default()
184 };
185 let config = ConfigOpts::new(&args).expect("Config should be valid for test");
186 assert!(matches!(config.offset_unit, OffsetUnit::Bytes));
187 assert_eq!(config.offset, 100);
188
189 let args = Args {
191 blocks: Some(2),
192 args: vec!["test.log".to_string()],
193 ..Default::default()
194 };
195 let config = ConfigOpts::new(&args).expect("Config should be valid for test");
196 assert!(matches!(config.offset_unit, OffsetUnit::Blocks));
197 assert_eq!(config.offset, 2);
198
199 let args = Args {
201 offset: Some(5),
202 args: vec!["test.log".to_string()],
203 ..Default::default()
204 };
205 let config = ConfigOpts::new(&args).expect("Config should be valid for test");
206 assert!(matches!(config.offset_unit, OffsetUnit::Lines));
207 assert_eq!(config.offset, 5);
208 }
209
210 #[test]
211 fn can_run_cli_and_emit_help() {
212 let output = std::process::Command::new("cargo")
213 .args(["run", "--", "--help"])
214 .output()
215 .expect("failed to execute");
216
217 assert!(output.status.success());
218 }
220}