Duibonduil commited on
Commit
4b33663
·
verified ·
1 Parent(s): e5b6c4c

Upload trace_ui.html

Browse files
aworld/cmd/web/webui/public/trace_ui.html ADDED
@@ -0,0 +1,524 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Trace Viewer V2</title>
8
+ <link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css">
9
+ <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
10
+ <script src="https://unpkg.com/element-plus"></script>
11
+ <script src="https://unpkg.com/@element-plus/icons-vue"></script>
12
+ <script src="https://d3js.org/d3.v7.min.js"></script>
13
+ <style>
14
+ .trace-container {
15
+ display: flex;
16
+ flex-direction: column;
17
+ height: 100vh;
18
+ font-family: 'Helvetica Neue', Arial, sans-serif;
19
+ }
20
+
21
+ .trace-content {
22
+ display: flex;
23
+ flex: 1;
24
+ overflow: hidden;
25
+ }
26
+
27
+ .trace-list {
28
+ width: 30%;
29
+ overflow-y: auto;
30
+ border-right: 1px solid #e6e6e6;
31
+ }
32
+
33
+ .trace-detail {
34
+ width: 70%;
35
+ padding: 20px;
36
+ overflow-y: auto;
37
+ }
38
+
39
+ .timeline {
40
+ height: 120px;
41
+ min-width: 100%;
42
+ background: #f5f5f5;
43
+ padding: 10px;
44
+ border-bottom: 1px solid #e6e6e6;
45
+ }
46
+
47
+ .span-node {
48
+ cursor: pointer;
49
+ padding: 5px 0;
50
+ }
51
+
52
+ .span-node:hover {
53
+ background-color: #f0f7ff;
54
+ }
55
+
56
+ .span-duration {
57
+ color: #666;
58
+ font-size: 12px;
59
+ }
60
+
61
+ .timeline-bg {
62
+ fill: #f8f8f8;
63
+ }
64
+
65
+ .axis--x path {
66
+ stroke: #333;
67
+ stroke-width: 1px;
68
+ }
69
+
70
+ .axis--x line {
71
+ stroke: #ddd;
72
+ }
73
+
74
+ .axis--x text {
75
+ font-size: 12px;
76
+ fill: #333;
77
+ }
78
+
79
+ .timeline-visualization {
80
+ flex: 1;
81
+ padding: 20px;
82
+ background: #f8f8f8;
83
+ border-left: 1px solid #e6e6e6;
84
+ overflow-y: auto;
85
+ position: relative;
86
+ height: 100%;
87
+ }
88
+
89
+ .span-visualization-container {
90
+ position: relative;
91
+ height: 100%;
92
+ margin-top: 40px;
93
+ }
94
+
95
+ .span-visualization {
96
+ height: 20px;
97
+ background: #409EFF;
98
+ position: absolute;
99
+ margin-top: 2px;
100
+ border-radius: 2px;
101
+ }
102
+
103
+ .span-label {
104
+ font-size: 8px;
105
+ color: white;
106
+ padding: 0 5px;
107
+ white-space: nowrap;
108
+ overflow: hidden;
109
+ text-overflow: ellipsis;
110
+ }
111
+
112
+ .trace-timeline {
113
+ background: #f5f5f5;
114
+ padding: 10px;
115
+ border-radius: 4px;
116
+ }
117
+
118
+ .trace-timeline svg {
119
+ display: block;
120
+ }
121
+
122
+ .trace-timeline .axis path {
123
+ stroke: #333;
124
+ stroke-width: 1px;
125
+ }
126
+
127
+ .trace-timeline .axis line {
128
+ stroke: #ddd;
129
+ }
130
+
131
+ .trace-timeline .axis text {
132
+ font-size: 12px;
133
+ fill: #333;
134
+ }
135
+ </style>
136
+ </head>
137
+
138
+ <body>
139
+ <div id="app" class="trace-container">
140
+ <!-- Top timeline -->
141
+ <div class="timeline">
142
+ <div id="timeline-chart"></div>
143
+ </div>
144
+ <div class="trace-content">
145
+ <div class="trace-list">
146
+ <div style="padding: 10px; border-bottom: 1px solid #e6e6e6;">
147
+ <el-input v-model="searchTraceId" placeholder="输入Trace ID搜索" style="width: 100%;"
148
+ @keyup.enter="searchByTraceId">
149
+ <template #append>
150
+ <el-button @click="searchByTraceId">
151
+ <el-icon>
152
+ <search />
153
+ </el-icon>
154
+ </el-button>
155
+ </template>
156
+ </el-input>
157
+ </div>
158
+ <el-tree :data="traceTree" node-key="span_id" :props="treeProps" :expand-on-click-node="false"
159
+ @node-click="handleNodeClick" :default-expanded-keys="expandedNodes">
160
+ <template #default="{ node, data }">
161
+ <span class="span-node">
162
+ {{ data.name }}
163
+ <span class="span-duration">({{ data.duration_ms.toFixed(2) }}ms)</span>
164
+ </span>
165
+ </template>
166
+ </el-tree>
167
+ </div>
168
+
169
+ <div class="timeline-visualization" v-if="selectedSpan" v-html="renderTimelineVisualization()">
170
+ </div>
171
+ </div>
172
+ <!-- Span detail -->
173
+ <el-dialog v-model="dialogVisible" title="Span Details" width="70%">
174
+ <el-descriptions :column="2" border>
175
+ <el-descriptions-item label="Trace ID">{{ selectedSpan.trace_id }}</el-descriptions-item>
176
+ <el-descriptions-item label="Span ID">{{ selectedSpan.span_id }}</el-descriptions-item>
177
+ <el-descriptions-item label="Parent Span ID">{{ selectedSpan.parent_id || 'None'
178
+ }}</el-descriptions-item>
179
+ <el-descriptions-item label="Name">{{ selectedSpan.name }}</el-descriptions-item>
180
+ <el-descriptions-item label="Status">
181
+ <div style="display: flex; justify-content: space-between; align-items: center;">
182
+ <span :style="{color: selectedSpan.status.code === 'StatusCode.ERROR' ? '#F56C6C' : ''}">
183
+ {{ selectedSpan.status.code }}
184
+ </span>
185
+ <el-button v-if="selectedSpan.status.code === 'StatusCode.ERROR'" type="text" size="small"
186
+ @click="showStacktrace = true" icon="View" style="color: #F56C6C">
187
+ View Stack
188
+ </el-button>
189
+ </div>
190
+ </el-descriptions-item>
191
+ <el-descriptions-item label="Start Time">{{ selectedSpan.start_time}}</el-descriptions-item>
192
+ <el-descriptions-item label="End Time">{{ selectedSpan.end_time }}</el-descriptions-item>
193
+ <el-descriptions-item label="Duration">{{ selectedSpan.duration_ms.toFixed(2) }}
194
+ ms</el-descriptions-item>
195
+ </el-descriptions>
196
+ <el-card style="margin-top: 20px;">
197
+ <template #header>
198
+ <h4>Attributes</h4>
199
+ </template>
200
+ <pre style="
201
+ max-height: 400px;
202
+ overflow: auto;
203
+ white-space: pre-wrap;
204
+ word-break: break-all;
205
+ background: #f8f8f8;
206
+ padding: 10px;
207
+ border-radius: 4px;
208
+ ">{{ formatAttributes(selectedSpan.attributes) }}</pre>
209
+ </el-card>
210
+ </el-dialog>
211
+ <el-dialog v-model="showStacktrace" title="Stacktrace Details" width="70%">
212
+ <pre>{{ formatStacktrace(selectedSpan.attributes?.['exception.stacktrace'] || "No stacktrace available") }}</pre>
213
+ </el-dialog>
214
+ </div>
215
+
216
+ <script>
217
+ const { createApp, ref, onMounted, nextTick } = Vue;
218
+ const { Search } = ElementPlusIconsVue;
219
+ createApp({
220
+ setup() {
221
+ const traces = ref([]);
222
+ const traceTree = ref([]);
223
+ const selectedSpan = ref(null);
224
+ const expandedNodes = ref([]);
225
+ const searchTraceId = ref('');
226
+ const showStacktrace = ref(false);
227
+
228
+ const treeProps = {
229
+ label: 'name',
230
+ children: 'children'
231
+ };
232
+ const dialogVisible = ref(false);
233
+
234
+ function searchByTraceId() {
235
+ if (!searchTraceId.value) {
236
+ buildTraceTree();
237
+ return;
238
+ }
239
+ const filtered = traces.value.filter(trace =>
240
+ trace.trace_id.includes(searchTraceId.value)
241
+ );
242
+
243
+ const tree = [];
244
+ filtered.forEach(trace => {
245
+ if (trace.root_span && trace.root_span.length > 0) {
246
+ const root = buildSpanTree(trace.root_span[0]);
247
+ tree.push(root);
248
+ }
249
+ });
250
+ traceTree.value = tree;
251
+ }
252
+
253
+ function initTimeline() {
254
+ const timelineContainer = document.getElementById('timeline-chart');
255
+ const width = timelineContainer.clientWidth;
256
+ const height = 100;
257
+ const margin = { top: 20, right: 20, bottom: 30, left: 20 };
258
+
259
+ const svg = d3.select(timelineContainer)
260
+ .append('svg')
261
+ .attr('width', width)
262
+ .attr('height', height);
263
+
264
+ const now = new Date();
265
+ const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
266
+
267
+ const x = d3.scaleTime()
268
+ .domain([oneDayAgo, now])
269
+ .range([margin.left, width - margin.right]);
270
+
271
+ svg.append('g')
272
+ .attr('transform', `translate(0,${height - margin.bottom})`)
273
+ .call(d3.axisBottom(x)
274
+ .ticks(d3.timeHour.every(2))
275
+ .tickFormat(d3.timeFormat("%H:%M")));
276
+
277
+ svg.append('g')
278
+ .attr('class', 'grid')
279
+ .attr('transform', `translate(0,${height - margin.bottom})`)
280
+ .call(d3.axisBottom(x)
281
+ .ticks(d3.timeMinute.every(10))
282
+ .tickSize(-5)
283
+ .tickFormat(''));
284
+
285
+ if (traces.value && traces.value.length > 0) {
286
+ const colorScale = d3.scaleOrdinal()
287
+ .domain(traces.value.map((_, i) => i))
288
+ .range(d3.schemeCategory10);
289
+ traces.value.forEach((trace, index) => {
290
+ if (trace.root_span && trace.root_span.length > 0) {
291
+ const span = trace.root_span[0];
292
+ const startTime = new Date(span.start_time);
293
+ const endTime = new Date(span.end_time);
294
+ const duration = endTime - startTime;
295
+
296
+ if (startTime >= oneDayAgo && startTime <= now) {
297
+ svg.append('rect')
298
+ .attr('x', x(startTime))
299
+ .attr('y', margin.top + 30)
300
+ .attr('width', Math.max(3, x(endTime) - x(startTime)))
301
+ .attr('height', 20)
302
+ .attr('fill', colorScale(index))
303
+ .attr('rx', 2)
304
+ .attr('opacity', 0.7)
305
+ .on('mouseover', function () {
306
+ d3.select(this).attr('opacity', 1);
307
+ })
308
+ .on('mouseout', function () {
309
+ d3.select(this).attr('opacity', 0.7);
310
+ });
311
+ }
312
+ }
313
+ });
314
+ }
315
+ }
316
+
317
+
318
+ function renderTimelineVisualization() {
319
+ if (!selectedSpan.value) return '';
320
+
321
+ const currentTrace = traceTree.value.find(t => t.trace_id === selectedSpan.value.trace_id);
322
+ if (!currentTrace) return '';
323
+
324
+ const rootSpan = currentTrace.root_span?.[0] || currentTrace;
325
+ let minTime = new Date(rootSpan.start_time).getTime();
326
+ let maxTime = new Date(rootSpan.end_time).getTime();
327
+
328
+ const timelineContainer = document.createElement('div');
329
+ timelineContainer.className = 'trace-timeline';
330
+ timelineContainer.style.height = '60px';
331
+ timelineContainer.style.marginBottom = '20px';
332
+ timelineContainer.style.width = '100%';
333
+
334
+ const svg = d3.select(timelineContainer)
335
+ .append('svg')
336
+ .attr('width', '100%')
337
+ .attr('height', '100%')
338
+ .attr('viewBox', '0 0 1000 60');
339
+
340
+ const margin = { top: 10, right: 0, bottom: 30, left: 0 };
341
+ const width = 1000 - margin.left - margin.right;
342
+ const height = 60 - margin.top - margin.bottom;
343
+
344
+ const g = svg.append('g')
345
+ .attr('transform', `translate(${margin.left},${margin.top})`);
346
+
347
+
348
+ const x = d3.scaleTime()
349
+ .domain([new Date(minTime), new Date(maxTime)])
350
+ .range([0, width]);
351
+
352
+ g.append('g')
353
+ .attr('class', 'axis axis--x')
354
+ .attr('transform', `translate(0,${height})`)
355
+ .call(d3.axisBottom(x)
356
+ .ticks(5)
357
+ .tickFormat(d3.timeFormat("%H:%M:%S.%L")));
358
+
359
+ g.selectAll(".grid-line")
360
+ .data(x.ticks(5))
361
+ .enter().append("line")
362
+ .attr("class", "grid-line")
363
+ .attr("x1", d => x(d))
364
+ .attr("x2", d => x(d))
365
+ .attr("y1", 0)
366
+ .attr("y2", height)
367
+ .attr("stroke", "#eee")
368
+ .attr("stroke-width", 1);
369
+
370
+ const timelineHtml = timelineContainer.outerHTML;
371
+
372
+ function renderSpans(span, depth = 0, rowIndex = 0) {
373
+ const spanStart = new Date(span.start_time).getTime();
374
+ const spanEnd = new Date(span.end_time).getTime();
375
+ const position = ((spanStart - minTime) / (maxTime - minTime)) * 10 * 0.9 + 5;
376
+ const width = ((spanEnd - spanStart) / (maxTime - minTime)) * 100 * 0.9 + 2;
377
+
378
+ const minWidth = 0.5;
379
+ const adjustedWidth = Math.max(width, minWidth);
380
+
381
+ const row = rowIndex * 24;
382
+
383
+ let childrenHtml = '';
384
+ let nextRowIndex = rowIndex + 1;
385
+
386
+ if (span.children && span.children.length > 0) {
387
+ childrenHtml = span.children.map(child => {
388
+ const childHtml = renderSpans(child, depth + 1, nextRowIndex);
389
+ nextRowIndex += countSpans(child);
390
+ return childHtml;
391
+ }).join('');
392
+ }
393
+ return `
394
+ <div class="span-visualization"
395
+ style="top: ${row}px;
396
+ left: ${position}%;
397
+ width: ${adjustedWidth}%;
398
+ background: ${span.status.code === 'StatusCode.ERROR' ? '#F56C6C' : '#409EFF'};
399
+ opacity: ${span.span_id === selectedSpan.value.span_id ? 1 : 0.6}"
400
+ onclick="window.handleSpanClick.call(this, ${JSON.stringify(span).replace(/"/g, '&quot;')})">
401
+ <span class="span-label">${span.duration_ms} ${span.name}</span>
402
+ </div>
403
+ ${childrenHtml}
404
+ `;
405
+ }
406
+
407
+ return `
408
+ <h3>Timeline Visualization</h3>
409
+ ${timelineHtml}
410
+ <div class="span-visualization-container" style="height: ${traceTree.value.length * 24 + 100}px">
411
+ ${renderSpans(rootSpan)}
412
+ </div>
413
+ `;
414
+ }
415
+
416
+ function countSpans(span) {
417
+ let count = 1;
418
+ if (span.children && span.children.length > 0) {
419
+ span.children.forEach(child => {
420
+ count += countSpans(child);
421
+ });
422
+ }
423
+ return count;
424
+ }
425
+
426
+ async function fetchTraces() {
427
+ try {
428
+ const response = await fetch('/api/trace/list');
429
+ const data = await response.json();
430
+ traces.value = data.data;
431
+ buildTraceTree();
432
+ initTimeline();
433
+ } catch (error) {
434
+ console.error('Error loading traces:', error);
435
+ }
436
+ }
437
+
438
+ function buildTraceTree() {
439
+ const tree = [];
440
+ traces.value.forEach(trace => {
441
+ if (trace.root_span && trace.root_span.length > 0) {
442
+ const root = buildSpanTree(trace.root_span[0]);
443
+ tree.push(root);
444
+ }
445
+ });
446
+ traceTree.value = tree;
447
+ }
448
+
449
+ function buildSpanTree(span) {
450
+ const node = {
451
+ ...span,
452
+ children: []
453
+ };
454
+
455
+ if (span.children && span.children.length > 0) {
456
+ span.children.forEach(child => {
457
+ node.children.push(buildSpanTree(child));
458
+ });
459
+ }
460
+
461
+ return node;
462
+ }
463
+
464
+ function handleNodeClick(data) {
465
+ selectedSpan.value = data;
466
+ nextTick(() => {
467
+ renderTimelineVisualization();
468
+ });
469
+ }
470
+
471
+ function handleSpanClick(data) {
472
+ selectedSpan.value = data;
473
+ dialogVisible.value = true;
474
+ if (!expandedNodes.value.includes(data.span_id)) {
475
+ expandedNodes.value.push(data.span_id);
476
+ }
477
+ }
478
+
479
+ function formatTime(timestamp) {
480
+ return timestamp.split('.')[0];
481
+ }
482
+
483
+ function formatAttributes(attrs) {
484
+ return JSON.stringify(attrs, null, 2);
485
+ }
486
+
487
+ function formatStacktrace(stacktrace) {
488
+ if (!stacktrace) return 'No stacktrace available';
489
+ try {
490
+ return JSON.stringify(JSON.parse(stacktrace), null, 2);
491
+ } catch {
492
+ return stacktrace;
493
+ }
494
+ }
495
+
496
+ onMounted(() => {
497
+ fetchTraces();
498
+ //setInterval(fetchTraces, 5000);
499
+ window.handleSpanClick = handleSpanClick;
500
+ });
501
+
502
+ return {
503
+ traces,
504
+ traceTree,
505
+ selectedSpan,
506
+ expandedNodes,
507
+ treeProps,
508
+ handleNodeClick,
509
+ handleSpanClick,
510
+ formatTime,
511
+ formatAttributes,
512
+ dialogVisible,
513
+ renderTimelineVisualization,
514
+ searchTraceId,
515
+ searchByTraceId,
516
+ showStacktrace,
517
+ formatStacktrace
518
+ };
519
+ }
520
+ }).use(ElementPlus).component('search', Search).mount('#app');
521
+ </script>
522
+ </body>
523
+
524
+ </html>