Merge pull request #38169 from ClickHouse/trace-visualizer

improve trace-visualizer UX
This commit is contained in:
Sergei Trifonov 2022-06-20 12:05:38 +02:00 committed by GitHub
commit c0a275984f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 126 additions and 49 deletions

View File

@ -1,17 +1,12 @@
Trace visualizer is a tool for representation of a tracing data as a Gantt diagram. Trace visualizer is a tool for representation of a tracing data as a Gantt diagram.
# Quick start # Quick start
For now this tool is not integrated into ClickHouse and requires a lot of manual adjustments. For now this tool is not integrated into ClickHouse and requires manual actions. Open `trace-visualizer/index.html` in your browser. It will show an example of data. To visualize your data click `Load` button and select your trace data JSON file.
```bash
cd utils/trace-visualizer
python3 -m http.server
```
Open [localhost](http://localhost:8000). It will show an example of data. To show your tracing data you have to put it in JSON format near `index.html` and change call to `fetchData()` function at the bottom of `index.html`. (Do not forget to disable browser caching while changing it).
# Visualizing query trace # Visualizing query trace
First of all [opentelemetry_span_log](https://clickhouse.com/docs/en/operations/opentelemetry/) system table must be enabled to save query traces. Then run a query you want to trace with a setting: First of all [opentelemetry_span_log](https://clickhouse.com/docs/en/operations/opentelemetry/) system table must be enabled to save query traces. Then run a query you want to trace with a setting:
```sql ```sql
set opentelemetry_start_trace_probability=1; SET opentelemetry_start_trace_probability=1;
SELECT 1; SELECT 1;
``` ```
@ -22,10 +17,9 @@ SELECT DISTINCT trace_id FROM system.opentelemetry_span_log ORDER BY query_start
To obtain JSON data suitable for visualizing run: To obtain JSON data suitable for visualizing run:
```sql ```sql
SELECT tuple (parent_span_id, attribute['clickhouse.thread_id'] || attribute['thread_number'] as thread_id)::Tuple(parent_span_id UInt64, thread_id String) as group, operation_name, start_time_us, finish_time_us, sipHash64(operation_name) as color, attribute SELECT tuple (leftPad(attribute['clickhouse.thread_id'] || attribute['thread_number'], 10, '0') as thread_id, parent_span_id)::Tuple(thread_id String, parent_span_id UInt64) as group, operation_name, start_time_us, finish_time_us, sipHash64(operation_name) as color, attribute
from system.opentelemetry_span_log FROM system.opentelemetry_span_log
WHERE trace_id = 'your-trace-id' WHERE trace_id = 'your-trace-id'
ORDER BY group ASC
FORMAT JSON SETTINGS output_format_json_named_tuples_as_objects = 1; FORMAT JSON SETTINGS output_format_json_named_tuples_as_objects = 1;
``` ```

View File

@ -8,7 +8,6 @@
} }
rect.zoom-panel { rect.zoom-panel {
/*cursor: ew-resize;*/
fill: none; fill: none;
pointer-events: all; pointer-events: all;
} }
@ -20,18 +19,18 @@ rect.zoom-panel {
} }
.axis.y { .axis.y {
font-size: 16px; font-size: 9px;
cursor: ns-resize; cursor: ns-resize;
} }
.axis.x { .axis.x {
font-size: 16px; font-size: 9px;
} }
#ruler { #ruler {
text-anchor: middle; text-anchor: middle;
alignment-baseline: before-edge; alignment-baseline: before-edge;
font-size: 16px; font-size: 9px;
font-family: sans-serif; font-family: sans-serif;
pointer-events: none; pointer-events: none;
} }

View File

@ -14,26 +14,77 @@
<script language="javascript" type="text/javascript" src="js/d3.v4.min.js"></script> <script language="javascript" type="text/javascript" src="js/d3.v4.min.js"></script>
<script language="javascript" type="text/javascript" src="js/d3-tip-0.8.0-alpha.1.js"></script> <script language="javascript" type="text/javascript" src="js/d3-tip-0.8.0-alpha.1.js"></script>
<script language="javascript" type="text/javascript" src="js/d3-gantt.js"></script> <script language="javascript" type="text/javascript" src="js/d3-gantt.js"></script>
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="#">ClickHouse trace-visualizer</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<form class="navbar-form navbar-left">
<button type="button" class="btn btn-primary" id="toolbar-load" data-toggle="modal" data-target="#loadModal">Load</button>
</form>
</div>
</div>
</nav>
<div id="placeholder" class="chart-placeholder"></div> <div id="placeholder" class="chart-placeholder"></div>
<div class="modal fade" id="loadModal" tabindex="-1" role="dialog" aria-labelledby="loadModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="loadModalLabel">Load Trace JSON</h4>
</div>
<div class="modal-body">
<input type="file" id="loadFiles" value="Load" /><br />
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="btnDoLoad">Load</button>
</div>
</div>
</div>
</div>
</body> </body>
</html> </html>
<script language="javascript"> <script language="javascript">
var example_json = [ var example_json = [];
{ t1: 100, t2: 200, band: "band1", color: "#888", text: "text1" }, var example_colors = ["#f88", "#8f8", "#88f", "#888", "#aaa"];
{ t1: 300, t2: 400, band: "band2", color: "#ff8", text: "text2" }, for (let i = 0; i < 1024; i++) {
{ t1: 100, t2: 400, band: "band3", color: "#888", text: "some very long text with a lot of letters in it" }, example_json.push({
{ t1: 300, t2: 400, band: "band1", color: "#8ff", text: "some_very_long_identifier_with_a_lot_of_letters_in_it" }, t1: i + 50 - Math.random() * 60,
{ t1: 500, t2: 800, band: "band2", color: "#f8f", text: "test\nif\nnew\nline\nworks\nhere?" } t2: i + 50 + Math.random() * 60,
]; band: "band" + (i % 128),
color: example_colors[i % example_colors.length],
var chart = d3.gantt() text: "it is span #" + i
.height(window.innerHeight - $("#placeholder")[0].getBoundingClientRect().y - window.scrollY) });
.selector("#placeholder"); }
example_json.bands = new Set(example_json.map(x => x.band));
example_json.max_time_ms = Math.max(...example_json.map(x => x.t2));
var data = null; var data = null;
var chart = null;
function renderChart(parsed) {
data = parsed;
if (chart != null) {
chart.destroy();
}
let view_height = window.innerHeight - $("#placeholder")[0].getBoundingClientRect().y - 40;
let data_height = parsed.bands.size * 8;
chart = d3.gantt()
.height(Math.max(view_height, data_height))
.view_height(view_height)
.selector("#placeholder")
.timeDomain([0, parsed.max_time_ms])
;
chart(data);
}
// Error message popup
$("<div id='errmsg'></div>").css({ $("<div id='errmsg'></div>").css({
position: "absolute", position: "absolute",
display: "none", display: "none",
@ -46,8 +97,7 @@
function fetchData(dataurl, parser = x => x) { function fetchData(dataurl, parser = x => x) {
function onDataReceived(json, textStatus, xhr) { function onDataReceived(json, textStatus, xhr) {
$("#errmsg").hide(); $("#errmsg").hide();
data = parser(json); renderChart(parser(json));
chart(data);
} }
function onDataError(xhr, error) { function onDataError(xhr, error) {
@ -70,6 +120,22 @@
} }
} }
$("#btnDoLoad").click(function(){
let element = document.getElementById('loadFiles');
let files = element.files;
if (files.length <= 0) {
return false;
}
let fr = new FileReader();
fr.onload = function(e) {
$("#errmsg").hide();
renderChart(parseClickHouseTrace(JSON.parse(e.target.result)));
}
fr.readAsText(files.item(0));
element.value = '';
$('#loadModal').modal('hide');
});
function parseClickHouseTrace(json) { function parseClickHouseTrace(json) {
let min_time_us = Number.MAX_VALUE; let min_time_us = Number.MAX_VALUE;
for (let i = 0; i < json.data.length; i++) { for (let i = 0; i < json.data.length; i++) {
@ -98,21 +164,25 @@
} }
let result = []; let result = [];
let bands = new Set();
for (let i = 0; i < json.data.length; i++) { for (let i = 0; i < json.data.length; i++) {
let span = json.data[i]; let span = json.data[i];
let band = Object.values(span.group).join(' ');
bands.add(band);
result.push({ result.push({
t1: convertTime(+span.start_time_us), t1: convertTime(+span.start_time_us),
t2: convertTime(+span.finish_time_us), t2: convertTime(+span.finish_time_us),
band: Object.values(span.group).join(' '), band,
color: d3.interpolateRainbow((strHash(span.color) % 256) / 256), color: d3.interpolateRainbow((strHash(span.color) % 256) / 256),
text: span.operation_name text: span.operation_name
}); });
} }
chart.timeDomain([0, max_time_ms]); result.bands = bands;
result.max_time_ms = max_time_ms;
return result; return result;
} }
fetchData(); // do not fetch, just draw example_json w/o parsing fetchData(); // do not fetch, just draw example_json w/o parsing
//fetchData("your-traces.json" , parseClickHouseTrace); //fetchData("your-traces.json", parseClickHouseTrace);
</script> </script>

View File

@ -37,6 +37,7 @@
.on("zoom", function() { .on("zoom", function() {
if (tipShown != null) { if (tipShown != null) {
tip.hide(tipShown); tip.hide(tipShown);
tipShown = null;
} }
var tr = d3.event.transform; var tr = d3.event.transform;
xZoomed = tr.rescaleX(x); xZoomed = tr.rescaleX(x);
@ -51,7 +52,7 @@
zoomContainer1.attr("transform", "translate(" + tr.x + ",0) scale(" + tr.k + ",1)"); zoomContainer1.attr("transform", "translate(" + tr.x + ",0) scale(" + tr.k + ",1)");
zoomContainer2.attr("transform", "translate(" + tr.x + ",0) scale(" + tr.k + ",1)"); zoomContainer2.attr("transform", "translate(" + tr.x + ",0) scale(" + tr.k + ",1)");
render(); //render();
}) })
.on("start", function() { .on("start", function() {
zoom.startScreenY = d3.event.sourceEvent.screenY; zoom.startScreenY = d3.event.sourceEvent.screenY;
@ -116,6 +117,12 @@
.call(xAxis) .call(xAxis)
; ;
// ruler should be drawn above x axis and under y axis
ruler = fixedContainer.append("g")
.attr("id", "ruler")
.attr("transform", "translate(0, 0)")
;
// create y axis // create y axis
fixedContainer.append("g") fixedContainer.append("g")
.attr("class", "y axis") .attr("class", "y axis")
@ -186,10 +193,6 @@
; ;
// ruler // ruler
ruler = fixedContainer.append("g")
.attr("id", "ruler")
.attr("transform", "translate(0, 0)")
;
ruler.append("rect") ruler.append("rect")
.attr("id", "ruler-line") .attr("id", "ruler-line")
.attr("x", 0) .attr("x", 0)
@ -218,10 +221,9 @@
// scroll handling // scroll handling
window.onscroll = function myFunction() { window.onscroll = function myFunction() {
documentBodyScrollLeft(document.body.scrollLeft); documentBodyScrollLeft(window.scrollX);
documentBodyScrollTop(document.body.scrollTop); documentBodyScrollTop(window.scrollY);
var scroll = scrollParams(); var scroll = scrollParams();
svgChartContainer svgChartContainer
.attr("transform", "translate(" + margin.left .attr("transform", "translate(" + margin.left
+ ", " + (margin.top + scroll.y1) + ")"); + ", " + (margin.top + scroll.y1) + ")");
@ -282,7 +284,7 @@
.attr("class", "bar") .attr("class", "bar")
.attr("vector-effect", "non-scaling-stroke") .attr("vector-effect", "non-scaling-stroke")
.style("fill", d => d.color) .style("fill", d => d.color)
.on('click', function(d) { .on('mouseover', function(d) {
if (tipShown != d) { if (tipShown != d) {
tipShown = d; tipShown = d;
tip.show(d); tip.show(d);
@ -314,12 +316,11 @@
//.clamp(true); // dosn't work with zoom/pan //.clamp(true); // dosn't work with zoom/pan
xZoomed = x; xZoomed = x;
y = d3.scaleBand() y = d3.scaleBand()
.domain(Object.values(data).map(d => d.band).sort()) .domain([...data.bands])
.rangeRound([0, height - margin.top - margin.bottom]) .range([1, height - margin.top - margin.bottom])
.padding(0.5); .padding(1/8);
xAxis = d3.axisBottom() xAxis = d3.axisBottom()
.scale(x) .scale(x)
//.tickSubdivide(true)
.tickSize(8) .tickSize(8)
.tickPadding(8); .tickPadding(8);
yAxis = d3.axisLeft() yAxis = d3.axisLeft()
@ -331,7 +332,7 @@
var documentBodyScrollLeft = function(value) { var documentBodyScrollLeft = function(value) {
if (!arguments.length) { if (!arguments.length) {
if (documentBodyScrollLeft.value === undefined) { if (documentBodyScrollLeft.value === undefined) {
documentBodyScrollLeft.value = document.body.scrollLeft; documentBodyScrollLeft.value = window.scrollX;
} }
return documentBodyScrollLeft.value; return documentBodyScrollLeft.value;
} else { } else {
@ -343,7 +344,7 @@
var documentBodyScrollTop = function(value) { var documentBodyScrollTop = function(value) {
if (!arguments.length) { if (!arguments.length) {
if (!documentBodyScrollTop.value === undefined) { if (!documentBodyScrollTop.value === undefined) {
documentBodyScrollTop.value = document.body.scrollTop; documentBodyScrollTop.value = window.scrollY;
} }
return documentBodyScrollTop.value; return documentBodyScrollTop.value;
} else { } else {
@ -353,7 +354,7 @@
var scrollParams = function() { var scrollParams = function() {
var y1 = documentBodyScrollTop(); var y1 = documentBodyScrollTop();
var y2 = y1 + window.innerHeight - margin.footer; var y2 = y1 + view_height;
y2 = Math.min(y2, height - margin.top - margin.bottom); y2 = Math.min(y2, height - margin.top - margin.bottom);
var h = y2 - y1; var h = y2 - y1;
return { return {
@ -401,7 +402,7 @@
var textWidth = 10 * posText.length; var textWidth = 10 * posText.length;
ruler.select("#bgrect") ruler.select("#bgrect")
.attr("x", -textWidth/2 - xpadding) .attr("x", -textWidth/2 - xpadding)
.attr("y", positionRuler.bbox.y - ypadding) .attr("y", positionRuler.bbox.y - ypadding + window.scrollY)
.attr("width", textWidth + (xpadding*2)) .attr("width", textWidth + (xpadding*2))
.attr("height", positionRuler.bbox.height + (ypadding*2)) .attr("height", positionRuler.bbox.height + (ypadding*2))
; ;
@ -425,6 +426,13 @@
return gantt; return gantt;
} }
gantt.view_height = function(value) {
if (!arguments.length)
return view_height;
view_height = +value;
return gantt;
}
gantt.selector = function(value) { gantt.selector = function(value) {
if (!arguments.length) if (!arguments.length)
return selector; return selector;
@ -444,12 +452,18 @@
return data; return data;
} }
gantt.destroy = function() {
tip.destroy();
d3.select(selector).selectAll("svg").remove();
}
// constructor // constructor
// Config // Config
var margin = { top: 20, right: 40, bottom: 20, left: 200, footer: 100 }, var margin = { top: 0, right: 30, bottom: 20, left: 150 },
height = document.body.clientHeight - margin.top - margin.bottom - 5, height = document.body.clientHeight - margin.top - margin.bottom - 5,
width = document.body.clientWidth - margin.right - margin.left - 5, width = document.body.clientWidth - margin.right - margin.left - 5,
view_height = window.innerHeight,
selector = 'body', selector = 'body',
timeDomainStart = 0, timeDomainStart = 0,
timeDomainEnd = 1000, timeDomainEnd = 1000,