mirror of
https://github.com/apache/skywalking-booster-ui.git
synced 2025-10-14 11:21:29 +00:00
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:
138
src/views/components/ConditionTags.vue
Normal file
138
src/views/components/ConditionTags.vue
Normal 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>
|
136
src/views/components/LogTable/Index.vue
Normal file
136
src/views/components/LogTable/Index.vue
Normal 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>
|
107
src/views/components/LogTable/LogBrowser.vue
Normal file
107
src/views/components/LogTable/LogBrowser.vue
Normal 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>
|
75
src/views/components/LogTable/LogDetail.vue
Normal file
75
src/views/components/LogTable/LogDetail.vue
Normal 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>
|
115
src/views/components/LogTable/LogService.vue
Normal file
115
src/views/components/LogTable/LogService.vue
Normal 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>
|
120
src/views/components/LogTable/data.ts
Normal file
120
src/views/components/LogTable/data.ts
Normal 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",
|
||||
},
|
||||
];
|
@@ -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();
|
||||
|
@@ -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",
|
||||
|
@@ -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";
|
@@ -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();
|
95
src/views/dashboard/controls/Trace.vue
Normal file
95
src/views/dashboard/controls/Trace.vue
Normal 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>
|
22
src/views/dashboard/controls/index.ts
Normal file
22
src/views/dashboard/controls/index.ts
Normal 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 };
|
@@ -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"];
|
||||
|
@@ -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[]) {
|
||||
|
@@ -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(
|
||||
|
279
src/views/dashboard/related/trace/Detail.vue
Normal file
279
src/views/dashboard/related/trace/Detail.vue
Normal 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>
|
239
src/views/dashboard/related/trace/Filter.vue
Normal file
239
src/views/dashboard/related/trace/Filter.vue
Normal 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>
|
213
src/views/dashboard/related/trace/TraceList.vue
Normal file
213
src/views/dashboard/related/trace/TraceList.vue
Normal 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>
|
354
src/views/dashboard/related/trace/components/D3Graph/Index.vue
Normal file
354
src/views/dashboard/related/trace/components/D3Graph/Index.vue
Normal 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>
|
@@ -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>
|
106
src/views/dashboard/related/trace/components/List.vue
Normal file
106
src/views/dashboard/related/trace/components/List.vue
Normal 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>
|
125
src/views/dashboard/related/trace/components/Statistics.vue
Normal file
125
src/views/dashboard/related/trace/components/Statistics.vue
Normal 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>
|
116
src/views/dashboard/related/trace/components/Table/Index.vue
Normal file
116
src/views/dashboard/related/trace/components/Table/Index.vue
Normal 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>
|
@@ -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>
|
304
src/views/dashboard/related/trace/components/Table/TableItem.vue
Normal file
304
src/views/dashboard/related/trace/components/Table/TableItem.vue
Normal 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>
|
120
src/views/dashboard/related/trace/components/Table/data.ts
Normal file
120
src/views/dashboard/related/trace/components/Table/data.ts
Normal 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",
|
||||
},
|
||||
];
|
@@ -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;
|
||||
}
|
100
src/views/dashboard/related/trace/components/Tree.vue
Normal file
100
src/views/dashboard/related/trace/components/Tree.vue
Normal 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>
|
28
src/views/dashboard/related/trace/components/index.ts
Normal file
28
src/views/dashboard/related/trace/components/index.ts
Normal 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,
|
||||
};
|
314
src/views/dashboard/related/trace/utils/d3-trace-list.ts
Normal file
314
src/views/dashboard/related/trace/utils/d3-trace-list.ts
Normal 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);
|
||||
}
|
||||
}
|
412
src/views/dashboard/related/trace/utils/d3-trace-tree.ts
Normal file
412
src/views/dashboard/related/trace/utils/d3-trace-tree.ts
Normal 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})`
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
332
src/views/dashboard/related/trace/utils/trace-table.ts
Normal file
332
src/views/dashboard/related/trace/utils/trace-table.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user