clickcheck/cli.rs
1//! Command-line interface definition for `clickcheck`.
2//!
3//! This module defines the CLI structure using [`clap`] derive macros. It handles:
4//!
5//! - Global arguments like config file path, output format, and active context.
6//! - Subcommands:
7//! - `queries`: Analyze and group normalized ClickHouse queries with filtering.
8//! - `errors`: Display frequent ClickHouse query errors with filtering.
9//! - `context`: Manage named connection profiles (contexts).
10//!
11//! This module also includes utility functions to parse human-friendly inputs
12//! like durations, dates, and secrets.
13//!
14//! The structure is designed to separate configuration parsing from execution logic,
15//! making it easier to test and extend.
16use crate::model::{OutputFormat, QueriesSortBy};
17use clap::{ArgGroup, Args, Parser, Subcommand};
18use std::path::PathBuf;
19use std::str::FromStr;
20use time::format_description::well_known::Rfc3339;
21use time::macros::format_description;
22use time::{Date, OffsetDateTime, Time};
23
24/// Analyze ClickHouse query_log and system tables to detect inefficient queries,
25/// anomalies, storage growth, and other potential issues for DBAs and SREs.
26#[derive(Parser)]
27#[command(
28 name = "clickcheck",
29 version,
30 about = "Tool to analyze ClickHouse system tables, to detect potential issues for DBAs."
31)]
32pub struct CliArgs {
33 /// The subcommand to execute (e.g. `queries`, `errors`, `context`).
34 #[command(subcommand)]
35 pub command: Command,
36
37 /// Path to a context configuration TOML file.
38 #[arg(long, global = true)]
39 pub config: Option<PathBuf>,
40
41 /// Optional override for which context (profile) to use.
42 /// Takes precedence over the stored default.
43 #[arg(long, global = true)]
44 pub context: Option<String>,
45
46 /// Output format for results: text (default), json, or yaml.
47 #[clap(long, global = true, default_value = "text")]
48 pub out: OutputFormat,
49}
50
51/// Subcommands for different analysis modes.
52#[derive(Subcommand)]
53pub enum Command {
54 /// Show top queries grouped by normalized_query_hash, with optional filters and sorting.
55 Queries {
56 #[clap(flatten)]
57 conn: ConnectArgs,
58
59 /// Field to sort queries by in top results, descending order.
60 #[arg(long, default_value = "total-impact")]
61 sort_by: QueriesSortBy,
62
63 #[clap(flatten)]
64 filter: QueriesFilterArgs,
65
66 /// number of output queries
67 #[arg(long, default_value_t = 5)]
68 limit: usize,
69 },
70
71 /// Analyze total number of queries and aggregated statistics (e.g. read rows/data) in a time range.
72 ///
73 /// This command shows cumulative metrics over the specified filter window,
74 /// helping track overall workload volume.
75 Total {
76 #[clap(flatten)]
77 conn: ConnectArgs,
78
79 #[clap(flatten)]
80 filter: QueriesFilterArgs,
81 },
82
83 /// Inspect a single query fingerprint with detailed info.
84 ///
85 /// This command streams all log entries matching the specified query fingerprint,
86 /// applying optional filters such as time ranges or users. It provides a detailed
87 /// view of the query's resource usage, execution times, affected tables, and more.
88 Inspect {
89 #[clap(flatten)]
90 conn: ConnectArgs,
91
92 #[arg(value_parser = parse_hex)]
93 fingerprint: u64,
94
95 #[clap(flatten)]
96 filter: QueriesFilterArgs,
97 },
98
99 /// Show top ClickHouse query errors with filtering options.
100 Errors {
101 #[clap(flatten)]
102 conn: ConnectArgs,
103
104 #[clap(flatten)]
105 filter: ErrorFilterArgs,
106
107 /// number of output queries
108 #[arg(long, default_value_t = 5)]
109 limit: usize,
110 },
111
112 /// Manage context profiles used for connecting to ClickHouse.
113 Context {
114 #[command(subcommand)]
115 command: ContextCommand,
116 },
117}
118
119/// Connection-related arguments used in multiple commands.
120#[derive(Args, Clone, Debug)]
121pub struct ConnectArgs {
122 /// ClickHouse node URL (can be specified multiple times)
123 #[arg(short = 'U', long = "url")]
124 pub urls: Vec<String>,
125
126 /// ClickHouse username
127 #[arg(short = 'u', long)]
128 pub user: Option<String>,
129
130 /// ClickHouse password
131 #[arg(short = 'p', long, value_parser = parse_secret_arg)]
132 pub password: Option<secrecy::SecretString>,
133
134 /// ClickHouse password from interactive prompt
135 #[arg(short = 'i', long, conflicts_with = "password")]
136 pub interactive_password: bool,
137
138 /// Accept invalid (e.g., self-signed) TLS certificates when connecting over HTTPS.
139 ///
140 /// This option is useful when connecting to ClickHouse instances with self-signed
141 /// or untrusted certificates. It **disables certificate validation**, which can be
142 /// helpful for development or internal environments, but is **not recommended for production**
143 /// due to potential security risks.
144 #[arg(long)]
145 pub accept_invalid_certificate: Option<bool>,
146}
147
148/// Filters for narrowing down which queries to include in `queries` analysis.
149/// Supports both absolute date ranges and relative durations.
150#[derive(Args, Clone)]
151#[command(group(
152 ArgGroup::new("from_or_last")
153 .args(["from", "last"])
154 .required(true)
155))]
156pub struct QueriesFilterArgs {
157 /// Lower bound for event_time (inclusive). Supports RFC3339 or YYYY-MM-DD.
158 /// Examples: "2024-05-04T15:00:00Z", "2024-05-04"
159 #[arg(
160 long,
161 value_parser = parse_datetime,
162 group = "from_or_last"
163 )]
164 pub from: Option<OffsetDateTime>,
165 /// Upper bound for event_time (exclusive). Supports RFC3339 or YYYY-MM-DD.
166 /// Examples: "2024-05-04T15:00:00Z", "2024-05-04"
167 #[arg(long, value_parser = parse_datetime)]
168 pub to: Option<OffsetDateTime>,
169
170 /// Only include queries from the last specified time period
171 /// Accepts human-readable durations like '15days 2min 2s', etc
172 #[arg(
173 long,
174 value_parser = humantime::parse_duration,
175 group = "from_or_last"
176 )]
177 pub last: Option<std::time::Duration>,
178
179 /// Filter by the user who executed the query. Can be specified multiple times.
180 #[arg(long = "query-user")]
181 pub query_user: Vec<String>,
182 /// Filter by database name. Can be specified multiple times.
183 #[arg(long)]
184 pub database: Vec<String>,
185 /// Filter by table name. Can be specified multiple times.
186 #[arg(long)]
187 pub table: Vec<String>,
188
189 /// Filter by minimum query duration (e.g., 100ms, 1s)
190 #[arg(long, value_parser = humantime::parse_duration)]
191 pub min_query_duration: Option<std::time::Duration>,
192 /// Filter by minimum number of rows read.
193 #[arg(long)]
194 pub min_read_rows: Option<u64>,
195 /// Filter by the minimum amount of data read (supports units like B, KB, MB, GiB)
196 #[arg(long, value_parser = bytesize::ByteSize::from_str)]
197 pub min_read_data: Option<bytesize::ByteSize>,
198}
199
200/// Filters for the `errors` command.
201#[derive(Args, Debug, Clone)]
202pub struct ErrorFilterArgs {
203 /// Only include errors that occurred within the last specified time period.
204 /// Accepts human-readable durations like '15days 2min 2s', etc
205 #[arg(long, value_parser = humantime::parse_duration)]
206 pub last: Option<std::time::Duration>,
207 /// Filter out errors that occurred fewer than N times across all nodes.
208 /// Useful to focus on recurring or high-impact issues.
209 #[arg(long)]
210 pub min_count: Option<usize>,
211 /// Filter errors by specific ClickHouse error code.
212 /// Can be used multiple times to include multiple codes.
213 #[arg(long)]
214 pub code: Vec<i32>,
215}
216
217/// Subcommands for inspecting or modifying context profiles.
218#[derive(Subcommand)]
219pub enum ContextCommand {
220 /// Show config file which store context profiles
221 ConfigPath,
222 /// List all available context profiles
223 List,
224 /// Show the active context (CLI override or stored default)
225 Current,
226 /// Show details for a specific profile by name
227 Show {
228 name: String,
229 /// Show sensitive information like passwords
230 #[arg(long, default_value = "false")]
231 show_secrets: bool,
232 },
233 /// Commands to add or modify context profiles
234 Set {
235 #[command(subcommand)]
236 command: ContextSetCommand,
237 },
238 /// Commands to delete context profiles
239 Delete { name: String },
240}
241
242/// Subcommands to set context values (profile definition or current profile).
243#[derive(Subcommand)]
244pub enum ContextSetCommand {
245 /// Create or update a context profile
246 Profile(SetProfileArgs),
247 /// Set the stored default context to an existing profile
248 Current { name: String },
249}
250
251/// Arguments for creating or updating a context profile.
252/// Requires either a password or interactive prompt (enforced by ArgGroup).
253#[derive(Args)]
254#[command(group( ArgGroup::new("auth") .args(["password", "interactive_password"]) .required(true)))]
255pub struct SetProfileArgs {
256 /// The name of the profile to create or update
257 pub name: String,
258
259 /// ClickHouse node URLs
260 #[arg(short = 'U', long = "url", required = true)]
261 pub urls: Vec<String>,
262
263 /// ClickHouse username
264 #[arg(short = 'u', long, required = true)]
265 pub user: String,
266
267 /// ClickHouse password (plaintext)
268 #[arg( short = 'p', long, value_parser = parse_secret_arg, group = "auth")]
269 pub password: Option<secrecy::SecretString>,
270
271 /// Get password via interactive prompt
272 #[arg(short = 'i', long, group = "auth")]
273 pub interactive_password: bool,
274
275 /// Accept invalid (e.g., self-signed) TLS certificates when connecting over HTTPS.
276 ///
277 /// This option is useful when connecting to ClickHouse instances with self-signed
278 /// or untrusted certificates. It **disables certificate validation**, which can be
279 /// helpful for development or internal environments, but is **not recommended for production**
280 /// due to potential security risks.
281 #[arg(long, default_value_t = false)]
282 pub accept_invalid_certificate: bool,
283}
284
285/// Parses either a full RFC3339 timestamp or a YYYY-MM-DD date.
286/// Returns an `OffsetDateTime` set to midnight if only a date is provided.
287fn parse_datetime(s: &str) -> Result<OffsetDateTime, String> {
288 if let Ok(dt) = OffsetDateTime::parse(s, &Rfc3339) {
289 return Ok(dt);
290 }
291
292 let date_format = format_description!("[year]-[month]-[day]");
293 if let Ok(date) = Date::parse(s, &date_format) {
294 let date = date.with_time(Time::MIDNIGHT).assume_utc();
295 return Ok(date);
296 }
297
298 Err("Invalid datetime format. Use RFC3339 (e.g. 2024-05-01T10:30:00Z) or YYYY-MM-DD.".into())
299}
300
301/// Parses a password from a CLI argument into a `SecretString`.
302/// Used to avoid leaking secrets in logs or stack traces.
303fn parse_secret_arg(s: &str) -> Result<secrecy::SecretString, String> {
304 Ok(secrecy::SecretString::new(s.to_string().into()))
305}
306
307fn parse_hex(s: &str) -> Result<u64, String> {
308 u64::from_str_radix(s.trim_start_matches("0x"), 16)
309 .map_err(|e| format!("Invalid hex value: {}", e))
310}