feat: add Trace in dashboards (#18)

* feat: add trace tool

* feat: add trace

* feat: add trace filters

* feat: add trace list

* feat: add trace detail

* fix: update trace detail

* feat: add trace list

* fix: update trace list

* feat: add trace tree

* fix: update trace tree

* feat: add trace table

* feat: add trace statistics

* fix: update trace statistics

* fix: update resize

* feat: add trace log

* feat: add related logs

* feat: add loading

* fix: update name

* feat: watch selectors

* fix: view span on table

* fix ci

* fix: update file name

* fix: update file name

* fix: update file name

* fix: update filters

* build: add package
This commit is contained in:
Fine0830
2022-02-26 22:47:53 +08:00
committed by GitHub
parent 7fe0a57e49
commit 977ffbaf74
74 changed files with 5507 additions and 53 deletions

View File

@@ -0,0 +1,138 @@
<!-- 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" :class="{ light: theme === 'light' }">
<div class="mr-10 pt-5">
<span class="sm grey" v-show="theme === 'dark'">{{ t("tags") }}: </span>
<span
class="trace-tags"
:style="type === 'LOG' ? `min-width: 122px;` : ''"
>
<span class="selected" v-for="(item, index) in tagsList" :key="index">
<span>{{ item }}</span>
<span class="remove-icon" @click="removeTags(index)">×</span>
</span>
</span>
<el-input v-model="tags" class="trace-new-tag" @change="addLabels" />
<span class="tags-tip">
<a
target="blank"
href="https://github.com/apache/skywalking/blob/master/docs/en/setup/backend/configuration-vocabulary.md"
>
{{ t("tagsLink") }}
</a>
<el-tooltip :content="t('traceTagsTip')">
<span>
<Icon class="icon-help mr-5" iconName="help" size="middle" />
</span>
</el-tooltip>
<b>{{ t("noticeTag") }}</b>
</span>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { useI18n } from "vue-i18n";
/*global defineEmits, defineProps */
const emit = defineEmits(["update"]);
defineProps({
type: { type: String, default: "TRACE" },
});
const { t } = useI18n();
const theme = ref<string>("dark");
const type = ref<string>("");
const tags = ref<string>("");
const tagsList = ref<string[]>([]);
function removeTags(index: number) {
tagsList.value.splice(index, 1);
updateTags();
localStorage.setItem("traceTags", JSON.stringify(this.tagsList));
}
function addLabels() {
if (!tags.value) {
return;
}
tagsList.value.push(tags.value);
tags.value = "";
updateTags();
}
function updateTags() {
const tagsMap = tagsList.value.map((item: string) => {
const key = item.substring(0, item.indexOf("="));
return {
key,
value: item.substring(item.indexOf("=") + 1, item.length),
};
});
emit("update", { tagsMap, tagsList: tagsList.value });
}
</script>
<style lang="scss" scoped>
.trace-tags {
padding: 1px 5px 0 0;
border-radius: 3px;
height: 24px;
display: inline-block;
vertical-align: top;
}
.selected {
display: inline-block;
padding: 0 3px;
border-radius: 3px;
overflow: hidden;
border: 1px dashed #aaa;
font-size: 12px;
margin: 3px 2px 0 2px;
}
.trace-new-tag {
border-style: unset;
outline: 0;
padding: 2px 5px;
border-radius: 3px;
width: 250px;
margin-right: 3px;
}
.remove-icon {
display: inline-block;
margin-left: 3px;
cursor: pointer;
}
.tags-tip {
color: #a7aebb;
}
.light {
color: #3d444f;
input {
border: 1px solid #ccc;
}
.selected {
color: #3d444f;
}
}
.icon-help {
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,136 @@
<!-- 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="log">
<div class="log-header">
<template v-for="(item, index) in columns">
<div
class="method"
:style="`width: ${item.method}px`"
v-if="item.drag"
:key="index"
>
<span class="r cp" ref="dragger" :data-index="index">
<Icon iconName="settings_ethernet" size="sm" />
</span>
{{ t(item.value) }}
</div>
<div v-else :class="item.label" :key="`col${index}`">
{{ t(item.value) }}
</div>
</template>
</div>
<div v-if="type === 'browser'">
<LogBrowser
v-for="(item, index) in tableData"
:data="item"
:key="'browser' + index"
@select="setCurrentLog"
/>
</div>
<div v-else>
<LogService
v-for="(item, index) in tableData"
:data="item"
:key="'service' + index"
:noLink="noLink"
@select="setCurrentLog"
/>
</div>
<slot></slot>
<el-dialog
v-model="showDetail"
:destroy-on-close="true"
fullscreen
@closed="showDetail = false"
:title="t('logDetail')"
>
<LogDetail :currentLog="currentLog" />
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import { ServiceLogConstants, BrowserLogConstants } from "./data";
import LogBrowser from "./LogBrowser.vue";
import LogService from "./LogService.vue";
import LogDetail from "./LogDetail.vue";
/*global defineProps, Nullable */
const props = defineProps({
type: { type: String, default: "service" },
tableData: { type: Array, default: () => [] },
noLink: { type: Boolean, default: true },
});
const { t } = useI18n();
const currentLog = ref<any>({});
const showDetail = ref<boolean>(false);
const dragger = ref<Nullable<HTMLSpanElement>>(null);
// const method = ref<number>(380);
const columns: any[] =
props.type === "browser" ? BrowserLogConstants : ServiceLogConstants;
function setCurrentLog(log: any) {
showDetail.value = true;
currentLog.value = log;
}
</script>
<style lang="scss" scoped>
.log {
font-size: 12px;
height: 100%;
overflow: auto;
}
.log-header {
/*display: flex;*/
white-space: nowrap;
user-select: none;
border-left: 0;
border-right: 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
/*background-color: #f3f4f9;*/
.traceId {
width: 390px;
}
.content,
.tags {
width: 300px;
}
.serviceInstanceName,
.serviceName {
width: 200px;
}
}
.log-header div {
/*min-width: 140px;*/
width: 140px;
/*flex-grow: 1;*/
display: inline-block;
padding: 0 4px;
border: 1px solid transparent;
border-right: 1px dotted silver;
line-height: 30px;
background-color: #f3f4f9;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</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 @click="showSelectSpan" :class="['log-item', 'clearfix']" ref="logItem">
<div
v-for="(item, index) in columns"
:key="index"
:class="[
'method',
['message', 'stack'].includes(item.label) ? 'autoHeight' : '',
]"
:style="{
lineHeight: 1.3,
width: `${item.drag ? item.method : ''}px`,
}"
>
<span v-if="item.label === 'time'">{{ dateFormat(data.time) }}</span>
<span v-else-if="item.label === 'errorUrl'">{{ data.pagePath }}</span>
<span v-else v-tooltip:bottom="data[item.label] || '-'">{{
data[item.label] || "-"
}}</span>
</div>
</div>
</template>
<script lang="ts" setup>
import dayjs from "dayjs";
import { BrowserLogConstants } from "./data";
/*global defineProps, defineEmits, NodeListOf */
const props = defineProps({
data: { type: Array as any, default: () => [] },
});
const columns = BrowserLogConstants;
const emit = defineEmits(["select"]);
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
dayjs(date).format(pattern);
function showSelectSpan() {
const items: NodeListOf<any> = document.querySelectorAll(".log-item");
for (const item of items) {
item.style.background = "#fff";
}
const logItem: any = this.$refs.logItem;
logItem.style.background = "rgba(0, 0, 0, 0.1)";
emit("select", props.data);
}
</script>
<style lang="scss" scoped>
.log-item {
white-space: nowrap;
position: relative;
cursor: pointer;
}
.log-item.selected {
background: rgba(0, 0, 0, 0.04);
}
.log-item:not(.level0):hover {
background: rgba(0, 0, 0, 0.04);
}
.log-item:hover {
background: rgba(0, 0, 0, 0.04) !important;
}
.log-item > div {
width: 140px;
padding: 0 5px;
display: inline-block;
border: 1px solid transparent;
border-right: 1px dotted silver;
overflow: hidden;
line-height: 30px;
text-overflow: ellipsis;
white-space: nowrap;
}
.log-item .text {
width: 100% !important;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.log-item > div.method {
padding: 7px 5px;
line-height: 30px;
}
</style>

View File

@@ -0,0 +1,75 @@
<!-- 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="log-detail">
<div
class="mb-10 clear rk-flex"
v-for="(item, index) in columns"
:key="index"
>
<template>
<span class="g-sm-4 grey">{{ t(item.value) }}:</span>
<span v-if="item.label === 'timestamp'" class="g-sm-8">
{{ dateFormat(currentLog[item.label]) }}
</span>
<textarea
class="content"
:readonly="true"
v-else-if="item.label === 'content'"
:value="currentLog[item.label]"
/>
<span v-else-if="item.label === 'tags'" class="g-sm-8">
<div v-for="(d, index) in logTags" :key="index">{{ d }}</div>
</span>
<span v-else class="g-sm-8">{{ currentLog[item.label] }}</span>
</template>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import dayjs from "dayjs";
import { ServiceLogDetail } from "@/views/components/LogTable/data";
/*global defineProps */
const props = defineProps({
currentLog: { type: Object as PropType<any>, default: () => ({}) },
});
const { t } = useI18n();
const columns = ServiceLogDetail;
const logTags = computed(() => {
if (!props.currentLog.tags) {
return [];
}
return props.currentLog.tags.map((d: { key: string; value: string }) => {
return `${d.key} = ${d.value}`;
});
});
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
dayjs(date).format(pattern);
</script>
<style lang="scss" scoped>
.content {
max-width: 700px;
min-width: 500px;
min-height: 500px;
border: none;
outline: none;
color: #3d444f;
overflow: auto;
}
</style>

View File

@@ -0,0 +1,115 @@
<!-- 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 @click="showSelectSpan" class="log-item">
<div v-for="(item, index) in columns" :key="index" :class="item.label">
<span v-if="item.label === 'timestamp'">
{{ dateFormat(data.timestamp) }}
</span>
<span v-else-if="item.label === 'tags'">
{{ tags }}
</span>
<router-link
v-else-if="item.label === 'traceId' && !noLink"
:to="{ name: 'trace', query: { traceid: data[item.label] } }"
>
<span>{{ data[item.label] }}</span>
</router-link>
<span v-else>{{ data[item.label] }}</span>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import dayjs from "dayjs";
import { ServiceLogConstants } from "./data";
/*global defineProps, defineEmits */
const props = defineProps({
data: { type: Array as any, default: () => [] },
noLink: { type: Boolean, default: true },
});
const emit = defineEmits(["select"]);
const columns = ServiceLogConstants;
const tags = computed(() => {
if (!props.data.tags) {
return "";
}
return String(props.data.tags.map((d: any) => `${d.key}=${d.value}`));
});
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
dayjs(date).format(pattern);
function showSelectSpan() {
emit("select", props.data);
}
</script>
<style lang="scss" scoped>
.log-item {
white-space: nowrap;
position: relative;
cursor: pointer;
.traceId {
width: 390px;
color: #448dfe;
cursor: pointer;
span {
display: inline-block;
width: 100%;
line-height: 30px;
}
}
.content,
.tags {
width: 300px;
}
.serviceInstanceName,
.serviceName {
width: 200px;
}
}
.log-item:hover {
background: rgba(0, 0, 0, 0.04);
}
.log-item > div {
width: 140px;
padding: 0 5px;
display: inline-block;
border: 1px solid transparent;
border-right: 1px dotted silver;
overflow: hidden;
line-height: 30px;
text-overflow: ellipsis;
white-space: nowrap;
}
.log-item .text {
width: 100%;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.log-item > div.method {
height: 100%;
padding: 3px 8px;
}
</style>

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.
*/
export const ServiceLogConstants = [
{
label: "serviceName",
value: "service",
},
{
label: "serviceInstanceName",
value: "instance",
},
{
label: "timestamp",
value: "time",
},
{
label: "contentType",
value: "contentType",
},
{
label: "tags",
value: "tags",
},
{
label: "content",
value: "content",
},
{
label: "traceId",
value: "traceID",
},
];
export const ServiceLogDetail = [
{
label: "serviceName",
value: "currentService",
},
{
label: "serviceInstanceName",
value: "currentInstance",
},
{
label: "timestamp",
value: "time",
},
{
label: "contentType",
value: "contentType",
},
{
label: "traceId",
value: "traceID",
},
{
label: "tags",
value: "tags",
},
{
label: "content",
value: "content",
},
];
// The order of columns should be time, service, error, stack, version, url, catalog, and grade.
export const BrowserLogConstants = [
{
label: "service",
value: "service",
},
{
label: "serviceVersion",
value: "serviceVersion",
},
{
label: "errorUrl",
value: "errorPage",
},
{
label: "time",
value: "time",
},
{
label: "message",
value: "message",
drag: true,
method: 350,
},
{
label: "stack",
value: "stack",
drag: true,
method: 350,
},
// {
// label: 'pagePath',
// value: 'Page Path',
// },
{
label: "category",
value: "category",
},
{
label: "grade",
value: "grade",
},
];

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<WidgetOptions />
<TopologyOptions />
<StyleOptions />
<div class="footer">
<el-button size="small">
{{ t("cancel") }}
@@ -26,8 +26,8 @@ limitations under the License. -->
</template>
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import WidgetOptions from "./WidgetOptions.vue";
import TopologyOptions from "./graph-styles/TopologyItem.vue";
import WidgetOptions from "./components/WidgetOptions.vue";
import StyleOptions from "./topology/StyleOptions.vue";
import { useDashboardStore } from "@/store/modules/dashboard";
const { t } = useI18n();

View File

@@ -76,10 +76,10 @@ import { useDashboardStore } from "@/store/modules/dashboard";
import { useAppStoreWithOut } from "@/store/modules/app";
import { Option } from "@/types/app";
import graphs from "../graphs";
import configs from "./graph-styles";
import WidgetOptions from "./WidgetOptions.vue";
import StandardOptions from "./StandardOptions.vue";
import MetricOptions from "./MetricOptions.vue";
import configs from "./widget/graph-styles";
import WidgetOptions from "./components/WidgetOptions.vue";
import StandardOptions from "./widget/StandardOptions.vue";
import MetricOptions from "./widget/MetricOptions.vue";
export default defineComponent({
name: "ConfigEdit",

View File

@@ -95,7 +95,7 @@ import {
ChartTypes,
PodsChartTypes,
TableEntity,
} from "../data";
} from "../../data";
import { ElMessage } from "element-plus";
import Icon from "@/components/Icon.vue";
import { useQueryProcessor, useSourceProcessor } from "@/hooks/useProcessor";

View File

@@ -118,7 +118,7 @@ limitations under the License. -->
<script lang="ts" setup>
import { reactive } from "vue";
import { useI18n } from "vue-i18n";
import { SortOrder } from "../data";
import { SortOrder } from "../../data";
import { useDashboardStore } from "@/store/modules/dashboard";
const dashboardStore = useDashboardStore();

View File

@@ -0,0 +1,95 @@
<!-- 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-wrapper flex-v">
<el-popover placement="bottom" trigger="click" :width="100">
<template #reference>
<span class="delete cp">
<Icon iconName="ellipsis_v" size="middle" class="operation" />
</span>
</template>
<div class="tools">
<span @click="removeWidget">{{ t("delete") }}</span>
</div>
</el-popover>
<div class="header">
<Filter />
</div>
<div class="trace flex-h">
<TraceList />
<TraceDetail />
</div>
</div>
</template>
<script lang="ts" setup>
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";
/*global defineProps */
const props = defineProps({
data: {
type: Object as PropType<any>,
default: () => ({ graph: {} }),
},
activeIndex: { type: String, default: "" },
});
const { t } = useI18n();
const dashboardStore = useDashboardStore();
function removeWidget() {
dashboardStore.removeControls(props.data);
}
</script>
<style lang="scss" scoped>
.trace-wrapper {
width: 100%;
height: 100%;
font-size: 12px;
position: relative;
}
.delete {
position: absolute;
top: 5px;
right: 3px;
}
.header {
padding: 10px;
font-size: 12px;
border-bottom: 1px solid #dcdfe6;
}
.tools {
padding: 5px 0;
color: #999;
cursor: pointer;
position: relative;
text-align: center;
&:hover {
color: #409eff;
background-color: #eee;
}
}
.trace {
width: 100%;
overflow: auto;
}
</style>

View File

@@ -0,0 +1,22 @@
/**
* 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 Topology from "./Topology.vue";
import Tab from "./Tab.vue";
import Widget from "./Widget.vue";
import Trace from "./Trace.vue";
export default { Tab, Widget, Trace, Topology };

View File

@@ -165,11 +165,11 @@ export const SortOrder = [
export const ToolIcons = [
{ name: "playlist_add", content: "Add Widget", id: "addWidget" },
{ name: "all_inbox", content: "Add Tab", id: "addTab" },
// { name: "insert_image", content: "Add Image", id: "addImage" },
{ name: "device_hub", content: "Add Topology", id: "topology" },
{ name: "merge", content: "Add Trace", id: "trace" },
// { name: "save_alt", content: "Export", id: "export" },
// { name: "folder_open", content: "Import", id: "import" },
// { name: "settings", content: "Settings", id: "settings" },
{ name: "device_hub", content: "Add Topology", id: "topology" },
// { name: "save", content: "Apply", id: "apply" },
];
export const ScopeType = [
@@ -203,3 +203,13 @@ export const Colors: any = {
black: "#000",
orange: "#E6A23C",
};
export const Status = [
{ label: "All", value: "ALL" },
{ label: "Success", value: "SUCCESS" },
{ label: "Error", value: "ERROR" },
];
export const QueryOrders = [
{ label: "startTime", value: "BY_START_TIME" },
{ label: "duration", value: "BY_DURATION" },
];
export const TraceEntitys = ["All", "Service", "ServiceInstance", "Endpoint"];

View File

@@ -31,6 +31,7 @@ limitations under the License. -->
:key="item.i"
@click="clickGrid(item)"
:class="{ active: dashboardStore.activedGridItem === item.i }"
drag-ignore-from="svg.d3-trace-tree"
>
<component :is="item.type" :data="item" />
</grid-item>
@@ -40,13 +41,11 @@ limitations under the License. -->
import { defineComponent } from "vue";
import { useDashboardStore } from "@/store/modules/dashboard";
import { LayoutConfig } from "@/types/dashboard";
import Widget from "../controls/Widget.vue";
import Tab from "../controls/Tab.vue";
import Topology from "../controls/Topology.vue";
import controls from "../controls/index";
export default defineComponent({
name: "Layout",
components: { Widget, Tab, Topology },
components: { ...controls },
setup() {
const dashboardStore = useDashboardStore();
function layoutUpdatedEvent(newLayout: LayoutConfig[]) {

View File

@@ -84,8 +84,10 @@ limitations under the License. -->
size="sm"
:iconName="t.name"
v-if="
t.id !== 'topology' ||
(t.id === 'topology' && hasTopology.includes(dashboardStore.entity))
!['topology', 'trace'].includes(t.id) ||
(t.id === 'topology' &&
hasTopology.includes(dashboardStore.entity)) ||
(t.id === 'trace' && TraceEntitys.includes(dashboardStore.entity))
"
/>
</span>
@@ -98,7 +100,7 @@ import { reactive, watch } from "vue";
import { useRoute } from "vue-router";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useAppStoreWithOut } from "@/store/modules/app";
import { EntityType, ToolIcons, hasTopology } from "../data";
import { EntityType, ToolIcons, hasTopology, TraceEntitys } from "../data";
import { useSelectorStore } from "@/store/modules/selectors";
import { ElMessage } from "element-plus";
import { Option } from "@/types/app";
@@ -290,8 +292,8 @@ function clickIcons(t: { id: string; content: string; name: string }) {
case "addTab":
dashboardStore.addControl("Tab");
break;
case "addImage":
dashboardStore.addControl("Image");
case "trace":
dashboardStore.addControl("Trace");
break;
case "topology":
dashboardStore.addControl("Topology");
@@ -351,7 +353,6 @@ async function fetchPods(type: string, serviceId: string, setPod: boolean) {
}
if (resp.errors) {
ElMessage.error(resp.errors);
return;
}
}
watch(

View File

@@ -0,0 +1,279 @@
<!-- 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-detail" v-loading="loading">
<div
class="trace-detail-wrapper clear"
v-if="traceStore.currentTrace.endpointNames"
>
<h5 class="mb-5 mt-0">
<Icon
icon="clear"
v-if="traceStore.currentTrace.isError"
class="red mr-5 sm"
/>
<span class="vm">{{ traceStore.currentTrace.endpointNames[0] }}</span>
<div class="trace-log-btn">
<el-button class="mr-10" type="primary" @click="searchTraceLogs">
{{ t("viewLogs") }}
</el-button>
</div>
<el-dialog
v-model="showTraceLogs"
:destroy-on-close="true"
fullscreen
@closed="showTraceLogs = false"
>
<div>
<el-pagination
v-model:currentPage="pageNum"
v-model:page-size="pageSize"
:small="true"
:total="traceStore.traceSpanLogsTotal"
@current-change="turnLogsPage"
/>
<LogTable
:tableData="traceStore.traceSpanLogs || []"
:type="`service`"
:noLink="true"
>
<div class="log-tips" v-if="!traceStore.traceSpanLogs.length">
{{ t("noData") }}
</div>
</LogTable>
</div>
</el-dialog>
</h5>
<div class="mb-5 blue sm">
<Selector
size="small"
:value="
traceStore.currentTrace.traceIds &&
traceStore.currentTrace.traceIds[0] &&
traceStore.currentTrace.traceIds[0].value
"
:options="traceStore.currentTrace.traceIds"
@change="changeTraceId"
class="trace-detail-ids"
/>
<Icon
size="sm"
class="icon grey link-hover cp ml-5"
iconName="review-list"
@click="handleClick"
/>
</div>
<div class="flex-h item">
<div>
<div class="tag mr-5">{{ t("start") }}</div>
<span class="mr-15 sm">
{{ dateFormat(parseInt(traceStore.currentTrace.start)) }}
</span>
<div class="tag mr-5">{{ t("duration") }}</div>
<span class="mr-15 sm"
>{{ traceStore.currentTrace.duration }} ms</span
>
<div class="tag mr-5">{{ t("spans") }}</div>
<span class="sm">{{ traceStore.traceSpans.length }}</span>
</div>
<div>
<el-button
class="grey"
:class="{ ghost: displayMode !== 'List' }"
@click="displayMode = 'List'"
>
<Icon class="mr-5" size="sm" iconName="list-bulleted" />
{{ t("list") }}
</el-button>
<el-button
class="grey"
:class="{ ghost: displayMode !== 'Tree' }"
@click="displayMode = 'Tree'"
>
<Icon class="mr-5" size="sm" iconName="issue-child" />
{{ t("tree") }}
</el-button>
<el-button
class="grey"
:class="{ ghost: displayMode !== 'Table' }"
@click="displayMode = 'Table'"
>
<Icon class="mr-5" size="sm" iconName="table" />
{{ t("table") }}
</el-button>
<el-button
class="grey"
:class="{ ghost: displayMode !== 'Statistics' }"
@click="displayMode = 'Statistics'"
>
<Icon class="mr-5" size="sm" iconName="statistics-bulleted" />
{{ t("statistics") }}
</el-button>
</div>
</div>
</div>
<div class="no-data" v-else>{{ t("noData") }}</div>
<component
v-if="traceStore.currentTrace.endpointNames"
:is="displayMode"
:data="traceStore.traceSpans"
:traceId="traceStore.currentTrace.traceIds[0].value"
:showBtnDetail="false"
HeaderType="trace"
/>
</div>
</template>
<script lang="ts">
import dayjs from "dayjs";
import { ref, defineComponent } from "vue";
import { useI18n } from "vue-i18n";
import { useTraceStore } from "@/store/modules/trace";
import { Option } from "@/types/app";
import copy from "@/utils/copy";
import List from "./components/List.vue";
import graphs from "./components/index";
import LogTable from "@/views/components/LogTable/Index.vue";
import { ElMessage } from "element-plus";
export default defineComponent({
name: "TraceDetail",
components: {
...graphs,
List,
LogTable,
},
setup() {
const { t } = useI18n();
const traceStore = useTraceStore();
const loading = ref<boolean>(false);
const traceId = ref<string>("");
const displayMode = ref<string>("List");
const pageNum = ref<number>(1);
const pageSize = 10;
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
dayjs(date).format(pattern);
const showTraceLogs = ref<boolean>(false);
function handleClick(ids: string[]) {
let copyValue = null;
if (ids.length === 1) {
copyValue = ids[0];
} else {
copyValue = ids.join(",");
}
copy(copyValue);
}
async function changeTraceId(opt: Option[]) {
traceId.value = opt[0].value;
loading.value = true;
const res = await traceStore.getTraceSpans({ traceId: opt[0].value });
if (res.errors) {
ElMessage.error(res.errors);
}
loading.value = false;
}
async function searchTraceLogs() {
showTraceLogs.value = true;
const res = await traceStore.getSpanLogs({
condition: {
relatedTrace: {
traceId: traceId.value || traceStore.currentTrace.traceIds[0],
},
paging: { pageNum: pageNum.value, pageSize, needTotal: true },
},
});
if (res.errors) {
ElMessage.error(res.errors);
}
}
function turnLogsPage(page: number) {
pageNum.value = page;
searchTraceLogs();
}
return {
traceStore,
displayMode,
dateFormat,
changeTraceId,
handleClick,
t,
searchTraceLogs,
showTraceLogs,
turnLogsPage,
pageSize,
pageNum,
loading,
};
},
});
</script>
<style lang="scss" scoped>
.trace-detail {
height: 100%;
width: 100%;
overflow: hidden;
}
.trace-detail-wrapper {
font-size: 12px;
padding: 5px 10px;
border-bottom: 1px solid #eee;
width: 100%;
height: 95px;
.grey {
color: #fff;
background-color: #448dfe;
}
.ghost {
cursor: pointer;
background: rgba(0, 0, 0, 0.3);
}
}
.item {
justify-content: space-between;
}
.trace-detail-ids {
background-color: rgba(0, 0, 0, 0);
outline: 0;
border-style: unset;
color: inherit;
border: 1px solid;
border-radius: 4px;
width: 300px;
}
.trace-log-btn {
float: right;
}
.tag {
display: inline-block;
border-radius: 4px;
padding: 0px 7px;
background-color: #40454e;
color: #eee;
}
.no-data {
padding-top: 50px;
width: 100%;
text-align: center;
}
</style>

View File

@@ -0,0 +1,239 @@
<!-- 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 row">
<div class="mr-5" v-if="dashboardStore.entity === EntityType[1].value">
<span class="grey mr-5">{{ t("service") }}:</span>
<Selector
size="small"
:value="state.service.value"
:options="traceStore.services"
placeholder="Select a service"
@change="changeField('service', $event)"
/>
</div>
<div class="mr-5" v-if="dashboardStore.entity !== EntityType[3].value">
<span class="grey mr-5">{{ t("instance") }}:</span>
<Selector
size="small"
:value="state.instance.value"
:options="traceStore.instances"
placeholder="Select a instance"
@change="changeField('instance', $event)"
/>
</div>
<div class="mr-5" v-if="dashboardStore.entity !== EntityType[2].value">
<span class="grey mr-5">{{ t("endpoint") }}:</span>
<Selector
size="small"
:value="state.endpoint.value"
:options="traceStore.endpoints"
placeholder="Select a endpoint"
@change="changeField('endpoint', $event)"
/>
</div>
<div class="mr-5">
<span class="grey mr-5">{{ t("status") }}:</span>
<Selector
size="small"
:value="state.status.value"
:options="Status"
placeholder="Select a status"
@change="changeField('status', $event)"
/>
</div>
<div class="mr-5">
<span class="grey mr-5">{{ t("traceID") }}:</span>
<el-input v-model="traceId" class="traceId" />
</div>
</div>
<div class="flex-h">
<!-- <div class="mr-5">
<span class="grey mr-5">{{ t("timeRange") }}:</span>
<TimePicker
:value="dateTime"
position="bottom"
format="YYYY-MM-DD HH:mm"
@input="changeTimeRange"
/>
</div> -->
<div class="mr-5">
<span class="sm b grey mr-5">{{ t("duration") }}:</span>
<el-input class="inputs mr-5" v-model="minTraceDuration" />
<span class="grey mr-5">-</span>
<el-input class="inputs" v-model="maxTraceDuration" />
</div>
<ConditionTags :type="'TRACE'" @update="updateTags" />
<el-button
class="search-btn"
size="small"
type="primary"
@click="searchTraces"
>
{{ t("search") }}
</el-button>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from "vue";
import { useI18n } from "vue-i18n";
import { Option } from "@/types/app";
import { Status } from "../../data";
import { useTraceStore } from "@/store/modules/trace";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useAppStoreWithOut } from "@/store/modules/app";
import { useSelectorStore } from "@/store/modules/selectors";
import ConditionTags from "@/views/components/ConditionTags.vue";
import { ElMessage } from "element-plus";
import { EntityType } from "../../data";
const { t } = useI18n();
const appStore = useAppStoreWithOut();
const selectorStore = useSelectorStore();
const dashboardStore = useDashboardStore();
const traceStore = useTraceStore();
const traceId = ref<string>("");
const minTraceDuration = ref<string>("");
const maxTraceDuration = ref<string>("");
const tagsList = ref<string[]>([]);
const tagsMap = ref<Option[]>([]);
const state = reactive<any>({
status: { label: "All", value: "ALL" },
instance: { value: "0", label: "All" },
endpoint: { value: "0", label: "All" },
service: { value: "0", label: "All" },
});
// const dateTime = computed(() => [
// appStore.durationRow.start,
// appStore.durationRow.end,
// ]);
init();
function init() {
searchTraces();
if (dashboardStore.entity === EntityType[1].value) {
getServices();
return;
}
if (dashboardStore.entity === EntityType[2].value) {
getInstances();
return;
}
if (dashboardStore.entity === EntityType[3].value) {
getEndpoints();
return;
}
if (dashboardStore.entity === EntityType[0].value) {
getInstances();
getEndpoints();
}
}
async function getServices() {
const resp = await traceStore.getServices(dashboardStore.layerId);
if (resp.errors) {
ElMessage.error(resp.errors);
return;
}
state.service = traceStore.services[0];
}
async function getEndpoints() {
const resp = await traceStore.getEndpoints();
if (resp.errors) {
ElMessage.error(resp.errors);
return;
}
state.endpoint = traceStore.endpoints[0];
}
async function getInstances() {
const resp = await traceStore.getInstances();
if (resp.errors) {
ElMessage.error(resp.errors);
return;
}
state.instance = traceStore.instances[0];
}
function searchTraces() {
let endpoint = "",
instance = "";
if (dashboardStore.entity === EntityType[2].value) {
endpoint = selectorStore.currentPod.id;
}
if (dashboardStore.entity === EntityType[3].value) {
instance = selectorStore.currentPod.id;
}
traceStore.setTraceCondition({
serviceId: selectorStore.currentService
? selectorStore.currentService.id
: state.service.id,
traceId: traceId.value || undefined,
endpointId: endpoint || state.endpoint.id || undefined,
serviceInstanceId: instance || state.instance.id || undefined,
traceState: state.status.value || "ALL",
queryDuration: appStore.durationTime,
minTraceDuration: appStore.minTraceDuration || undefined,
maxTraceDuration: appStore.maxTraceDuration || undefined,
queryOrder: "BY_DURATION",
tags: tagsMap.value.length ? tagsMap.value : undefined,
paging: { pageNum: 1, pageSize: 15, needTotal: true },
});
queryTraces();
}
async function queryTraces() {
const res = await traceStore.getTraces();
if (res && res.errors) {
ElMessage.error(res.errors);
}
}
function changeField(type: string, opt: any[]) {
state[type] = opt[0];
if (type === "service") {
getEndpoints();
getInstances();
}
}
function updateTags(data: { tagsMap: Array<Option>; tagsList: string[] }) {
tagsList.value = data.tagsList;
tagsMap.value = data.tagsMap;
}
watch(
() => selectorStore.currentService,
() => {
if (dashboardStore.entity !== EntityType[0].value) {
return;
}
init();
}
);
</script>
<style lang="scss" scoped>
.inputs {
width: 120px;
}
.row {
margin-bottom: 5px;
}
.traceId {
width: 270px;
}
.search-btn {
margin-left: 20px;
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,213 @@
<!-- 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-t flex-v">
<div class="trace-t-tool flex-h">
<el-pagination
v-model:currentPage="traceStore.conditions.paging.pageNum"
v-model:page-size="pageSize"
:small="true"
layout="prev, pager, next, jumper"
:total="traceStore.traceTotal"
v-model:pager-count="pageCount"
@current-change="updatePage"
/>
<div class="selectors">
<Selector
size="small"
:value="traceStore.conditions.queryOrder"
:options="QueryOrders"
placeholder="Select a option"
@change="changeSort"
/>
</div>
</div>
<div class="trace-t-wrapper" v-loading="loading">
<table class="list" v-if="traceStore.traceList.length">
<tr
class="trace-tr cp"
v-for="(i, index) in traceStore.traceList"
@click="selectTrace(i)"
:key="index"
>
<td
class="trace-td"
:class="{
'trace-success': !i.isError,
'trace-error': i.isError,
selected: selectedKey == i.key,
}"
>
<div
class="ell mb-5"
:class="{
blue: !i.isError,
red: i.isError,
}"
>
<span class="b">{{ i.endpointNames[0] }}</span>
</div>
<div class="grey ell sm">
<span class="tag mr-10 sm">{{ i.duration }} ms</span
>{{ dateFormat(parseInt(i.start, 10)) }}
</div>
</td>
</tr>
</table>
<div class="no-data" v-else>{{ t("noData") }}</div>
</div>
</div>
</template>
<script lang="ts" setup>
import dayjs from "dayjs";
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import { useTraceStore } from "@/store/modules/trace";
import { ElMessage } from "element-plus";
import { QueryOrders } from "../../data";
import { Option } from "@/types/app";
import { Trace } from "@/types/trace";
const { t } = useI18n();
const traceStore = useTraceStore();
const loading = ref<boolean>(false);
const selectedKey = ref<string>("");
const pageSize = ref<number>(15);
const pageCount = ref<number>(5);
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
dayjs(date).format(pattern);
function searchTrace() {
loading.value = true;
queryTraces();
loading.value = false;
}
function updatePage(p: number) {
traceStore.setTraceCondition({
paging: { pageNum: p, pageSize: pageSize.value, needTotal: true },
});
searchTrace();
}
function changeSort(opt: Option[]) {
traceStore.setTraceCondition({
queryOrder: opt[0].value,
paging: { pageNum: 1, pageSize: pageSize.value, needTotal: true },
});
searchTrace();
}
async function selectTrace(i: Trace) {
traceStore.setCurrentTrace(i);
selectedKey.value = i.key;
if (i.traceIds.length) {
const res = await traceStore.getTraceSpans({
traceId: i.traceIds[0].value,
});
if (res.errors) {
ElMessage.error(res.errors);
}
}
}
async function queryTraces() {
const res = await traceStore.getTraces();
if (res.errors) {
ElMessage.error(res.errors);
}
}
</script>
<style lang="scss">
.trace-t-tool {
background-color: rgba(196, 200, 225, 0.2);
justify-content: space-between;
border-bottom: 1px solid #c1c5ca41;
border-right: 1px solid #c1c5ca41;
height: 35px;
}
.selectors {
margin: 2px 2px 0 0;
}
.trace-t-wrapper {
overflow: auto;
border-right: 1px solid rgba(0, 0, 0, 0.1);
}
.trace-t-loading {
text-align: center;
position: absolute;
width: 420px;
height: 70px;
margin-top: 40px;
line-height: 88px;
overflow: hidden;
.icon {
width: 30px;
height: 30px;
}
}
.trace-t {
width: 420px;
}
.list {
width: 400px;
}
.trace-tr {
&:hover {
background-color: rgba(0, 0, 0, 0.04);
}
}
.trace-td {
padding: 5px;
border-bottom: 1px solid rgba(0, 0, 0, 0.07);
&.selected {
background-color: #ededed;
}
}
.trace-success {
border-left: 4px solid rgba(46, 47, 51, 0.1);
}
.trace-warning {
border-left: 4px solid #fbb03b;
}
.trace-error {
border-left: 4px solid #e54c17;
}
.tag {
border-radius: 4px;
padding-right: 5px;
padding-left: 5px;
background-color: #40454e;
color: #eee;
}
.no-data {
padding-top: 50px;
width: 100%;
text-align: center;
}
</style>

View File

@@ -0,0 +1,354 @@
<!-- 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-t-loading" v-show="loading">
<Icon iconName="spinner" size="sm" />
</div>
<div ref="traceGraph" class="d3-graph"></div>
<el-dialog
v-model="showDetail"
:destroy-on-close="true"
fullscreen
@closed="showDetail = false"
>
<SpanDetail :currentSpan="currentSpan" />
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, watch, onBeforeUnmount, onMounted } from "vue";
import type { PropType } from "vue";
import _ from "lodash";
import * as d3 from "d3";
import ListGraph from "../../utils/d3-trace-list";
import TreeGraph from "../../utils/d3-trace-tree";
import { Span } from "@/types/trace";
import SpanDetail from "./SpanDetail.vue";
/* global defineProps, Nullable, defineExpose*/
const props = defineProps({
data: { type: Array as PropType<Span[]>, default: () => [] },
traceId: { type: String, default: "" },
type: { type: String, default: "List" },
});
const loading = ref<boolean>(false);
const showDetail = ref<boolean>(false);
const fixSpansSize = ref<number>(0);
const segmentId = ref<any>([]);
const currentSpan = ref<Array<Span>>([]);
const tree = ref<any>(null);
const traceGraph = ref<Nullable<HTMLDivElement>>(null);
defineExpose({
tree,
});
onMounted(() => {
loading.value = true;
changeTree();
if (!traceGraph.value) {
loading.value = false;
return;
}
if (props.type === "List") {
tree.value = new ListGraph(traceGraph.value, handleSelectSpan);
tree.value.init(
{ label: "TRACE_ROOT", children: segmentId.value },
props.data,
fixSpansSize.value
);
tree.value.draw();
} else {
tree.value = new TreeGraph(traceGraph.value, handleSelectSpan);
tree.value.init(
{ label: `${props.traceId}`, children: segmentId.value },
props.data
);
}
loading.value = false;
window.addEventListener("resize", resize);
});
function resize() {
tree.value.resize();
}
function handleSelectSpan(i: any) {
currentSpan.value = i.data;
showDetail.value = true;
}
function traverseTree(node: any, spanId: string, segmentId: string, data: any) {
if (!node || node.isBroken) {
return;
}
if (node.spanId === spanId && node.segmentId === segmentId) {
node.children.push(data);
return;
}
if (node.children && node.children.length > 0) {
node.children.forEach((nodeItem: any) => {
traverseTree(nodeItem, spanId, segmentId, data);
});
}
}
function changeTree() {
if (props.data.length === 0) {
return [];
}
segmentId.value = [];
const segmentGroup: any = {};
const segmentIdGroup: any = [];
const fixSpans: any[] = [];
const segmentHeaders: any = [];
for (const span of props.data) {
if (span.parentSpanId === -1) {
segmentHeaders.push(span);
} else {
const index = props.data.findIndex(
(i: any) =>
i.segmentId === span.segmentId && i.spanId === span.spanId - 1
);
const fixSpanKeyContent = {
traceId: span.traceId,
segmentId: span.segmentId,
spanId: span.spanId - 1,
parentSpanId: span.spanId - 2,
};
if (index === -1 && !_.find(fixSpans, fixSpanKeyContent)) {
fixSpans.push({
...fixSpanKeyContent,
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: [],
});
}
}
}
segmentHeaders.forEach((span: Span) => {
if (span.refs.length) {
span.refs.forEach((ref) => {
const index = props.data.findIndex(
(i: any) =>
ref.parentSegmentId === i.segmentId && ref.parentSpanId === i.spanId
);
if (index === -1) {
// create a known broken node.
const i = ref.parentSpanId;
const fixSpanKeyContent = {
traceId: ref.traceId,
segmentId: ref.parentSegmentId,
spanId: i,
parentSpanId: i > -1 ? 0 : -1,
};
if (!_.find(fixSpans, fixSpanKeyContent)) {
fixSpans.push({
...fixSpanKeyContent,
refs: [],
endpointName: `VNode: ${ref.parentSegmentId}`,
serviceCode: "VirtualNode",
type: `[Broken] ${ref.type}`,
peer: "",
component: `VirtualNode: #${i}`,
isError: true,
isBroken: true,
layer: "Broken",
tags: [],
logs: [],
});
}
// if root broken node is not exist, create a root broken node.
if (fixSpanKeyContent.parentSpanId > -1) {
const fixRootSpanKeyContent = {
traceId: ref.traceId,
segmentId: ref.parentSegmentId,
spanId: 0,
parentSpanId: -1,
};
if (!_.find(fixSpans, fixRootSpanKeyContent)) {
fixSpans.push({
...fixRootSpanKeyContent,
refs: [],
endpointName: `VNode: ${ref.parentSegmentId}`,
serviceCode: "VirtualNode",
type: `[Broken] ${ref.type}`,
peer: "",
component: `VirtualNode: #0`,
isError: true,
isBroken: true,
layer: "Broken",
tags: [],
logs: [],
});
}
}
}
});
}
});
[...fixSpans, ...props.data].forEach((i) => {
i.label = i.endpointName || "no operation name";
i.children = [];
if (segmentGroup[i.segmentId] === undefined) {
segmentIdGroup.push(i.segmentId);
segmentGroup[i.segmentId] = [];
segmentGroup[i.segmentId].push(i);
} else {
segmentGroup[i.segmentId].push(i);
}
});
fixSpansSize.value = fixSpans.length;
segmentIdGroup.forEach((id: string) => {
const currentSegment = segmentGroup[id].sort(
(a: Span, b: Span) => b.parentSpanId - a.parentSpanId
);
currentSegment.forEach((s: any) => {
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 = _.filter(props.data, (span: Span) => {
return _.find(span.refs, {
traceId: s.traceId,
parentSegmentId: s.segmentId,
parentSpanId: s.spanId,
});
});
if (children.length > 0) {
s.children.push(...children);
}
}
});
segmentGroup[id] = currentSegment[currentSegment.length - 1];
});
segmentIdGroup.forEach((id: string) => {
segmentGroup[id].refs.forEach((ref: any) => {
if (ref.traceId === props.traceId) {
traverseTree(
segmentGroup[ref.parentSegmentId],
ref.parentSpanId,
ref.parentSegmentId,
segmentGroup[id]
);
}
});
});
for (const i in segmentGroup) {
if (segmentGroup[i].refs.length === 0) {
segmentId.value.push(segmentGroup[i]);
}
}
segmentId.value.forEach((i: any) => {
collapse(i);
});
}
function collapse(d: Span) {
if (d.children) {
let dur = d.endTime - d.startTime;
d.children.forEach((i: Span) => {
dur -= i.endTime - i.startTime;
});
d.dur = dur < 0 ? 0 : dur;
d.children.forEach((i: Span) => collapse(i));
}
}
onBeforeUnmount(() => {
d3.selectAll(".d3-tip").remove();
window.removeEventListener("resize", resize);
});
watch(
() => props.data,
() => {
if (!props.data.length) {
return;
}
loading.value = true;
changeTree();
tree.value.init(
{ label: "TRACE_ROOT", children: segmentId.value },
props.data,
fixSpansSize.value
);
tree.value.draw(() => {
setTimeout(() => {
loading.value = false;
}, 200);
});
}
);
</script>
<style lang="scss" scoped>
.d3-graph {
height: 100%;
}
.trace-node .group {
cursor: pointer;
fill-opacity: 0;
}
.trace-node-container {
fill: rgba(0, 0, 0, 0);
stroke-width: 5px;
cursor: pointer;
&:hover {
fill: rgba(0, 0, 0, 0.05);
}
}
.trace-node .node-text {
font: 12.5px sans-serif;
pointer-events: none;
}
.domain {
display: none;
}
.time-charts-item {
display: inline-block;
padding: 2px 8px;
border: 1px solid;
font-size: 11px;
border-radius: 4px;
}
.trace-list .trace-node rect {
cursor: pointer;
&:hover {
fill: rgba(0, 0, 0, 0.05);
}
}
.dialog-c-text {
white-space: pre;
overflow: auto;
font-family: monospace;
}
</style>

View File

@@ -0,0 +1,164 @@
<!-- 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>
<h5 class="mb-15">{{ t("tags") }}.</h5>
<div class="mb-10 clear">
<span class="g-sm-4 grey">{{ t("service") }}:</span>
<span class="g-sm-8 wba">{{ currentSpan.serviceCode }}</span>
</div>
<div class="mb-10 clear">
<span class="g-sm-4 grey">{{ t("instance") }}:</span>
<span class="g-sm-8 wba">{{ currentSpan.serviceInstanceName }}</span>
</div>
<div class="mb-10 clear">
<span class="g-sm-4 grey">{{ t("endpoint") }}:</span>
<span class="g-sm-8 wba">{{ currentSpan.label }}</span>
</div>
<div class="mb-10 clear">
<span class="g-sm-4 grey">{{ t("spanType") }}:</span>
<span class="g-sm-8 wba">{{ currentSpan.type }}</span>
</div>
<div class="mb-10 clear">
<span class="g-sm-4 grey">{{ t("component") }}:</span>
<span class="g-sm-8 wba">{{ currentSpan.component }}</span>
</div>
<div class="mb-10 clear">
<span class="g-sm-4 grey">Peer:</span>
<span class="g-sm-8 wba">{{ currentSpan.peer || "No Peer" }}</span>
</div>
<div class="mb-10 clear">
<span class="g-sm-4 grey">{{ t("error") }}:</span>
<span class="g-sm-8 wba">{{ currentSpan.isError }}</span>
</div>
<div class="mb-10 clear" v-for="i in currentSpan.tags" :key="i.key">
<span class="g-sm-4 grey">{{ i.key }}:</span>
<span class="g-sm-8 wba">
{{ i.value }}
<svg
v-if="i.key === 'db.statement'"
class="icon vm grey link-hover cp ml-5"
@click="copy(i.value)"
>
<use xlink:href="#review-list"></use>
</svg>
</span>
</div>
<h5 class="mb-10" v-if="currentSpan.logs" v-show="currentSpan.logs.length">
{{ t("logs") }}.
</h5>
<div v-for="(i, index) in currentSpan.logs" :key="index">
<div class="mb-10 sm">
<span class="mr-10">{{ t("time") }}:</span
><span class="grey">{{ dateFormat(i.time) }}</span>
</div>
<div class="mb-15 clear" v-for="(_i, _index) in i.data" :key="_index">
<div class="mb-10">
{{ _i.key }}:<span
v-if="_i.key === 'stack'"
class="r rk-sidebox-magnify"
@click="showCurrentSpanDetail(_i.value)"
>
<svg class="icon">
<use xlink:href="#magnify"></use>
</svg>
</span>
</div>
<pre class="pl-15 mt-0 mb-0 sm oa">{{ _i.value }}</pre>
</div>
</div>
<div @click="getTaceLogs()">
<el-button class="popup-btn" type="primary">
{{ t("relatedTraceLogs") }}
</el-button>
</div>
</div>
<el-dialog
v-model="showRelatedLogs"
:destroy-on-close="true"
fullscreen
@closed="showRelatedLogs = false"
>
<el-pagination
v-model:currentPage="pageNum"
v-model:page-size="pageSize"
:small="true"
:total="traceStore.traceSpanLogsTotal"
@current-change="turnPage"
/>
<LogTable
:tableData="traceStore.traceSpanLogs || []"
:type="`service`"
:noLink="true"
>
<div class="log-tips" v-if="!traceStore.traceSpanLogs.length">
{{ t("noData") }}
</div>
</LogTable>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import type { PropType } from "vue";
import dayjs from "dayjs";
import { useTraceStore } from "@/store/modules/trace";
import copy from "@/utils/copy";
import { ElMessage } from "element-plus";
import LogTable from "@/views/components/LogTable/Index.vue";
/* global defineProps */
const props = defineProps({
currentSpan: { type: Object as PropType<any>, default: () => ({}) },
});
const { t } = useI18n();
const traceStore = useTraceStore();
const pageNum = ref<number>(1);
const showRelatedLogs = ref<boolean>(false);
const pageSize = 10;
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
dayjs(date).format(pattern);
async function getTaceLogs() {
showRelatedLogs.value = true;
const res = await traceStore.getSpanLogs({
condition: {
relatedTrace: {
traceId: props.currentSpan.traceId,
segmentId: props.currentSpan.segmentId,
spanId: props.currentSpan.spanId,
},
paging: { pageNum: pageNum.value, pageSize, needTotal: true },
},
});
if (res.errors) {
ElMessage.error(res.errors);
}
}
function turnPage(p: number) {
pageNum.value = p;
getTaceLogs();
}
function showCurrentSpanDetail(text: string) {
copy(text);
}
</script>
<style lang="scss" scoped>
.popup-btn {
color: #fff;
margin-top: 40px;
width: 100%;
text-align: center;
}
</style>

View File

@@ -0,0 +1,106 @@
<!-- 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="charts">
<div>
<span
class="charts-item mr-5"
v-for="(i, index) in list"
:key="index"
:style="`color:${computedScale(index)}`"
>
<Icon iconName="issue-open-m" class="mr-5" size="sm" />
<span>{{ i }}</span>
</span>
<el-button class="btn" type="primary" @click="downloadTrace">
{{ t("exportImage") }}
</el-button>
</div>
<div>
<Graph :data="data" :traceId="traceId" type="List" />
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import * as d3 from "d3";
import { Span } from "@/types/trace";
import Graph from "./D3Graph/Index.vue";
/* global defineProps*/
const props = defineProps({
data: { type: Array as PropType<Span[]>, default: () => [] },
traceId: { type: String, default: "" },
});
const { t } = useI18n();
const list = computed(() =>
Array.from(new Set(props.data.map((i: Span) => i.serviceCode)))
);
function computedScale(i: number) {
const sequentialScale = d3
.scaleSequential()
.domain([0, list.value.length + 1])
.interpolator(d3.interpolateCool);
return sequentialScale(i);
}
function downloadTrace() {
const serializer = new XMLSerializer();
const svgNode: any = d3.select(".trace-list-dowanload").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-dowanload") as any
)._groups[0][0].clientWidth;
canvas.height = (
d3.select(".trace-list-dowanload") as any
)._groups[0][0].clientHeight;
context.fillStyle = "#fff";
context.fillRect(0, 0, canvas.width, canvas.height);
const image = new Image();
image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(source)}`;
image.onload = () => {
context.drawImage(image, 0, 0);
const tagA = document.createElement("a");
tagA.download = "trace-list.png";
tagA.href = canvas.toDataURL("image/png");
tagA.click();
};
}
</script>
<style lang="scss" scoped>
.charts {
overflow: auto;
padding: 10px;
height: calc(100% - 95px);
width: 100%;
}
.charts-item {
display: inline-block;
padding: 2px 8px;
border: 1px solid;
font-size: 11px;
border-radius: 4px;
}
.btn {
float: right;
}
</style>

View File

@@ -0,0 +1,125 @@
<!-- 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-statistics">
<div class="trace-t-loading" v-show="loading">
<Icon iconName="spinner" size="sm" />
</div>
<TableContainer
:tableData="tableData"
type="statistics"
:HeaderType="HeaderType"
>
<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 { StatisticsSpan, Span, StatisticsGroupRef } from "@/types/trace";
/* global defineProps, defineEmits */
const props = defineProps({
data: { type: Array as PropType<any>, default: () => [] },
traceId: { type: String, default: "" },
showBtnDetail: { type: Boolean, default: false },
HeaderType: { type: String, default: "" },
});
const emit = defineEmits(["load"]);
const loading = ref<boolean>(true);
const tableData = ref<any>([]);
const list = ref<any[]>([]);
onMounted(() => {
tableData.value = calculationDataforStatistics(props.data);
loading.value = false;
emit("load", () => {
loading.value = true;
});
});
function calculationDataforStatistics(data: Span[]): StatisticsSpan[] {
list.value = traceTable.buildTraceDataList(data);
const result: StatisticsSpan[] = [];
const map = traceTable.changeStatisticsTree(data, props.traceId);
map.forEach((nodes, nodeKey) => {
const nodeKeyData = nodeKey.split(":");
result.push(
getSpanGroupData(nodes, {
endpointName: nodeKeyData[0],
type: nodeKeyData[1],
})
);
});
return result;
}
function getSpanGroupData(
groupspans: Span[],
groupRef: StatisticsGroupRef
): StatisticsSpan {
let maxTime = 0;
let minTime = 0;
let sumTime = 0;
const count = groupspans.length;
groupspans.forEach((groupspan: Span) => {
const duration = groupspan.dur || 0;
if (duration > maxTime) {
maxTime = duration;
}
if (duration < minTime) {
minTime = duration;
}
sumTime = sumTime + duration;
});
const avgTime = count === 0 ? 0 : sumTime / count;
return {
groupRef,
maxTime,
minTime,
sumTime,
avgTime,
count,
};
}
watch(
() => props.data,
() => {
if (!props.data.length) {
tableData.value = [];
return;
}
tableData.value = calculationDataforStatistics(props.data);
loading.value = false;
}
);
</script>
<style lang="scss" scoped>
.trace-tips {
width: 100%;
text-align: center;
margin-top: 10px;
}
.trace-statistics {
padding: 10px;
height: calc(100% - 95px);
width: 100%;
}
</style>

View File

@@ -0,0 +1,116 @@
<!-- 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-table">
<div class="trace-t-loading" v-show="loading">
<Icon iconName="spinner" size="sm" />
</div>
<TableContainer
:tableData="tableData"
type="table"
:HeaderType="HeaderType"
>
<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 "./TableContainer.vue";
import traceTable from "../../utils/trace-table";
/* global defineProps, defineEmits */
const props = defineProps({
data: { type: Array as PropType<any>, default: () => [] },
traceId: { type: String, default: "" },
showBtnDetail: { type: Boolean, default: false },
HeaderType: { type: String, default: "" },
});
const emit = defineEmits(["select", "view", "load"]);
const loading = ref<boolean>(true);
const tableData = ref<any>([]);
const showDetail = ref<boolean>(false);
const currentSpan = ref<any[]>([]);
onMounted(() => {
tableData.value = formatData(
traceTable.changeTree(props.data, props.traceId)
);
loading.value = false;
emit("select", handleSelectSpan);
emit("view", handleViewSpan);
emit("load", () => {
loading.value = true;
});
});
function formatData(arr: any[], level = 1, totalExec?: number) {
for (const item of arr) {
item.level = level;
totalExec = totalExec || item.endTime - item.startTime;
item.totalExec = totalExec;
if (item.children && item.children.length > 0) {
formatData(item.children, level + 1, totalExec);
}
}
return arr;
}
function handleSelectSpan(data: any[]) {
currentSpan.value = data;
if (!props.showBtnDetail) {
showDetail.value = true;
}
emit("select", data);
}
function handleViewSpan() {
showDetail.value = true;
}
watch(
() => props.data,
() => {
if (!props.data.length) {
tableData.value = [];
return;
}
tableData.value = formatData(
traceTable.changeTree(props.data, props.traceId)
);
loading.value = false;
}
);
</script>
<style lang="scss" scoped>
.dialog-c-text {
white-space: pre;
overflow: auto;
font-family: monospace;
}
.trace-tips {
width: 100%;
text-align: center;
margin-top: 10px;
}
.trace-table {
padding: 10px;
height: calc(100% - 95px);
width: 100%;
}
</style>

View File

@@ -0,0 +1,172 @@
<!-- 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">
<div class="trace-header" v-if="type === 'statistics'">
<div :class="item.label" v-for="(item, index) in headerData" :key="index">
{{ item.value }}
<span
class="r cp"
@click="sortStatistics(item.key)"
:key="componentKey"
v-if="item.key !== 'endpointName' && item.key !== 'type'"
>
<Icon iconName="sort" size="sm" />
</span>
</div>
</div>
<div class="trace-header" v-else>
<div class="method" :style="`width: ${method}px`">
<span class="r cp" ref="dragger">
<Icon iconName="settings_ethernet" size="sm" />
</span>
{{ headerData[0].value }}
</div>
<div
:class="item.label"
v-for="(item, index) in headerData.slice(1)"
:key="index"
>
{{ item.value }}
</div>
</div>
<table-item
:method="method"
v-for="(item, index) in tableData"
:data="item"
:key="'key' + index"
:type="type"
/>
<slot></slot>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from "vue";
import type { PropType } from "vue";
import TableItem from "./TableItem.vue";
import { ProfileConstant, TraceConstant, StatisticsConstant } from "./data";
/* global defineProps, Nullable */
const props = defineProps({
tableData: { type: Array as PropType<any>, default: () => [] },
type: { type: String, default: "" },
HeaderType: { type: String, default: "" },
});
const method = ref<number>(300);
const componentKey = ref<number>(300);
const flag = ref<boolean>(true);
const dragger = ref<Nullable<HTMLSpanElement>>(null);
let headerData: any[] = TraceConstant;
if (props.HeaderType === "profile") {
headerData = ProfileConstant;
}
if (props.type === "statistics") {
headerData = StatisticsConstant;
}
onMounted(() => {
if (props.type === "statistics") {
return;
}
const drag: any = dragger.value;
drag.onmousedown = (event: any) => {
const diffX = event.clientX;
const copy = method.value;
document.onmousemove = (documentEvent) => {
const moveX = documentEvent.clientX - diffX;
method.value = copy + moveX;
};
document.onmouseup = () => {
document.onmousemove = null;
document.onmouseup = null;
};
};
});
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;
if (key === "maxTime") {
val1 = element[j].maxTime;
val2 = element[j + 1].maxTime;
}
if (key === "minTime") {
val1 = element[j].minTime;
val2 = element[j + 1].minTime;
}
if (key === "avgTime") {
val1 = element[j].avgTime;
val2 = element[j + 1].avgTime;
}
if (key === "sumTime") {
val1 = element[j].sumTime;
val2 = element[j + 1].sumTime;
}
if (key === "count") {
val1 = element[j].count;
val2 = element[j + 1].count;
}
if (flag.value) {
if (val1 < val2) {
const tmp = element[j];
element[j] = element[j + 1];
element[j + 1] = tmp;
}
} else {
if (val1 > val2) {
const tmp = element[j];
element[j] = element[j + 1];
element[j + 1] = tmp;
}
}
}
}
this.tableData = element;
this.componentKey += 1;
this.flag = !this.flag;
}
</script>
<style lang="scss" scoped>
@import "./table.scss";
.trace {
font-size: 12px;
height: 100%;
overflow: auto;
width: 100%;
}
.trace-header {
white-space: nowrap;
user-select: none;
border-left: 0;
border-right: 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
.trace-header div {
display: inline-block;
background-color: #f3f4f9;
padding: 0 4px;
border: 1px solid transparent;
border-right: 1px dotted silver;
overflow: hidden;
line-height: 30px;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View File

@@ -0,0 +1,304 @@
<!-- 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 v-if="type === 'statistics'">
<div :class="['trace-item']" ref="traceItem">
<div :class="['method']">
<el-tooltip :content="data.groupRef.endpointName" placement="bottom">
<span>
{{ data.groupRef.endpointName }}
</span>
</el-tooltip>
</div>
<div :class="['type']">
<el-tooltip :content="data.groupRef.type" placement="bottom">
<span>
{{ data.groupRef.type }}
</span>
</el-tooltip>
</div>
<div class="max-time">
{{ data.maxTime }}
</div>
<div class="min-time">
{{ data.minTime }}
</div>
<div class="sum-time">
{{ data.sumTime }}
</div>
<div class="avg-time">
{{ parseInt(data.avgTime) }}
</div>
<div class="count">
{{ data.count }}
</div>
</div>
</div>
<div v-else>
<div
@click="viewSpanDetail"
:class="[
'trace-item',
'level' + (data.level - 1),
{ 'trace-item-error': data.isError },
]"
ref="traceItem"
>
<div
:class="['method', 'level' + (data.level - 1)]"
:style="{
'text-indent': (data.level - 1) * 10 + 'px',
width: `${method}px`,
}"
>
<Icon
:style="!displayChildren ? 'transform: rotate(-90deg);' : ''"
@click.stop="toggle"
v-if="data.children && data.children.length"
iconName="arrow-down"
size="sm"
/>
<el-tooltip :content="data.endpointName" placement="bottom">
<span>
{{ data.endpointName }}
</span>
</el-tooltip>
</div>
<div class="start-time">
{{ dateFormat(data.startTime) }}
</div>
<div class="exec-ms">
{{
data.endTime - data.startTime ? data.endTime - data.startTime : "0"
}}
</div>
<div class="exec-percent">
<div class="outer-progress_bar" :style="{ width: outterPercent }">
<div
class="inner-progress_bar"
:style="{ width: innerPercent }"
></div>
</div>
</div>
<div class="self">
{{ data.dur ? data.dur + "" : "0" }}
</div>
<div class="api">
<el-tooltip :content="data.component || '-'" placement="bottom">
<span>{{ data.component || "-" }}</span>
</el-tooltip>
</div>
<div class="application">
<el-tooltip :content="data.serviceCode || '-'" placement="bottom">
<span>{{ data.serviceCode }}</span>
</el-tooltip>
</div>
<div class="application" v-show="type === 'profile'">
<span @click="viewSpanDetail">{{ t("view") }}</span>
</div>
</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"
:key="index"
:data="child"
:type="type"
/>
</div>
<el-dialog
v-model="showDetail"
:destroy-on-close="true"
fullscreen
@closed="showDetail = false"
>
<SpanDetail :currentSpan="data" />
</el-dialog>
</div>
</template>
<script lang="ts">
import dayjs from "dayjs";
import { useI18n } from "vue-i18n";
import { ref, watch, computed, defineComponent } from "vue";
import type { PropType } from "vue";
import SpanDetail from "../D3Graph/SpanDetail.vue";
const props = {
data: { type: Object as PropType<any>, default: () => ({}) },
method: { type: Number, default: 0 },
type: { type: String, default: "" },
};
export default defineComponent({
name: "TableItem",
props,
emits: ["select"],
components: { SpanDetail },
setup(props, { emit }) {
/* global Nullable */
const displayChildren = ref<boolean>(true);
const showDetail = ref<boolean>(false);
const { t } = useI18n();
const traceItem = ref<Nullable<HTMLDivElement>>(null);
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
dayjs(date).format(pattern);
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
);
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) * 100;
result = result > 100 ? 100 : result;
const resultStr = result.toFixed(4) + "%";
return resultStr === "0.0000%" ? "0.9%" : resultStr;
}
});
const innerPercent = computed(() => {
const result = (selfTime.value / execTime.value) * 100;
const resultStr = result.toFixed(4) + "%";
return resultStr === "0.0000%" ? "0.9%" : resultStr;
});
function toggle() {
displayChildren.value = !this.displayChildren.value;
}
function showSelectSpan() {
const items: any = document.querySelectorAll(".trace-item");
for (const item of items) {
item.style.background = "#fff";
}
if (!traceItem.value) {
return;
}
traceItem.value.style.background = "rgba(0, 0, 0, 0.1)";
}
function viewSpanDetail() {
showDetail.value = true;
showSelectSpan();
emit("select", props.data);
}
watch(
() => props.data,
() => {
showSelectSpan();
}
);
return {
displayChildren,
outterPercent,
innerPercent,
viewSpanDetail,
toggle,
dateFormat,
showSelectSpan,
showDetail,
t,
};
},
});
</script>
<style lang="scss" scoped>
@import "./table.scss";
.trace-item.level0 {
color: #448dfe;
&:hover {
background: rgba(0, 0, 0, 0.04);
color: #448dfe;
}
&::before {
position: absolute;
content: "";
width: 5px;
height: 100%;
background: #448dfe;
left: 0;
}
}
.trace-item-error {
color: #e54c17;
}
.trace-item {
// display: flex;
white-space: nowrap;
position: relative;
cursor: pointer;
}
.trace-item.selected {
background: rgba(0, 0, 0, 0.04);
}
.trace-item:not(.level0):hover {
background: rgba(0, 0, 0, 0.04);
}
.trace-item > div {
padding: 0 5px;
display: inline-block;
border: 1px solid transparent;
border-right: 1px dotted silver;
overflow: hidden;
line-height: 30px;
text-overflow: ellipsis;
white-space: nowrap;
}
.trace-item > div.method {
padding-left: 10px;
}
.trace-item div.exec-percent {
width: 100px;
height: 30px;
padding: 0 8px;
.outer-progress_bar {
width: 100%;
height: 6px;
border-radius: 3px;
background: rgb(63, 177, 227);
position: relative;
margin-top: 11px;
border: none;
}
.inner-progress_bar {
position: absolute;
background: rgb(110, 64, 170);
height: 4px;
border-radius: 2px;
left: 0;
border: none;
top: 1px;
}
}
</style>

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.
*/
export const ProfileConstant = [
{
label: "method",
value: "Span",
},
{
label: "start-time",
value: "Start Time",
},
{
label: "exec-ms",
value: "Exec(ms)",
},
{
label: "exec-percent",
value: "Exec(%)",
},
{
label: "self",
value: "Self(ms)",
},
{
label: "api",
value: "API",
},
{
label: "application",
value: "Service",
},
{
label: "application",
value: "Operation",
},
];
export const TraceConstant = [
{
label: "method",
value: "Method",
},
{
label: "start-time",
value: "Start Time",
},
{
label: "exec-ms",
value: "Exec(ms)",
},
{
label: "exec-percent",
value: "Exec(%)",
},
{
label: "self",
value: "Self(ms)",
},
{
label: "api",
value: "API",
},
{
label: "application",
value: "Service",
},
];
export const StatisticsConstant = [
{
label: "method",
value: "Endpoint Name",
key: "endpointName",
},
{
label: "type",
value: "Type",
key: "type",
},
{
label: "max-time",
value: "Max Time(ms)",
key: "maxTime",
},
{
label: "min-time",
value: "Min Time(ms)",
key: "minTime",
},
{
label: "sum-time",
value: "Sum Time(ms)",
key: "sumTime",
},
{
label: "avg-time",
value: "Avg Time(ms)",
key: "avgTime",
},
{
label: "count",
value: "Hits",
key: "count",
},
];

View File

@@ -0,0 +1,76 @@
/**
* 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.
*/
.argument {
width: 150px;
}
.start-time {
width: 150px;
}
.exec-ms {
width: 80px;
}
.exec-percent {
width: 100px;
}
.self {
width: 100px;
}
.api {
width: 120px;
}
.agent {
width: 150px;
}
.application {
width: 150px;
text-align: center;
}
.max-time {
width: 150px;
}
.method {
width: 300px;
}
.avg-time {
width: 150px;
}
.min-time {
width: 150px;
}
.count {
width: 120px;
}
.sum-time {
width: 150px;
}
.type {
width: 60px;
}

View File

@@ -0,0 +1,100 @@
<!-- 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-tree-charts flex-v">
<div>
<span
class="time-charts-item mr-5"
v-for="(i, index) in list"
:key="index"
:style="`color:${computedScale(index)}`"
>
<Icon iconName="issue-open-m" class="mr-5" size="sm" />
<span>{{ i }}</span>
</span>
</div>
<div style="padding: 10px 0">
<a class="trace-tree-btn mr-10" @click="charts.tree.setDefault()">
{{ t("default") }}
</a>
<a class="trace-tree-btn mr-10" @click="charts.tree.getTopSlow()">
{{ t("topSlow") }}
</a>
<a class="trace-tree-btn mr-10" @click="charts.tree.getTopChild()">
{{ t("topChildren") }}
</a>
</div>
<div class="trace-tree">
<Graph ref="charts" :data="data" :traceId="traceId" type="Tree" />
</div>
</div>
</template>
<script lang="ts" setup>
import * as d3 from "d3";
import Graph from "./D3Graph/Index.vue";
import type { PropType } from "vue";
import { Span } from "@/types/trace";
import { useI18n } from "vue-i18n";
import { ref, onMounted } from "vue";
/* global defineProps */
const props = defineProps({
data: { type: Array as PropType<Span[]>, default: () => [] },
traceId: { type: String, default: "" },
});
const { t } = useI18n();
const list = ref<string[]>([]);
const charts = ref<any>(null);
onMounted(() => {
list.value = Array.from(new Set(props.data.map((i: Span) => i.serviceCode)));
});
function computedScale(i: number) {
const sequentialScale = d3
.scaleSequential()
.domain([0, list.value.length + 1])
.interpolator(d3.interpolateCool);
return sequentialScale(i);
}
</script>
<style lang="scss" scoped>
.trace-tree {
height: 100%;
overflow: auto;
}
.trace-tree-btn {
display: inline-block;
border-radius: 4px;
padding: 0px 7px;
background-color: #40454e;
color: #eee;
font-size: 11px;
}
.trace-tree-charts {
overflow: auto;
padding: 10px;
position: relative;
height: calc(100% - 95px);
width: 100%;
}
.time-charts-item {
display: inline-block;
padding: 2px 8px;
border: 1px solid;
font-size: 11px;
border-radius: 4px;
}
</style>

View File

@@ -0,0 +1,28 @@
/**
* 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 List from "./List.vue";
import Tree from "./Tree.vue";
import Table from "./Table/Index.vue";
import Statistics from "./Statistics.vue";
export default {
List,
Tree,
Table,
Statistics,
};

View File

@@ -0,0 +1,314 @@
/**</template>
* 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 * as d3 from "d3";
import d3tip from "d3-tip";
import { Trace } from "@/types/trace";
export default class ListGraph {
private barHeight = 48;
private handleSelectSpan: Nullable<(i: Trace) => void> = null;
private el: Nullable<HTMLDivElement> = null;
private i = 0;
private width = 0;
private height = 0;
private svg: any = null;
private tip: any = null;
private row: any[] = [];
private data: any = [];
private min = 0;
private max = 0;
private list: any[] = [];
private xScale: any = null;
private xAxis: any = null;
private sequentialScale: any = null;
private root: any = null;
constructor(el: HTMLDivElement, handleSelectSpan: (i: Trace) => void) {
this.handleSelectSpan = handleSelectSpan;
this.el = el;
this.width = el.clientWidth - 20;
this.height = el.clientHeight;
this.svg = d3
.select(this.el)
.append("svg")
.attr("class", "trace-list-dowanload")
.attr("width", this.width)
.attr("height", this.height);
this.tip = (d3tip as any)()
.attr("class", "d3-tip")
.offset([-8, 0])
.html((d: any) => {
return `
<div class="mb-5">${d.data.label}</div>
${
d.data.dur
? '<div class="sm">SelfDuration: ' + d.data.dur + "ms</div>"
: ""
}
${
d.data.endTime - d.data.startTime
? '<div class="sm">TotalDuration: ' +
(d.data.endTime - d.data.startTime) +
"ms</div>"
: ""
}
`;
});
this.svg.call(this.tip);
}
diagonal(d: any) {
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: any, row: any[], fixSpansSize: number) {
d3.select(".trace-xaxis").remove();
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 = Array.from(new Set(this.row.map((i) => i.serviceCode)));
this.xScale = d3
.scaleLinear()
.range([0, this.width * 0.387])
.domain([0, this.max]);
this.xAxis = d3.axisTop(this.xScale).tickFormat((d: any) => {
if (d === 0) return 0;
if (d >= 1000) return d / 1000 + "s";
return d;
});
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})`)
.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;
}
draw(callback: any) {
this.update(this.root, callback);
}
click(d: any, scope: any) {
if (!d.data.type) return;
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
scope.update(d);
}
update(source: any, callback: any) {
const t = this;
const nodes = this.root.descendants();
let index = -1;
this.root.eachBefore((n: any) => {
n.x = ++index * this.barHeight + 24;
n.y = n.depth * 12;
});
const node = this.svg
.selectAll(".trace-node")
.data(nodes, (d: any) => d.id || (d.id = ++this.i));
const nodeEnter = node
.enter()
.append("g")
.attr("transform", `translate(${source.y0},${source.x0})`)
.attr("class", "trace-node")
.style("opacity", 0)
.on("mouseover", function (event: any, d: Trace) {
t.tip.show(d, this);
})
.on("mouseout", function (event: any, d: Trace) {
t.tip.hide(d, this);
})
.on("click", (event: any, d: Trace) => {
if (this.handleSelectSpan) {
this.handleSelectSpan(d);
}
});
nodeEnter
.append("rect")
.attr("height", 42)
.attr("ry", 2)
.attr("rx", 2)
.attr("y", -22)
.attr("x", 20)
.attr("width", "100%")
.attr("fill", "rgba(0,0,0,0)");
nodeEnter
.append("text")
.attr("x", 13)
.attr("y", 5)
.attr("fill", "#E54C17")
.html((d: any) => (d.data.isError ? "◉" : ""));
nodeEnter
.append("text")
.attr("class", "node-text")
.attr("x", 35)
.attr("y", -6)
.attr("fill", "#333")
.text((d: any) => {
if (d.data.label === "TRACE_ROOT") {
return "";
}
return d.data.label.length > 40
? `${d.data.label.slice(0, 40)}...`
: `${d.data.label}`;
});
nodeEnter
.append("text")
.attr("class", "node-text")
.attr("x", 35)
.attr("y", 12)
.attr("fill", "#ccc")
.style("font-size", "11px")
.text(
(d: any) =>
`${d.data.layer || ""} ${
d.data.component ? "- " + d.data.component : d.data.component || ""
}`
);
nodeEnter
.append("rect")
.attr("rx", 2)
.attr("ry", 2)
.attr("height", 4)
.attr("width", (d: any) => {
if (!d.data.endTime || !d.data.startTime) return 0;
return this.xScale(d.data.endTime - d.data.startTime) + 1 || 0;
})
.attr("x", (d: any) =>
!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: any) =>
`${this.sequentialScale(this.list.indexOf(d.data.serviceCode))}`
);
nodeEnter
.transition()
.duration(400)
.attr("transform", (d: any) => `translate(${d.y},${d.x})`)
.style("opacity", 1);
nodeEnter
.append("circle")
.attr("r", 3)
.style("cursor", "pointer")
.attr("stroke-width", 2.5)
.attr("fill", (d: any) =>
d._children
? `${this.sequentialScale(this.list.indexOf(d.data.serviceCode))}`
: "rbga(0,0,0,0)"
)
.style("stroke", (d: any) =>
d.data.label === "TRACE_ROOT"
? ""
: `${this.sequentialScale(this.list.indexOf(d.data.serviceCode))}`
)
.on("click", (d: any) => {
this.click(d, this);
});
node
.transition()
.duration(400)
.attr("transform", (d: any) => `translate(${d.y},${d.x})`)
.style("opacity", 1)
.select("circle")
.attr("fill", (d: any) =>
d._children
? `${this.sequentialScale(this.list.indexOf(d.data.serviceCode))}`
: ""
);
// Transition exiting nodes to the parent's new position.
node
.exit()
.transition()
.duration(400)
.attr("transform", `translate(${source.y},${source.x})`)
.style("opacity", 0)
.remove();
const link = this.svg
.selectAll(".trace-link")
.data(this.root.links(), function (d: any) {
return d.target.id;
});
link
.enter()
.insert("path", "g")
.attr("class", "trace-link")
.attr("fill", "rgba(0,0,0,0)")
.attr("stroke", "rgba(0, 0, 0, 0.1)")
.attr("stroke-width", 2)
.attr("d", () => {
const o = { x: source.x0 + 35, y: source.y0 };
return this.diagonal({ source: o, target: o });
})
.transition()
.duration(400)
.attr("d", this.diagonal);
link.transition().duration(400).attr("d", this.diagonal);
link
.exit()
.transition()
.duration(400)
.attr("d", () => {
const o = { x: source.x + 35, y: source.y };
return this.diagonal({ source: o, target: o });
})
.remove();
this.root.each(function (d: any) {
d.x0 = d.x;
d.y0 = d.y;
});
if (callback) {
callback();
}
}
resize() {
if (!this.el) {
return;
}
this.width = this.el.clientWidth - 20;
this.height = this.el.clientHeight;
this.svg.attr("width", this.width).attr("height", this.height);
this.svg.select("g").attr("transform", () => `translate(160, 0)`);
const transform = d3.zoomTransform(this.svg).translate(0, 0);
d3.zoom().transform(this.svg, transform);
}
}

View File

@@ -0,0 +1,412 @@
/**
* 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 * as d3 from "d3";
import d3tip from "d3-tip";
import { Trace, Span } from "@/types/trace";
export default class TraceMap {
private i = 0;
private el: Nullable<HTMLDivElement> = null;
private handleSelectSpan: Nullable<(i: Trace) => void> = null;
private topSlow: any = [];
private height = 0;
private width = 0;
private topChild: any[] = [];
private body: any = null;
private tip: any = null;
private svg: any = null;
private treemap: any = null;
private data: any = null;
private row: any = null;
private min = 0;
private max = 0;
private list: string[] = [];
private xScale: any = null;
private sequentialScale: any = null;
private root: any = null;
private topSlowMax: number[] = [];
private topSlowMin: number[] = [];
private topChildMax: number[] = [];
private topChildMin: number[] = [];
private nodeUpdate: any = null;
constructor(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.body = d3
.select(this.el)
.append("svg")
.attr("class", "d3-trace-tree")
.attr("width", this.width)
.attr("height", this.height);
this.tip = (d3tip as any)()
.attr("class", "d3-tip")
.offset([-8, 0])
.html(
(d: any) => `
<div class="mb-5">${d.data.label}</div>
${
d.data.dur
? '<div class="sm">SelfDuration: ' + d.data.dur + "ms</div>"
: ""
}
${
d.data.endTime - d.data.startTime
? '<div class="sm">TotalDuration: ' +
(d.data.endTime - d.data.startTime) +
"ms</div>"
: ""
}
`
);
this.svg = this.body
.append("g")
.attr("transform", () => `translate(120, 0)`);
this.svg.call(this.tip);
}
resize() {
if (!this.el) {
return;
}
this.width = this.el.clientWidth;
this.height = this.el.clientHeight + 100;
this.body.attr("width", this.width).attr("height", this.height);
this.body.select("g").attr("transform", () => `translate(160, 0)`);
const transform = d3.zoomTransform(this.body).translate(0, 0);
d3.zoom().transform(this.body, transform);
}
init(data: any, row: any) {
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 = Array.from(new Set(this.row.map((i: Span) => i.serviceCode)));
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.body.call(this.getZoomBehavior(this.svg));
this.root = d3.hierarchy(this.data, (d) => d.children);
this.root.x0 = this.height / 2;
this.root.y0 = 0;
this.topSlow = [];
this.topChild = [];
const that = this;
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];
this.topChildMin = this.topChild.sort((a: number, b: number) => b - a)[4];
this.update(this.root);
// Collapse the node and all it's children
function collapse(d: any) {
if (d.children) {
let dur = d.data.endTime - d.data.startTime;
d.children.forEach((i: any) => {
dur -= i.data.endTime - i.data.startTime;
});
d.dur = dur < 0 ? 0 : dur;
that.topSlow.push(dur);
that.topChild.push(d.children.length);
d.childrenLength = d.children.length;
d.children.forEach(collapse);
}
}
}
draw() {
this.update(this.root);
}
update(source: any) {
const that: any = this;
const treeData = this.treemap(this.root);
const nodes = treeData.descendants(),
links = treeData.descendants().slice(1);
nodes.forEach(function (d: any) {
d.y = d.depth * 140;
});
const node = this.svg.selectAll("g.node").data(nodes, (d: any) => {
return d.id || (d.id = ++this.i);
});
const nodeEnter = node
.enter()
.append("g")
.attr("class", "node")
.attr("cursor", "pointer")
.attr("transform", function () {
return "translate(" + source.y0 + "," + source.x0 + ")";
})
.on("mouseover", function (event: any, d: any) {
that.tip.show(d, this);
if (!that.timeUpdate) {
return;
}
const _node = that.timeUpdate._groups[0].filter(
(group: any) => group.__data__.id === that.i + 1
);
if (_node.length) {
that.timeTip.show(d, _node[0].children[1]);
}
})
.on("mouseout", function (event: any, d: any) {
that.tip.hide(d, this);
if (!that.timeUpdate) {
return;
}
const _node = that.timeUpdate._groups[0].filter(
(group: any) => group.__data__.id === that.i + 1
);
if (_node.length) {
that.timeTip.hide(d, _node[0].children[1]);
}
})
.on("click", function (event: any, d: any) {
that.handleSelectSpan(d);
});
nodeEnter
.append("circle")
.attr("class", "node")
.attr("r", 1e-6)
.style("fill", (d: any) =>
d._children
? this.sequentialScale(this.list.indexOf(d.data.serviceCode))
: "#fff"
)
.attr("stroke", (d: any) =>
this.sequentialScale(this.list.indexOf(d.data.serviceCode))
)
.attr("stroke-width", 2.5);
nodeEnter
.append("text")
.attr("font-size", 11)
.attr("dy", "-0.5em")
.attr("x", function (d: any) {
return d.children || d._children ? -15 : 15;
})
.attr("text-anchor", function (d: any) {
return d.children || d._children ? "end" : "start";
})
.text((d: any) =>
d.data.label.length > 19
? (d.data.isError ? "◉ " : "") + d.data.label.slice(0, 19) + "..."
: (d.data.isError ? "◉ " : "") + d.data.label
)
.style("fill", (d: any) => (!d.data.isError ? "#3d444f" : "#E54C17"));
nodeEnter
.append("text")
.attr("class", "node-text")
.attr("x", function (d: any) {
return d.children || d._children ? -15 : 15;
})
.attr("dy", "1em")
.attr("fill", "#bbb")
.attr("text-anchor", function (d: any) {
return d.children || d._children ? "end" : "start";
})
.style("font-size", "10px")
.text(
(d: any) =>
`${d.data.layer || ""}${
d.data.component ? "-" + d.data.component : d.data.component || ""
}`
);
nodeEnter
.append("rect")
.attr("rx", 1)
.attr("ry", 1)
.attr("height", 2)
.attr("width", 100)
.attr("x", function (d: any) {
return d.children || d._children ? "-110" : "10";
})
.attr("y", -1)
.style("fill", "#00000020");
nodeEnter
.append("rect")
.attr("rx", 1)
.attr("ry", 1)
.attr("height", 2)
.attr("width", (d: any) => {
if (!d.data.endTime || !d.data.startTime) return 0;
return this.xScale(d.data.endTime - d.data.startTime) + 1 || 0;
})
.attr("x", (d: any) => {
if (!d.data.endTime || !d.data.startTime) {
return 0;
}
if (d.children || d._children) {
return -110 + this.xScale(d.data.startTime - this.min);
}
return 10 + this.xScale(d.data.startTime - this.min);
})
.attr("y", -1)
.style("fill", (d: any) =>
this.sequentialScale(this.list.indexOf(d.data.serviceCode))
);
const nodeUpdate = nodeEnter.merge(node);
this.nodeUpdate = nodeUpdate;
nodeUpdate
.transition()
.duration(600)
.attr("transform", function (d: any) {
return "translate(" + d.y + "," + d.x + ")";
});
nodeUpdate
.select("circle.node")
.attr("r", 5)
.style("fill", (d: any) =>
d._children
? this.sequentialScale(this.list.indexOf(d.data.serviceCode))
: "#fff"
)
.attr("cursor", "pointer")
.on("click", (d: any) => {
click(d);
});
const nodeExit = node
.exit()
.transition()
.duration(600)
.attr("transform", function () {
return "translate(" + source.y + "," + source.x + ")";
})
.remove();
nodeExit.select("circle").attr("r", 1e-6);
nodeExit.select("text").style("fill-opacity", 1e-6);
const link = this.svg
.selectAll("path.tree-link")
.data(links, function (d: { id: string }) {
return d.id;
})
.style("stroke-width", 1.5);
const linkEnter = link
.enter()
.insert("path", "g")
.attr("class", "tree-link")
.attr("d", function () {
const o = { x: source.x0, y: source.y0 };
return diagonal(o, o);
})
.attr("stroke", "rgba(0, 0, 0, 0.1)")
.style("stroke-width", 1.5)
.style("fill", "none");
const linkUpdate = linkEnter.merge(link);
linkUpdate
.transition()
.duration(600)
.attr("d", function (d: any) {
return diagonal(d, d.parent);
});
link
.exit()
.transition()
.duration(600)
.attr("d", function () {
const o = { x: source.x, y: source.y };
return diagonal(o, o);
})
.style("stroke-width", 1.5)
.remove();
nodes.forEach(function (d: any) {
d.x0 = d.x;
d.y0 = d.y;
});
function diagonal(s: any, d: any) {
return `M ${s.y} ${s.x}
C ${(s.y + d.y) / 2} ${s.x}, ${(s.y + d.y) / 2} ${d.x},
${d.y} ${d.x}`;
}
function click(d: any) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
that.update(d);
}
}
setDefault() {
d3.selectAll(".time-inner").style("opacity", 1);
d3.selectAll(".time-inner-duration").style("opacity", 0);
d3.selectAll(".trace-tree-node-selfdur").style("opacity", 0);
d3.selectAll(".trace-tree-node-selfchild").style("opacity", 0);
this.nodeUpdate._groups[0].forEach((i: any) => {
d3.select(i).style("opacity", 1);
});
}
getTopChild() {
d3.selectAll(".time-inner").style("opacity", 1);
d3.selectAll(".time-inner-duration").style("opacity", 0);
d3.selectAll(".trace-tree-node-selfdur").style("opacity", 0);
d3.selectAll(".trace-tree-node-selfchild").style("opacity", 1);
this.nodeUpdate._groups[0].forEach((i: any) => {
d3.select(i).style("opacity", 0.2);
if (
i.__data__.data.children.length >= this.topChildMin &&
i.__data__.data.children.length <= this.topChildMax
) {
d3.select(i).style("opacity", 1);
}
});
}
getTopSlow() {
d3.selectAll(".time-inner").style("opacity", 0);
d3.selectAll(".time-inner-duration").style("opacity", 1);
d3.selectAll(".trace-tree-node-selfchild").style("opacity", 0);
d3.selectAll(".trace-tree-node-selfdur").style("opacity", 1);
this.nodeUpdate._groups[0].forEach((i: any) => {
d3.select(i).style("opacity", 0.2);
if (
i.__data__.data.dur >= this.topSlowMin &&
i.__data__.data.dur <= this.topSlowMax
) {
d3.select(i).style("opacity", 1);
}
});
}
getZoomBehavior(g: any) {
return d3
.zoom()
.scaleExtent([0.3, 10])
.on("zoom", (d: any) => {
g.attr("transform", d3.zoomTransform(this.svg.node())).attr(
`translate(${d.transform.x},${d.transform.y})scale(${d.transform.k})`
);
});
}
}

View File

@@ -0,0 +1,332 @@
/**
* 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,
Span,
StatisticsSpan,
StatisticsGroupRef,
TraceTreeRef,
} from "@/types/trace";
import lodash from "lodash";
export default class TraceUtil {
public static buildTraceDataList(data: Span[]): string[] {
return Array.from(new Set(data.map((span: Span) => span.serviceCode)));
}
public static changeTree(data: Span[], cureentTraceId: string) {
const segmentIdList: Span[] = [];
const traceTreeRef: any = this.changeTreeCore(data);
traceTreeRef.segmentIdGroup.forEach((segmentId: string) => {
if (traceTreeRef.segmentMap.get(segmentId).refs) {
traceTreeRef.segmentMap.get(segmentId).refs.forEach((ref: Ref) => {
if (ref.traceId === cureentTraceId) {
this.traverseTree(
traceTreeRef.segmentMap.get(ref.parentSegmentId) as Span,
ref.parentSpanId,
ref.parentSegmentId,
traceTreeRef.segmentMap.get(segmentId) as Span
);
}
});
}
});
// set a breakpoint at this line
traceTreeRef.segmentMap.forEach((value: Span) => {
if ((value.refs && value.refs.length === 0) || !value.refs) {
segmentIdList.push(value as Span);
}
});
segmentIdList.forEach((segmentId: Span) => {
this.collapse(segmentId);
});
return segmentIdList;
}
public static changeStatisticsTree(data: Span[]): Map<string, Span[]> {
const result = new Map<string, Span[]>();
const traceTreeRef = this.changeTreeCore(data);
traceTreeRef.segmentMap.forEach((span) => {
const groupRef = span.endpointName + ":" + span.type;
if (span.children && span.children.length > 0) {
this.calculationChildren(span.children, result);
this.collapse(span);
}
if (result.get(groupRef) === undefined) {
result.set(groupRef, []);
result.get(groupRef)!.push(span);
} else {
result.get(groupRef)!.push(span);
}
});
return result;
}
private static changeTreeCore(data: Span[]): TraceTreeRef {
// set a breakpoint at this line
if (data.length === 0) {
return {
segmentMap: new Map(),
segmentIdGroup: [],
};
}
const segmentGroup: any = {};
const segmentMap: Map<string, Span> = new Map();
const segmentIdGroup: string[] = [];
const fixSpans: Span[] = [];
const segmentHeaders: Span[] = [];
data.forEach((span) => {
if (span.parentSpanId === -1) {
segmentHeaders.push(span);
} else {
const index = data.findIndex((patchSpan: Span) => {
return (
patchSpan.segmentId === span.segmentId &&
patchSpan.spanId === span.spanId - 1
);
});
const fixSpanKeyContent = {
traceId: span.traceId,
segmentId: span.segmentId,
spanId: span.spanId - 1,
parentSpanId: span.spanId - 2,
};
if (index === -1 && !lodash.find(fixSpans, fixSpanKeyContent)) {
fixSpans.push({
...fixSpanKeyContent,
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,
});
}
}
});
segmentHeaders.forEach((span) => {
if (span.refs && span.refs.length) {
span.refs.forEach((ref) => {
const index = data.findIndex((patchSpan: Span) => {
return (
ref.parentSegmentId === patchSpan.segmentId &&
ref.parentSpanId === patchSpan.spanId
);
});
if (index === -1) {
// create a known broken node.
const parentSpanId: number = ref.parentSpanId;
const fixSpanKeyContent = {
traceId: ref.traceId,
segmentId: ref.parentSegmentId,
spanId: parentSpanId,
parentSpanId: parentSpanId > -1 ? 0 : -1,
};
if (lodash.find(fixSpans, fixSpanKeyContent)) {
fixSpans.push({
...fixSpanKeyContent,
refs: [],
endpointName: `VNode: ${ref.parentSegmentId}`,
serviceCode: "VirtualNode",
type: `[Broken] ${ref.type}`,
peer: "",
component: `VirtualNode: #${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 (fixSpanKeyContent.parentSpanId > -1) {
const fixRootSpanKeyContent = {
traceId: ref.traceId,
segmentId: ref.parentSegmentId,
spanId: 0,
parentSpanId: -1,
};
if (!lodash.find(fixSpans, fixRootSpanKeyContent)) {
fixSpans.push({
...fixRootSpanKeyContent,
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,
});
}
}
}
});
}
});
[...fixSpans, ...data].forEach((fixSpan: Span) => {
fixSpan.label = fixSpan.endpointName || "no operation name";
fixSpan.children = [];
const id = fixSpan.segmentId || "top";
if (segmentGroup[id] === undefined) {
segmentIdGroup.push(id);
segmentGroup[id] = [];
segmentGroup[id].push(fixSpan);
} else {
segmentGroup[id].push(fixSpan);
}
});
segmentIdGroup.forEach((segmentId: string) => {
const currentSegmentSet = segmentGroup[segmentId].sort(
(a: Span, b: Span) => b.parentSpanId - a.parentSpanId
);
currentSegmentSet.forEach((curSegment: Span) => {
const index = currentSegmentSet.findIndex(
(curSegment2: Span) => curSegment2.spanId === curSegment.parentSpanId
);
if (index !== -1) {
if (
(currentSegmentSet[index].isBroken &&
currentSegmentSet[index].parentSpanId === -1) ||
!currentSegmentSet[index].isBroken
) {
currentSegmentSet[index].children.push(curSegment);
currentSegmentSet[index].children.sort(
(a: Span, b: Span) => a.spanId - b.spanId
);
}
}
if (curSegment.isBroken) {
const children = lodash.filter(data, (span: Span) => {
return lodash.find(span.refs, {
traceId: curSegment.traceId,
parentSegmentId: curSegment.segmentId,
parentSpanId: curSegment.spanId,
});
}) as Span[];
if (children.length) {
curSegment.children = curSegment.children || [];
curSegment.children.push(...children);
}
}
});
segmentMap.set(
segmentId,
currentSegmentSet[currentSegmentSet.length - 1]
);
});
return {
segmentMap,
segmentIdGroup,
};
}
private static collapse(span: Span) {
if (span.children) {
let dur = span.endTime - span.startTime;
span.children.forEach((chlid: Span) => {
dur -= chlid.endTime - chlid.startTime;
});
span.dur = dur < 0 ? 0 : dur;
span.children.forEach((chlid) => this.collapse(chlid));
}
}
private static traverseTree(
node: Span,
spanId: number,
segmentId: string,
childNode: Span
) {
if (!node || node.isBroken) {
return;
}
if (node.spanId === spanId && node.segmentId === segmentId) {
node.children!.push(childNode);
return;
}
if (node.children && node.children.length > 0) {
for (const grandchild of node.children) {
this.traverseTree(grandchild, spanId, segmentId, childNode);
}
}
}
private static getSpanGroupData(
groupspans: Span[],
groupRef: StatisticsGroupRef
): StatisticsSpan {
let maxTime = 0;
let minTime = 0;
let sumTime = 0;
const count = groupspans.length;
groupspans.forEach((groupspan: Span) => {
const duration = groupspan.dur || 0;
if (duration > maxTime) {
maxTime = duration;
}
if (duration < minTime) {
minTime = duration;
}
sumTime = sumTime + duration;
});
const avgTime = count === 0 ? 0 : sumTime / count;
return {
groupRef,
maxTime,
minTime,
sumTime,
avgTime,
count,
};
}
private static calculationChildren(
nodes: Span[],
result: Map<string, Span[]>
): void {
nodes.forEach((node: Span) => {
const groupRef = node.endpointName + ":" + node.type;
if (node.children && node.children.length > 0) {
this.calculationChildren(node.children, result);
}
if (result.get(groupRef) === undefined) {
result.set(groupRef, []);
result.get(groupRef)!.push(node);
} else {
result.get(groupRef)!.push(node);
}
});
}
}