Skip to content

Commit 48433c0

Browse files
authored
Merge pull request ClickHouse#79042 from ClickHouse/improve-trace-visualizer-ux-2
trace-visualizer: load data from clickhouse server
2 parents 7f7935c + 61c511d commit 48433c0

File tree

2 files changed

+226
-10
lines changed

2 files changed

+226
-10
lines changed

utils/trace-visualizer/index.html

Lines changed: 154 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,31 @@
77
<title>Trace Gantt</title>
88
<link rel="stylesheet" href="css/bootstrap.min.css">
99
<link rel="stylesheet" href="css/d3-gantt.css">
10+
<style>
11+
#hourglass
12+
{
13+
display: none;
14+
font-size: 110%;
15+
color: #888;
16+
animation: hourglass-animation 2s linear infinite;
17+
}
18+
19+
@keyframes hourglass-animation {
20+
0% { transform: none; }
21+
25% { transform: rotate(180deg); }
22+
50% { transform: rotate(180deg); }
23+
75% { transform: rotate(360deg); }
24+
100% { transform: rotate(360deg); }
25+
}
26+
</style>
1027
</head>
1128
<body>
1229
<script language="javascript" type="text/javascript" src="js/jquery.min.js"></script>
1330
<script language="javascript" type="text/javascript" src="js/bootstrap.min.js"></script>
1431
<script language="javascript" type="text/javascript" src="js/d3.v4.min.js"></script>
1532
<script language="javascript" type="text/javascript" src="js/d3-tip-0.8.0-alpha.1.js"></script>
1633
<script language="javascript" type="text/javascript" src="js/d3-gantt.js"></script>
34+
<script language="javascript" type="text/javascript" src="js/query-clickhouse.js"></script>
1735

1836
<nav class="navbar navbar-default">
1937
<div class="container-fluid">
@@ -36,16 +54,77 @@
3654
<div class="modal-dialog" role="document">
3755
<div class="modal-content">
3856
<div class="modal-header">
39-
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
40-
aria-hidden="true">&times;</span></button>
41-
<h4 class="modal-title" id="loadModalLabel">Load Trace JSON</h4>
57+
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
58+
<span aria-hidden="true">&times;</span>
59+
</button>
60+
<h4 class="modal-title" id="loadModalLabel">Load Data</h4>
4261
</div>
4362
<div class="modal-body">
44-
<input type="file" id="loadFiles" value="Load" /><br />
63+
<!-- Nav tabs -->
64+
<ul class="nav nav-tabs" role="tablist">
65+
<li class="active">
66+
<a href="#tabFileUpload" role="tab" data-toggle="tab">Upload JSON</a>
67+
</li>
68+
<li>
69+
<a href="#tabClickhouse" role="tab" data-toggle="tab">From ClickHouse</a>
70+
</li>
71+
</ul>
72+
73+
<!-- Tab panes -->
74+
<div class="tab-content" style="margin-top: 15px;">
75+
<!-- File Upload Tab -->
76+
<div class="tab-pane active" id="tabFileUpload">
77+
<input type="file" id="loadFiles" value="Load" />
78+
</div>
79+
80+
<!-- ClickHouse Query Tab -->
81+
<div class="tab-pane" id="tabClickhouse">
82+
<form id="params">
83+
<div id="connection-params" style="margin-bottom: 10px;">
84+
<input spellcheck="false" id="url" type="text" value="http://localhost:8123" placeholder="URL" class="form-control" style="margin-bottom: 5px;" />
85+
<input spellcheck="false" id="user" type="text" value="" placeholder="user" class="form-control" style="margin-bottom: 5px;" />
86+
<input spellcheck="false" id="password" type="password" placeholder="password" value="" class="form-control" style="margin-bottom: 5px;" />
87+
</div>
88+
<textarea spellcheck="false" id="query" rows="26" class="form-control" placeholder="SQL Query">WITH '__PUT_YOUR_QUERY_ID_HERE__' AS my_query_id
89+
SELECT
90+
('thread #' || leftPad(attribute['clickhouse.thread_id'], 6, '0')) AS group,
91+
replaceRegexpOne(operation_name, '(.*)_.*', '\\1') AS operation_name,
92+
start_time_us,
93+
finish_time_us,
94+
sipHash64(operation_name) AS color,
95+
attribute
96+
FROM system.opentelemetry_span_log
97+
WHERE 1
98+
AND trace_id IN (
99+
SELECT trace_id
100+
FROM system.opentelemetry_span_log
101+
WHERE (attribute['clickhouse.query_id']) IN (SELECT query_id FROM system.query_log WHERE initial_query_id = my_query_id)
102+
)
103+
AND operation_name !='query'
104+
AND operation_name NOT LIKE '%Pipeline%'
105+
AND operation_name NOT LIKE 'TCPHandler%'
106+
AND operation_name NOT LIKE 'Query%'
107+
ORDER BY
108+
group ASC,
109+
parent_span_id ASC,
110+
start_time_us ASC
111+
SETTINGS output_format_json_named_tuples_as_objects = 1, skip_unavailable_shards = 1</textarea>
112+
<div class="checkbox" style="margin-top: 10px;">
113+
<label>
114+
<input type="checkbox" id="flushLogsCheckbox" checked> Run SYSTEM FLUSH LOGS
115+
</label>
116+
</div>
117+
<div id="query-error" style="color: red; margin-top: 10px; display: none;"></div>
118+
</form>
119+
</div>
120+
</div>
45121
</div>
122+
46123
<div class="modal-footer">
47124
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
48125
<button type="button" class="btn btn-primary" id="btnDoLoad">Load</button>
126+
<button type="button" class="btn btn-success" id="btnQueryClickhouse" style="display: none;">Query</button>
127+
<div id="hourglass"></div>
49128
</div>
50129
</div>
51130
</div>
@@ -163,6 +242,71 @@ <h4 class="modal-title" id="findModalLabel">Span Filter</h4>
163242
}
164243
}
165244

245+
// Show correct button depending on tab
246+
$('#loadModal').on('shown.bs.modal', function () {
247+
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
248+
if ($(e.target).attr('href') === '#tabClickhouse') {
249+
$('#btnDoLoad').hide();
250+
$('#btnQueryClickhouse').show();
251+
} else {
252+
$('#btnDoLoad').show();
253+
$('#btnQueryClickhouse').hide();
254+
}
255+
});
256+
});
257+
258+
// Handle ClickHouse query
259+
let query_controller = null;
260+
$('#btnQueryClickhouse').on('click', async function () {
261+
if (query_controller == null) {
262+
let rows = [];
263+
let error = '';
264+
const errorDiv = document.getElementById('query-error');
265+
errorDiv.style.display = 'none';
266+
errorDiv.textContent = '';
267+
document.getElementById('hourglass').style.display = 'inline-block';
268+
$('#btnQueryClickhouse').text("Stop");
269+
if ($('#flushLogsCheckbox').is(':checked')) {
270+
query_controller = new AbortController();
271+
await queryClickHouse({
272+
host: $('#url').val(),
273+
user: $('#user').val(),
274+
password: $('#password').val(),
275+
query: "SYSTEM FLUSH LOGS",
276+
for_each_row: (data) => {},
277+
on_error: (errorMsg) => error = errorMsg,
278+
controller: query_controller
279+
});
280+
}
281+
if (error == '') {
282+
query_controller = new AbortController();
283+
await queryClickHouse({
284+
host: $('#url').val(),
285+
user: $('#user').val(),
286+
password: $('#password').val(),
287+
query: $('#query').val(),
288+
for_each_row: (data) => rows.push(data),
289+
on_error: (errorMsg) => error = errorMsg,
290+
controller: query_controller
291+
});
292+
}
293+
query_controller = null;
294+
$('#btnQueryClickhouse').text("Query");
295+
document.getElementById('hourglass').style.display = 'none';
296+
297+
if (error != '') {
298+
errorDiv.textContent = error;
299+
errorDiv.style.display = 'block';
300+
} else {
301+
renderChart(parseClickHouseTrace(rows));
302+
$('#loadModal').modal('hide');
303+
}
304+
} else { // Cancel query
305+
query_controller.abort();
306+
}
307+
});
308+
309+
// Handle uploaded JSON
166310
$("#btnDoLoad").click(function(){
167311
let element = document.getElementById('loadFiles');
168312
let files = element.files;
@@ -172,7 +316,7 @@ <h4 class="modal-title" id="findModalLabel">Span Filter</h4>
172316
let fr = new FileReader();
173317
fr.onload = function(e) {
174318
$("#errmsg").hide();
175-
renderChart(parseClickHouseTrace(JSON.parse(e.target.result)));
319+
renderChart(parseClickHouseTrace(JSON.parse(e.target.result).data));
176320
}
177321
fr.readAsText(files.item(0));
178322
element.value = '';
@@ -197,10 +341,10 @@ <h4 class="modal-title" id="findModalLabel">Span Filter</h4>
197341
doFindPattern();
198342
});
199343

200-
function parseClickHouseTrace(json) {
344+
function parseClickHouseTrace(rows) {
201345
let min_time_us = Number.MAX_VALUE;
202-
for (let i = 0; i < json.data.length; i++) {
203-
let span = json.data[i];
346+
for (let i = 0; i < rows.length; i++) {
347+
let span = rows[i];
204348
min_time_us = Math.min(min_time_us, +span.start_time_us);
205349
}
206350

@@ -226,8 +370,8 @@ <h4 class="modal-title" id="findModalLabel">Span Filter</h4>
226370

227371
let result = [];
228372
let bands = new Set();
229-
for (let i = 0; i < json.data.length; i++) {
230-
let span = json.data[i];
373+
for (let i = 0; i < rows.length; i++) {
374+
let span = rows[i];
231375
let band = Object.values(span.group).join(' ');
232376
bands.add(band);
233377
result.push({
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
let add_http_cors_header = (location.protocol != 'file:');
2+
3+
async function queryClickHouse({host, user, password, query, is_stopping, for_each_row, on_error, controller}) {
4+
// Construct URL
5+
let url = `${host}?default_format=JSONEachRow&enable_http_compression=1`
6+
if (add_http_cors_header)
7+
// For debug purposes, you may set add_http_cors_header from the browser console
8+
url += '&add_http_cors_header=1';
9+
if (user)
10+
url += `&user=${encodeURIComponent(user)}`;
11+
if (password)
12+
url += `&password=${encodeURIComponent(password)}`;
13+
14+
console.log("QUERY", query);
15+
16+
let response, reply, error;
17+
try {
18+
// Send the query
19+
response = await fetch(url, {
20+
method: "POST",
21+
body: query,
22+
signal: controller.signal,
23+
headers: { 'Authorization': 'never' }
24+
});
25+
26+
if (!response.ok) {
27+
const reply = await response.text();
28+
console.log(reply);
29+
for (line of reply.split('\n')) {
30+
if (line.startsWith(`{"exception":`)) {
31+
throw new Error(`HTTP Status: ${response.status}. Error: ${JSON.parse(line).exception}`);
32+
}
33+
}
34+
throw new Error(`HTTP Status: ${response.status}. Error: ${reply.toString()}`);
35+
}
36+
37+
// Initiate stream processing of response body
38+
const reader = response.body.getReader();
39+
const decoder = new TextDecoder();
40+
41+
// Read data row by row
42+
let buffer = '';
43+
while (true) {
44+
const { done, value } = await reader.read();
45+
if (done)
46+
break;
47+
if (is_stopping && is_stopping())
48+
break;
49+
50+
buffer += decoder.decode(value, { stream: true });
51+
let lines = buffer.split('\n');
52+
for (const line of lines.slice(0, -1)) {
53+
if (is_stopping && is_stopping())
54+
break;
55+
const data = JSON.parse(line);
56+
await for_each_row(data);
57+
}
58+
buffer = lines[lines.length - 1];
59+
}
60+
} catch (e) {
61+
console.log("CLICKHOUSE QUERY FAILED", e);
62+
if (on_error) {
63+
if (e instanceof TypeError) {
64+
on_error("Network error");
65+
} else if (e.name === 'AbortError') {
66+
on_error("Query was cancelled");
67+
} else {
68+
on_error(e.toString());
69+
}
70+
}
71+
}
72+
}

0 commit comments

Comments
 (0)