Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ log = { version = "0.4", default-features = false }
futures-util = { version = "*", default-features = false }
semver = { version = "*", default-features = false }
serde = { version = "*", features = ["derive"] }
serde_json = { version = "*", default-features = false, features = ["std"] }
serde_yaml = { version = "*", default-features = false }
quick-xml = { version = "*", features = ["serialize"] }
urlencoding = { version = "*", default-features = false }
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ introspection, like `top` for Linux.
- [Flamegraphs](Documentation/FAQ.md#what-is-flamegraph) (CPU/Real/Memory/Live) in TUI (thanks to [flamelens](https://github.com/ys-l/flamelens))
- Share flamegraphs (using [pastila.nl](https://pastila.nl/) and [speedscope](https://www.speedscope.app/))
- Share logs via [pastila.nl](https://pastila.nl/)
- Share query pipelines (using [GraphvizOnline](https://dreampuf.github.io/GraphvizOnline/?engine=dot#digraph%0A%7B%0A%20%20rankdir%3D%22LR%22%3B%0A%20%20%7B%20node%20%5Bshape%20%3D%20rect%5D%0A%20%20%20%20n0%5Blabel%3D%22CountingTransform_7%22%5D%3B%0A%20%20%20%20n1%5Blabel%3D%22AddDeduplicationInfoTransform_6%22%5D%3B%0A%20%20%20%20n2%5Blabel%3D%22PlanSquashingTransform_5%22%5D%3B%0A%20%20%20%20n3%5Blabel%3D%22ApplySquashingTransform_4%22%5D%3B%0A%20%20%20%20n4%5Blabel%3D%22ConvertingTransform_0%22%5D%3B%0A%20%20%20%20n5%5Blabel%3D%22RemovingReplicatedColumnsTransform_1%22%5D%3B%0A%20%20%20%20n6%5Blabel%3D%22NestedElementsValidationTransform_2%22%5D%3B%0A%20%20%20%20n7%5Blabel%3D%22SharedMergeTreeSink_3%22%5D%3B%0A%20%20%20%20n8%5Blabel%3D%22EmptySink_8%22%5D%3B%0A%20%20%7D%0A%20%20n0%20-%3E%20n1%3B%0A%20%20n1%20-%3E%20n2%3B%0A%20%20n2%20-%3E%20n3%3B%0A%20%20n3%20-%3E%20n4%3B%0A%20%20n4%20-%3E%20n5%3B%0A%20%20n5%20-%3E%20n6%3B%0A%20%20n6%20-%3E%20n7%3B%0A%20%20n7%20-%3E%20n8%3B%0A%7D))
- Share query pipelines (using [viz.js](https://github.com/mdaines/viz-js) and [pastila.nl](https://pastila.nl/))
- Cluster support (`--cluster`) - aggregate data from all hosts in the cluster
- Historical support (`--history`) - includes rotated `system.*_log_*` tables
- `clickhouse-client` compatibility (including `--connection`) for options and configuration files
Expand Down
3 changes: 1 addition & 2 deletions src/interpreter/flamegraph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,7 @@ pub async fn open_in_speedscope(
return Err(Error::msg("Flamegraph is empty"));
}

let pastila_url =
pastila::upload_to_pastila(&data, pastila_clickhouse_host, pastila_url).await?;
let pastila_url = pastila::upload(&data, pastila_clickhouse_host, pastila_url).await?;

let url = format!(
"https://www.speedscope.app/#profileURL={}",
Expand Down
40 changes: 20 additions & 20 deletions src/interpreter/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::{
flamegraph,
},
pastila,
utils::{highlight_sql, open_graph_in_browser},
utils::{highlight_sql, share_graph},
view::{self, Navigation},
};
use anyhow::{Result, anyhow};
Expand All @@ -31,7 +31,7 @@ pub enum Event {
LastQueryLog(String, RelativeDateTime, RelativeDateTime, u64),
// (view_name, args)
TextLog(&'static str, TextLogArguments),
// [bool (true - show in TUI, false - open in browser), type, start, end]
// [bool (true - show in TUI, false - share via pastila), type, start, end]
ServerFlameGraph(bool, TraceType, DateTime<Local>, DateTime<Local>),
// (type, bool (true - show in TUI, false - open in browser), start time, end time, [query_ids])
QueryFlameGraph(
Expand All @@ -55,7 +55,7 @@ pub enum Event {
// (database, query)
ExplainPipeline(String, String),
// (database, query)
ExplainPipelineOpenGraphInBrowser(String, String),
ExplainPipelineShareGraph(String, String),
// (database, query)
ExplainPlanIndexes(String, String),
// (database, table)
Expand All @@ -74,7 +74,7 @@ pub enum Event {
TableParts(String, String),
// (database, table)
AsynchronousInserts(String, String),
// (content to share)
// (content to share via pastila)
ShareLogs(String),
}

Expand All @@ -94,9 +94,7 @@ impl Event {
Event::ExplainSyntax(..) => "ExplainSyntax".to_string(),
Event::ExplainPlan(..) => "ExplainPlan".to_string(),
Event::ExplainPipeline(..) => "ExplainPipeline".to_string(),
Event::ExplainPipelineOpenGraphInBrowser(..) => {
"ExplainPipelineOpenGraphInBrowser".to_string()
}
Event::ExplainPipelineShareGraph(..) => "ExplainPipelineShareGraph".to_string(),
Event::ExplainPlanIndexes(..) => "ExplainPlanIndexes".to_string(),
Event::ShowCreateTable(..) => "ShowCreateTable".to_string(),
Event::SQLQuery(view_name, _query) => format!("SQLQuery({})", view_name),
Expand Down Expand Up @@ -541,21 +539,24 @@ async fn process_event(context: ContextArc, event: Event, need_clear: &mut bool)
}))
.map_err(|_| anyhow!("Cannot send message to UI"))?;
}
Event::ExplainPipelineOpenGraphInBrowser(database, query) => {
Event::ExplainPipelineShareGraph(database, query) => {
let pipeline = clickhouse
.explain_pipeline_graph(database.as_str(), query.as_str())
.await?
.join("\n");
cb_sink
.send(Box::new(move |siv: &mut cursive::Cursive| {
open_graph_in_browser(pipeline)
.or_else(|err| {
siv.add_layer(views::Dialog::info(err.to_string()));
return anyhow::Ok(());
})
.unwrap();
}))
.map_err(|_| anyhow!("Cannot send message to UI"))?;

// Upload graph to pastila and open in browser
match share_graph(pipeline, &pastila_clickhouse_host, &pastila_url).await {
Ok(_) => {}
Err(err) => {
let error_msg = err.to_string();
cb_sink
.send(Box::new(move |siv: &mut cursive::Cursive| {
siv.add_layer(views::Dialog::info(error_msg));
}))
.map_err(|_| anyhow!("Cannot send message to UI"))?;
}
}
}
Event::ShowCreateTable(database, table) => {
let create_statement = clickhouse
Expand Down Expand Up @@ -718,8 +719,7 @@ async fn process_event(context: ContextArc, event: Event, need_clear: &mut bool)
}
Event::ShareLogs(content) => {
let url =
pastila::upload_logs_encrypted(&content, &pastila_clickhouse_host, &pastila_url)
.await?;
pastila::upload_encrypted(&content, &pastila_clickhouse_host, &pastila_url).await?;

let url_clone = url.clone();
cb_sink
Expand Down
4 changes: 2 additions & 2 deletions src/pastila.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ async fn get_pastila_client(pastila_clickhouse_host: &str) -> Result<clickhouse_
Ok(client)
}

pub async fn upload_to_pastila(
pub async fn upload(
content: &str,
pastila_clickhouse_host: &str,
pastila_url: &str,
Expand Down Expand Up @@ -210,7 +210,7 @@ pub async fn upload_to_pastila(
Ok(clickhouse_url)
}

pub async fn upload_logs_encrypted(
pub async fn upload_encrypted(
content: &str,
pastila_clickhouse_host: &str,
pastila_url: &str,
Expand Down
54 changes: 48 additions & 6 deletions src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::actions::ActionDescription;
use crate::pastila;
use crate::view::Navigation;
use anyhow::{Context, Error, Result};
use cursive::Cursive;
Expand All @@ -16,7 +17,6 @@ use std::io::Write;
use std::process::{Command, Stdio};
use syntect::{highlighting::ThemeSet, parsing::SyntaxSet};
use tempfile::Builder;
use urlencoding::encode;

pub fn fuzzy_actions<F>(siv: &mut Cursive, actions: Vec<ActionDescription>, on_select: F)
where
Expand Down Expand Up @@ -228,16 +228,58 @@ pub fn open_url_command(url: &str) -> Command {
cmd
}

pub fn open_graph_in_browser(graph: String) -> Result<()> {
pub async fn share_graph(
graph: String,
pastila_clickhouse_host: &str,
pastila_url: &str,
) -> Result<()> {
if graph.is_empty() {
return Err(Error::msg("Graph is empty"));
}
let url = format!(
"https://dreampuf.github.io/GraphvizOnline/#{}",
encode(&graph)

// Create a self-contained HTML file that renders the Graphviz graph
// Using viz.js from CDN for client-side rendering
let html = format!(
r#"<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Graphviz Graph</title>
<style>
body {{ margin: 0; padding: 20px; font-family: sans-serif; }}
#graph {{ text-align: center; }}
</style>
</head>
<body>
<div id="graph">Loading graph...</div>
<script src="https://cdn.jsdelivr.net/npm/@viz-js/viz@3.2.4/lib/viz-standalone.js"></script>
<script>
const dot = {};
Viz.instance().then(viz => {{
const svg = viz.renderSVGElement(dot);
const container = document.getElementById('graph');
container.innerHTML = '';
container.appendChild(svg);
}}).catch(err => {{
document.getElementById('graph').textContent = 'Error rendering graph: ' + err;
}});
</script>
</body>
</html>"#,
serde_json::to_string(&graph)?
);

// Upload HTML to pastila with end-to-end encryption
let mut url = pastila::upload_encrypted(&html, pastila_clickhouse_host, pastila_url).await?;

if let Some(anchor_pos) = url.find('#') {
url.insert_str(anchor_pos, ".html");
}

// Open the URL in the browser
open_url_command(&url).status()?;
return Ok(());

Ok(())
}

pub fn find_common_hostname_prefix_and_suffix<'a, I>(hostnames: I) -> (String, String)
Expand Down
4 changes: 2 additions & 2 deletions src/view/queries_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,7 @@ impl QueriesView {
let mut context_locked = self.context.lock().unwrap();
context_locked.worker.send(
true,
WorkerEvent::ExplainPipelineOpenGraphInBrowser(database, query),
WorkerEvent::ExplainPipelineShareGraph(database, query),
);
Ok(Some(EventResult::consumed()))
}
Expand Down Expand Up @@ -1104,7 +1104,7 @@ impl QueriesView {
add_action!(context, &mut event_view, "Share Query events flamegraph", action_show_flamegraph(false, Some(TraceType::ProfileEvents)));
add_action!(context, &mut event_view, "Share Query live flamegraph", action_show_flamegraph(false, None));
add_action!(context, &mut event_view, "EXPLAIN INDEXES", 'I', action_explain_indexes);
add_action!(context, &mut event_view, "EXPLAIN PIPELINE graph=1 (open in browser)", 'G', action_explain_pipeline_graph);
add_action!(context, &mut event_view, "EXPLAIN PIPELINE graph=1 (share)", 'G', action_explain_pipeline_graph);
add_action!(context, &mut event_view, "KILL query", 'K', action_kill_query);
add_action!(context, &mut event_view, "Increase number of queries to render to 20", '(', action_increase_limit);
add_action!(context, &mut event_view, "Decrease number of queries to render to 20", ')', action_decrease_limit);
Expand Down