feat: adapt new trace protocol and implement new trace view (#499)

This commit is contained in:
Fine0830
2025-09-28 19:01:23 +08:00
committed by GitHub
parent 730515e304
commit dd90ab5ea7
52 changed files with 2889 additions and 937 deletions

View File

@@ -0,0 +1,17 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<svg t="1758874892311" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path d="M731.428571 341.333333h73.142858a73.142857 73.142857 0 0 1 73.142857 73.142857v414.476191a73.142857 73.142857 0 0 1-73.142857 73.142857H219.428571a73.142857 73.142857 0 0 1-73.142857-73.142857V414.47619a73.142857 73.142857 0 0 1 73.142857-73.142857h73.142858v73.142857H219.428571v414.476191h585.142858V414.47619h-73.142858v-73.142857z m-176.90819-242.590476l0.048762 397.092572 84.577524-84.601905 51.687619 51.712-172.373334 172.397714-172.397714-172.373333 51.712-51.736381 83.626667 83.626666V98.742857h73.142857z" p-id="4697"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -310,6 +310,9 @@ limitations under the License. -->
break;
}
dates.value = [start, end];
if (!props.showButtons) {
ok(true);
}
};
const submit = () => {
inputDates.value = dates.value;

View File

@@ -163,3 +163,73 @@ export const TraceSpansFromColdStage = {
}
`,
};
export const HasQueryTracesV2Support = {
query: `
hasQueryTracesV2Support
`,
};
export const QueryV2Traces = {
variable: "$condition: TraceQueryCondition",
query: `
queryTraces(condition: $condition) {
traces {
spans {
traceId
segmentId
spanId
parentSpanId
refs {
traceId
parentSegmentId
parentSpanId
type
}
serviceCode
serviceInstanceName
startTime
endTime
endpointName
type
peer
component
isError
layer
tags {
key
value
}
logs {
time
data {
key
value
}
}
attachedEvents {
startTime {
seconds
nanos
}
event
endTime {
seconds
nanos
}
tags {
key
value
}
summary {
key
value
}
}
}
}
retrievedTimeRange {
startTime
endTime
}
}`,
};

View File

@@ -17,11 +17,45 @@
import { httpQuery } from "../base";
import { HttpURL } from "./url";
export default async function fetchQuery({ method, json, path }: { method: string; json?: unknown; path: string }) {
const response = await httpQuery({
export default async function fetchQuery({
method,
json,
url: (HttpURL as { [key: string]: string })[path],
path,
}: {
method: string;
json?: Record<string, unknown>;
path: string;
}) {
const upperMethod = method.toUpperCase();
let url = (HttpURL as Record<string, string>)[path];
let body: unknown | undefined = json;
if (upperMethod === "GET" && json && typeof json === "object") {
const params = new URLSearchParams();
const stringifyValue = (val: unknown): string => {
if (val instanceof Date) return val.toISOString();
if (typeof val === "object") return JSON.stringify(val);
return String(val);
};
for (const [key, value] of Object.entries(json)) {
if (value === undefined || value === null) continue;
if (Array.isArray(value)) {
for (const v of value as unknown[]) params.append(key, stringifyValue(v));
continue;
}
params.append(key, stringifyValue(value));
}
const queryString = params.toString();
if (queryString) {
url += (url.includes("?") ? "&" : "?") + queryString;
}
body = undefined;
}
const response = await httpQuery({
method: upperMethod,
json: body,
url,
});
if (response.errors) {
response.errors = response.errors.map((e: { message: string }) => e.message).join(" ");

View File

@@ -15,7 +15,15 @@
* limitations under the License.
*/
import { Traces, TraceSpans, TraceTagKeys, TraceTagValues, TraceSpansFromColdStage } from "../fragments/trace";
import {
Traces,
TraceSpans,
TraceTagKeys,
TraceTagValues,
TraceSpansFromColdStage,
HasQueryTracesV2Support,
QueryV2Traces,
} from "../fragments/trace";
export const queryTraces = `query queryTraces(${Traces.variable}) {${Traces.query}}`;
@@ -26,3 +34,7 @@ export const queryTraceTagKeys = `query queryTraceTagKeys(${TraceTagKeys.variabl
export const queryTraceTagValues = `query queryTraceTagValues(${TraceTagValues.variable}) {${TraceTagValues.query}}`;
export const queryTraceSpansFromColdStage = `query queryTraceSpansFromColdStage(${TraceSpansFromColdStage.variable}) {${TraceSpansFromColdStage.query}}`;
export const queryHasQueryTracesV2Support = `query queryHasQueryTracesV2Support {${HasQueryTracesV2Support.query}}`;
export const queryV2Traces = `query queryV2Traces(${QueryV2Traces.variable}) {${QueryV2Traces.query}}`;

View File

@@ -89,6 +89,7 @@ limitations under the License. -->
import router from "@/router";
import { useAppStoreWithOut, InitializationDurationRow } from "@/store/modules/app";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useTraceStore } from "@/store/modules/trace";
import type { DashboardItem } from "@/types/dashboard";
import timeFormat from "@/utils/timeFormat";
import { MetricCatalog } from "@/views/dashboard/data";
@@ -98,10 +99,10 @@ limitations under the License. -->
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
/*global Indexable */
const { t, te } = useI18n();
const appStore = useAppStoreWithOut();
const dashboardStore = useDashboardStore();
const traceStore = useTraceStore();
const route = useRoute();
const pathNames = ref<{ path?: string; name: string; selected: boolean }[][]>([]);
const showTimeRangeTips = ref<boolean>(false);
@@ -124,6 +125,7 @@ limitations under the License. -->
getVersion();
getNavPaths();
setTTL();
traceStore.getHasQueryTracesV2Support();
function changeTheme() {
const root = document.documentElement;
@@ -391,7 +393,7 @@ limitations under the License. -->
}
function resetDuration() {
const { duration }: Indexable = route.params;
const { duration } = route.params as { duration: string };
if (duration) {
const d = JSON.parse(duration);

View File

@@ -17,7 +17,7 @@ limitations under the License. -->
<div :class="isCollapse ? 'logo-icon-collapse' : 'logo-icon'">
<Icon :size="isCollapse ? 'xl' : 'logo'" :iconName="isCollapse ? 'logo' : 'logo-sw'" />
</div>
<div class="menu scroll_bar_dark" :style="isCollapse ? {} : { width: '220px' }">
<div class="menu scroll_bar_style" :style="isCollapse ? {} : { width: '220px' }">
<el-menu
active-text-color="#448dfe"
background-color="#252a2f"

View File

@@ -31,7 +31,7 @@ const msg = {
profiles: "Profiles",
database: "Database",
mySQL: "MySQL/MariaDB",
serviceName: "Service Name",
serviceName: "Service name",
technologies: "Technologies",
health: "Health",
groupName: "Group Name",
@@ -406,5 +406,11 @@ const msg = {
minutes: "Minutes",
invalidProfilingDurationRange: "Please enter a valid duration between 1 and 900 seconds",
taskCreatedSuccessfully: "Task created successfully",
runQuery: "Run Query",
spansTable: "Spans Table",
download: "Download",
totalSpans: "Total Spans",
spanName: "Span name",
parentId: "Parent ID",
};
export default msg;

View File

@@ -406,5 +406,11 @@ const msg = {
minutes: "Minutos",
invalidProfilingDurationRange: "Por favor ingrese una duración válida entre 1 y 900 segundos",
taskCreatedSuccessfully: "Tarea creada exitosamente",
runQuery: "Ejecutar Consulta",
spansTable: "Tabla de Lapso",
download: "Descargar",
totalSpans: "Total Lapso",
spanName: "Nombre de Lapso",
parentId: "ID Padre",
};
export default msg;

View File

@@ -404,5 +404,11 @@ const msg = {
minutes: "分钟",
invalidProfilingDurationRange: "请输入1到900秒之间的有效时长",
taskCreatedSuccessfully: "任务创建成功",
runQuery: "运行查询",
spansTable: "Spans表格",
download: "下载",
totalSpans: "总跨度",
spanName: "跨度名称",
parentId: "父ID",
};
export default msg;

View File

@@ -37,9 +37,15 @@ interface TraceState {
selectorStore: ReturnType<typeof useSelectorStore>;
selectedSpan: Nullable<Span>;
serviceList: string[];
currentSpan: Nullable<Span>;
hasQueryTracesV2Support: boolean;
v2Traces: Trace[];
loading: boolean;
}
const { getDurationTime } = useDuration();
export const PageSize = 20;
export const traceStore = defineStore({
id: "trace",
state: (): TraceState => ({
@@ -54,24 +60,39 @@ export const traceStore = defineStore({
queryDuration: getDurationTime(),
traceState: "ALL",
queryOrder: QueryOrders[0].value,
paging: { pageNum: 1, pageSize: 20 },
paging: { pageNum: 1, pageSize: PageSize },
},
traceSpanLogs: [],
selectorStore: useSelectorStore(),
serviceList: [],
currentSpan: null,
hasQueryTracesV2Support: false,
v2Traces: [],
loading: false,
}),
actions: {
setTraceCondition(data: Recordable) {
this.conditions = { ...this.conditions, ...data };
},
setCurrentTrace(trace: Trace) {
this.currentTrace = trace;
setCurrentTrace(trace: Nullable<Trace>) {
this.currentTrace = trace || {};
},
setTraceSpans(spans: Span[]) {
this.traceSpans = spans;
},
setSelectedSpan(span: Span) {
this.selectedSpan = span;
setSelectedSpan(span: Nullable<Span>) {
this.selectedSpan = span || {};
},
setCurrentSpan(span: Nullable<Span>) {
this.currentSpan = span || {};
},
setV2Spans(traceId: string) {
const trace = this.traceList.find((d: Trace) => d.traceId === traceId);
this.setTraceSpans(trace?.spans || []);
this.serviceList = Array.from(new Set(trace?.spans.map((i: Span) => i.serviceCode)));
},
setTraceList(traces: Trace[]) {
this.traceList = traces;
},
resetState() {
this.traceSpans = [];
@@ -156,14 +177,20 @@ export const traceStore = defineStore({
return response;
},
async getTraces() {
if (this.hasQueryTracesV2Support) {
return this.fetchV2Traces();
}
this.loading = true;
const response = await graphql.query("queryTraces").params({ condition: this.conditions });
if (response.errors) {
this.loading = false;
return response;
}
if (!response.data.data.traces.length) {
this.traceList = [];
this.setCurrentTrace({});
this.setTraceSpans([]);
this.loading = false;
return response;
}
this.getTraceSpans({ traceId: response.data.data.traces[0].traceIds[0] });
@@ -177,8 +204,13 @@ export const traceStore = defineStore({
return response;
},
async getTraceSpans(params: { traceId: string }) {
if (this.hasQueryTracesV2Support) {
this.setV2Spans(params.traceId);
return new Promise((resolve) => resolve({}));
}
const appStore = useAppStoreWithOut();
let response;
this.loading = true;
if (appStore.coldStageMode) {
response = await graphql
.query("queryTraceSpansFromColdStage")
@@ -186,6 +218,7 @@ export const traceStore = defineStore({
} else {
response = await graphql.query("querySpans").params(params);
}
this.loading = false;
if (response.errors) {
return response;
}
@@ -209,6 +242,61 @@ export const traceStore = defineStore({
async getTagValues(tagKey: string) {
return await graphql.query("queryTraceTagValues").params({ tagKey, duration: useAppStoreWithOut().durationTime });
},
async getHasQueryTracesV2Support() {
const response = await graphql.query("queryHasQueryTracesV2Support").params({});
this.hasQueryTracesV2Support = response.data.hasQueryTracesV2Support;
return response;
},
async fetchV2Traces() {
this.loading = true;
const response = await graphql.query("queryV2Traces").params({ condition: this.conditions });
this.loading = false;
if (response.errors) {
this.traceList = [];
this.setCurrentTrace({});
this.setTraceSpans([]);
return response;
}
this.v2Traces = response.data.queryTraces.traces || [];
this.traceList = this.v2Traces
.map((d: Trace) => {
const newSpans = d.spans.map((span: Span) => {
return {
...span,
traceId: span.traceId,
duration: span.endTime - span.startTime,
label: `${span.serviceCode}: ${span.endpointName}`,
};
});
const trace =
newSpans.find((span: Span) => span.parentSpanId === -1 && span.refs.length === 0) || newSpans[0];
return {
endpointNames: trace.endpointName ? [trace.endpointName] : [],
traceIds: trace.traceId ? [{ value: trace.traceId, label: trace.traceId }] : [],
start: trace.startTime,
duration: trace.endTime - trace.startTime,
isError: trace.isError,
spans: newSpans,
traceId: trace.traceId,
key: trace.traceId,
serviceCode: trace.serviceCode,
label: `${trace.serviceCode}: ${trace.endpointName}`,
};
})
.sort((a: Trace, b: Trace) => b.duration - a.duration);
const trace = this.traceList[0];
if (!trace) {
this.traceList = [];
this.setCurrentTrace({});
this.setTraceSpans([]);
return response;
}
this.serviceList = Array.from(new Set(trace.spans.map((i: Span) => i.serviceCode)));
this.setTraceSpans(trace.spans);
this.setCurrentTrace(trace || {});
return response;
},
},
});

View File

@@ -185,13 +185,13 @@
}
.scroll_bar_style::-webkit-scrollbar {
width: 4px;
height: 4px;
background-color: #eee;
width: 5px;
height: 5px;
background-color: var(--sw-scrollbar-track);
}
.scroll_bar_style::-webkit-scrollbar-track {
background-color: #eee;
background-color: var(--sw-scrollbar-track);
border-radius: 3px;
box-shadow: inset 0 0 6px $disabled-color;
}
@@ -199,26 +199,9 @@
.scroll_bar_style::-webkit-scrollbar-thumb {
border-radius: 3px;
box-shadow: inset 0 0 6px $disabled-color;
background-color: #aaa;
background-color: var(--sw-scrollbar-thumb);
}
.scroll_bar_dark::-webkit-scrollbar {
width: 6px;
height: 6px;
background-color: #666;
}
.scroll_bar_dark::-webkit-scrollbar-track {
background-color: #252a2f;
border-radius: 3px;
box-shadow: inset 0 0 6px #999;
}
.scroll_bar_dark::-webkit-scrollbar-thumb {
border-radius: 3px;
box-shadow: inset 0 0 6px #888;
background-color: #999;
}
.d3-tip {
line-height: 1;
padding: 8px;

View File

@@ -71,6 +71,11 @@ html {
--sw-marketplace-border: #dedfe0;
--sw-grid-item-active: #d4d7de;
--sw-trace-line: #999;
--sw-scrollbar-track: #eee;
--sw-scrollbar-thumb: #aaa;
--sw-font-grey-color: #a7aebb;
--sw-trace-list-path: rgba(0, 0, 0, 0.1);
--sw-trace-table-selected: rgba(0, 0, 0, 0.1);
}
html.dark {
@@ -81,7 +86,7 @@ html.dark {
--disabled-color: #999;
--dashboard-tool-bg: #000;
--text-color-placeholder: #ccc;
--border-color: #262629;
--border-color: #333;
--border-color-primary: #4b4b52;
--layout-background: #000;
--box-shadow-color: #606266;
@@ -114,6 +119,11 @@ html.dark {
--sw-marketplace-border: #606266;
--sw-grid-item-active: #73767a;
--sw-trace-line: #e8e8e8;
--sw-scrollbar-track: #252a2f;
--sw-scrollbar-thumb: #888;
--sw-font-grey-color: #a7aebb;
--sw-trace-list-path: rgba(244, 244, 244, 0.4);
--sw-trace-table-selected: rgba(255, 255, 255, 0.1);
}
.el-drawer__header {

View File

@@ -27,6 +27,10 @@ export interface Trace {
spans: Span[];
endpointNames: string[];
traceId: string;
serviceCode: string;
label: string;
id: string;
parentId: string;
}
export interface Span {
@@ -46,6 +50,7 @@ export interface Span {
refs: Array<Ref>;
startTime: number;
endTime: number;
duration?: number;
dur?: number;
children?: Span[];
tags?: { value: string; key: string }[];
@@ -63,6 +68,8 @@ export interface Span {
avgTime?: number;
count?: number;
profiled?: boolean;
startTimeNanos?: number;
event?: string;
}
export type Ref = {
type?: string;

70
src/utils/color.ts Normal file
View File

@@ -0,0 +1,70 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Service color palette for consistent coloring across trace visualizations
export const ServicePalette = [
"#10b981", // emerald-500
"#f59e0b", // amber-500
"#8b5cf6", // violet-500
"#06b6d4", // cyan-500
"#84cc16", // lime-500
"#f97316", // orange-500
"#ec4899", // pink-500
"#6366f1", // indigo-500
"#14b8a6", // teal-500
"#a855f7", // purple-500
"#22c55e", // green-500
"#eab308", // yellow-500
"#f43f5e", // rose-500
"#0ea5e9", // sky-500
"#8b5a2b", // brown-500
"#64748b", // slate-500
"#dc2626", // red-600
"#059669", // emerald-600
"#d97706", // amber-600
"#7c3aed", // violet-600
"#0891b2", // cyan-600
"#65a30d", // lime-600
"#ea580ce6", // orange-600
"#db2777", // pink-600
"#4f46e5", // indigo-600
"#0d9488", // teal-600
"#9333ea", // purple-600
"#16a34a", // green-600
"#ca8a04", // yellow-600
"#e11d48", // rose-600
"#0284c7", // sky-600
"#92400e", // brown-600
"#475569", // slate-600
];
// Reuse the same service color function from TracesTable
function generateHash(str: string) {
let hash = 0;
if (str.length === 0) return hash;
for (let i = 0; i < str.length; i += 1) {
const c = str.charCodeAt(i);
hash = (hash << 5) - hash + c;
hash |= 0; // Convert to 32bit integer
}
return Math.abs(hash); // Only positive number.
}
export function getServiceColor(serviceName: string): string {
if (!serviceName) return "#eee";
const hash = generateHash(serviceName);
return ServicePalette[hash % ServicePalette.length];
}

View File

@@ -31,16 +31,15 @@ export const readFile = (event: any) => {
};
});
};
export const saveFile = (data: any, name: string) => {
const newData = JSON.stringify(data);
const tagA = document.createElement("a");
tagA.download = name;
tagA.style.display = "none";
const blob = new Blob([newData]);
const url = URL.createObjectURL(blob);
tagA.href = url;
document.body.appendChild(tagA);
tagA.click();
document.body.removeChild(tagA);
URL.revokeObjectURL(url);
export const saveFileAsJSON = (data: unknown, filename: string) => {
const jsonContent = JSON.stringify(data, null, 2);
const blob = new Blob([jsonContent], { type: "application/json;charset=utf-8;" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = filename;
link.style.display = "none";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
};

View File

@@ -179,7 +179,7 @@ limitations under the License. -->
import { useDashboardStore } from "@/store/modules/dashboard";
import router from "@/router";
import type { DashboardItem, LayoutConfig } from "@/types/dashboard";
import { saveFile, readFile } from "@/utils/file";
import { saveFileAsJSON, readFile } from "@/utils/file";
import { EntityType } from "./data";
import { isEmptyObject } from "@/utils/is";
import { WidgetType } from "@/views/dashboard/data";
@@ -356,7 +356,7 @@ limitations under the License. -->
optimizeTemplate(item.configuration.children);
}
const name = `dashboards.json`;
saveFile(templates, name);
saveFileAsJSON(templates, name);
setTimeout(() => {
multipleTableRef.value!.clearSelection();
}, 2000);

View File

@@ -24,36 +24,16 @@ limitations under the License. -->
<span>{{ t("delete") }}</span>
</div>
</el-popover>
<div class="header">
<Filter :needQuery="needQuery" :data="data" @get="getService" @search="popSegmentList" />
</div>
<div class="trace flex-h">
<TraceList class="trace-list" :style="`width: ${currentWidth}px;`" />
<div
@mouseover="showIcon = true"
@mouseout="showIcon = false"
@mousedown="mousedown($event)"
@mouseup="mouseup($event)"
>
<div class="trace-line" />
<span class="trace-icon" v-show="showIcon" @mousedown="triggerArrow" @mouseup="stopObserve($event)">
<Icon class="trace-arrow" :icon-name="isLeft ? 'chevron-left' : 'chevron-right'" size="lg" />
</span>
</div>
<TraceDetail :serviceId="serviceId" />
</div>
<Content :data="data" />
</div>
</template>
<script lang="ts" setup>
import { provide, ref, onMounted, onUnmounted } from "vue";
import { provide } from "vue";
import type { PropType } from "vue";
import Filter from "../related/trace/Filter.vue";
import TraceList from "../related/trace/TraceList.vue";
import TraceDetail from "../related/trace/Detail.vue";
import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard";
import { mutationObserver } from "@/utils/mutation";
import type { LayoutConfig } from "@/types/dashboard";
import Content from "@/views/dashboard/related/trace/Content.vue";
/* global defineProps */
const props = defineProps({
@@ -65,76 +45,12 @@ limitations under the License. -->
needQuery: { type: Boolean, default: true },
});
provide("options", props.data);
const defaultWidth = 280,
minArrowLeftWidth = 120;
const serviceId = ref<string>("");
const { t } = useI18n();
const dashboardStore = useDashboardStore();
const isLeft = ref<boolean>(true);
const showIcon = ref<boolean>(false);
const currentWidth = ref<number>(defaultWidth);
const isDrag = ref<boolean>(false);
function removeWidget() {
dashboardStore.removeControls(props.data);
}
function getService(id: string) {
serviceId.value = id;
}
// When click the arrow, the width of the segment list is determined by the direction it points to.
function triggerArrow() {
currentWidth.value = isLeft.value ? 0 : defaultWidth;
isLeft.value = !isLeft.value;
startObserve();
}
function popSegmentList() {
if (currentWidth.value >= defaultWidth) {
return;
}
currentWidth.value = defaultWidth;
isLeft.value = true;
}
function startObserve() {
mutationObserver.observe("trigger-resize", document.querySelector(".trace-list")!, {
attributes: true,
attributeFilter: ["style"],
});
}
function stopObserve(event: MouseEvent) {
mutationObserver.disconnect("trigger-resize");
event.stopPropagation();
}
const mousemove = (event: MouseEvent) => {
if (!isDrag.value) {
return;
}
const diffX = event.clientX;
let leftWidth = document.querySelector(".trace-list")!.getBoundingClientRect();
currentWidth.value = diffX - leftWidth.left;
isLeft.value = currentWidth.value >= minArrowLeftWidth;
};
const mouseup = (event: MouseEvent) => {
showIcon.value = false;
isDrag.value = false;
stopObserve(event);
};
const mousedown = (event: MouseEvent) => {
if ((event.target as HTMLDivElement)?.className === "trace-line") {
isDrag.value = true;
startObserve();
event.stopPropagation();
}
};
onMounted(() => {
document.addEventListener("mousedown", mousedown);
document.addEventListener("mousemove", mousemove);
document.addEventListener("mouseup", mouseup);
});
onUnmounted(() => {
document.removeEventListener("mousedown", mousedown);
document.removeEventListener("mousemove", mousemove);
document.removeEventListener("mouseup", mouseup);
});
</script>
<style lang="scss" scoped>
.trace-wrapper {
@@ -150,13 +66,6 @@ limitations under the License. -->
right: 3px;
}
.header {
padding: 10px;
font-size: $font-size-smaller;
border-bottom: 1px solid $border-color;
min-width: 1000px;
}
.tools {
padding: 5px 0;
color: #999;
@@ -169,47 +78,4 @@ limitations under the License. -->
background-color: $popper-hover-bg-color;
}
}
.trace {
min-height: calc(100% - 150px);
width: 100%;
overflow: auto;
min-width: 1000px;
}
.trace-list {
max-width: 480px;
}
.trace-line {
position: relative;
width: 2px;
height: 100%;
background-color: var(--sw-trace-line);
cursor: ew-resize;
&:hover {
color: $active-color;
background-color: $active-background;
}
}
.trace-icon {
position: absolute;
cursor: pointer;
top: calc(50% - 15px);
text-align: center;
width: 24px;
height: 24px;
transform: translateX(-11px);
line-height: 24px;
border-radius: 50%;
background-color: $layout-background;
box-shadow: 0 3px 5px rgb(45 60 80 / 20%);
}
.trace-arrow {
padding-bottom: 1px;
color: $active-color;
}
</style>

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
export const dragIgnoreFrom =
"svg.d3-trace-tree, .dragger, .micro-topo-chart, .schedules, .vis-item, .vis-timeline, .process-svg";
"svg.d3-trace-tree, .dragger, .micro-topo-chart, .schedules, .vis-item, .vis-timeline, .process-svg, .trace-query-content";
export const PodsChartTypes = ["EndpointList", "InstanceList"];

View File

@@ -16,7 +16,7 @@ limitations under the License. -->
<div class="flex-h content">
<TaskList />
<div class="vis-graph ml-5">
<div class="mb-20">
<div class="mb-20 mt-10">
<Filter />
</div>
<div class="stack" v-loading="asyncProfilingStore.loadingTree">

View File

@@ -41,7 +41,7 @@ limitations under the License. -->
</div>
<div class="profile-table">
<Table
:data="(profileStore.segmentSpans as SegmentSpan[])"
:tableData="(profileStore.segmentSpans as SegmentSpan[])"
:traceId="profileStore.currentSegment?.traceId"
:headerType="WidgetType.Profile"
@select="selectSpan"
@@ -52,7 +52,7 @@ limitations under the License. -->
<script lang="ts" setup>
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import Table from "../../trace/components/Table.vue";
import Table from "@/views/dashboard/related/trace/components/Table/TableContainer.vue";
import { useProfileStore } from "@/store/modules/profile";
import Selector from "@/components/Selector.vue";
import type { SegmentSpan, ProfileTimeRange, ProfileAnalyzeParams } from "@/types/profile";

View File

@@ -0,0 +1,193 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div class="search-bar">
<Filter :needQuery="needQuery" :data="data" @get="getService" @search="popSegmentList" />
<div class="filter-row flex-h mt-10" v-if="traceStore.hasQueryTracesV2Support">
<div class="grey mr-10 label">{{ t("limit") }}</div>
<el-input-number size="small" v-model="limit" :min="10" @change="changeLimit" />
</div>
</div>
<TraceQuery v-if="traceStore.hasQueryTracesV2Support" style="height: 100%" />
<div v-else class="trace flex-h">
<SegmentList class="trace-list" :style="`width: ${currentWidth}px;`" />
<div
@mouseover="showIcon = true"
@mouseout="showIcon = false"
@mousedown="mousedown($event)"
@mouseup="mouseup($event)"
>
<div class="trace-line" />
<span class="trace-icon" v-show="showIcon" @mousedown="triggerArrow" @mouseup="stopObserve($event)">
<Icon class="trace-arrow" :icon-name="isLeft ? 'chevron-left' : 'chevron-right'" size="lg" />
</span>
</div>
<SpanList :serviceId="serviceId" />
</div>
</template>
<script lang="ts" setup>
import { provide, ref, onMounted, onUnmounted } from "vue";
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import { useTraceStore, PageSize } from "@/store/modules/trace";
import Filter from "./components/TraceList/Filter.vue";
import SegmentList from "./components/TraceList/SegmentList.vue";
import SpanList from "./components/TraceList/SpanList.vue";
import type { LayoutConfig } from "@/types/dashboard";
import { mutationObserver } from "@/utils/mutation";
import TraceQuery from "./components/TraceQuery/Index.vue";
/*global defineProps */
const props = defineProps({
data: {
type: Object as PropType<LayoutConfig>,
default: () => ({}),
},
});
provide("options", props.data);
const { t } = useI18n();
const traceStore = useTraceStore();
const serviceId = ref<string>("");
const showIcon = ref<boolean>(false);
const isLeft = ref<boolean>(true);
const currentWidth = ref<number>(280);
const needQuery = ref<boolean>(true);
const isDrag = ref<boolean>(false);
const limit = ref(PageSize);
const defaultWidth = 280;
const minArrowLeftWidth = 120;
function getService(id: string) {
serviceId.value = id;
}
function changeLimit(val: number | undefined) {
if (!val) return;
traceStore.setTraceCondition({
paging: { pageNum: 1, pageSize: val },
});
}
// When click the arrow, the width of the segment list is determined by the direction it points to.
function triggerArrow() {
currentWidth.value = isLeft.value ? 0 : defaultWidth;
isLeft.value = !isLeft.value;
startObserve();
}
function popSegmentList() {
if (currentWidth.value >= defaultWidth) {
return;
}
currentWidth.value = defaultWidth;
isLeft.value = true;
}
function startObserve() {
mutationObserver.observe("trigger-resize", document.querySelector(".trace-list")!, {
attributes: true,
attributeFilter: ["style"],
});
}
function stopObserve(event: MouseEvent) {
mutationObserver.disconnect("trigger-resize");
event.stopPropagation();
}
const mousemove = (event: MouseEvent) => {
if (!isDrag.value) {
return;
}
const diffX = event.clientX;
let leftWidth = document.querySelector(".trace-list")!.getBoundingClientRect();
currentWidth.value = diffX - leftWidth.left;
isLeft.value = currentWidth.value >= minArrowLeftWidth;
};
const mouseup = (event: MouseEvent) => {
showIcon.value = false;
isDrag.value = false;
stopObserve(event);
};
const mousedown = (event: MouseEvent) => {
if ((event.target as HTMLDivElement)?.className === "trace-line") {
isDrag.value = true;
startObserve();
event.stopPropagation();
}
};
onMounted(() => {
document.addEventListener("mousedown", mousedown);
document.addEventListener("mousemove", mousemove);
document.addEventListener("mouseup", mouseup);
});
onUnmounted(() => {
document.removeEventListener("mousedown", mousedown);
document.removeEventListener("mousemove", mousemove);
document.removeEventListener("mouseup", mouseup);
});
</script>
<style lang="scss" scoped>
.search-bar {
padding: 10px;
font-size: $font-size-smaller;
border-bottom: 1px solid $border-color;
min-width: 1000px;
}
.trace {
min-height: calc(100% - 160px);
width: 100%;
overflow: auto;
min-width: 1000px;
}
.trace-list {
max-width: 480px;
}
.trace-line {
position: relative;
width: 2px;
height: 100%;
background-color: var(--sw-trace-line);
cursor: ew-resize;
&:hover {
color: $active-color;
background-color: $active-background;
}
}
.trace-icon {
position: absolute;
cursor: pointer;
top: calc(50% - 15px);
text-align: center;
width: 24px;
height: 24px;
transform: translateX(-11px);
line-height: 24px;
border-radius: 50%;
background-color: $layout-background;
box-shadow: 0 3px 5px rgb(45 60 80 / 20%);
}
.trace-arrow {
padding-bottom: 1px;
color: $active-color;
}
.label {
height: 28px;
line-height: 28px;
}
</style>

View File

@@ -14,27 +14,27 @@ See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div class="trace-wrapper flex-v">
<div class="header">
<Header :data="data" />
<div class="search-bar">
<SearchBar :data="data" />
</div>
<div class="trace flex-h">
<TraceList />
<TraceDetail />
<SegmentList />
<SpanList />
</div>
</div>
</template>
<script lang="ts" setup>
import { provide } from "vue";
import type { PropType } from "vue";
import Header from "./Header.vue";
import TraceList from "./TraceList.vue";
import TraceDetail from "./Detail.vue";
import type { LayoutConfig } from "@/types/dashboard";
import SearchBar from "./components/TraceList/SearchBar.vue";
import SegmentList from "./components/TraceList/SegmentList.vue";
import SpanList from "./components/TraceList/SpanList.vue";
/*global defineProps */
const props = defineProps({
data: {
type: Object as PropType<LayoutConfig>,
default: () => ({ graph: {} }),
default: () => ({}),
},
});
provide("options", props.data);
@@ -47,18 +47,4 @@ limitations under the License. -->
position: relative;
overflow: auto;
}
.header {
padding: 10px;
font-size: $font-size-smaller;
border-bottom: 1px solid $border-color;
min-width: 1200px;
}
.trace {
width: 100%;
overflow: auto;
min-width: 1200px;
height: calc(100% - 100px);
}
</style>

View File

@@ -20,6 +20,8 @@ limitations under the License. -->
:type="type"
:headerType="headerType"
:traceId="traceId"
:selectedMaxTimestamp="selectedMaxTimestamp"
:selectedMinTimestamp="selectedMinTimestamp"
@select="handleSelectSpan"
>
<div class="trace-tips" v-if="!segmentId.length">{{ $t("noData") }}</div>
@@ -47,47 +49,74 @@ limitations under the License. -->
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, watch, onBeforeUnmount, onMounted } from "vue";
import type { PropType } from "vue";
import { ref, watch, onBeforeUnmount, onMounted, nextTick } from "vue";
import * as d3 from "d3";
import dayjs from "dayjs";
import ListGraph from "../../utils/d3-trace-list";
import TreeGraph from "../../utils/d3-trace-tree";
import ListGraph from "./utils/d3-trace-list";
import TreeGraph from "./utils/d3-trace-tree";
import type { Span, Ref } from "@/types/trace";
import SpanDetail from "./SpanDetail.vue";
import TableContainer from "../Table/TableContainer.vue";
import { useAppStoreWithOut } from "@/store/modules/app";
import { useTraceStore } from "@/store/modules/trace";
import { debounce } from "@/utils/debounce";
import { mutationObserver } from "@/utils/mutation";
import { TraceGraphType } from "../constant";
import { Themes } from "@/constants/data";
import { TraceGraphType } from "../VisGraph/constant";
import type { SegmentSpan } from "@/types/profile";
import { buildSegmentForest, collapseTree, getRefsAllNodes } from "./utils/helper";
/* global Recordable, Nullable */
const props = defineProps({
data: { type: Array as PropType<(Span | SegmentSpan)[]>, default: () => [] },
traceId: { type: String, default: "" },
type: { type: String, default: TraceGraphType.LIST },
headerType: { type: String, default: "" },
});
const emits = defineEmits(["select"]);
/* global Nullable */
type Props = {
data: (Span | SegmentSpan)[];
traceId: string;
type: string;
headerType?: string;
selectedMaxTimestamp?: number;
selectedMinTimestamp?: number;
minTimestamp: number;
maxTimestamp: number;
};
type Emits = {
(e: "select", value: Span): void;
};
const props = defineProps<Props>();
const emits = defineEmits<Emits>();
const appStore = useAppStoreWithOut();
const loading = ref<boolean>(false);
const showDetail = ref<boolean>(false);
const fixSpansSize = ref<number>(0);
const segmentId = ref<Recordable[]>([]);
const segmentId = ref<Span[]>([]);
const currentSpan = ref<Nullable<Span>>(null);
const refSpans = ref<Array<Ref>>([]);
const tree = ref<Nullable<any>>(null);
const traceGraph = ref<Nullable<HTMLDivElement>>(null);
const parentSpans = ref<Array<Span | SegmentSpan>>([]);
const refParentSpans = ref<Array<Span | SegmentSpan>>([]);
const traceStore = useTraceStore();
const debounceFunc = debounce(draw, 500);
const visDate = (date: number, pattern = "YYYY-MM-DD HH:mm:ss:SSS") => dayjs(date).format(pattern);
// Store previous timestamp values to check for significant changes
const prevSelectedMaxTimestamp = ref<number>(props.selectedMaxTimestamp || 0);
const prevSelectedMinTimestamp = ref<number>(props.selectedMinTimestamp || 0);
onMounted(() => {
const visDate = (date: number, pattern = "YYYY-MM-DD HH:mm:ss:SSS") => dayjs(date).format(pattern);
// Debounced version of onSpanPanelToggled to prevent excessive re-renders
const debouncedOnSpanPanelToggled = debounce(draw, 150);
// Check if timestamp change is significant enough to warrant a redraw
function isTimestampChangeSignificant(newMax: number, newMin: number): boolean {
const maxDiff = Math.abs(newMax - prevSelectedMaxTimestamp.value);
const minDiff = Math.abs(newMin - prevSelectedMinTimestamp.value);
const totalRange = props.maxTimestamp - props.minTimestamp;
// Consider change significant if it's more than 0.1% of the total range
const threshold = totalRange * 0.001;
return maxDiff > threshold || minDiff > threshold;
}
onMounted(async () => {
loading.value = true;
changeTree();
await nextTick();
draw();
loading.value = false;
// monitor segment list width changes.
@@ -96,11 +125,12 @@ limitations under the License. -->
debounceFunc();
});
window.addEventListener("resize", debounceFunc);
window.addEventListener("spanPanelToggled", draw);
});
function draw() {
if (props.type === TraceGraphType.TABLE) {
segmentId.value = setLevel(segmentId.value);
segmentId.value = setLevel(segmentId.value) as Span[];
return;
}
if (!traceGraph.value) {
@@ -108,21 +138,35 @@ limitations under the License. -->
}
d3.selectAll(".d3-tip").remove();
if (props.type === TraceGraphType.LIST) {
tree.value = new ListGraph(traceGraph.value, handleSelectSpan);
tree.value.init(
{ label: "TRACE_ROOT", children: segmentId.value },
getRefsAllNodes({ label: "TRACE_ROOT", children: segmentId.value }),
fixSpansSize.value,
);
tree.value = new ListGraph({ el: traceGraph.value, handleSelectSpan: handleSelectSpan });
tree.value.init({
data: { label: "TRACE_ROOT", children: segmentId.value },
row: getRefsAllNodes({ label: "TRACE_ROOT", children: segmentId.value }),
fixSpansSize: fixSpansSize.value,
selectedMaxTimestamp: props.selectedMaxTimestamp,
selectedMinTimestamp: props.selectedMinTimestamp,
});
tree.value.draw();
selectInitialSpan();
return;
}
if (props.type === TraceGraphType.TREE) {
tree.value = new TreeGraph(traceGraph.value, handleSelectSpan);
tree.value.init(
{ label: `${props.traceId}`, children: segmentId.value },
getRefsAllNodes({ label: "TRACE_ROOT", children: segmentId.value }),
);
tree.value = new TreeGraph({ el: traceGraph.value, handleSelectSpan });
tree.value.init({
data: { label: `${props.traceId}`, children: segmentId.value },
row: getRefsAllNodes({ label: "TRACE_ROOT", children: segmentId.value }),
selectedMaxTimestamp: props.selectedMaxTimestamp,
selectedMinTimestamp: props.selectedMinTimestamp,
});
}
}
function selectInitialSpan() {
if (segmentId.value && segmentId.value.length > 0) {
const root = segmentId.value[0];
traceStore.setCurrentSpan(root);
if (tree.value && typeof tree.value.highlightSpan === "function") {
tree.value.highlightSpan(root as any);
}
}
}
function handleSelectSpan(i: any) {
@@ -162,7 +206,7 @@ limitations under the License. -->
item && parentSpans.value.push(item);
}
}
function viewParentSpan(span: Recordable) {
function viewParentSpan(span: Span) {
if (props.type === TraceGraphType.TABLE) {
setTableSpanStyle(span);
return;
@@ -173,234 +217,34 @@ limitations under the License. -->
showDetail.value = true;
hideActionBox();
}
function setTableSpanStyle(span: Recordable) {
const itemDom: any = document.querySelector(`.trace-item-${span.key}`);
const items: any = document.querySelectorAll(".trace-item");
function setTableSpanStyle(span: Span) {
const itemDom: HTMLSpanElement | null = document.querySelector(`.trace-item-${span.key}`);
const items: HTMLSpanElement[] = Array.from(document.querySelectorAll(".trace-item")) as HTMLSpanElement[];
for (const item of items) {
item.style.background = appStore.theme === Themes.Dark ? "#212224" : "#fff";
item.style.background = "transparent";
}
if (itemDom) {
itemDom.style.background = "var(--sw-trace-table-selected)";
}
itemDom.style.background = appStore.theme === Themes.Dark ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.1)";
hideActionBox();
}
function hideActionBox() {
const box: any = document.querySelector("#trace-action-box");
box.style.display = "none";
}
function traverseTree(node: Recordable, spanId: string, segmentId: string, data: Recordable) {
if (!node || node.isBroken) {
return;
}
if (node.spanId === spanId && node.segmentId === segmentId) {
node.children.push(data);
return;
}
for (const nodeItem of node.children || []) {
traverseTree(nodeItem, spanId, segmentId, data);
}
}
function changeTree() {
if (props.data.length === 0) {
if (!props.data.length) {
return [];
}
segmentId.value = [];
const segmentGroup: Recordable = {};
const segmentIdGroup: string[] = [];
const fixSpans: Span[] = [];
const segmentHeaders: Span[] = [];
for (const span of props.data) {
if (span.refs.length) {
refSpans.value.push(...span.refs);
}
if (span.parentSpanId === -1) {
segmentHeaders.push(span);
} else {
const item = props.data.find(
(i: Span) => i.traceId === span.traceId && i.segmentId === span.segmentId && i.spanId === span.spanId - 1,
);
const content = fixSpans.find(
(i: Span) =>
i.traceId === span.traceId &&
i.segmentId === span.segmentId &&
i.spanId === span.spanId - 1 &&
i.parentSpanId === span.spanId - 2,
);
if (!item && !content) {
fixSpans.push({
traceId: span.traceId,
segmentId: span.segmentId,
spanId: span.spanId - 1,
parentSpanId: span.spanId - 2,
refs: [],
endpointName: `VNode: ${span.segmentId}`,
serviceCode: "VirtualNode",
type: `[Broken] ${span.type}`,
peer: "",
component: `VirtualNode: #${span.spanId - 1}`,
isError: true,
isBroken: true,
layer: "Broken",
tags: [],
logs: [],
startTime: 0,
endTime: 0,
});
const { roots, fixSpansSize: fixSize, refSpans: refs } = buildSegmentForest(props.data as Span[], props.traceId);
segmentId.value = roots;
fixSpansSize.value = fixSize;
refSpans.value = refs;
for (const root of segmentId.value) {
collapseTree(root, refSpans.value);
}
}
}
for (const span of segmentHeaders) {
if (span.refs.length) {
let exit = null;
for (const ref of span.refs) {
const e = props.data.find(
(i: Recordable) =>
ref.traceId === i.traceId && ref.parentSegmentId === i.segmentId && ref.parentSpanId === i.spanId,
);
if (e) {
exit = e;
}
}
if (!exit) {
const ref = span.refs[0];
// create a known broken node.
const parentSpanId = ref.parentSpanId > -1 ? 0 : -1;
const content = fixSpans.find(
(i: Span) =>
i.traceId === ref.traceId &&
i.segmentId === ref.parentSegmentId &&
i.spanId === ref.parentSpanId &&
i.parentSpanId === parentSpanId,
);
if (!content) {
fixSpans.push({
traceId: ref.traceId,
segmentId: ref.parentSegmentId,
spanId: ref.parentSpanId,
parentSpanId,
refs: [],
endpointName: `VNode: ${ref.parentSegmentId}`,
serviceCode: "VirtualNode",
type: `[Broken] ${ref.type}`,
peer: "",
component: `VirtualNode: #${ref.parentSpanId}`,
isError: true,
isBroken: true,
layer: "Broken",
tags: [],
logs: [],
startTime: 0,
endTime: 0,
});
}
// if root broken node is not exist, create a root broken node.
if (parentSpanId > -1) {
const content = fixSpans.find(
(i: Span) =>
i.traceId === ref.traceId &&
i.segmentId === ref.parentSegmentId &&
i.spanId === 0 &&
i.parentSpanId === -1,
);
if (!content) {
fixSpans.push({
traceId: ref.traceId,
segmentId: ref.parentSegmentId,
spanId: 0,
parentSpanId: -1,
refs: [],
endpointName: `VNode: ${ref.parentSegmentId}`,
serviceCode: "VirtualNode",
type: `[Broken] ${ref.type}`,
peer: "",
component: `VirtualNode: #0`,
isError: true,
isBroken: true,
layer: "Broken",
tags: [],
logs: [],
startTime: 0,
endTime: 0,
});
}
}
}
}
}
for (const i of [...fixSpans, ...props.data]) {
i.label = i.endpointName || "no operation name";
i.key = Math.random().toString(36).substring(2, 36);
i.children = [];
if (segmentGroup[i.segmentId]) {
segmentGroup[i.segmentId].push(i);
} else {
segmentIdGroup.push(i.segmentId);
segmentGroup[i.segmentId] = [i];
}
}
fixSpansSize.value = fixSpans.length;
for (const id of segmentIdGroup) {
const currentSegment = segmentGroup[id].sort((a: Span, b: Span) => b.parentSpanId - a.parentSpanId);
for (const s of currentSegment) {
const index = currentSegment.findIndex((i: Span) => i.spanId === s.parentSpanId);
if (index > -1) {
if (
(currentSegment[index].isBroken && currentSegment[index].parentSpanId === -1) ||
!currentSegment[index].isBroken
) {
currentSegment[index].children.push(s);
currentSegment[index].children.sort((a: Span, b: Span) => a.spanId - b.spanId);
}
}
if (s.isBroken) {
const children = props.data.filter((span: Span) =>
span.refs.find(
(d) => d.traceId === s.traceId && d.parentSegmentId === s.segmentId && d.parentSpanId === s.spanId,
),
);
if (children.length) {
s.children.push(...children);
}
}
}
segmentGroup[id] = currentSegment[currentSegment.length - 1];
}
for (const id of segmentIdGroup) {
for (const ref of segmentGroup[id].refs) {
if (ref.traceId === props.traceId) {
traverseTree(segmentGroup[ref.parentSegmentId], ref.parentSpanId, ref.parentSegmentId, segmentGroup[id]);
}
}
}
for (const i in segmentGroup) {
for (const ref of segmentGroup[i].refs) {
if (!segmentGroup[ref.parentSegmentId]) {
segmentId.value.push(segmentGroup[i]);
}
}
if (!segmentGroup[i].refs.length && segmentGroup[i].parentSpanId === -1) {
segmentId.value.push(segmentGroup[i]);
}
}
for (const i of segmentId.value) {
collapse(i);
}
}
function collapse(d: Span | Recordable) {
if (d.children) {
const item = refSpans.value.find((s: Ref) => s.parentSpanId === d.spanId && s.parentSegmentId === d.segmentId);
let dur = d.endTime - d.startTime;
for (const i of d.children) {
dur -= i.endTime - i.startTime;
}
d.dur = dur < 0 ? 0 : dur;
if (item) {
d.children = d.children.sort(compare("startTime"));
}
for (const i of d.children) {
collapse(i);
}
}
}
function setLevel(arr: Recordable[], level = 1, totalExec?: number) {
function setLevel(arr: Span[], level = 1, totalExec?: number) {
for (const item of arr) {
item.level = level;
totalExec = totalExec || item.endTime - item.startTime;
@@ -411,34 +255,12 @@ limitations under the License. -->
}
return arr;
}
function getRefsAllNodes(tree: Recordable) {
let nodes = [];
let stack = [tree];
while (stack.length > 0) {
const node = stack.pop();
nodes.push(node);
if (node?.children && node.children.length > 0) {
for (let i = node.children.length - 1; i >= 0; i--) {
stack.push(node.children[i]);
}
}
}
return nodes;
}
function compare(p: string) {
return (m: Recordable, n: Recordable) => {
const a = m[p];
const b = n[p];
return a - b;
};
}
onBeforeUnmount(() => {
d3.selectAll(".d3-tip").remove();
window.removeEventListener("resize", debounceFunc);
mutationObserver.deleteObserve("trigger-resize");
window.removeEventListener("spanPanelToggled", draw);
});
watch(
() => props.data,
@@ -455,7 +277,22 @@ limitations under the License. -->
watch(
() => appStore.theme,
() => {
tree.value.init({ label: "TRACE_ROOT", children: segmentId.value }, props.data, fixSpansSize.value);
if (props.type === TraceGraphType.LIST) {
tree.value.init({
data: { label: "TRACE_ROOT", children: segmentId.value },
row: getRefsAllNodes({ label: "TRACE_ROOT", children: segmentId.value }),
fixSpansSize: fixSpansSize.value,
selectedMaxTimestamp: props.selectedMaxTimestamp,
selectedMinTimestamp: props.selectedMinTimestamp,
});
} else if (props.type === TraceGraphType.TREE) {
tree.value.init({
data: { label: `${props.traceId}`, children: segmentId.value },
row: getRefsAllNodes({ label: "TRACE_ROOT", children: segmentId.value }),
selectedMaxTimestamp: props.selectedMaxTimestamp,
selectedMinTimestamp: props.selectedMinTimestamp,
});
}
tree.value.draw(() => {
setTimeout(() => {
loading.value = false;
@@ -463,6 +300,20 @@ limitations under the License. -->
});
},
);
watch(
() => [props.selectedMaxTimestamp, props.selectedMinTimestamp],
([newMax, newMin]) => {
// Only trigger redraw if the change is significant
if (isTimestampChangeSignificant(newMax as number, newMin as number)) {
// Update previous values
prevSelectedMaxTimestamp.value = newMax as number;
prevSelectedMinTimestamp.value = newMin as number;
// Use debounced version to prevent excessive re-renders
debouncedOnSpanPanelToggled();
}
},
);
</script>
<style lang="scss">
.d3-graph {

View File

@@ -83,7 +83,7 @@ limitations under the License. -->
ref="eventGraph"
v-if="currentSpan.attachedEvents && currentSpan.attachedEvents.length"
></div>
<el-button class="popup-btn" type="primary" @click="getTaceLogs">
<el-button class="popup-btn" @click="getTaceLogs">
{{ t("relatedTraceLogs") }}
</el-button>
</div>
@@ -149,9 +149,8 @@ limitations under the License. -->
<script lang="ts" setup>
import { ref, computed, onMounted, inject } from "vue";
import { useI18n } from "vue-i18n";
import type { PropType } from "vue";
import dayjs from "dayjs";
import ListGraph from "../../utils/d3-trace-list";
import ListGraph from "./utils/d3-trace-list";
import copy from "@/utils/copy";
import { ElMessage } from "element-plus";
import { dateFormat } from "@/utils/dateFormat";
@@ -163,10 +162,11 @@ limitations under the License. -->
import { WidgetType } from "@/views/dashboard/data";
import type { LayoutConfig, DashboardItem } from "@/types/dashboard";
/*global defineProps, Nullable, Recordable */
const props = defineProps({
currentSpan: { type: Object as PropType<Span>, default: () => ({}) },
traceId: { type: String, default: "" },
});
type Props = {
currentSpan: Span;
traceId?: string;
};
const props = defineProps<Props>();
const options: LayoutConfig | null = inject("options") || null;
const { t } = useI18n();
const traceStore = useTraceStore();
@@ -233,7 +233,7 @@ limitations under the License. -->
return a.startTime - b.startTime;
});
tree.value = new ListGraph(eventGraph.value, selectEvent);
tree.value = new ListGraph({ el: eventGraph.value, handleSelectSpan: selectEvent });
tree.value.init(
{
children: events,
@@ -287,9 +287,9 @@ limitations under the License. -->
}
.popup-btn {
margin-top: 40px;
width: 100%;
margin-top: 20px;
text-align: center;
padding: 0 30px;
}
.item span {

View File

@@ -17,13 +17,12 @@
import * as d3 from "d3";
import d3tip from "d3-tip";
import type { Trace } from "@/types/trace";
import type { Trace, Span } from "@/types/trace";
import dayjs from "dayjs";
import icons from "@/assets/img/icons";
import { useAppStoreWithOut } from "@/store/modules/app";
import { Themes } from "@/constants/data";
import { useTraceStore } from "@/store/modules/trace";
import { getServiceColor } from "@/utils/color";
const xScaleWidth = 0.6;
export default class ListGraph {
private barHeight = 48;
private handleSelectSpan: Nullable<(i: Trace) => void> = null;
@@ -36,19 +35,16 @@ export default class ListGraph {
private prompt: any = null;
private row: any[] = [];
private data: any = [];
private min = 0;
private max = 0;
private list: any[] = [];
private minTimestamp = 0;
private maxTimestamp = 0;
private xScale: any = null;
private xAxis: any = null;
private sequentialScale: any = null;
private root: any = null;
private selectedNode: any = null;
constructor(el: HTMLDivElement, handleSelectSpan: (i: Trace) => void) {
constructor({ el, handleSelectSpan }: { el: HTMLDivElement; handleSelectSpan: (i: Trace) => void }) {
this.handleSelectSpan = handleSelectSpan;
this.el = el;
this.width = el.getBoundingClientRect().width - 10;
this.height = el.getBoundingClientRect().height - 10;
d3.select(`.${this.el?.className} .trace-list`).remove();
this.svg = d3
.select(this.el)
@@ -80,39 +76,48 @@ export default class ListGraph {
this.svg.call(this.tip);
this.svg.call(this.prompt);
}
diagonal(d: Recordable) {
diagonal(d: Indexable) {
return `M ${d.source.y} ${d.source.x + 5}
L ${d.source.y} ${d.target.x - 30}
L${d.target.y} ${d.target.x - 20}
L${d.target.y} ${d.target.x - 5}`;
}
init(data: Recordable, row: Recordable[], fixSpansSize: number) {
init({
data,
row,
fixSpansSize,
selectedMaxTimestamp,
selectedMinTimestamp,
}: {
data: Indexable;
row: Indexable[];
fixSpansSize: number;
selectedMaxTimestamp?: number;
selectedMinTimestamp?: number;
}) {
d3.select(`.${this.el?.className} .trace-xaxis`).remove();
d3.select("#trace-action-box").style("display", "none");
this.row = row;
this.data = data;
this.min = d3.min(this.row.map((i) => i.startTime));
this.max = d3.max(this.row.map((i) => i.endTime - this.min)) || 0;
this.list = useTraceStore().serviceList || [];
const min = d3.min(this.row.map((i) => i.startTime));
const max = d3.max(this.row.map((i) => i.endTime));
this.minTimestamp = selectedMinTimestamp ?? min;
this.maxTimestamp = selectedMaxTimestamp ?? max;
this.xScale = d3
.scaleLinear()
.range([0, this.width * 0.387])
.domain([0, this.max]);
.range([0, this.width * xScaleWidth])
.domain([this.minTimestamp - min, this.maxTimestamp - min]);
this.xAxis = d3.axisTop(this.xScale).tickFormat((d: any) => {
if (d === 0) return 0;
if (d >= 1000) return d / 1000 + "s";
return d;
if (d === 0) return d.toFixed(2);
if (d >= 1000) return (d / 1000).toFixed(2) + "s";
return d.toFixed(2);
});
this.svg.attr("height", (this.row.length + fixSpansSize + 1) * this.barHeight);
this.svg
.append("g")
.attr("class", "trace-xaxis")
.attr("transform", `translate(${this.width * 0.618 - 20},${30})`)
.attr("transform", `translate(${this.width * (1 - xScaleWidth) - 20},${30})`)
.call(this.xAxis);
this.sequentialScale = d3
.scaleSequential()
.domain([0, this.list.length + 1])
.interpolator(d3.interpolateCool);
this.root = d3.hierarchy(this.data, (d) => d.children);
this.root.x0 = 0;
this.root.y0 = 0;
@@ -141,7 +146,6 @@ export default class ListGraph {
}
update(source: Recordable, callback: Function) {
const t = this;
const appStore = useAppStoreWithOut();
const nodes = this.root.descendants();
let index = -1;
this.root.eachBefore((n: Recordable) => {
@@ -186,12 +190,12 @@ export default class ListGraph {
});
nodeEnter
.append("rect")
.attr("height", 42)
.attr("height", this.barHeight)
.attr("ry", 2)
.attr("rx", 2)
.attr("y", -22)
.attr("y", -this.barHeight / 2)
.attr("x", 20)
.attr("width", "100%")
.attr("width", "200px")
.attr("fill", "rgba(0,0,0,0)");
nodeEnter
.append("image")
@@ -242,24 +246,18 @@ export default class ListGraph {
event.stopPropagation();
t.prompt.hide(d, this);
});
nodeEnter
.append("text")
.attr("x", 13)
.attr("y", 5)
.attr("fill", `#e54c17`)
.html((d: Recordable) => (d.data.isError ? "◉" : ""));
nodeEnter
.append("text")
.attr("class", "node-text")
.attr("x", 35)
.attr("y", -6)
.attr("fill", (d: Recordable) => (d.data.isError ? `#e54c17` : appStore.theme === Themes.Dark ? "#eee" : "#333"))
.html((d: Recordable) => {
.attr("fill", (d: { data: Span }) => (d.data.isError ? `var(--el-color-danger)` : `var(--font-color)`))
.html((d: { data: Span }) => {
const { label } = d.data;
if (d.data.label === "TRACE_ROOT") {
return "";
}
const label = d.data.label.length > 30 ? `${d.data.label.slice(0, 30)}...` : `${d.data.label}`;
return label;
return (label || "").length > 30 ? `${label?.slice(0, 30)}...` : `${label}`;
});
nodeEnter
.append("circle")
@@ -289,7 +287,7 @@ export default class ListGraph {
.attr("y", -1)
.attr("fill", "#e66")
.style("font-size", "10px")
.text((d: Recordable) => {
.text((d: { data: Span }) => {
const events = d.data.attachedEvents;
if (events && events.length) {
return `${events.length}`;
@@ -302,18 +300,15 @@ export default class ListGraph {
.attr("class", "node-text")
.attr("x", 35)
.attr("y", 12)
.attr("fill", appStore.theme === Themes.Dark ? "#777" : "#ccc")
.attr("fill", (d: { data: Span }) => (d.data.isError ? `var(--el-color-danger)` : `var(--disabled-color)`))
.style("font-size", "11px")
.text(
(d: Recordable) =>
`${d.data.layer || ""} ${
d.data.component
? "- " + d.data.component
: d.data.event
? this.visDate(d.data.startTime) + ":" + d.data.startTimeNanos
: ""
}`,
);
.text((d: { data: Span }) => {
const { layer, component, event, startTime, startTimeNanos } = d.data;
const text = `${layer || ""} ${
component ? "- " + component : event ? this.visDate(startTime) + ":" + startTimeNanos : ""
}`;
return text.length > 20 ? `${text?.slice(0, 20)}...` : `${text}`;
});
nodeEnter
.append("rect")
.attr("rx", 2)
@@ -321,15 +316,33 @@ export default class ListGraph {
.attr("height", 4)
.attr("width", (d: Recordable) => {
if (!d.data.endTime || !d.data.startTime) return 0;
return this.xScale(d.data.endTime - d.data.startTime) + 1 || 0;
// Calculate the actual start and end times within the visible range
let spanStart = d.data.startTime;
let spanEnd = d.data.endTime;
const isIn = d.data.startTime > this.maxTimestamp || d.data.endTime < this.minTimestamp;
if (isIn) return 0;
// If the span is completely outside the visible range, don't show it
if (spanStart >= spanEnd) return 0;
if (spanStart < this.minTimestamp) spanStart = this.minTimestamp;
if (spanEnd > this.maxTimestamp) spanEnd = this.maxTimestamp;
if (spanStart >= spanEnd) return 0;
const min = d3.min(this.row.map((i) => i.startTime));
return this.xScale(spanEnd - min) - this.xScale(spanStart - min) + 1 || 0;
})
.attr("x", (d: Recordable) => {
if (!d.data.endTime || !d.data.startTime) return 0;
const isIn = d.data.startTime > this.maxTimestamp || d.data.endTime < this.minTimestamp;
if (isIn) return 0;
// Calculate the actual start time within the visible range
let spanStart = d.data.startTime;
if (spanStart < this.minTimestamp) spanStart = this.minTimestamp;
const min = d3.min(this.row.map((i) => i.startTime));
return this.width * (1 - xScaleWidth) - d.y - 25 + this.xScale(spanStart - min) || 0;
})
.attr("x", (d: Recordable) =>
!d.data.endTime || !d.data.startTime
? 0
: this.width * 0.618 - 20 - d.y + this.xScale(d.data.startTime - this.min) || 0,
)
.attr("y", -2)
.style("fill", (d: Recordable) => `${this.sequentialScale(this.list.indexOf(d.data.serviceCode))}`);
.style("fill", (d: Recordable) => `${getServiceColor(d.data.serviceCode || "")}`);
const nodeUpdate = nodeEnter.merge(node);
nodeUpdate
.transition()
@@ -338,14 +351,12 @@ export default class ListGraph {
.style("opacity", 1);
nodeUpdate
.append("circle")
.attr("r", appStore.theme === Themes.Dark ? 4 : 3)
.attr("r", 3)
.style("cursor", "pointer")
.attr("stroke-width", appStore.theme === Themes.Dark ? 3 : 2.5)
.style("fill", (d: Recordable) =>
d._children ? `${this.sequentialScale(this.list.indexOf(d.data.serviceCode))}` : "#eee",
)
.attr("stroke-width", 3)
.style("fill", (d: Recordable) => (d._children ? `${getServiceColor(d.data.serviceCode)}` : "#eee"))
.style("stroke", (d: Recordable) =>
d.data.label === "TRACE_ROOT" ? "" : `${this.sequentialScale(this.list.indexOf(d.data.serviceCode))}`,
d.data.label === "TRACE_ROOT" ? "" : `${getServiceColor(d.data.serviceCode)}`,
)
.on("click", (event: any, d: Recordable) => {
event.stopPropagation();
@@ -368,8 +379,8 @@ export default class ListGraph {
.enter()
.insert("path", "g")
.attr("class", "trace-link")
.attr("fill", appStore.theme === Themes.Dark ? "rgba(244,244,244,0)" : "rgba(0,0,0,0)")
.attr("stroke", appStore.theme === Themes.Dark ? "rgba(244,244,244,0.4)" : "rgba(0, 0, 0, 0.1)")
.attr("fill", "none")
.attr("stroke", "var(--sw-trace-list-path)")
.attr("stroke-width", 2)
.attr("transform", `translate(5, 0)`)
.attr("d", () => {
@@ -432,6 +443,23 @@ export default class ListGraph {
behavior: "smooth",
});
}
highlightSpan(span: Recordable) {
if (!span) {
return;
}
const nodes = this.root?.descendants ? this.root.descendants() : [];
const targetNode = nodes.find(
(node: { data: Span }) =>
span.spanId === node.data.spanId &&
span.segmentId === node.data.segmentId &&
span.traceId === node.data.traceId,
);
if (!targetNode) return;
this.selectedNode?.classed("highlighted", false);
const sel = d3.select(`#list-node-${targetNode.id}`);
sel.classed("highlighted", true);
this.selectedNode = sel;
}
visDate(date: number, pattern = "YYYY-MM-DD HH:mm:ss:SSS") {
return dayjs(date).format(pattern);
}

View File

@@ -19,8 +19,8 @@ import * as d3 from "d3";
import d3tip from "d3-tip";
import type { Trace, Span } from "@/types/trace";
import { useAppStoreWithOut } from "@/store/modules/app";
import { useTraceStore } from "@/store/modules/trace";
import { Themes } from "@/constants/data";
import { getServiceColor } from "@/utils/color";
export default class TraceMap {
private i = 0;
@@ -36,11 +36,9 @@ export default class TraceMap {
private treemap: Nullable<any> = null;
private data: Nullable<any> = null;
private row: Nullable<any> = null;
private min = 0;
private max = 0;
private list: string[] = [];
private minTimestamp = 0;
private maxTimestamp = 0;
private xScale: Nullable<any> = null;
private sequentialScale: Nullable<any> = null;
private root: Nullable<any> = null;
private topSlowMax: number[] = [];
private topSlowMin: number[] = [];
@@ -49,14 +47,14 @@ export default class TraceMap {
private nodeUpdate: Nullable<any> = null;
private selectedNode: any = null;
constructor(el: HTMLDivElement, handleSelectSpan: (i: Trace) => void) {
constructor({ el, handleSelectSpan }: { el: HTMLDivElement; handleSelectSpan: (i: Trace) => void }) {
this.el = el;
this.handleSelectSpan = handleSelectSpan;
this.i = 0;
this.topSlow = [];
this.topChild = [];
this.width = el.clientWidth - 20;
this.height = el.clientHeight - 30;
this.width = el.getBoundingClientRect().width - 10;
this.height = el.getBoundingClientRect().height - 10;
d3.select(`.${this.el?.className} .d3-trace-tree`).remove();
this.body = d3
.select(this.el)
@@ -81,19 +79,28 @@ export default class TraceMap {
this.svg = this.body.append("g").attr("transform", () => `translate(120, 0)`);
this.svg.call(this.tip);
}
init(data: Recordable, row: Recordable) {
init({
data,
row,
selectedMaxTimestamp,
selectedMinTimestamp,
}: {
data: Span;
row: Span[];
selectedMaxTimestamp?: number;
selectedMinTimestamp?: number;
}) {
d3.select("#trace-action-box").style("display", "none");
this.treemap = d3.tree().size([row.length * 35, this.width]);
this.row = row;
this.data = data;
this.min = Number(d3.min(this.row.map((i: Span) => i.startTime)));
this.max = Number(d3.max(this.row.map((i: Span) => i.endTime - this.min)));
this.list = useTraceStore().serviceList || [];
this.xScale = d3.scaleLinear().range([0, 100]).domain([0, this.max]);
this.sequentialScale = d3
.scaleSequential()
.domain([0, this.list.length + 1])
.interpolator(d3.interpolateCool);
this.minTimestamp = selectedMinTimestamp ?? Number(d3.min(this.row.map((i: Span) => i.startTime)));
this.maxTimestamp =
selectedMaxTimestamp ?? Number(d3.max(this.row.map((i: Span) => i.endTime - this.minTimestamp)));
this.xScale = d3
.scaleLinear()
.range([0, 100])
.domain([0, this.maxTimestamp - this.minTimestamp]);
this.body.call(this.getZoomBehavior(this.svg));
this.root = d3.hierarchy(this.data, (d) => d.children);
@@ -102,7 +109,7 @@ export default class TraceMap {
this.topSlow = [];
this.topChild = [];
const that = this;
this.root.children.forEach(collapse);
this.root.children?.forEach(collapse);
this.topSlowMax = this.topSlow.sort((a: number, b: number) => b - a)[0];
this.topSlowMin = this.topSlow.sort((a: number, b: number) => b - a)[4];
this.topChildMax = this.topChild.sort((a: number, b: number) => b - a)[0];
@@ -218,11 +225,9 @@ export default class TraceMap {
.append("circle")
.attr("class", "node")
.attr("r", 2)
.attr("stroke", (d: Recordable) => this.sequentialScale(this.list.indexOf(d.data.serviceCode)))
.attr("stroke", (d: Recordable) => getServiceColor(d.data.serviceCode))
.attr("stroke-width", 2.5)
.attr("fill", (d: Recordable) =>
d.data.children.length ? this.sequentialScale(this.list.indexOf(d.data.serviceCode)) : "#fff",
);
.attr("fill", (d: Recordable) => (d.data.children.length ? getServiceColor(d.data.serviceCode) : "#fff"));
nodeEnter
.append("text")
.attr("class", "trace-node-text")
@@ -276,21 +281,31 @@ export default class TraceMap {
.attr("rx", 1)
.attr("ry", 1)
.attr("height", 2)
.attr("width", (d: Recordable) => {
if (!d.data.endTime || !d.data.startTime) return 0;
return this.xScale(d.data.endTime - d.data.startTime) + 1 || 0;
.attr("width", (d: { data: Span }) => {
let spanStart = d.data.startTime;
let spanEnd = d.data.endTime;
if (!spanEnd || !spanStart) return 0;
if (spanStart > this.maxTimestamp || spanEnd < this.minTimestamp) return 0;
if (spanStart < this.minTimestamp) spanStart = this.minTimestamp;
if (spanEnd > this.maxTimestamp) spanEnd = this.maxTimestamp;
return this.xScale(spanEnd - spanStart) + 1 || 0;
})
.attr("x", (d: Recordable) => {
if (!d.data.endTime || !d.data.startTime) {
.attr("x", (d: Indexable) => {
let spanStart = d.data.startTime;
let spanEnd = d.data.endTime;
if (!spanEnd || !spanStart) {
return 0;
}
if (spanStart > this.maxTimestamp || spanEnd < this.minTimestamp) return 0;
if (spanStart < this.minTimestamp) spanStart = this.minTimestamp;
if (spanEnd > this.maxTimestamp) spanEnd = this.maxTimestamp;
if (d.children || d._children) {
return -110 + this.xScale(d.data.startTime - this.min);
return -110 + this.xScale(spanStart - this.minTimestamp);
}
return 10 + this.xScale(d.data.startTime - this.min);
return 10 + this.xScale(spanStart - this.minTimestamp);
})
.attr("y", -1)
.style("fill", (d: Recordable) => this.sequentialScale(this.list.indexOf(d.data.serviceCode)));
.style("fill", (d: { data: Span }) => getServiceColor(d.data.serviceCode));
const nodeUpdate = nodeEnter.merge(node);
this.nodeUpdate = nodeUpdate;
nodeUpdate

View File

@@ -0,0 +1,281 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Span, Ref } from "@/types/trace";
/* global Indexable */
export interface BuildTreeResult {
roots: Span[];
fixSpansSize: number;
refSpans: Ref[];
}
export function buildSegmentForest(data: Span[], traceId: string): BuildTreeResult {
const refSpans: Ref[] = [];
const segmentGroup: { [key: string]: any } = {};
const segmentIdGroup: string[] = [];
const fixSpans: Span[] = [];
const segmentHeaders: Span[] = [];
if (!data || data.length === 0) {
return { roots: [], fixSpansSize: 0, refSpans };
}
for (const span of data) {
if (span.refs && span.refs.length) {
refSpans.push(...span.refs);
}
if (span.parentSpanId === -1) {
segmentHeaders.push(span);
} else {
const item = data.find(
(i: Span) => i.traceId === span.traceId && i.segmentId === span.segmentId && i.spanId === span.spanId - 1,
);
const content = fixSpans.find(
(i: Span) =>
i.traceId === span.traceId &&
i.segmentId === span.segmentId &&
i.spanId === span.spanId - 1 &&
i.parentSpanId === span.spanId - 2,
);
if (!item && !content) {
fixSpans.push({
traceId: span.traceId,
segmentId: span.segmentId,
spanId: span.spanId - 1,
parentSpanId: span.spanId - 2,
refs: [],
endpointName: `VNode: ${span.segmentId}`,
serviceCode: "VirtualNode",
type: `[Broken] ${span.type}`,
peer: "",
component: `VirtualNode: #${span.spanId - 1}`,
isError: true,
isBroken: true,
layer: "Broken",
tags: [],
logs: [],
startTime: 0,
endTime: 0,
} as Span);
}
}
}
for (const span of segmentHeaders) {
if (span.refs && span.refs.length) {
let exit: Span | null = null;
for (const ref of span.refs) {
const e = data.find(
(i: Span) =>
ref.traceId === i.traceId && ref.parentSegmentId === i.segmentId && ref.parentSpanId === i.spanId,
);
if (e) {
exit = e;
}
}
if (!exit) {
const ref = span.refs[0];
const parentSpanId = ref.parentSpanId > -1 ? 0 : -1;
const content = fixSpans.find(
(i: Span) =>
i.traceId === ref.traceId &&
i.segmentId === ref.parentSegmentId &&
i.spanId === ref.parentSpanId &&
i.parentSpanId === parentSpanId,
);
if (!content) {
fixSpans.push({
traceId: ref.traceId,
segmentId: ref.parentSegmentId,
spanId: ref.parentSpanId,
parentSpanId,
refs: [],
endpointName: `VNode: ${ref.parentSegmentId}`,
serviceCode: "VirtualNode",
type: `[Broken] ${ref.type}`,
peer: "",
component: `VirtualNode: #${ref.parentSpanId}`,
isError: true,
isBroken: true,
layer: "Broken",
tags: [],
logs: [],
startTime: 0,
endTime: 0,
} as Span);
}
if (parentSpanId > -1) {
const exists = fixSpans.find(
(i: Span) =>
i.traceId === ref.traceId &&
i.segmentId === ref.parentSegmentId &&
i.spanId === 0 &&
i.parentSpanId === -1,
);
if (!exists) {
fixSpans.push({
traceId: ref.traceId,
segmentId: ref.parentSegmentId,
spanId: 0,
parentSpanId: -1,
refs: [],
endpointName: `VNode: ${ref.parentSegmentId}`,
serviceCode: "VirtualNode",
type: `[Broken] ${ref.type}`,
peer: "",
component: `VirtualNode: #0`,
isError: true,
isBroken: true,
layer: "Broken",
tags: [],
logs: [],
startTime: 0,
endTime: 0,
} as Span);
}
}
}
}
}
for (const i of [...fixSpans, ...data]) {
i.label = i.endpointName || "no operation name";
i.key = Math.random().toString(36).substring(2, 36);
i.children = [];
if (segmentGroup[i.segmentId]) {
segmentGroup[i.segmentId].push(i);
} else {
segmentIdGroup.push(i.segmentId);
segmentGroup[i.segmentId] = [i];
}
}
for (const id of segmentIdGroup) {
const currentSegment = segmentGroup[id].sort((a: Span, b: Span) => b.parentSpanId - a.parentSpanId);
for (const s of currentSegment) {
const index = currentSegment.findIndex((i: Span) => i.spanId === s.parentSpanId);
if (index > -1) {
if (
(currentSegment[index].isBroken && currentSegment[index].parentSpanId === -1) ||
!currentSegment[index].isBroken
) {
currentSegment[index].children?.push(s);
currentSegment[index].children?.sort((a: Span, b: Span) => a.spanId - b.spanId);
}
}
if (s.isBroken) {
const children = data.filter(
(span: Span) =>
!!span.refs?.find(
(d) => d.traceId === s.traceId && d.parentSegmentId === s.segmentId && d.parentSpanId === s.spanId,
),
);
if (children.length) {
s.children?.push(...children);
}
}
}
segmentGroup[id] = currentSegment[currentSegment.length - 1];
}
for (const id of segmentIdGroup) {
for (const ref of segmentGroup[id].refs || []) {
if (ref.traceId === traceId) {
traverseTree(segmentGroup[ref.parentSegmentId], ref.parentSpanId, ref.parentSegmentId, segmentGroup[id]);
}
}
}
const roots: Span[] = [];
for (const i in segmentGroup) {
let pushed = false;
for (const ref of segmentGroup[i].refs || []) {
if (!segmentGroup[ref.parentSegmentId]) {
roots.push(segmentGroup[i]);
pushed = true;
break;
}
}
if (
!pushed &&
(!segmentGroup[i].refs || segmentGroup[i].refs.length === 0) &&
segmentGroup[i].parentSpanId === -1
) {
roots.push(segmentGroup[i]);
}
}
return { roots, fixSpansSize: fixSpans.length, refSpans };
}
export function collapseTree(d: Span, refSpans: Ref[]): void {
if (d.children) {
const item = refSpans.find((s: Ref) => s.parentSpanId === d.spanId && s.parentSegmentId === d.segmentId);
let dur = d.endTime - d.startTime;
for (const i of d.children) {
dur -= i.endTime - i.startTime;
}
d.dur = dur < 0 ? 0 : dur;
if (item) {
d.children = d.children?.sort(compare("startTime"));
}
for (const i of d.children) {
collapseTree(i, refSpans);
}
}
}
function traverseTree(node: Span, spanId: number, segmentId: string, data: Span) {
if (!node || node.isBroken) {
return;
}
if (node.spanId === spanId && node.segmentId === segmentId) {
node.children?.push(data);
return;
}
for (const nodeItem of (node as Span).children || []) {
traverseTree(nodeItem, spanId, segmentId, data);
}
}
function compare(p: string) {
return (m: Span, n: Span) => {
const a = (m as Indexable)[p];
const b = (n as Indexable)[p];
return a - b;
};
}
export function getRefsAllNodes(tree: Indexable) {
const nodes = [];
const stack = [tree];
while (stack.length > 0) {
const node = stack.pop();
nodes.push(node);
if (node?.children && node.children.length > 0) {
for (let i = node.children.length - 1; i >= 0; i--) {
stack.push(node.children[i]);
}
}
}
return nodes;
}

View File

@@ -16,11 +16,11 @@ limitations under the License. -->
<template>
<div class="trace-table">
<div class="trace-table-header" v-if="type === TraceGraphType.STATISTICS">
<div :class="item.label" v-for="(item, index) in headerData" :key="index">
<div :class="item.label" v-for="(item, index) in headerData as typeof StatisticsConstant" :key="index">
{{ item.value }}
<span
class="r cp"
@click="sortStatistics(item.key)"
@click="sortStatistics(item.key || '')"
:key="componentKey"
v-if="item.key !== 'endpointName' && item.key !== 'type'"
>
@@ -47,34 +47,41 @@ limitations under the License. -->
:key="`key${index}`"
:type="type"
:headerType="headerType"
@click="selectItem"
:selectedMaxTimestamp="selectedMaxTimestamp"
:selectedMinTimestamp="selectedMinTimestamp"
@selectedSpan="selectItem"
/>
<slot></slot>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from "vue";
import type { PropType } from "vue";
import { useTraceStore } from "@/store/modules/trace";
import type { Span } from "@/types/trace";
import TableItem from "./TableItem.vue";
import { ProfileConstant, TraceConstant, StatisticsConstant } from "./data";
import { TraceGraphType } from "../constant";
import { TraceGraphType } from "../VisGraph/constant";
import { WidgetType } from "@/views/dashboard/data";
/* global defineProps, Nullable, defineEmits, Recordable*/
const props = defineProps({
tableData: { type: Array as PropType<Recordable>, default: () => [] },
type: { type: String, default: "" },
headerType: { type: String, default: "" },
traceId: { type: String, default: "" },
});
const emits = defineEmits(["select"]);
const traceStore = useTraceStore();
/* global defineProps, Nullable, defineEmits*/
type Props = {
tableData: Span[];
type?: string;
headerType?: string;
traceId: string;
selectedMaxTimestamp?: number;
selectedMinTimestamp?: number;
};
type Emits = {
(e: "select", value: Span): void;
};
const props = defineProps<Props>();
const emits = defineEmits<Emits>();
const method = ref<number>(300);
const componentKey = ref<number>(300);
const flag = ref<boolean>(true);
const dragger = ref<Nullable<HTMLSpanElement>>(null);
let headerData: Recordable[] = TraceConstant;
let headerData: typeof TraceConstant | typeof ProfileConstant | typeof StatisticsConstant = TraceConstant;
if (props.headerType === WidgetType.Profile) {
headerData = ProfileConstant;
@@ -104,31 +111,15 @@ limitations under the License. -->
};
};
});
function selectItem(event: MouseEvent) {
emits("select", traceStore.selectedSpan);
if (props.headerType === WidgetType.Profile) {
return;
}
if (props.type === TraceGraphType.STATISTICS) {
return;
}
const item: any = document.querySelector("#trace-action-box");
const tableBox = document.querySelector(".trace-table-charts")?.getBoundingClientRect();
if (!tableBox) {
return;
}
const offsetX = event.x - tableBox.x;
const offsetY = event.y - tableBox.y;
item.style.display = "block";
item.style.top = `${offsetY + 20}px`;
item.style.left = `${offsetX + 10}px`;
function selectItem(span: Span) {
emits("select", span);
}
function sortStatistics(key: string) {
const element = props.tableData;
for (let i = 0; i < element.length; i++) {
for (let j = 0; j < element.length - i - 1; j++) {
let val1;
let val2;
let val1: number | undefined;
let val2: number | undefined;
if (key === "maxTime") {
val1 = element[j].maxTime;
val2 = element[j + 1].maxTime;
@@ -150,13 +141,13 @@ limitations under the License. -->
val2 = element[j + 1].count;
}
if (flag.value) {
if (val1 < val2) {
if (val1 && val2 && val1 < val2) {
const tmp = element[j];
element[j] = element[j + 1];
element[j + 1] = tmp;
}
} else {
if (val1 > val2) {
if (val1 && val2 && val1 > val2) {
const tmp = element[j];
element[j] = element[j + 1];
element[j + 1] = tmp;

View File

@@ -49,15 +49,16 @@ limitations under the License. -->
</div>
<div v-else>
<div
@click="selectSpan"
:class="[
'trace-item',
'level' + ((data.level || 0) - 1),
{ 'trace-item-error': data.isError },
{ profiled: data.profiled === false },
`trace-item-${data.key}`,
{ highlighted: inTimeRange },
]"
:data-text="data.profiled === false ? 'No Thread Dump' : ''"
@click="hideActionBox"
>
<div
:class="['method', 'level' + ((data.level || 0) - 1)]"
@@ -65,6 +66,8 @@ limitations under the License. -->
'text-indent': ((data.level || 0) - 1) * 10 + 'px',
width: `${method}px`,
}"
@click="selectSpan"
@click.stop
>
<Icon
:style="!displayChildren ? 'transform: rotate(-90deg);' : ''"
@@ -73,6 +76,7 @@ limitations under the License. -->
iconName="arrow-down"
size="sm"
class="mr-5"
@click="hideActionBox"
/>
<el-tooltip
:content="data.type === 'Entry' ? 'Entry' : 'Exit'"
@@ -90,7 +94,7 @@ limitations under the License. -->
</span>
</el-tooltip>
<el-tooltip :content="data.endpointName" placement="top" :show-after="300">
<span>
<span class="link-span">
{{ data.endpointName }}
</span>
</el-tooltip>
@@ -116,7 +120,7 @@ limitations under the License. -->
</div>
<div class="application">
<el-tooltip :show-after="300" :content="data.serviceCode || '-'" placement="top">
<span>{{ data.serviceCode }}</span>
<span :style="{ color: getServiceColor(data.serviceCode || '') }">{{ data.serviceCode }}</span>
</el-tooltip>
</div>
<div class="application" v-show="headerType === WidgetType.Profile" @click="viewSpan($event)">
@@ -128,12 +132,15 @@ limitations under the License. -->
</div>
<div v-show="data.children && data.children.length > 0 && displayChildren" class="children-trace">
<table-item
:method="method"
v-for="(child, index) in data.children"
:method="method"
:key="index"
:data="child"
:type="type"
:headerType="headerType"
:selectedMaxTimestamp="selectedMaxTimestamp"
:selectedMinTimestamp="selectedMinTimestamp"
@selectedSpan="selectItem"
/>
</div>
<el-dialog v-model="showDetail" :destroy-on-close="true" fullscreen @closed="showDetail = false">
@@ -141,32 +148,32 @@ limitations under the License. -->
</el-dialog>
</div>
</template>
<script lang="ts">
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import { ref, computed, defineComponent, watch } from "vue";
import type { PropType } from "vue";
import { ref, computed, watch } from "vue";
import SpanDetail from "../D3Graph/SpanDetail.vue";
import { dateFormat } from "@/utils/dateFormat";
import { useAppStoreWithOut } from "@/store/modules/app";
import { useTraceStore } from "@/store/modules/trace";
import { Themes } from "@/constants/data";
import { TraceGraphType } from "../constant";
import { TraceGraphType } from "../VisGraph/constant";
import { WidgetType } from "@/views/dashboard/data";
import type { Span, Ref } from "@/types/trace";
import { getServiceColor } from "@/utils/color";
/*global Recordable*/
const props = {
data: { type: Object as PropType<Span>, default: () => ({}) },
method: { type: Number, default: 0 },
type: { type: String, default: "" },
headerType: { type: String, default: "" },
traceId: { type: String, traceId: "" },
};
export default defineComponent({
name: "TableItem",
props,
components: { SpanDetail },
setup(props) {
interface Props {
data: Span;
method: number;
type?: string;
headerType?: string;
traceId?: string;
selectedMaxTimestamp?: number;
selectedMinTimestamp?: number;
}
interface Emits {
(e: "selectedSpan", value: Span): void;
}
const emits = defineEmits<Emits>();
const props = defineProps<Props>();
const appStore = useAppStoreWithOut();
const traceStore = useTraceStore();
const displayChildren = ref<boolean>(true);
@@ -174,15 +181,14 @@ limitations under the License. -->
const { t } = useI18n();
const selfTime = computed(() => (props.data.dur ? props.data.dur : 0));
const execTime = computed(() =>
props.data.endTime - props.data.startTime ? props.data.endTime - props.data.startTime : 0,
props.data.endTime - props.data.startTime > 0 ? props.data.endTime - props.data.startTime : 0,
);
const outterPercent = computed(() => {
if (props.data.level === 1) {
return "100%";
} else {
const data = props.data;
const exec = data.endTime - data.startTime ? data.endTime - data.startTime : 0;
let result = (exec / (data.totalExec || 0)) * 100;
const { data } = props;
let result = (execTime.value / (data.totalExec || 0)) * 100;
result = result > 100 ? 100 : result;
const resultStr = result.toFixed(4) + "%";
return resultStr === "0.0000%" ? "0.9%" : resultStr;
@@ -197,40 +203,69 @@ limitations under the License. -->
const key = props.data.refs?.findIndex((d: Ref) => d.type === "CROSS_THREAD") ?? -1;
return key > -1 ? true : false;
});
const inTimeRange = computed(() => {
if (props.selectedMinTimestamp === undefined || props.selectedMaxTimestamp === undefined) {
return true;
}
return props.data.startTime <= props.selectedMaxTimestamp && props.data.endTime >= props.selectedMinTimestamp;
});
function toggle() {
displayChildren.value = !displayChildren.value;
}
function selectItem(span: Span) {
emits("selectedSpan", span);
}
function showSelectSpan(dom: HTMLSpanElement) {
if (!dom) {
return;
}
const items: any = document.querySelectorAll(".trace-item");
const items: HTMLSpanElement[] = Array.from(document.querySelectorAll(".trace-item")) as HTMLSpanElement[];
for (const item of items) {
item.style.background = appStore.theme === Themes.Dark ? "#212224" : "#fff";
item.style.background = "transparent";
}
dom.style.background = appStore.theme === Themes.Dark ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.1)";
const p: any = document.getElementsByClassName("profiled")[0];
dom.style.background = "var(--sw-trace-table-selected)";
const p = document.getElementsByClassName("profiled")[0] as HTMLSpanElement | null;
if (p) {
p.style.background = appStore.theme === Themes.Dark ? "#333" : "#eee";
p.style.background = "var(--border-color-primary)";
}
}
function selectSpan(event: Recordable) {
const dom = event.composedPath().find((d: Recordable) => d.className.includes("trace-item"));
function selectSpan(event: MouseEvent) {
emits("selectedSpan", props.data);
const dom = event
.composedPath()
.find((d: EventTarget) => (d as HTMLElement).className.includes("trace-item")) as HTMLSpanElement;
selectedItem(props.data);
if (props.headerType === WidgetType.Profile) {
showSelectSpan(dom);
return;
}
viewSpanDetail(dom);
if (props.type === TraceGraphType.STATISTICS) {
return;
}
function viewSpan(event: Recordable) {
const item: HTMLSpanElement | null = document.querySelector("#trace-action-box");
const tableBox = document.querySelector(".trace-table-charts")?.getBoundingClientRect();
if (!tableBox || !item) {
return;
}
const offsetX = event.x - tableBox.x;
const offsetY = event.y - tableBox.y;
item.style.display = "block";
item.style.top = `${offsetY + 20}px`;
item.style.left = `${offsetX + 10}px`;
}
function viewSpan(event: MouseEvent) {
showDetail.value = true;
const dom = event.composedPath().find((d: Recordable) => d.className.includes("trace-item"));
const dom = event
.composedPath()
.find((d: EventTarget) => (d as HTMLElement).className.includes("trace-item")) as HTMLSpanElement;
selectedItem(props.data);
viewSpanDetail(dom);
}
function selectedItem(span: Span) {
traceStore.setSelectedSpan(span);
emits("selectedSpan", span);
}
function viewSpanDetail(dom: HTMLSpanElement) {
showSelectSpan(dom);
@@ -238,39 +273,25 @@ limitations under the License. -->
showDetail.value = true;
}
}
function hideActionBox() {
const item: HTMLSpanElement | null = document.querySelector("#trace-action-box");
if (item) {
item.style.display = "none";
}
}
watch(
() => appStore.theme,
() => {
const items: any = document.querySelectorAll(".trace-item");
const items: HTMLSpanElement[] = Array.from(document.querySelectorAll(".trace-item")) as HTMLSpanElement[];
for (const item of items) {
item.style.background = appStore.theme === Themes.Dark ? "#212224" : "#fff";
item.style.background = "transparent";
}
const p: any = document.getElementsByClassName("profiled")[0];
const p = document.getElementsByClassName("profiled")[0] as HTMLSpanElement | null;
if (p) {
p.style.background = appStore.theme === Themes.Dark ? "#333" : "#eee";
p.style.background = "var(--border-color-primary)";
}
},
);
return {
displayChildren,
outterPercent,
innerPercent,
isCrossThread,
viewSpanDetail,
toggle,
dateFormat,
showSelectSpan,
showDetail,
selectSpan,
selectedItem,
viewSpan,
t,
appStore,
TraceGraphType,
WidgetType,
};
},
});
</script>
<style lang="scss" scoped>
@import url("./table.scss");
@@ -290,6 +311,10 @@ limitations under the License. -->
}
}
.highlighted {
color: var(--el-color-primary);
}
.profiled {
background-color: var(--sw-table-header);
position: relative;
@@ -334,7 +359,6 @@ limitations under the License. -->
.trace-item {
white-space: nowrap;
position: relative;
cursor: pointer;
}
.trace-item.selected {
@@ -358,6 +382,7 @@ limitations under the License. -->
.trace-item > div.method {
padding-left: 10px;
cursor: pointer;
}
.trace-item div.exec-percent {
@@ -385,4 +410,8 @@ limitations under the License. -->
top: 1px;
}
}
.link-span {
text-decoration: underline;
}
</style>

View File

@@ -48,7 +48,7 @@ export const ProfileConstant = [
label: "application",
value: "Operation",
},
];
] as const;
export const TraceConstant = [
{
@@ -83,7 +83,7 @@ export const TraceConstant = [
label: "application",
value: "Attached Events",
},
];
] as const;
export const StatisticsConstant = [
{
@@ -121,4 +121,4 @@ export const StatisticsConstant = [
value: "Hits",
key: "count",
},
];
] as const;

View File

@@ -13,7 +13,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div class="flex-h row">
<div class="flex-h row" style="justify-content: space-between">
<div class="flex-h">
<div class="mr-10 flex-h" v-if="dashboardStore.entity === EntityType[1].value">
<span class="grey mr-5 label">{{ t("service") }}:</span>
<Selector
@@ -56,10 +57,13 @@ limitations under the License. -->
@change="changeField('status', $event)"
/>
</div>
<el-button size="small" type="primary" @click="searchTraces" class="search-btn">
{{ t("search") }}
</div>
<div class="mr-10">
<el-button type="primary" @click="searchTraces" :loading="traceStore.loading">
{{ t("runQuery") }}
</el-button>
</div>
</div>
<div class="flex-h row">
<div class="mr-10">
<span class="grey mr-5">{{ t("traceID") }}:</span>
@@ -72,7 +76,7 @@ limitations under the License. -->
<el-input size="small" class="inputs" v-model="maxTraceDuration" type="number" />
</div>
<div>
<span class="sm b grey mr-5">{{ t("timeRange") }}:</span>
<span class="sm b grey mr-5">{{ t("timeRange") }}</span>
<TimePicker
:value="[durationRow.start, durationRow.end]"
:maxRange="maxRange"
@@ -91,15 +95,15 @@ limitations under the License. -->
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import type { Option, DurationTime, Duration } from "@/types/app";
import { useTraceStore } from "@/store/modules/trace";
import { useTraceStore, PageSize } from "@/store/modules/trace";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useAppStoreWithOut, InitializationDurationRow } from "@/store/modules/app";
import { useSelectorStore } from "@/store/modules/selectors";
import timeFormat from "@/utils/timeFormat";
import ConditionTags from "@/views/components/ConditionTags.vue";
import { ElMessage } from "element-plus";
import { EntityType, QueryOrders, Status } from "../../data";
import type { LayoutConfig } from "@/types/dashboard";
import { EntityType, QueryOrders, Status } from "@/views/dashboard/data";
import type { LayoutConfig, FilterDuration } from "@/types/dashboard";
import { useDuration } from "@/hooks/useDuration";
/*global defineProps, defineEmits, Recordable */
@@ -108,7 +112,7 @@ limitations under the License. -->
needQuery: { type: Boolean, default: true },
data: {
type: Object as PropType<LayoutConfig>,
default: () => ({ graph: {} }),
default: () => ({}),
},
});
const { t } = useI18n();
@@ -117,20 +121,19 @@ limitations under the License. -->
const dashboardStore = useDashboardStore();
const traceStore: ReturnType<typeof useTraceStore> = useTraceStore();
const { setDurationRow, getDurationTime, getMaxRange } = useDuration();
const filters = reactive<Recordable>(props.data.filters || {});
const traceId = ref<string>(filters.traceId || "");
const { duration: filtersDuration } = filters;
const duration = ref<DurationTime>(
const filters = computed(() => props.data.filters || {});
const traceId = ref<string>(filters.value.traceId || "");
const { duration: filtersDuration } = filters.value;
const duration = ref<DurationTime | FilterDuration>(
filtersDuration
? { start: filtersDuration.startTime || "", end: filtersDuration.endTime || "", step: filtersDuration.step || "" }
: getDurationTime(),
);
const minTraceDuration = ref<number>();
const maxTraceDuration = ref<number>();
const tagsList = ref<string[]>([]);
const tagsMap = ref<Option[]>([]);
const state = reactive<Recordable>({
status: filters.status === "ERROR" ? Status[2] : Status[0],
status: filters.value.status === "ERROR" ? Status[2] : Status[0],
instance: { value: "0", label: "All" },
endpoint: { value: "0", label: "All" },
service: { value: "", label: "" },
@@ -139,9 +142,9 @@ limitations under the License. -->
const maxRange = computed(() =>
getMaxRange(appStore.coldStageMode ? appStore.recordsTTL?.coldTrace || 0 : appStore.recordsTTL?.trace || 0),
);
if (filters.queryOrder) {
if (filters.value.queryOrder) {
traceStore.setTraceCondition({
queryOrder: filters.queryOrder,
queryOrder: filters.value.queryOrder,
});
}
if (props.needQuery) {
@@ -149,7 +152,7 @@ limitations under the License. -->
}
async function init() {
duration.value = filters.duration || appStore.durationTime;
duration.value = filters.value.duration || appStore.durationTime;
if (dashboardStore.entity === EntityType[1].value) {
await getServices();
}
@@ -221,8 +224,10 @@ limitations under the License. -->
minTraceDuration: Number(minTraceDuration.value),
maxTraceDuration: Number(maxTraceDuration.value),
traceId: traceId.value || undefined,
paging: { pageNum: 1, pageSize: 20 },
};
if (!traceStore.hasQueryTracesV2Support) {
param.paging = { pageNum: 1, pageSize: PageSize };
}
if (props.data.filters && props.data.filters.id) {
param = {
...param,
@@ -267,8 +272,7 @@ limitations under the License. -->
emits("get", state.service.id);
}
}
function updateTags(data: { tagsMap: Array<Option>; tagsList: string[] }) {
tagsList.value = data.tagsList;
function updateTags(data: { tagsMap: Array<Option> }) {
tagsMap.value = data.tagsMap;
}
async function searchEndpoints(keyword: string) {
@@ -345,21 +349,12 @@ limitations under the License. -->
.row {
margin-bottom: 5px;
position: relative;
}
.traceId {
width: 270px;
}
.search-btn {
cursor: pointer;
width: 80px;
position: absolute;
top: 0;
right: 10px;
}
.label {
line-height: 24px;
}

View File

@@ -91,7 +91,7 @@ limitations under the License. -->
import timeFormat from "@/utils/timeFormat";
import ConditionTags from "@/views/components/ConditionTags.vue";
import { ElMessage } from "element-plus";
import { EntityType, QueryOrders, Status } from "../../data";
import { EntityType, QueryOrders, Status } from "@/views/dashboard/data";
import type { LayoutConfig } from "@/types/dashboard";
const FiltersKeys: { [key: string]: string } = {

View File

@@ -83,9 +83,9 @@ limitations under the License. -->
<script lang="ts" setup>
import { ref, computed } from "vue";
import { useI18n } from "vue-i18n";
import { useTraceStore } from "@/store/modules/trace";
import { useTraceStore, PageSize } from "@/store/modules/trace";
import { ElMessage } from "element-plus";
import { QueryOrders } from "../../data";
import { QueryOrders } from "@/views/dashboard/data";
import type { Option } from "@/types/app";
import type { Trace } from "@/types/trace";
import { dateFormat } from "@/utils/dateFormat";
@@ -94,7 +94,7 @@ limitations under the License. -->
const traceStore = useTraceStore();
const loading = ref<boolean>(false);
const selectedKey = ref<string>("");
const pageSize = 20;
const pageSize = PageSize;
const total = computed(() =>
traceStore.traceList.length === pageSize
? pageSize * traceStore.conditions.paging.pageNum + 1

View File

@@ -13,14 +13,16 @@ limitations under the License. -->
<template>
<div class="trace-detail" v-loading="loading">
<div class="trace-detail-wrapper clear" v-if="traceStore.currentTrace?.endpointNames">
<div class="flex-h" style="justify-content: space-between">
<h5 class="mb-5 mt-0">
<span class="vm">{{ traceStore.currentTrace?.endpointNames?.[0] }}</span>
<div class="trace-log-btn">
<el-button size="small" class="mr-10" type="primary" @click="searchTraceLogs">
</h5>
<div>
<el-button size="small" class="mr-10" @click="searchTraceLogs">
{{ t("viewLogs") }}
</el-button>
</div>
</h5>
</div>
<div class="mb-5 blue">
<Selector
size="small"
@@ -46,33 +48,13 @@ limitations under the License. -->
<div class="tag mr-5">{{ t("spans") }}</div>
<span class="sm">{{ traceStore.traceSpans.length }}</span>
</div>
<div>
<el-button class="grey" size="small" :class="{ ghost: displayMode !== 'List' }" @click="displayMode = 'List'">
<Icon class="mr-5" size="sm" iconName="list-bulleted" />
{{ t("list") }}
</el-button>
<el-button class="grey" size="small" :class="{ ghost: displayMode !== 'Tree' }" @click="displayMode = 'Tree'">
<Icon class="mr-5" size="sm" iconName="issue-child" />
{{ t("tree") }}
</el-button>
<el-button
class="grey"
size="small"
:class="{ ghost: displayMode !== 'Table' }"
@click="displayMode = 'Table'"
>
<Icon class="mr-5" size="sm" iconName="table" />
{{ t("table") }}
</el-button>
<el-button
class="grey"
size="small"
:class="{ ghost: displayMode !== 'Statistics' }"
@click="displayMode = 'Statistics'"
>
<Icon class="mr-5" size="sm" iconName="statistics-bulleted" />
{{ t("statistics") }}
</el-button>
<div class="flex-h trace-type item">
<el-radio-group v-model="spansGraphType" size="small">
<el-radio-button v-for="option in GraphTypeOptions" :key="option.value" :value="option.value">
<Icon :iconName="option.icon" />
{{ t(option.label) }}
</el-radio-button>
</el-radio-group>
</div>
</div>
</div>
@@ -80,7 +62,7 @@ limitations under the License. -->
<div class="trace-chart">
<component
v-if="traceStore.currentTrace?.endpointNames"
:is="displayMode"
:is="spansGraphType"
:data="traceStore.traceSpans"
:traceId="traceStore.currentTrace?.traceIds?.[0]?.value"
:showBtnDetail="false"
@@ -95,13 +77,14 @@ limitations under the License. -->
import { useTraceStore } from "@/store/modules/trace";
import type { Option } from "@/types/app";
import copy from "@/utils/copy";
import graphs from "./components/index";
import graphs from "../VisGraph/index";
import { ElMessage } from "element-plus";
import getDashboard from "@/hooks/useDashboardsSession";
import type { LayoutConfig } from "@/types/dashboard";
import { dateFormat } from "@/utils/dateFormat";
import { useAppStoreWithOut } from "@/store/modules/app";
import { WidgetType } from "@/views/dashboard/data";
import { GraphTypeOptions } from "../VisGraph/constant";
const props = {
serviceId: { type: String, default: "" },
@@ -120,7 +103,7 @@ limitations under the License. -->
const traceStore = useTraceStore();
const loading = ref<boolean>(false);
const traceId = ref<string>("");
const displayMode = ref<string>("List");
const spansGraphType = ref<string>("List");
function handleClick() {
copy(traceId.value || traceStore.currentTrace?.traceIds?.[0]?.value);
@@ -150,7 +133,6 @@ limitations under the License. -->
}
return {
traceStore,
displayMode,
dateFormat,
changeTraceId,
handleClick,
@@ -160,6 +142,8 @@ limitations under the License. -->
loading,
traceId,
WidgetType,
spansGraphType,
GraphTypeOptions,
};
},
});
@@ -196,6 +180,7 @@ limitations under the License. -->
.item {
justify-content: space-between;
align-items: center;
}
.trace-detail-ids {
@@ -208,10 +193,6 @@ limitations under the License. -->
width: 300px;
}
.trace-log-btn {
float: right;
}
.tag {
display: inline-block;
border-radius: 4px;
@@ -225,4 +206,16 @@ limitations under the License. -->
width: 100%;
text-align: center;
}
.trace-type {
// padding: 10px 0 10px 10px;
border-bottom: 1px solid $border-color-primary;
/* Make radio buttons content align nicely with icons */
:deep(.el-radio-button__inner) {
display: inline-flex;
align-items: center;
gap: 5px;
}
}
</style>

View File

@@ -0,0 +1,47 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div class="trace-query flex-v">
<TracesTable />
</div>
</template>
<script lang="ts" setup>
import { onUnmounted } from "vue";
import type { PropType } from "vue";
import type { LayoutConfig } from "@/types/dashboard";
import { useTraceStore } from "@/store/modules/trace";
import TracesTable from "./TracesTable.vue";
/*global defineProps */
defineProps({
data: {
type: Object as PropType<LayoutConfig>,
default: () => ({}),
},
});
const traceStore = useTraceStore();
onUnmounted(() => {
traceStore.setTraceList([]);
traceStore.setSelectedSpan(null);
});
</script>
<style lang="scss" scoped>
.trace-query {
width: 100%;
font-size: $font-size-smaller;
overflow: auto;
height: 100%;
}
</style>

View File

@@ -0,0 +1,107 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div class="trace-min-timeline">
<div class="timeline-marker-fixed">
<svg width="100%" height="20px">
<MinTimelineMarker :minTimestamp="minTimestamp" :maxTimestamp="maxTimestamp" :lineHeight="20" />
</svg>
</div>
<div
class="timeline-content scroll_bar_style"
:style="{ paddingRight: (trace.spans.length + 1) * rowHeight < 200 ? '20px' : '14px' }"
>
<svg ref="svgEle" width="100%" :height="`${(trace.spans.length + 1) * rowHeight}px`">
<MinTimelineOverlay
:minTimestamp="minTimestamp"
:maxTimestamp="maxTimestamp"
@setSelectedMinTimestamp="setSelectedMinTimestamp"
@setSelectedMaxTimestamp="setSelectedMaxTimestamp"
/>
<MinTimelineSelector
:minTimestamp="minTimestamp"
:maxTimestamp="maxTimestamp"
:selectedMinTimestamp="selectedMinTimestamp"
:selectedMaxTimestamp="selectedMaxTimestamp"
@setSelectedMinTimestamp="setSelectedMinTimestamp"
@setSelectedMaxTimestamp="setSelectedMaxTimestamp"
/>
<g
v-for="(item, index) in trace.spans"
:key="index"
:transform="`translate(0, ${(index + 1) * rowHeight + 3})`"
>
<SpanNode :span="item" :minTimestamp="minTimestamp" :maxTimestamp="maxTimestamp" :depth="index + 1" />
</g>
</svg>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import type { Trace } from "@/types/trace";
import SpanNode from "./SpanNode.vue";
import MinTimelineMarker from "./MinTimelineMarker.vue";
import MinTimelineOverlay from "./MinTimelineOverlay.vue";
import MinTimelineSelector from "./MinTimelineSelector.vue";
interface Props {
trace: Trace;
minTimestamp: number;
maxTimestamp: number;
}
const props = defineProps<Props>();
const svgEle = ref<SVGSVGElement | null>(null);
const rowHeight = 12;
const selectedMinTimestamp = ref<number>(props.minTimestamp);
const selectedMaxTimestamp = ref<number>(props.maxTimestamp);
const emit = defineEmits(["updateSelectedMaxTimestamp", "updateSelectedMinTimestamp"]);
const setSelectedMinTimestamp = (value: number) => {
selectedMinTimestamp.value = value;
emit("updateSelectedMinTimestamp", value);
};
const setSelectedMaxTimestamp = (value: number) => {
selectedMaxTimestamp.value = value;
emit("updateSelectedMaxTimestamp", value);
};
</script>
<style lang="scss" scoped>
.trace-min-timeline {
width: 100%;
max-height: 200px;
border-bottom: 1px solid var(--el-border-color-light);
display: flex;
flex-direction: column;
}
.timeline-marker-fixed {
width: 100%;
padding-right: 20px;
padding-top: 5px;
background: var(--el-bg-color);
border-bottom: 1px solid var(--el-border-color-light);
z-index: 1;
}
.timeline-content {
flex: 1;
width: 100%;
overflow: auto;
}
</style>

View File

@@ -0,0 +1,57 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<g v-for="(marker, index) in markers" :key="marker.duration">
<line
:x1="`${marker.position}%`"
:y1="0"
:x2="`${marker.position}%`"
:y2="lineHeight ? `${lineHeight}` : '100%'"
stroke="var(--el-border-color-light)"
/>
<text
:key="`label-${marker.duration}`"
:x="`${marker.position}%`"
:y="12"
font-size="10"
fill="var(--sw-font-grey-color)"
text-anchor="right"
:transform="`translate(${index === markers.length - 1 ? -50 : 5}, 0)`"
>
{{ marker.duration }}ms
</text>
</g>
</template>
<script lang="ts" setup>
import { computed } from "vue";
interface Props {
minTimestamp: number;
maxTimestamp: number;
lineHeight?: number | string;
}
const props = defineProps<Props>();
const markers = computed(() => {
const maxDuration = props.maxTimestamp - props.minTimestamp;
const markerDurations = [0, (maxDuration * 1) / 3, (maxDuration * 2) / 3, maxDuration];
return markerDurations.map((duration) => ({
duration: duration.toFixed(2),
position: maxDuration > 0 ? (duration / maxDuration) * 100 : 0,
}));
});
</script>
<style scoped></style>

View File

@@ -0,0 +1,147 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<g>
<rect
v-if="mouseDownX !== undefined && currentX !== undefined"
:x="`${Math.min(mouseDownX, currentX)}%`"
y="0"
:width="`${Math.abs(mouseDownX - currentX)}%`"
height="100%"
fill="var(--el-color-primary-light-5)"
fill-opacity="0.2"
pointer-events="none"
/>
<rect
ref="rootEl"
x="0"
y="0"
width="100%"
height="100%"
@mousedown="handleMouseDown"
@mousemove="handleMouseHoverMove"
@mouseleave="handleMouseHoverLeave"
fill-opacity="0"
cursor="col-resize"
/>
<line
v-if="hoverX"
:x1="`${hoverX}%`"
:y1="0"
:x2="`${hoverX}%`"
y2="100%"
stroke="var(--el-color-primary-light-5)"
stroke-width="1"
pointer-events="none"
/>
</g>
</template>
<script lang="ts" setup>
import { onBeforeUnmount, ref } from "vue";
interface Props {
minTimestamp: number;
maxTimestamp: number;
}
interface Emits {
(e: "setSelectedMaxTimestamp", value: number): void;
(e: "setSelectedMinTimestamp", value: number): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const rootEl = ref<SVGRectElement | null>(null);
const mouseDownX = ref<number | undefined>(undefined);
const currentX = ref<number | undefined>(undefined);
const hoverX = ref<number | undefined>(undefined);
const mouseDownXRef = ref<number | undefined>(undefined);
const isDragging = ref(false);
function handleMouseMove(e: MouseEvent) {
if (!rootEl.value) {
return;
}
const x = calculateX(rootEl.value.getBoundingClientRect(), e.pageX);
currentX.value = x;
}
function handleMouseUp(e: MouseEvent) {
if (!isDragging.value || !rootEl.value || mouseDownXRef.value === undefined) {
return;
}
const x = calculateX(rootEl.value.getBoundingClientRect(), e.pageX);
const adjustedX = Math.abs(x - mouseDownXRef.value) < 1 ? x + 1 : x;
const t1 = (mouseDownXRef.value / 100) * (props.maxTimestamp - props.minTimestamp) + props.minTimestamp;
const t2 = (adjustedX / 100) * (props.maxTimestamp - props.minTimestamp) + props.minTimestamp;
const newMinTimestmap = Math.min(t1, t2);
const newMaxTimestamp = Math.max(t1, t2);
emit("setSelectedMinTimestamp", newMinTimestmap);
emit("setSelectedMaxTimestamp", newMaxTimestamp);
currentX.value = undefined;
mouseDownX.value = undefined;
mouseDownXRef.value = undefined;
isDragging.value = false;
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
}
const calculateX = (parentRect: DOMRect, x: number) => {
const value = ((x - parentRect.left) / (parentRect.right - parentRect.left)) * 100;
if (value <= 0) {
return 0;
}
if (value >= 100) {
return 100;
}
return value;
};
function handleMouseDown(e: MouseEvent) {
if (!rootEl.value) {
return;
}
const x = calculateX(rootEl.value.getBoundingClientRect(), e.pageX);
currentX.value = x;
mouseDownX.value = x;
mouseDownXRef.value = x;
isDragging.value = true;
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
}
function handleMouseHoverMove(e: MouseEvent) {
if (e.buttons !== 0 || !rootEl.value) {
return;
}
const x = calculateX(rootEl.value.getBoundingClientRect(), e.pageX);
hoverX.value = x;
}
function handleMouseHoverLeave() {
hoverX.value = undefined;
}
onBeforeUnmount(() => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
isDragging.value = false;
});
</script>

View File

@@ -0,0 +1,157 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<g>
<rect
x="0"
y="0"
:width="`${boundaryLeft}%`"
height="100%"
fill="var(--el-color-primary-light-8)"
fill-opacity="0.6"
pointer-events="none"
/>
<rect
:x="`${boundaryRight}%`"
y="0"
:width="`${100 - boundaryRight}%`"
height="100%"
fill="var(--el-color-primary-light-8)"
fill-opacity="0.6"
pointer-events="none"
/>
<rect
:x="`${boundaryLeft}%`"
y="0"
width="3"
height="100%"
fill="var(--el-color-primary-light-5)"
transform="translate(-1)"
pointer-events="none"
/>
<rect
:x="`${boundaryRight}%`"
y="0"
width="3"
height="100%"
fill="var(--el-color-primary-light-5)"
transform="translate(-1)"
pointer-events="none"
/>
<rect
v-if="minMouseDownX !== undefined && minCurrentX !== undefined"
:x="`${Math.min(minMouseDownX, minCurrentX)}%`"
y="0"
:width="`${Math.abs(minMouseDownX - minCurrentX)}%`"
height="100%"
fill="var(--el-color-primary-light-6)"
fill-opacity="0.4"
pointer-events="none"
/>
<rect
v-if="maxMouseDownX !== undefined && maxCurrentX !== undefined"
:x="`${Math.min(maxMouseDownX, maxCurrentX)}%`"
y="0"
:width="`${Math.abs(maxMouseDownX - maxCurrentX)}%`"
height="100%"
fill="var(--el-color-primary-light-6)"
fill-opacity="0.4"
pointer-events="none"
/>
<rect
:x="`${boundaryLeft}%`"
y="0"
width="6"
height="40%"
fill="var(--el-color-primary)"
@mousedown="minRangeHandler.onMouseDown"
cursor="pointer"
transform="translate(-3)"
/>
<rect
:x="`${boundaryRight}%`"
y="0"
width="6"
height="40%"
fill="var(--el-color-primary)"
@mousedown="maxRangeHandler.onMouseDown"
cursor="pointer"
transform="translate(-3)"
/>
</g>
</template>
<script lang="ts" setup>
import { computed, ref, onMounted } from "vue";
import { useRangeTimestampHandler, adjustPercentValue } from "./useHooks";
interface Props {
minTimestamp: number;
maxTimestamp: number;
selectedMinTimestamp: number;
selectedMaxTimestamp: number;
}
const emit = defineEmits<{
(e: "setSelectedMinTimestamp", value: number): void;
(e: "setSelectedMaxTimestamp", value: number): void;
}>();
const props = defineProps<Props>();
const svgEle = ref<SVGSVGElement | null>(null);
onMounted(() => {
const element = document.querySelector(".trace-min-timeline svg") as SVGSVGElement;
if (element) {
svgEle.value = element;
}
});
const maxOpositeX = computed(
() => ((props.selectedMaxTimestamp - props.minTimestamp) / (props.maxTimestamp - props.minTimestamp)) * 100,
);
const minOpositeX = computed(
() => ((props.selectedMinTimestamp - props.minTimestamp) / (props.maxTimestamp - props.minTimestamp)) * 100,
);
const minRangeHandler = computed(() => {
return useRangeTimestampHandler({
rootEl: svgEle.value,
minTimestamp: props.minTimestamp,
maxTimestamp: props.maxTimestamp,
selectedTimestamp: props.selectedMaxTimestamp,
isSmallerThanOpositeX: true,
setTimestamp: (value) => emit("setSelectedMinTimestamp", value),
});
});
const maxRangeHandler = computed(() =>
useRangeTimestampHandler({
rootEl: svgEle.value,
minTimestamp: props.minTimestamp,
maxTimestamp: props.maxTimestamp,
selectedTimestamp: props.selectedMinTimestamp,
isSmallerThanOpositeX: false,
setTimestamp: (value) => emit("setSelectedMaxTimestamp", value),
}),
);
const boundaryLeft = computed(() => {
return adjustPercentValue(minOpositeX.value);
});
const boundaryRight = computed(() => adjustPercentValue(maxOpositeX.value));
const minMouseDownX = computed(() => minRangeHandler.value.mouseDownX.value);
const minCurrentX = computed(() => minRangeHandler.value.currentX.value);
const maxMouseDownX = computed(() => maxRangeHandler.value.mouseDownX.value);
const maxCurrentX = computed(() => maxRangeHandler.value.currentX.value);
</script>

View File

@@ -0,0 +1,120 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<rect :x="`${startPct}%`" :y="0" :width="`${widthPct}%`" :height="barHeight" :fill="barColor" rx="2" ry="2" />
<!-- Label and Duration Text -->
<text
v-if="showLabel"
:x="0"
:y="barHeight - 7"
font-size="10"
fill="var(--sw-font-grey-color)"
text-anchor="start"
class="span-label"
>
{{ span.label || "Unknown" }}
</text>
<text
v-if="showDuration"
:x="`${100}%`"
:y="barHeight - 7"
font-size="10"
fill="var(--sw-font-grey-color)"
text-anchor="end"
class="span-duration"
>
{{ span.duration }}ms
</text>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import type { Span } from "@/types/trace";
import { getServiceColor } from "@/utils/color";
interface Props {
span: Span;
minTimestamp: number;
maxTimestamp: number;
depth: number;
showDuration?: boolean;
showLabel?: boolean;
selectedMaxTimestamp?: number;
selectedMinTimestamp?: number;
}
const props = defineProps<Props>();
const barHeight = 3;
const widthScale = computed(() => {
const { selectedMinTimestamp, selectedMaxTimestamp, minTimestamp, maxTimestamp } = props;
let max = maxTimestamp - minTimestamp;
if (selectedMaxTimestamp !== undefined && selectedMinTimestamp !== undefined) {
max = selectedMaxTimestamp - selectedMinTimestamp;
}
return (duration: number | undefined | null) => {
const d = Math.max(0, duration || 0);
return (d / max) * 100;
};
});
const startPct = computed(() => {
const { span, selectedMinTimestamp, minTimestamp } = props;
const end = span.endTime;
let start = span.startTime;
if (selectedMinTimestamp !== undefined) {
start = selectedMinTimestamp > start ? (end < selectedMinTimestamp ? 0 : selectedMinTimestamp) : start;
}
const dur = start - (selectedMinTimestamp || minTimestamp);
return Math.max(0, widthScale.value(dur));
});
const widthPct = computed(() => {
const { span, selectedMinTimestamp, selectedMaxTimestamp } = props;
let start = span.startTime;
let end = span.endTime;
if (selectedMinTimestamp !== undefined) {
start = selectedMinTimestamp > start ? selectedMinTimestamp : start;
if (end < selectedMinTimestamp) {
return 0;
}
}
if (selectedMaxTimestamp !== undefined) {
end = selectedMaxTimestamp < end ? selectedMaxTimestamp : end;
if (span.startTime > selectedMaxTimestamp) {
return 0;
}
}
const dur = end - start;
return Math.max(0, widthScale.value(dur));
});
const barColor = computed(() => {
const serviceName = props.span.serviceCode || "";
return getServiceColor(serviceName);
});
</script>
<style lang="scss" scoped>
.span-label {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-weight: 500;
}
.span-duration {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-weight: 400;
}
</style>

View File

@@ -0,0 +1,59 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div class="timeline-tool flex-h">
<div class="flex-h trace-type item">
<el-radio-group v-model="spansGraphType" size="small">
<el-radio-button v-for="option in GraphTypeOptions" :key="option.value" :value="option.value">
<Icon :iconName="option.icon" />
{{ t(option.label) }}
</el-radio-button>
</el-radio-group>
</div>
<div>
<el-button size="small" @click="onToggleMinTimeline">
<Icon iconName="sort" size="middle" />
</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { GraphTypeOptions } from "../VisGraph/constant";
const emit = defineEmits<{
(e: "toggleMinTimeline"): void;
(e: "updateSpansGraphType", value: string): void;
}>();
const { t } = useI18n();
const spansGraphType = ref<string>(GraphTypeOptions[2].value);
function onToggleMinTimeline() {
emit("toggleMinTimeline");
}
// Watch for changes in spansGraphType and emit to parent
watch(spansGraphType, (newValue) => {
emit("updateSpansGraphType", newValue);
});
</script>
<style lang="scss" scoped>
.timeline-tool {
justify-content: space-between;
padding: 10px 5px;
border-bottom: 1px solid var(--el-border-color-light);
}
</style>

View File

@@ -0,0 +1,175 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div class="trace-query-content">
<div class="trace-info">
<div class="flex-h" style="justify-content: space-between">
<h3>{{ trace.label }}</h3>
<div>
<el-dropdown @command="handleDownload" trigger="click">
<el-button size="small">
{{ t("download") }}
<el-icon class="el-icon--right"><arrow-down /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="json">Download JSON</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
<div class="trace-meta flex-h">
<div>
<span class="grey mr-5">{{ t("duration") }}</span>
<span class="value">{{ trace.duration }}ms</span>
</div>
<div>
<span class="grey mr-5">{{ t("services") }}</span>
<span class="value">{{ trace.serviceCode || "Unknown" }}</span>
</div>
<div>
<span class="grey mr-5">{{ t("totalSpans") }}</span>
<span class="value">{{ trace.spans?.length || 0 }}</span>
</div>
<div>
<span class="grey mr-5">{{ t("traceID") }}</span>
<span class="value">{{ trace.traceId }}</span>
</div>
</div>
</div>
<div class="flex-h">
<div class="detail-section-timeline flex-v">
<MinTimeline
v-show="minTimelineVisible"
:trace="trace"
:minTimestamp="minTimestamp"
:maxTimestamp="maxTimestamp"
@updateSelectedMaxTimestamp="handleSelectedMaxTimestamp"
@updateSelectedMinTimestamp="handleSelectedMinTimestamp"
/>
<TimelineTool @toggleMinTimeline="toggleMinTimeline" @updateSpansGraphType="handleSpansGraphTypeUpdate" />
<component
v-if="traceStore.currentTrace?.endpointNames"
:is="graphs[spansGraphType as keyof typeof graphs]"
:data="trace.spans"
:traceId="traceStore.currentTrace?.traceId"
:showBtnDetail="false"
:headerType="WidgetType.Trace"
:selectedMaxTimestamp="selectedMaxTimestamp"
:selectedMinTimestamp="selectedMinTimestamp"
:minTimestamp="minTimestamp"
:maxTimestamp="maxTimestamp"
/>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from "vue";
import { useI18n } from "vue-i18n";
import { ElMessage } from "element-plus";
import { ArrowDown } from "@element-plus/icons-vue";
import type { Trace } from "@/types/trace";
import MinTimeline from "./MinTimeline.vue";
import { saveFileAsJSON } from "@/utils/file";
import { useTraceStore } from "@/store/modules/trace";
import TimelineTool from "./TimelineTool.vue";
import graphs from "../VisGraph/index";
import { WidgetType } from "@/views/dashboard/data";
import { GraphTypeOptions } from "../VisGraph/constant";
interface Props {
trace: Trace;
}
const { t } = useI18n();
const props = defineProps<Props>();
const traceStore = useTraceStore();
// Time range like xScale domain [0, max]
const minTimestamp = computed(() => {
if (!props.trace.spans.length) return 0;
return Math.min(...props.trace.spans.map((s) => s.startTime || 0));
});
const maxTimestamp = computed(() => {
const timestamps = props.trace.spans.map((span) => span.endTime || 0);
if (timestamps.length === 0) return 0;
return Math.max(...timestamps);
});
const selectedMaxTimestamp = ref<number>(maxTimestamp.value);
const selectedMinTimestamp = ref<number>(minTimestamp.value);
const minTimelineVisible = ref<boolean>(true);
const spansGraphType = ref<string>(GraphTypeOptions[2].value);
function handleSelectedMaxTimestamp(value: number) {
selectedMaxTimestamp.value = value;
}
function handleSelectedMinTimestamp(value: number) {
selectedMinTimestamp.value = value;
}
function toggleMinTimeline() {
minTimelineVisible.value = !minTimelineVisible.value;
}
function handleSpansGraphTypeUpdate(value: string) {
spansGraphType.value = value;
}
function handleDownload() {
const trace = props.trace;
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const baseFilename = `trace-${trace.traceId}-${timestamp}`;
const spans = trace.spans.map((span) => {
const { duration, label, ...newSpan } = span;
return newSpan;
});
try {
saveFileAsJSON(spans, `${baseFilename}.json`);
ElMessage.success("Trace data downloaded as JSON");
} catch (error) {
console.error("Download error:", error);
ElMessage.error("Failed to download file");
}
}
</script>
<style lang="scss" scoped>
.trace-info {
padding-bottom: 15px;
border-bottom: 1px solid var(--el-border-color-light);
}
.trace-info h3 {
margin: 0 0 10px;
color: var(--el-text-color-primary);
font-size: 18px;
font-weight: 600;
}
.trace-meta {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.detail-section-timeline {
width: 100%;
}
</style>

View File

@@ -0,0 +1,379 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div class="flex-h result-header">
<div style="align-items: center"> {{ filteredTraces.length }} of {{ totalTraces }} Results </div>
<div class="flex-h" style="align-items: center">
<el-switch
v-model="expandAll"
size="large"
active-text="Expand All"
inactive-text="Collapse All"
class="mr-20"
@change="toggleAllExpansion"
/>
<Selector
placeholder="Service filters"
@change="changeServiceFilters"
:value="selectedServiceNames"
:options="serviceNames"
:multiple="true"
style="width: 500px"
/>
</div>
</div>
<div class="trace-query-table scroll_bar_style">
<el-table
ref="tableRef"
:data="filteredTraces"
:border="false"
:preserve-expanded-content="true"
:default-sort="{ prop: 'duration', order: 'descending' }"
style="width: 100%"
v-loading="traceStore.loading"
>
<el-table-column type="expand">
<template #default="props">
<div class="flex-h service-tags">
<el-tag
v-for="(value, key) in getEndpoints(props.row.spans)"
:key="key"
class="mr-5 cp"
disable-transitions
:style="{ backgroundColor: getServiceColor(value[0]), color: 'white', border: 'none' }"
@click="toggleServiceTags(value[0], props.row)"
>
{{ value[0] }}: {{ value[1] }}
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="Root" prop="label" />
<el-table-column label="Start Time" prop="timestamp" width="220">
<template #default="props">
{{ dateFormat(props.row.start) }}
</template>
</el-table-column>
<el-table-column label="Spans" prop="spans.length" width="100" />
<el-table-column label="Duration (ms)" prop="duration" width="200" sortable>
<template #default="props">
<div class="duration-cell">
<el-progress
:percentage="getDurationProgress(props.row.duration)"
:stroke-width="22"
:text-inside="true"
color="rgba(64, 158, 255, 0.4)"
>
<div class="duration-value">{{ props.row.duration || 0 }}ms</div>
</el-progress>
</div>
</template>
</el-table-column>
<el-table-column fixed="right" label="Operations" width="120">
<template #default="props">
<el-button size="small" @click="(e: MouseEvent) => handleShowTrace(e, props.row)"> Show </el-button>
</template>
</el-table-column>
</el-table>
<!-- Loading indicator -->
<div v-if="loading" class="loading-container">
<el-icon class="is-loading">
<Loading />
</el-icon>
<span>Loading more traces...</span>
</div>
<!-- Load more button (fallback) -->
<div v-else-if="hasMoreItems" class="load-more-container">
<el-button @click="loadMoreItems" :loading="loading">
Load More ({{ totalTraces - filteredTraces.length }} remaining)
</el-button>
</div>
</div>
<!-- Trace Details Dialog -->
<el-dialog
v-model="dialogVisible"
fullscreen
:close-on-click-modal="false"
:close-on-press-escape="true"
destroy-on-close
>
<TraceContent v-if="selectedTrace" :trace="selectedTrace" />
</el-dialog>
</template>
<script lang="ts" setup>
import { computed, ref, nextTick, watch, onMounted, onUnmounted } from "vue";
import { ElTable, ElDialog, ElIcon } from "element-plus";
import { Loading } from "@element-plus/icons-vue";
import { dateFormat } from "@/utils/dateFormat";
import { useTraceStore } from "@/store/modules/trace";
import type { Trace } from "@/types/trace";
import type { Option } from "@/types/app";
import { getServiceColor } from "@/utils/color";
import TraceContent from "./TraceContent.vue";
const PageSize = 10;
const traceStore = useTraceStore();
const expandAll = ref<boolean>(true);
const tableRef = ref<InstanceType<typeof ElTable>>();
const selectedServiceNames = ref<string[]>([]);
const dialogVisible = ref<boolean>(false);
const selectedTrace = ref<Trace | null>(null);
// Infinite scroll state
const loadedItemsCount = ref<number>(PageSize);
const loading = ref<boolean>(false);
const loadMoreThreshold = 100; // pixels from bottom to trigger load
// Calculate max duration for progress bar scaling
const maxDuration = computed(() => {
if (!traceStore.traceList.length) return 1;
const durations = traceStore.traceList.map((trace: Trace) => trace.duration || 0);
return Math.max(...durations);
});
// All filtered traces based on selected service names
const allFilteredTraces = computed<Trace[]>(() => {
const rows = traceStore.traceList as Trace[];
if (!selectedServiceNames.value.length) return rows;
const selected = new Set(selectedServiceNames.value);
return rows.filter((row) => {
const rowService = row?.serviceCode;
if (rowService && selected.has(rowService)) return true;
for (const s of row.spans || []) {
const name = s?.serviceCode;
if (name && selected.has(name)) return true;
}
return false;
});
});
const filteredTraces = computed<Trace[]>(() => {
return allFilteredTraces.value.slice(0, loadedItemsCount.value);
});
const totalTraces = computed<number>(() => allFilteredTraces.value.length);
const hasMoreItems = computed<boolean>(() => {
return loadedItemsCount.value < allFilteredTraces.value.length;
});
// Service names options for the header Selector
const serviceNames = computed<Option[]>(() => {
const names = new Set<string>();
const rows = traceStore.traceList as Trace[];
for (const row of rows) {
if (row?.serviceCode) {
names.add(row.serviceCode);
}
for (const s of row.spans || []) {
if (s?.serviceCode) {
names.add(s.serviceCode);
}
}
}
return Array.from(names)
.sort((a, b) => a.localeCompare(b))
.map((n) => ({ label: n, value: n }));
});
function getEndpoints(spans: Trace[]) {
const endpoints = new Map<string, number>();
for (const d of spans) {
endpoints.set(d.serviceCode, (endpoints.get(d.serviceCode) || 0) + 1);
}
return endpoints;
}
function handleShowTrace(e: MouseEvent, row: Trace) {
traceStore.setCurrentTrace(row);
selectedTrace.value = row;
dialogVisible.value = true;
}
function getDurationProgress(duration: number): number {
if (maxDuration.value === 0) return 0;
return Math.round((duration / maxDuration.value) * 100);
}
function toggleServiceTags(serviceName: string, row: Trace) {
selectedServiceNames.value = selectedServiceNames.value.includes(serviceName)
? selectedServiceNames.value.filter((name) => name !== serviceName)
: [...selectedServiceNames.value, serviceName];
// Expand the row to show the service tags
nextTick(() => {
if (tableRef.value) {
tableRef.value.toggleRowExpansion(row, true);
}
});
}
function changeServiceFilters(selected: Option[]) {
selectedServiceNames.value = selected.map((o) => String(o.value));
}
// Infinite scroll handlers
function loadMoreItems() {
if (loading.value || !hasMoreItems.value) return;
loading.value = true;
// Simulate loading delay for better UX
setTimeout(() => {
const nextBatch = Math.min(PageSize, allFilteredTraces.value.length - loadedItemsCount.value);
loadedItemsCount.value += nextBatch;
// Apply current expand state to all currently visible items
nextTick(() => {
if (tableRef.value) {
const allVisibleItems = filteredTraces.value;
for (const row of allVisibleItems) {
tableRef.value.toggleRowExpansion(row, expandAll.value);
}
}
});
loading.value = false;
}, 300);
}
function handleScroll(event: Event) {
const target = event.target as HTMLElement;
const { scrollTop, scrollHeight, clientHeight } = target;
// Check if user is near the bottom
if (scrollHeight - scrollTop - clientHeight < loadMoreThreshold && hasMoreItems.value && !loading.value) {
loadMoreItems();
}
}
// Reset loaded items when filters change
watch(selectedServiceNames, () => {
loadedItemsCount.value = PageSize;
// Apply expand state to newly visible items after filter change
nextTick(() => {
if (tableRef.value) {
const visibleItems = filteredTraces.value;
for (const row of visibleItems) {
tableRef.value.toggleRowExpansion(row, expandAll.value);
}
}
});
});
// Watch for expandAll state changes to apply to all visible items
watch(expandAll, () => {
nextTick(() => {
if (tableRef.value) {
const visibleItems = filteredTraces.value;
for (const row of visibleItems) {
tableRef.value.toggleRowExpansion(row, expandAll.value);
}
}
});
});
// Watch for trace list changes (when new query is executed) to reapply expand state
watch(
() => traceStore.traceList,
() => {
// Reset loaded items count when new data is loaded
loadedItemsCount.value = PageSize;
// Reapply expand state to newly loaded traces
nextTick(() => {
if (tableRef.value) {
const visibleItems = filteredTraces.value;
for (const row of visibleItems) {
tableRef.value.toggleRowExpansion(row, expandAll.value);
}
}
});
},
{ deep: true },
);
// Setup scroll listener
onMounted(() => {
const tableContainer = document.querySelector(".trace-query-table");
if (tableContainer) {
tableContainer.addEventListener("scroll", handleScroll);
}
});
onUnmounted(() => {
const tableContainer = document.querySelector(".trace-query-table");
if (tableContainer) {
tableContainer.removeEventListener("scroll", handleScroll);
}
});
// Toggle all table row expansions (only for loaded items)
function toggleAllExpansion() {
// The expandAll watcher will handle applying the state to all visible items
// This function just needs to trigger the change
}
</script>
<style lang="scss" scoped>
.trace-query-table {
width: 100%;
font-size: $font-size-smaller;
overflow: auto;
height: calc(100% - 100px);
}
.service-tags {
width: 100%;
padding-left: 60px;
}
.duration-cell {
display: flex;
flex-direction: column;
gap: 4px;
padding: 4px 0;
}
.duration-value {
font-size: $font-size-smaller;
font-weight: 500;
color: var(--el-text-color-primary);
}
.result-header {
justify-content: space-between;
padding: 20px 10px;
font-size: 14px;
}
.loading-container {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 20px 0;
color: var(--el-text-color-secondary);
font-size: $font-size-smaller;
}
.load-more-container {
display: flex;
justify-content: center;
padding: 20px 0;
border-top: 1px solid var(--el-border-color-light);
margin-top: 10px;
}
</style>

View File

@@ -0,0 +1,128 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ref, computed } from "vue";
export const adjustPercentValue = (value: number) => {
if (value <= 0) {
return 0;
}
if (value >= 100) {
return 100;
}
return value;
};
const calculateX = ({
parentRect,
x,
opositeX,
isSmallerThanOpositeX,
}: {
parentRect: DOMRect;
x: number;
opositeX: number;
isSmallerThanOpositeX: boolean;
}) => {
let value = ((x - parentRect.left) / (parentRect.right - parentRect.left)) * 100;
if (isSmallerThanOpositeX) {
if (value >= opositeX) {
value = opositeX - 1;
}
} else if (value <= opositeX) {
value = opositeX + 1;
}
return adjustPercentValue(value);
};
export const useRangeTimestampHandler = ({
rootEl,
minTimestamp,
maxTimestamp,
selectedTimestamp,
isSmallerThanOpositeX,
setTimestamp,
}: {
rootEl: SVGSVGElement | null;
minTimestamp: number;
maxTimestamp: number;
selectedTimestamp: number;
isSmallerThanOpositeX: boolean;
setTimestamp: (value: number) => void;
}) => {
const currentX = ref<number>();
const mouseDownX = ref<number>();
const isDragging = ref(false);
const selectedTimestampComputed = ref(selectedTimestamp);
const opositeX = computed(() => {
return ((selectedTimestampComputed.value - minTimestamp) / (maxTimestamp - minTimestamp)) * 100;
});
const onMouseMove = (e: MouseEvent) => {
if (!rootEl) {
return;
}
const x = calculateX({
parentRect: rootEl.getBoundingClientRect(),
x: e.pageX,
opositeX: opositeX.value,
isSmallerThanOpositeX,
});
currentX.value = x;
};
const onMouseUp = (e: MouseEvent) => {
if (!rootEl) {
return;
}
const x = calculateX({
parentRect: rootEl.getBoundingClientRect(),
x: e.pageX,
opositeX: opositeX.value,
isSmallerThanOpositeX,
});
const timestamp = (x / 100) * (maxTimestamp - minTimestamp) + minTimestamp;
selectedTimestampComputed.value = timestamp;
setTimestamp(timestamp);
currentX.value = undefined;
mouseDownX.value = undefined;
isDragging.value = false;
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
};
const onMouseDown = (e: MouseEvent) => {
if (!rootEl) {
return;
}
const x = calculateX({
parentRect: rootEl.getBoundingClientRect(),
x: (e.currentTarget as SVGRectElement).getBoundingClientRect().x + 3,
opositeX: opositeX.value,
isSmallerThanOpositeX,
});
currentX.value = x;
mouseDownX.value = x;
isDragging.value = true;
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
};
return { currentX, mouseDownX, onMouseDown, isDragging };
};

View File

@@ -17,56 +17,59 @@ limitations under the License. -->
class="charts-item mr-5"
v-for="(i, index) in traceStore.serviceList"
:key="index"
:style="`color:${computedScale(index)}`"
:style="`color:${getServiceColor(i)}`"
>
<Icon iconName="issue-open-m" class="mr-5" size="sm" />
<span>{{ i }}</span>
</span>
<el-button class="btn" size="small" type="primary" @click="downloadTrace">
{{ t("exportImage") }}
<el-button class="btn" size="small" @click="downloadTrace">
<Icon iconName="download" size="sm" />
</el-button>
</div>
<div class="list">
<Graph :data="data" :traceId="traceId" :type="TraceGraphType.LIST" />
<Graph
:data="data"
:traceId="traceId"
:type="TraceGraphType.LIST"
:selectedMaxTimestamp="selectedMaxTimestamp"
:selectedMinTimestamp="selectedMinTimestamp"
:minTimestamp="minTimestamp"
:maxTimestamp="maxTimestamp"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import * as d3 from "d3";
import { useAppStoreWithOut } from "@/store/modules/app";
import { useTraceStore } from "@/store/modules/trace";
import type { Span } from "@/types/trace";
import Graph from "./D3Graph/Index.vue";
import Graph from "../D3Graph/Index.vue";
import { Themes } from "@/constants/data";
import { TraceGraphType } from "./constant";
import { getServiceColor } from "@/utils/color";
/* global defineProps, Recordable*/
defineProps({
data: { type: Array as PropType<Span[]>, default: () => [] },
traceId: { type: String, default: "" },
});
const { t } = useI18n();
/* global defineProps, Indexable*/
type Props = {
data: Span[];
traceId: string;
selectedMaxTimestamp?: number;
selectedMinTimestamp?: number;
minTimestamp: number;
maxTimestamp: number;
};
defineProps<Props>();
const appStore = useAppStoreWithOut();
const traceStore = useTraceStore();
function computedScale(i: number) {
const sequentialScale = d3
.scaleSequential()
.domain([0, traceStore.serviceList.length + 1])
.interpolator(d3.interpolateCool);
return sequentialScale(i);
}
function downloadTrace() {
const serializer = new XMLSerializer();
const svgNode: any = d3.select(".trace-list").node();
const source = `<?xml version="1.0" standalone="no"?>\r\n${serializer.serializeToString(svgNode)}`;
const canvas = document.createElement("canvas");
const context: any = canvas.getContext("2d");
canvas.width = (d3.select(".trace-list") as Recordable)._groups[0][0].clientWidth;
canvas.height = (d3.select(".trace-list") as Recordable)._groups[0][0].clientHeight;
canvas.width = (d3.select(".trace-list") as Indexable)._groups[0][0].clientWidth;
canvas.height = (d3.select(".trace-list") as Indexable)._groups[0][0].clientHeight;
context.fillStyle = appStore.theme === Themes.Dark ? "#212224" : `#fff`;
context.fillRect(0, 0, canvas.width, canvas.height);
const image = new Image();
@@ -94,6 +97,7 @@ limitations under the License. -->
border: 1px solid;
font-size: 11px;
border-radius: 4px;
margin: 3px;
}
.btn {

View File

@@ -17,29 +17,39 @@ limitations under the License. -->
<div class="trace-t-loading" v-show="loading">
<Icon iconName="spinner" size="sm" />
</div>
<TableContainer :tableData="tableData" :type="TraceGraphType.STATISTICS" :headerType="headerType">
<TableContainer
:tableData="tableData"
:type="TraceGraphType.STATISTICS"
:headerType="headerType"
:traceId="traceId"
>
<div class="trace-tips" v-if="!tableData.length">{{ $t("noData") }}</div>
</TableContainer>
</div>
</template>
<script lang="ts" setup>
import { ref, watch, onMounted } from "vue";
import type { PropType } from "vue";
import TableContainer from "./Table/TableContainer.vue";
import traceTable from "../utils/trace-table";
import TableContainer from "../Table/TableContainer.vue";
import traceTable from "../D3Graph/utils/trace-table";
import type { StatisticsSpan, Span, StatisticsGroupRef } from "@/types/trace";
import { TraceGraphType } from "./constant";
/* global defineProps, defineEmits, Recordable*/
const props = defineProps({
data: { type: Array as PropType<Span[]>, default: () => [] },
traceId: { type: String, default: "" },
showBtnDetail: { type: Boolean, default: false },
headerType: { type: String, default: "" },
});
const emit = defineEmits(["load"]);
/* global defineProps, defineEmits*/
type Props = {
data: Span[];
traceId: string;
showBtnDetail: boolean;
headerType: string;
selectedMaxTimestamp: number;
selectedMinTimestamp: number;
};
type Emits = {
(e: "load", callback: () => void): void;
};
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const loading = ref<boolean>(true);
const tableData = ref<Recordable>([]);
const tableData = ref<any[]>([]);
const list = ref<any[]>([]);
onMounted(() => {

View File

@@ -18,24 +18,34 @@ limitations under the License. -->
:type="TraceGraphType.TABLE"
:headerType="headerType"
@select="getSelectedSpan"
:selectedMaxTimestamp="selectedMaxTimestamp"
:selectedMinTimestamp="selectedMinTimestamp"
:minTimestamp="minTimestamp"
:maxTimestamp="maxTimestamp"
/>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from "vue";
import type { Span } from "@/types/trace";
import type { SegmentSpan } from "@/types/profile";
import Graph from "./D3Graph/Index.vue";
import { TraceGraphType } from "./constant";
import Graph from "../D3Graph/Index.vue";
import { TraceGraphType } from "../VisGraph/constant";
defineProps({
data: { type: Array as PropType<(Span | SegmentSpan)[]>, default: () => [] },
traceId: { type: String, default: "" },
headerType: { type: String, default: "" },
});
const emits = defineEmits(["select"]);
type Props = {
data: SegmentSpan[];
traceId: string;
selectedMaxTimestamp?: number;
selectedMinTimestamp?: number;
minTimestamp: number;
maxTimestamp: number;
headerType?: string;
};
type Emits = {
(e: "select", value: SegmentSpan): void;
};
defineProps<Props>();
const emits = defineEmits<Emits>();
function getSelectedSpan(span: Span) {
function getSelectedSpan(span: SegmentSpan) {
emits("select", span);
}
</script>

View File

@@ -17,7 +17,7 @@ limitations under the License. -->
class="time-charts-item mr-5"
v-for="(i, index) in traceStore.serviceList"
:key="index"
:style="`color:${computedScale(index)}`"
:style="`color:${getServiceColor(i)}`"
>
<Icon iconName="issue-open-m" class="mr-5" size="sm" />
<span>{{ i }}</span>
@@ -35,40 +35,44 @@ limitations under the License. -->
</a>
</div>
<div class="trace-tree">
<Graph ref="charts" :data="data" :traceId="traceId" :type="TraceGraphType.TREE" />
<Graph
ref="charts"
:data="data"
:traceId="traceId"
:type="TraceGraphType.TREE"
:selectedMaxTimestamp="selectedMaxTimestamp"
:selectedMinTimestamp="selectedMinTimestamp"
:minTimestamp="minTimestamp"
:maxTimestamp="maxTimestamp"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import * as d3 from "d3";
import Graph from "./D3Graph/Index.vue";
import type { PropType } from "vue";
import Graph from "../D3Graph/Index.vue";
import type { Span } from "@/types/trace";
import { useI18n } from "vue-i18n";
import { ref } from "vue";
import { TraceGraphType } from "./constant";
import { useTraceStore } from "@/store/modules/trace";
import { getServiceColor } from "@/utils/color";
/* global defineProps */
defineProps({
data: { type: Array as PropType<Span[]>, default: () => [] },
traceId: { type: String, default: "" },
});
type Props = {
data: Span[];
traceId: string;
selectedMaxTimestamp?: number;
selectedMinTimestamp?: number;
minTimestamp: number;
maxTimestamp: number;
};
defineProps<Props>();
const { t } = useI18n();
const traceStore = useTraceStore();
const charts = ref<any>(null);
function computedScale(i: number) {
const sequentialScale = d3
.scaleSequential()
.domain([0, traceStore.serviceList.length + 1])
.interpolator(d3.interpolateCool);
return sequentialScale(i);
}
</script>
<style lang="scss" scoped>
.trace-tree {
height: 100%;
min-height: 400px;
overflow: auto;
}
@@ -84,7 +88,7 @@ limitations under the License. -->
.trace-tree-charts {
overflow: auto;
padding: 10px;
position: relative;
// position: relative;
height: 100%;
width: 100%;
}
@@ -95,5 +99,6 @@ limitations under the License. -->
border: 1px solid;
font-size: 11px;
border-radius: 4px;
margin: 3px;
}
</style>

View File

@@ -21,3 +21,9 @@ export enum TraceGraphType {
TABLE = "Table",
STATISTICS = "Statistics",
}
export const GraphTypeOptions = [
{ value: "List", icon: "list-bulleted", label: "list" },
{ value: "Tree", icon: "issue-child", label: "tree" },
{ value: "Table", icon: "table", label: "table" },
{ value: "Statistics", icon: "statistics-bulleted", label: "statistics" },
] as const;