diff --git a/src/common/relative_date_time.rs b/src/common/relative_date_time.rs index 5054fe5..524c751 100644 --- a/src/common/relative_date_time.rs +++ b/src/common/relative_date_time.rs @@ -58,6 +58,16 @@ impl RelativeDateTime { self.date_time } + pub fn to_editable_string(&self) -> String { + match (&self.date_time, &self.offset) { + (None, Some(offset)) => { + humantime::format_duration(offset.to_std().unwrap_or_default()).to_string() + } + (Some(dt), _) => dt.format("%Y-%m-%dT%H:%M:%S").to_string(), + (None, None) => String::new(), + } + } + pub fn to_sql_datetime_64(&self) -> Option { match (self.date_time, self.offset) { (Some(date_time), Some(offset)) => Some(format!( diff --git a/src/interpreter/options.rs b/src/interpreter/options.rs index af25ca0..df91be9 100644 --- a/src/interpreter/options.rs +++ b/src/interpreter/options.rs @@ -286,16 +286,30 @@ pub struct ServiceOptions { pub chdig_config: Option, } -#[derive(Deserialize, Default, Clone)] +#[derive(Deserialize, Clone)] #[serde(default)] pub struct ChDigPerfettoConfig { - pub opentelemetry_span_log: Option, - pub trace_log: Option, - pub query_metric_log: Option, - pub part_log: Option, - pub query_thread_log: Option, - pub text_log: Option, - pub per_server: Option, + pub opentelemetry_span_log: bool, + pub trace_log: bool, + pub query_metric_log: bool, + pub part_log: bool, + pub query_thread_log: bool, + pub text_log: bool, + pub per_server: bool, +} + +impl Default for ChDigPerfettoConfig { + fn default() -> Self { + Self { + opentelemetry_span_log: true, + trace_log: true, + query_metric_log: false, + part_log: true, + query_thread_log: true, + text_log: true, + per_server: true, + } + } } #[derive(Deserialize, Default)] @@ -1289,31 +1303,31 @@ mod tests { fn test_chdig_config_perfetto() { let config = read_chdig_config("tests/configs/chdig_basic.yaml").unwrap(); - assert_eq!(config.perfetto.opentelemetry_span_log, Some(true)); - assert_eq!(config.perfetto.trace_log, Some(true)); - assert_eq!(config.perfetto.query_metric_log, Some(true)); - assert_eq!(config.perfetto.part_log, Some(false)); - assert_eq!(config.perfetto.query_thread_log, Some(true)); - assert_eq!(config.perfetto.text_log, Some(false)); + assert_eq!(config.perfetto.opentelemetry_span_log, true); + assert_eq!(config.perfetto.trace_log, true); + assert_eq!(config.perfetto.query_metric_log, true); + assert_eq!(config.perfetto.part_log, false); + assert_eq!(config.perfetto.query_thread_log, true); + assert_eq!(config.perfetto.text_log, false); let mut options = ChDigOptions::parse_from(["chdig"]); apply_chdig_config(&mut options, &config); - assert_eq!(options.perfetto.opentelemetry_span_log, Some(true)); - assert_eq!(options.perfetto.part_log, Some(false)); - assert_eq!(options.perfetto.query_metric_log, Some(true)); + assert_eq!(options.perfetto.opentelemetry_span_log, true); + assert_eq!(options.perfetto.part_log, false); + assert_eq!(options.perfetto.query_metric_log, true); } #[test] fn test_chdig_config_perfetto_defaults() { let config = read_chdig_config("tests/configs/chdig_empty.yaml").unwrap(); - assert!(config.perfetto.opentelemetry_span_log.is_none()); - assert!(config.perfetto.trace_log.is_none()); - assert!(config.perfetto.query_metric_log.is_none()); - assert!(config.perfetto.part_log.is_none()); - assert!(config.perfetto.query_thread_log.is_none()); - assert!(config.perfetto.text_log.is_none()); + assert_eq!(config.perfetto.opentelemetry_span_log, true); + assert_eq!(config.perfetto.trace_log, true); + assert_eq!(config.perfetto.query_metric_log, false); + assert_eq!(config.perfetto.part_log, true); + assert_eq!(config.perfetto.query_thread_log, true); + assert_eq!(config.perfetto.text_log, true); } #[test] diff --git a/src/interpreter/worker.rs b/src/interpreter/worker.rs index ed571d5..e6655dd 100644 --- a/src/interpreter/worker.rs +++ b/src/interpreter/worker.rs @@ -774,7 +774,7 @@ async fn process_event(context: ContextArc, event: Event, need_clear: &mut bool) } Event::PerfettoExport(queries, query_ids, start, end) => { let perfetto_cfg = context.lock().unwrap().options.perfetto.clone(); - let mut builder = PerfettoTraceBuilder::new(perfetto_cfg.per_server.unwrap_or(true)); + let mut builder = PerfettoTraceBuilder::new(perfetto_cfg.per_server); for q in &queries { log::info!( @@ -795,7 +795,7 @@ async fn process_event(context: ContextArc, event: Event, need_clear: &mut bool) let (otel, trace_log, metrics, parts, threads, stack_traces, text_logs) = tokio::join!( async { - if perfetto_cfg.opentelemetry_span_log.unwrap_or(true) { + if perfetto_cfg.opentelemetry_span_log { Some( clickhouse .get_otel_spans_for_perfetto(&query_ids, start, end_time) @@ -806,7 +806,7 @@ async fn process_event(context: ContextArc, event: Event, need_clear: &mut bool) } }, async { - if perfetto_cfg.trace_log.unwrap_or(true) { + if perfetto_cfg.trace_log { Some( clickhouse .get_trace_log_counters_for_perfetto(&query_ids, start, end_time) @@ -818,7 +818,7 @@ async fn process_event(context: ContextArc, event: Event, need_clear: &mut bool) }, async { // Consider using ProfileEvents instead - if perfetto_cfg.query_metric_log.unwrap_or(false) { + if perfetto_cfg.query_metric_log { Some( clickhouse .get_query_metrics_for_perfetto(&query_ids, start, end_time) @@ -829,7 +829,7 @@ async fn process_event(context: ContextArc, event: Event, need_clear: &mut bool) } }, async { - if perfetto_cfg.part_log.unwrap_or(true) { + if perfetto_cfg.part_log { Some( clickhouse .get_part_log_for_perfetto(&query_ids, start, end_time) @@ -840,7 +840,7 @@ async fn process_event(context: ContextArc, event: Event, need_clear: &mut bool) } }, async { - if perfetto_cfg.query_thread_log.unwrap_or(true) { + if perfetto_cfg.query_thread_log { Some( clickhouse .get_query_thread_log_for_perfetto(&query_ids, start, end_time) @@ -851,7 +851,7 @@ async fn process_event(context: ContextArc, event: Event, need_clear: &mut bool) } }, async { - if perfetto_cfg.trace_log.unwrap_or(true) { + if perfetto_cfg.trace_log { Some( clickhouse .get_stack_traces_for_perfetto(&query_ids, start, end_time) @@ -862,7 +862,7 @@ async fn process_event(context: ContextArc, event: Event, need_clear: &mut bool) } }, async { - if perfetto_cfg.text_log.unwrap_or(true) { + if perfetto_cfg.text_log { Some( clickhouse .get_text_log_for_perfetto(&query_ids, start, end_time) diff --git a/src/view/navigation.rs b/src/view/navigation.rs index 653e472..db3fd7b 100644 --- a/src/view/navigation.rs +++ b/src/view/navigation.rs @@ -12,7 +12,10 @@ use cursive::{ theme::{BaseColor, Color, ColorStyle, Effect, PaletteColor, Style, Theme}, utils::{markup::StyledString, span::SpannedString}, view::{IntoBoxedView, Nameable, Resizable, View}, - views::{Dialog, DummyView, EditView, LinearLayout, OnEventView, SelectView, TextView}, + views::{ + Checkbox, Dialog, DummyView, EditView, LinearLayout, OnEventView, ScrollView, SelectView, + TextView, + }, }; use cursive_flexi_logger_view::toggle_flexi_logger_debug_console; @@ -25,6 +28,9 @@ fn make_menu_text() -> StyledString { // F2 text.append_plain("F2"); text.append_styled("Views", ColorStyle::highlight()); + // F3 + text.append_plain("F3"); + text.append_styled("Settings", ColorStyle::highlight()); // F8 text.append_plain("F8"); text.append_styled("Actions", ColorStyle::highlight()); @@ -47,6 +53,7 @@ pub trait Navigation { fn chdig(&mut self, context: ContextArc); fn show_help_dialog(&mut self); + fn show_settings_dialog(&mut self); fn show_views(&mut self); fn show_actions(&mut self); fn show_fuzzy_actions(&mut self); @@ -254,6 +261,7 @@ impl Navigation for Cursive { let mut context = context.lock().unwrap(); context.add_global_action(self, "Show help", Key::F1, |siv| siv.show_help_dialog()); + context.add_global_action(self, "Settings", Key::F3, |siv| siv.show_settings_dialog()); context.add_global_action(self, "Views", Key::F2, |siv| siv.show_views()); context.add_global_action(self, "Show actions", Key::F8, |siv| siv.show_actions()); @@ -374,6 +382,303 @@ impl Navigation for Cursive { self.add_layer(Dialog::info(text).with_name("help")); } + fn show_settings_dialog(&mut self) { + if self.has_view("settings") { + self.pop_layer(); + return; + } + + let context = self.user_data::().unwrap().clone(); + let (opts, server_version, selected_host, current_view) = { + let ctx = context.lock().unwrap(); + ( + ctx.options.clone(), + ctx.server_version.clone(), + ctx.selected_host.clone(), + ctx.current_view, + ) + }; + + let bold = |s: &str| TextView::new(StyledString::styled(s, Effect::Bold)); + let checkbox_row = |label: &str, name: &str, checked: bool| { + LinearLayout::horizontal() + .child(DummyView.fixed_width(2)) + .child(Checkbox::new().with_checked(checked).with_name(name)) + .child(TextView::new(format!(" {}", label))) + }; + let edit_row = |label: &str, name: &str, value: &str, width: usize| { + LinearLayout::horizontal() + .child(TextView::new(format!(" {}: ", label))) + .child( + EditView::new() + .content(value) + .with_name(name) + .fixed_width(width), + ) + }; + + let mut layout = LinearLayout::vertical(); + + // ClickHouse + layout.add_child(bold("ClickHouse:")); + layout.add_child(TextView::new(format!( + " url: {}", + opts.clickhouse.url_safe + ))); + if let Some(ref cluster) = opts.clickhouse.cluster { + layout.add_child(TextView::new(format!(" cluster: {}", cluster))); + } + layout.add_child(checkbox_row( + "history", + "set_history", + opts.clickhouse.history, + )); + layout.add_child(checkbox_row( + "internal_queries", + "set_internal_queries", + opts.clickhouse.internal_queries, + )); + layout.add_child(edit_row( + "limit", + "set_limit", + &opts.clickhouse.limit.to_string(), + 12, + )); + layout.add_child(checkbox_row( + "skip_unavailable_shards", + "set_skip_unavailable_shards", + opts.clickhouse.skip_unavailable_shards, + )); + layout.add_child(TextView::new(format!( + " server_version: {}", + server_version + ))); + layout.add_child(DummyView); + + // View + layout.add_child(bold("View:")); + layout.add_child(edit_row( + "delay_interval (ms)", + "set_delay_interval", + &opts.view.delay_interval.as_millis().to_string(), + 12, + )); + layout.add_child(checkbox_row("group_by", "set_group_by", opts.view.group_by)); + layout.add_child(checkbox_row( + "no_subqueries", + "set_no_subqueries", + opts.view.no_subqueries, + )); + layout.add_child(checkbox_row("wrap", "set_wrap", opts.view.wrap)); + layout.add_child(checkbox_row( + "no_strip_hostname_suffix", + "set_no_strip_hostname_suffix", + opts.view.no_strip_hostname_suffix, + )); + layout.add_child(edit_row( + "start", + "set_start", + &opts.view.start.to_editable_string(), + 22, + )); + layout.add_child(edit_row( + "end", + "set_end", + &opts.view.end.to_editable_string(), + 22, + )); + layout.add_child(DummyView); + + // Service (read-only) + layout.add_child(bold("Service:")); + layout.add_child(TextView::new(format!( + " log: {}", + opts.service.log.as_deref().unwrap_or("(none)") + ))); + layout.add_child(TextView::new(format!( + " chdig_config: {}", + opts.service.chdig_config.as_deref().unwrap_or("(none)") + ))); + layout.add_child(DummyView); + + // Perfetto + layout.add_child(bold("Perfetto:")); + layout.add_child(checkbox_row( + "opentelemetry_span_log", + "set_otel", + opts.perfetto.opentelemetry_span_log, + )); + layout.add_child(checkbox_row( + "trace_log", + "set_trace_log", + opts.perfetto.trace_log, + )); + layout.add_child(checkbox_row( + "query_metric_log", + "set_query_metric_log", + opts.perfetto.query_metric_log, + )); + layout.add_child(checkbox_row( + "part_log", + "set_part_log", + opts.perfetto.part_log, + )); + layout.add_child(checkbox_row( + "query_thread_log", + "set_query_thread_log", + opts.perfetto.query_thread_log, + )); + layout.add_child(checkbox_row( + "text_log", + "set_text_log", + opts.perfetto.text_log, + )); + layout.add_child(checkbox_row( + "per_server", + "set_per_server", + opts.perfetto.per_server, + )); + layout.add_child(DummyView); + + // Runtime (read-only) + layout.add_child(bold("Runtime:")); + layout.add_child(TextView::new(format!( + " selected_host: {}", + selected_host.as_deref().unwrap_or("(all)") + ))); + layout.add_child(TextView::new(format!( + " current_view: {:?}", + current_view.unwrap_or(ChDigViews::Queries) + ))); + + let context_for_apply = context; + let dialog = Dialog::new() + .title("Settings") + .content(ScrollView::new(layout)) + .button("Apply", move |siv| { + let history = siv + .call_on_name("set_history", |v: &mut Checkbox| v.is_checked()) + .unwrap(); + let internal_queries = siv + .call_on_name("set_internal_queries", |v: &mut Checkbox| v.is_checked()) + .unwrap(); + let limit_str = siv + .call_on_name("set_limit", |v: &mut EditView| v.get_content()) + .unwrap(); + let skip_unavailable_shards = siv + .call_on_name("set_skip_unavailable_shards", |v: &mut Checkbox| { + v.is_checked() + }) + .unwrap(); + + let delay_str = siv + .call_on_name("set_delay_interval", |v: &mut EditView| v.get_content()) + .unwrap(); + let group_by = siv + .call_on_name("set_group_by", |v: &mut Checkbox| v.is_checked()) + .unwrap(); + let no_subqueries = siv + .call_on_name("set_no_subqueries", |v: &mut Checkbox| v.is_checked()) + .unwrap(); + let wrap = siv + .call_on_name("set_wrap", |v: &mut Checkbox| v.is_checked()) + .unwrap(); + let no_strip = siv + .call_on_name("set_no_strip_hostname_suffix", |v: &mut Checkbox| { + v.is_checked() + }) + .unwrap(); + let start_str = siv + .call_on_name("set_start", |v: &mut EditView| v.get_content()) + .unwrap(); + let end_str = siv + .call_on_name("set_end", |v: &mut EditView| v.get_content()) + .unwrap(); + + let otel = siv + .call_on_name("set_otel", |v: &mut Checkbox| v.is_checked()) + .unwrap(); + let trace_log = siv + .call_on_name("set_trace_log", |v: &mut Checkbox| v.is_checked()) + .unwrap(); + let query_metric = siv + .call_on_name("set_query_metric_log", |v: &mut Checkbox| v.is_checked()) + .unwrap(); + let part_log = siv + .call_on_name("set_part_log", |v: &mut Checkbox| v.is_checked()) + .unwrap(); + let query_thread = siv + .call_on_name("set_query_thread_log", |v: &mut Checkbox| v.is_checked()) + .unwrap(); + let text_log = siv + .call_on_name("set_text_log", |v: &mut Checkbox| v.is_checked()) + .unwrap(); + let per_server = siv + .call_on_name("set_per_server", |v: &mut Checkbox| v.is_checked()) + .unwrap(); + + let limit: u64 = match limit_str.parse() { + Ok(v) => v, + Err(_) => { + siv.add_layer(Dialog::info("Invalid limit value")); + return; + } + }; + let delay_ms: u64 = match delay_str.parse() { + Ok(v) => v, + Err(_) => { + siv.add_layer(Dialog::info("Invalid delay_interval value")); + return; + } + }; + let new_start = match start_str.parse::() { + Ok(v) => v, + Err(err) => { + siv.add_layer(Dialog::info(format!("Invalid start: {}", err))); + return; + } + }; + let new_end = match end_str.parse::() { + Ok(v) => v, + Err(err) => { + siv.add_layer(Dialog::info(format!("Invalid end: {}", err))); + return; + } + }; + + { + let mut ctx = context_for_apply.lock().unwrap(); + ctx.options.clickhouse.history = history; + ctx.options.clickhouse.internal_queries = internal_queries; + ctx.options.clickhouse.limit = limit; + ctx.options.clickhouse.skip_unavailable_shards = skip_unavailable_shards; + + ctx.options.view.delay_interval = std::time::Duration::from_millis(delay_ms); + ctx.options.view.group_by = group_by; + ctx.options.view.no_subqueries = no_subqueries; + ctx.options.view.wrap = wrap; + ctx.options.view.no_strip_hostname_suffix = no_strip; + ctx.options.view.start = new_start; + ctx.options.view.end = new_end; + + ctx.options.perfetto.opentelemetry_span_log = otel; + ctx.options.perfetto.trace_log = trace_log; + ctx.options.perfetto.query_metric_log = query_metric; + ctx.options.perfetto.part_log = part_log; + ctx.options.perfetto.query_thread_log = query_thread; + ctx.options.perfetto.text_log = text_log; + ctx.options.perfetto.per_server = per_server; + + ctx.trigger_view_refresh(); + } + siv.pop_layer(); + }) + .button("Cancel", |siv| { + siv.pop_layer(); + }); + self.add_layer(dialog.with_name("settings")); + } + fn show_views(&mut self) { let mut has_views = false; let context = self.user_data::().unwrap().clone();