Skip to main content

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}