feat: Implement task timeline and policy list widget for continous profiling (#280)

This commit is contained in:
Fine0830
2023-06-12 16:17:38 +08:00
committed by GitHub
parent 7738695601
commit 22db68646c
48 changed files with 2088 additions and 45 deletions

View File

@@ -0,0 +1,51 @@
<!-- 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 content">
<policy-list />
<div class="flex-v list" v-loading="continousProfilingStore.instancesLoading">
<instance-list :config="config" />
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from "vue";
import { useContinousProfilingStore } from "@/store/modules/continous-profiling";
import PolicyList from "./components/PolicyList.vue";
import InstanceList from "./components/InstanceList.vue";
const continousProfilingStore = useContinousProfilingStore();
/*global defineProps */
defineProps({
config: {
type: Object as PropType<any>,
default: () => ({}),
},
});
</script>
<style lang="scss" scoped>
.content {
height: calc(100% - 50px);
width: 100%;
}
.list {
height: 100%;
flex-grow: 2;
min-width: 600px;
overflow: hidden;
}
</style>

View File

@@ -0,0 +1,134 @@
<!-- 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="policy-list">
<el-collapse v-model="activeNames">
<el-collapse-item v-for="(_, index) in policyList" :key="index" :name="String(index)">
<template #title>
<div>
<span class="title">{{ `Policy - ${index + 1}` }}</span>
<Icon
class="mr-5 cp"
iconName="remove_circle_outline"
size="middle"
v-show="policyList.length !== 1"
@click="removePolicy($event, index)"
/>
<Icon
class="cp"
v-show="index === policyList.length - 1"
iconName="add_circle_outlinecontrol_point"
size="middle"
@click="createPolicy"
/>
</div>
</template>
<Policy :policyList="policyList" @edit="changePolicy" :order="index" />
</el-collapse-item>
</el-collapse>
<div>
<el-button @click="save" type="primary" class="save-btn">
{{ t("save") }}
</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import Policy from "./Policy.vue";
import type { StrategyItem, CheckItems } from "@/types/continous-profiling";
/* global defineEmits, defineProps */
const props = defineProps({
policyList: {
type: Array as PropType<StrategyItem[]>,
default: () => [],
},
});
const emits = defineEmits(["save"]);
const { t } = useI18n();
const activeNames = ref(["0"]);
const policyList = ref<StrategyItem[]>([...props.policyList]);
function changePolicy(params: StrategyItem, order: number) {
policyList.value = policyList.value.map((d: StrategyItem, index: number) => {
if (order === index) {
return params;
}
return d;
});
}
function removePolicy(e: PointerEvent, key: number) {
e.stopPropagation();
if (policyList.value.length === 1) {
return;
}
policyList.value = policyList.value.filter((_, index: number) => index !== key);
}
function createPolicy(e: PointerEvent) {
e.stopPropagation();
policyList.value.push({
type: "",
checkItems: [
{
type: "",
threshold: "",
period: NaN,
count: NaN,
},
],
});
activeNames.value = [String(policyList.value.length - 1)];
}
function save() {
const params = [];
for (const d of policyList.value) {
const checkItems = d.checkItems.filter(
(item: CheckItems) => item.type && item.threshold && item.period && item.count,
);
if (d.type && checkItems.length) {
const v = {
targetType: d.type,
checkItems,
};
params.push(v);
}
}
emits("save", params);
}
</script>
<style lang="scss" scoped>
.policy-list {
margin: 0 auto;
width: 300px;
}
.save-btn {
width: 300px;
margin-top: 10px;
}
.title {
display: inline-block;
margin-right: 5px;
}
</style>

View File

@@ -0,0 +1,185 @@
<!-- 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="header">
{{ t("monitorInstances") }}
</div>
<el-table :data="currentInstances" style="width: 99%" height="440">
<el-table-column type="expand">
<template #default="props">
<div class="child">
<div class="title">{{ t("attributes") }}</div>
<div v-for="(attr, index) in props.row.attributes" :key="index">
{{ `${attr.name}: ${attr.value}` }}
</div>
<div class="title mt-10">{{ t("processes") }}</div>
<el-table :data="props.row.processes" size="small" max-height="300">
<el-table-column prop="name" label="Name">
<template #default="scope">
<span
:class="config.processDashboardName ? 'link' : ''"
@click="viewProcessDashboard(scope.row, props.row)"
>
{{ scope.row.name }}
</span>
</template>
</el-table-column>
<el-table-column
v-for="item in HeaderChildLabels"
:key="item.value"
:label="item.label"
:prop="item.value"
:width="item.width"
/>
</el-table>
</div>
</template>
</el-table-column>
<el-table-column prop="name" label="Name">
<template #default="scope">
<span :class="config.instanceDashboardName ? 'link' : ''" @click="viewInstanceDashboard(scope.row)">
{{ scope.row.name }}
</span>
</template>
</el-table-column>
<el-table-column
v-for="item in HeaderLabels"
:key="item.value"
:label="item.label"
:prop="item.value"
:width="item.width"
/>
</el-table>
<el-pagination
class="mt-10"
small
background
layout="prev, pager, next"
:page-size="pageSize"
:total="instances.length"
@current-change="changePage"
@prev-click="changePage"
@next-click="changePage"
/>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from "vue";
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import { useContinousProfilingStore } from "@/store/modules/continous-profiling";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useSelectorStore } from "@/store/modules/selectors";
import type { MonitorInstance, MonitorProcess } from "@/types/continous-profiling";
import router from "@/router";
import { HeaderLabels, HeaderChildLabels } from "../data";
import { EntityType } from "../../../data";
import { dateFormat } from "@/utils/dateFormat";
/*global defineProps */
const props = defineProps({
config: {
type: Object as PropType<any>,
default: () => ({}),
},
});
const { t } = useI18n();
const dashboardStore = useDashboardStore();
const selectorStore = useSelectorStore();
const continousProfilingStore = useContinousProfilingStore();
const pageSize = 10;
const instances = computed(() => {
return continousProfilingStore.instances
.map((d: MonitorInstance) => {
const processes = (d.processes || [])
.sort((c: MonitorProcess, d: MonitorProcess) => d.lastTriggerTimestamp - c.lastTriggerTimestamp)
.map((p: MonitorProcess) => {
return {
...p,
lastTriggerTime: d.lastTriggerTimestamp ? dateFormat(d.lastTriggerTimestamp) : "",
labels: p.labels.join("; "),
};
});
return { ...d, processes, lastTriggerTime: d.lastTriggerTimestamp ? dateFormat(d.lastTriggerTimestamp) : "" };
})
.sort((a: MonitorInstance, b: MonitorInstance) => b.lastTriggerTimestamp - a.lastTriggerTimestamp);
});
const currentInstances = ref<MonitorInstance[]>([]);
function viewProcessDashboard(process: MonitorProcess, instance: MonitorInstance) {
if (!props.config.processDashboardName) {
return;
}
router.push(
`/dashboard/${dashboardStore.layerId}/${EntityType[8].value}/${selectorStore.currentService.id}/${instance.id}/${process.id}/${props.config.processDashboardName}`,
);
}
function viewInstanceDashboard(instance: MonitorInstance) {
if (!props.config.instanceDashboardName) {
return;
}
router.push(
`/dashboard/${dashboardStore.layerId}/${EntityType[3].value}/${selectorStore.currentService.id}/${instance.id}/${props.config.instanceDashboardName}`,
);
}
async function changePage(pageIndex: number) {
currentInstances.value = instances.value.filter((d: unknown, index: number) => {
if (index >= (pageIndex - 1) * pageSize && index < pageIndex * pageSize) {
return d;
}
});
}
watch(
() => instances.value,
() => {
currentInstances.value = instances.value.filter((_: unknown, index: number) => index < pageSize);
},
);
</script>
<style lang="scss" scoped>
.title {
font-size: 12px;
font-weight: bold;
}
.child {
padding-left: 20px;
}
.header {
font-size: 13px;
font-weight: bold;
border-bottom: 1px solid rgb(0 0 0 / 7%);
padding: 10px 20px;
background-color: #f3f4f9;
}
.settings {
padding: 1px 0;
border: 1px solid #666;
border-radius: 3px;
color: #666;
cursor: pointer;
}
.link {
cursor: pointer;
color: #409eff;
text-decoration: underline;
}
</style>

View File

@@ -0,0 +1,226 @@
<!-- 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>
<div class="label">{{ t("targetTypes") }}</div>
<Selector
class="profile-input"
size="small"
:value="states.type"
:options="TargetTypes"
placeholder="Select a type"
@change="changeType"
/>
</div>
<div v-for="(item, index) in states.checkItems" :key="index">
<div class="item-title">
<span class="title">{{ `Item - ${index + 1}` }}</span>
<Icon
class="ml-5 cp"
iconName="remove_circle_outline"
size="middle"
v-show="states.checkItems.length !== 1"
@click="removeItem($event, index)"
/>
<Icon
class="ml-5 cp"
v-show="index === states.checkItems.length - 1"
iconName="add_circle_outlinecontrol_point"
size="middle"
@click="createItem"
/>
</div>
<div>
<div class="label">{{ t("monitorType") }}</div>
<Selector
class="profile-input"
size="small"
:value="item.type"
:options="MonitorType"
placeholder="Select a type"
@change="changeMonitorType($event, index)"
/>
</div>
<div>
<div class="label">{{ t("count") }}</div>
<el-input-number size="small" class="profile-input" :min="0" v-model="item.count" @change="changeParam" />
</div>
<div>
<div class="label">
<span class="mr-5">{{ t("threshold") }}</span>
<span>({{ getNotice(item.type) }} )</span>
</div>
<el-input
type="number"
size="small"
class="profile-input"
v-model="item.threshold"
@change="changeThreshold(index)"
/>
</div>
<div>
<div class="label">{{ t("period") }}</div>
<el-input-number size="small" class="profile-input" :min="0" v-model="item.period" @change="changeParam" />
</div>
<div v-show="TYPES.includes(item.type)">
<div class="label">{{ t("uriRegex") }}</div>
<el-input size="small" class="profile-input" v-model="item.uriRegex" @change="changeParam(index)" />
</div>
<div v-show="TYPES.includes(item.type)">
<div class="label">{{ t("uriList") }}</div>
<div id="uri-param" contenteditable="true" @input="changeURI($event, index)" class="profile-input">
{{ (item.uriList || []).join("; ") }}
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive } from "vue";
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import { ElMessage } from "element-plus";
import type { StrategyItem, CheckItems } from "@/types/continous-profiling";
import { MonitorType, TargetTypes } from "../data";
/* global defineEmits, defineProps */
const props = defineProps({
policyList: {
type: Object as PropType<StrategyItem[]>,
default: () => ({}),
},
order: {
type: Number,
default: 0,
},
});
const emits = defineEmits(["edit"]);
const { t } = useI18n();
const states = reactive<StrategyItem>(props.policyList[props.order]);
const TYPES = ["HTTP_ERROR_RATE", "HTTP_AVG_RESPONSE_TIME"];
function changeType(opt: { value: string }[]) {
const types = props.policyList.map((item: StrategyItem) => item.type);
if (types.includes(opt[0].value)) {
return ElMessage.warning("Target type cannot be configured repeatedly.");
}
states.type = opt[0].value;
emits("edit", states, props.order);
}
function changeMonitorType(opt: { value: string }[], index: number) {
const types = states.checkItems.map((item: CheckItems) => item.type);
if (types.includes(opt[0].value)) {
return ElMessage.warning("Monitor type cannot be configured repeatedly.");
}
states.checkItems[index].type = opt[0].value;
emits("edit", states, props.order);
}
function changeURI(event: any, index: number) {
if (states.checkItems[index].uriRegex) {
return ElMessage.warning("UriList or UriRegex only be configured with one option.");
}
const params = (event.target.textContent || "").replace(/\s+/g, "");
const arr = params.splice(";");
states.checkItems[index].uriList = arr.length ? arr : null;
emits("edit", states, props.order);
}
function changeThreshold(index: number) {
let regex = /^(100(\.0{1,2})?|[1-9]?\d(\.\d{1,2})?)$/;
if (MonitorType[1].value === states.checkItems[index].type) {
regex = /^\d+$/;
}
if (MonitorType[2].value === states.checkItems[index].type) {
regex = /^(\d+)(\.\d+)?$/;
}
if (MonitorType[4].value === states.checkItems[index].type) {
regex = /^[+]{0,1}(\d+)$|^[+]{0,1}(\d+\.\d+)$/;
}
if (!regex.test(states.checkItems[index].threshold)) {
return ElMessage.error(getNotice(states.checkItems[index].type));
}
emits("edit", states, props.order);
}
function changeParam(index?: any) {
if (index !== undefined && (states.checkItems[index] || {}).uriList) {
return ElMessage.warning("UriList or UriRegex only be configured with one option");
}
const checkItems = states.checkItems.map((d: CheckItems) => {
d.count = Number(d.count);
d.period = Number(d.period);
return d;
});
emits("edit", { ...states, checkItems }, props.order);
}
function createItem(e: PointerEvent) {
e.stopPropagation();
states.checkItems.push({
type: "",
threshold: "",
period: NaN,
count: NaN,
});
emits("edit", states, props.order);
}
function removeItem(e: PointerEvent, key: number) {
e.stopPropagation();
if (states.checkItems.length === 1) {
return;
}
states.checkItems = states.checkItems.filter((_, index: number) => index !== key);
emits("edit", states, props.order);
}
function getNotice(type: string) {
const map: { [key: string]: string } = {
PROCESS_CPU: "It is a percentage data",
PROCESS_THREAD_COUNT: "It is a positive integer",
SYSTEM_LOAD: "It is a floating point number",
HTTP_ERROR_RATE: "It is percentage data",
HTTP_AVG_RESPONSE_TIME: "It is a response time in milliseconds",
};
return map[type];
}
</script>
<style lang="scss" scoped>
.profile-input {
width: 300px;
margin-bottom: 10px;
}
#uri-param {
border: 1px solid #dcdfe6;
cursor: text;
padding: 0 5px;
border-radius: 4px;
color: #606266;
outline: none;
height: 100px;
&:focus {
border-color: #409eff;
}
}
.item-title {
margin-bottom: 5px;
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,205 @@
<!-- 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="profile-task-list flex-v" v-loading="continousProfilingStore.policyLoading">
<div class="profile-task-wrapper flex-v">
<div class="profile-t-tool">
<span>{{ t("policyList") }}</span>
<span class="new-task cp" @click="setStrategies">
<Icon iconName="edit" size="middle" />
</span>
</div>
<div class="profile-t-wrapper">
<div class="no-data" v-show="!continousProfilingStore.strategyList.length">
{{ t("noData") }}
</div>
<table class="profile-t">
<tr
class="profile-tr cp"
v-for="(i, index) in continousProfilingStore.strategyList"
@click="changePolicy(i)"
:key="index"
>
<td
class="profile-td"
:class="{
selected: continousProfilingStore.selectedStrategy.id === i.id,
}"
>
<div class="ell">
<span class="sm">
{{ i.type }}
</span>
</div>
<div class="grey ell sm" v-for="(item, index) in i.checkItems" :key="index">
<span class="sm">
{{
`${item.type} >= ${item.threshold}${
[MonitorType[0].value, MonitorType[3].value].includes(item.type) ? "%" : ""
}; `
}}
</span>
</div>
</td>
</tr>
</table>
</div>
</div>
</div>
<el-dialog
v-model="updateStrategies"
:title="t('editStrategy')"
:destroy-on-close="true"
fullscreen
@closed="updateStrategies = false"
>
<EditPolicy :policyList="continousProfilingStore.strategyList" @save="editStrategies" />
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { useContinousProfilingStore } from "@/store/modules/continous-profiling";
import { useSelectorStore } from "@/store/modules/selectors";
import type { StrategyItem, CheckItems } from "@/types/continous-profiling";
import { ElMessage } from "element-plus";
import EditPolicy from "./EditPolicy.vue";
import { MonitorType } from "../data";
const { t } = useI18n();
const selectorStore = useSelectorStore();
const continousProfilingStore = useContinousProfilingStore();
const updateStrategies = ref<boolean>(false);
const inProcess = ref<boolean>(false);
fetchStrategyList();
async function changePolicy(item: StrategyItem) {
continousProfilingStore.setSelectedStrategy(item);
const serviceId = (selectorStore.currentService && selectorStore.currentService.id) || "";
await continousProfilingStore.getMonitoringInstances(serviceId);
}
function setStrategies() {
updateStrategies.value = true;
}
async function editStrategies(
targets: {
targetType: string;
checkItems: CheckItems[];
}[],
) {
const serviceId = (selectorStore.currentService && selectorStore.currentService.id) || "";
if (!serviceId) {
return ElMessage.error("No Service ID");
}
const res = await continousProfilingStore.setContinuousProfilingPolicy(serviceId, targets);
if (res.errors) {
ElMessage.error(res.errors);
return;
}
if (!res.data.strategy.status) {
ElMessage.error(res.data.strategy.errorReason);
return;
}
updateStrategies.value = false;
await fetchStrategyList();
}
async function fetchStrategyList() {
const serviceId = (selectorStore.currentService && selectorStore.currentService.id) || "";
const res = await continousProfilingStore.getStrategyList({
serviceId,
});
if (res.errors) {
return ElMessage.error(res.errors);
}
if (!continousProfilingStore.strategyList.length) {
return;
}
}
watch(
() => selectorStore.currentService,
() => {
inProcess.value = false;
fetchStrategyList();
},
);
</script>
<style lang="scss" scoped>
.profile-task-list {
width: 300px;
height: 98%;
overflow: auto;
border-right: 1px solid rgb(0 0 0 / 10%);
}
.item span {
height: 21px;
}
.profile-td {
padding: 10px 5px 10px 10px;
border-bottom: 1px solid rgb(0 0 0 / 7%);
&.selected {
background-color: #ededed;
}
}
.no-data {
text-align: center;
margin-top: 10px;
}
.profile-t-wrapper {
overflow: auto;
flex-grow: 1;
}
.profile-t {
width: 100%;
border-spacing: 0;
table-layout: fixed;
flex-grow: 1;
position: relative;
border: none;
}
.profile-tr {
&:hover {
background-color: rgb(0 0 0 / 4%);
}
}
.profile-t-tool {
padding: 10px 5px 10px 10px;
border-bottom: 1px solid rgb(0 0 0 / 7%);
background: #f3f4f9;
width: 100%;
font-weight: bold;
}
.new-task {
float: right;
}
.reload {
margin-left: 30px;
}
</style>

View File

@@ -0,0 +1,44 @@
/**
* 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 MonitorType: any = [
{ label: "PROCESS_CPU", value: "PROCESS_CPU" },
{ label: "PROCESS_THREAD_COUNT", value: "PROCESS_THREAD_COUNT" },
{ label: "SYSTEM_LOAD", value: "SYSTEM_LOAD" },
{ label: "HTTP_ERROR_RATE", value: "HTTP_ERROR_RATE" },
{ label: "HTTP_AVG_RESPONSE_TIME", value: "HTTP_AVG_RESPONSE_TIME" },
];
export const TargetTypes = [
{ label: "ON_CPU", value: "ON_CPU" },
{ label: "OFF_CPU", value: "OFF_CPU" },
{ label: "NETWORK", value: "NETWORK" },
];
export const ComponentType = "CONTINOUS_PROFILING";
export const HeaderLabels = [
{ value: "triggeredCount", label: "Triggered Count", width: 150 },
{ value: "lastTriggerTime", label: "Last Trigger Time", width: 170 },
];
export const HeaderChildLabels = [
{ value: "detectType", label: "Detect Type", width: 100 },
{ value: "triggeredCount", label: "Triggered Count", width: 120 },
{ value: "lastTriggerTime", label: "Last Trigger Time", width: 160 },
{ value: "labels", label: "Labels" },
];

View File

@@ -33,6 +33,7 @@ limitations under the License. -->
import { useDashboardStore } from "@/store/modules/dashboard";
import { useAppStoreWithOut } from "@/store/modules/app";
import { EntityType } from "../../data";
import { EBPFProfilingTriggerType } from "@/store/data";
/*global defineProps */
const props = defineProps({
@@ -54,6 +55,7 @@ limitations under the License. -->
const res = await ebpfStore.getTaskList({
serviceId,
targets: ["ON_CPU", "OFF_CPU"],
triggerType: EBPFProfilingTriggerType.FIXED_TIME,
});
if (res.errors) {

View File

@@ -42,7 +42,7 @@ limitations under the License. -->
/>
<el-popover placement="bottom" :width="680" trigger="click" :persistent="false">
<template #reference>
<el-button type="primary" size="small">
<el-button size="small">
{{ t("processSelect") }}
</el-button>
</template>
@@ -98,12 +98,21 @@ limitations under the License. -->
import type { Option } from "@/types/app";
import { TableHeader, AggregateTypes } from "./data";
import { useEbpfStore } from "@/store/modules/ebpf";
import { useContinousProfilingStore } from "@/store/modules/continous-profiling";
import type { EBPFProfilingSchedule, Process } from "@/types/ebpf";
import { ElMessage, ElTable } from "element-plus";
import { dateFormat } from "@/utils/dateFormat";
import { ComponentType } from "@/views/dashboard/related/continuous-profiling/data";
const { t } = useI18n();
const ebpfStore = useEbpfStore();
/*global defineProps*/
const props = defineProps({
type: {
type: String,
default: "",
},
});
const ebpfStore = props.type === ComponentType ? useContinousProfilingStore() : useEbpfStore();
const pageSize = 5;
const multipleTableRef = ref<InstanceType<typeof ElTable>>();
const selectedProcesses = ref<string[]>([]);

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div id="graph-stack" ref="graph">
<span class="tip" v-show="ebpfStore.tip">{{ ebpfStore.tip }}</span>
<span class="tip" v-show="ebpfStore.ebpfTips">{{ ebpfStore.ebpfTips }}</span>
</div>
</template>
<script lang="ts" setup>
@@ -23,12 +23,20 @@ limitations under the License. -->
import d3tip from "d3-tip";
import { flamegraph } from "d3-flame-graph";
import { useEbpfStore } from "@/store/modules/ebpf";
import { useContinousProfilingStore } from "@/store/modules/continous-profiling";
import { ComponentType } from "@/views/dashboard/related/continuous-profiling/data";
import type { StackElement } from "@/types/ebpf";
import { AggregateTypes } from "./data";
import "d3-flame-graph/dist/d3-flamegraph.css";
/*global Nullable*/
const ebpfStore = useEbpfStore();
/*global Nullable, defineProps*/
const props = defineProps({
type: {
type: String,
default: "",
},
});
const ebpfStore = props.type === ComponentType ? useContinousProfilingStore() : useEbpfStore();
const stackTree = ref<Nullable<StackElement>>(null);
const selectStack = ref<Nullable<StackElement>>(null);
const graph = ref<Nullable<HTMLDivElement>>(null);
@@ -90,7 +98,7 @@ limitations under the License. -->
.setColorMapper((d, originalColor) => (d.highlight ? "#6aff8f" : originalColor));
const tip = (d3tip as any)()
.attr("class", "d3-tip")
.direction("w")
.direction("s")
.html((d: { data: StackElement } & { parent: { data: StackElement } }) => {
const name = d.data.name.replace("<", "&lt;").replace(">", "&gt;");
const valStr =
@@ -111,7 +119,7 @@ limitations under the License. -->
}</div>`;
return `<div class="mb-5 name">Symbol: ${name}</div>${valStr}${rateOfParent}${rateOfRoot}`;
})
.style("max-width", "500px");
.style("max-width", "400px");
flameChart.value.tooltip(tip);
d3.select("#graph-stack").datum(stackTree.value).call(flameChart.value);
}

View File

@@ -34,7 +34,7 @@ limitations under the License. -->
defineProps({
config: {
type: Object as PropType<any>,
default: () => ({ graph: {} }),
default: () => ({}),
},
});
const networkProfilingStore = useNetworkProfilingStore();

View File

@@ -19,7 +19,7 @@ limitations under the License. -->
<g class="hex-polygon">
<path :d="getHexPolygonVertices()" stroke="#D5DDF6" stroke-width="2" fill="none" />
<text :x="0" :y="radius - 15" fill="#000" text-anchor="middle">
{{ selectorStore.currentPod.label }}
{{ selectorStore.currentPod && selectorStore.currentPod.label }}
</text>
</g>
<g class="nodes">
@@ -530,11 +530,11 @@ limitations under the License. -->
border-radius: 3px;
position: absolute;
top: 10px;
right: 10px;
right: 20px;
}
.range {
right: 50px;
right: 60px;
}
.topo-call {

View File

@@ -77,6 +77,7 @@ limitations under the License. -->
import getLocalTime from "@/utils/localtime";
import { useAppStoreWithOut } from "@/store/modules/app";
import NewTask from "./NewTask.vue";
import { EBPFProfilingTriggerType } from "@/store/data";
/*global Nullable */
const { t } = useI18n();
@@ -185,6 +186,7 @@ limitations under the License. -->
serviceId,
serviceInstanceId,
targets: ["NETWORK"],
triggerType: EBPFProfilingTriggerType.FIXED_TIME,
});
if (res.errors) {

View File

@@ -0,0 +1,40 @@
<!-- 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-v content">
<Timeline />
<ProfilingPanel :config="config" />
</div>
</template>
<script lang="ts" setup>
import type { PropType } from "vue";
import Timeline from "./components/Timeline.vue";
import ProfilingPanel from "./components/ProfilingPanel.vue";
/* global defineProps */
defineProps({
config: {
type: Object as PropType<any>,
default: () => ({}),
},
});
</script>
<style lang="scss" scoped>
.content {
height: calc(100% - 50px);
width: 100%;
padding: 0 10px;
}
</style>

View File

@@ -0,0 +1,83 @@
<!-- 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="content" v-if="taskTimelineStore.selectedTask.targetType === TargetTypes[2].value">
<process-topology v-if="networkProfilingStore.nodes.length" :config="config" />
</div>
<div
class="content"
v-if="[TargetTypes[1].value, TargetTypes[0].value].includes(taskTimelineStore.selectedTask.targetType)"
>
<div class="schedules">
<EBPFSchedules />
</div>
<div class="item">
<EBPFStack />
</div>
</div>
<div class="text" v-if="!taskTimelineStore.selectedTask.targetType">
{{ t("noData") }}
</div>
</template>
<script lang="ts" setup>
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import { useTaskTimelineStore } from "@/store/modules/task-timeline";
import { useNetworkProfilingStore } from "@/store/modules/network-profiling";
import { TargetTypes } from "../../continuous-profiling/data";
import ProcessTopology from "@/views/dashboard/related/network-profiling/components/ProcessTopology.vue";
import EBPFSchedules from "@/views/dashboard/related/ebpf/components/EBPFSchedules.vue";
import EBPFStack from "@/views/dashboard/related/ebpf/components/EBPFStack.vue";
/*global defineProps */
defineProps({
config: {
type: Object as PropType<any>,
default: () => ({}),
},
});
const { t } = useI18n();
const taskTimelineStore = useTaskTimelineStore();
const networkProfilingStore = useNetworkProfilingStore();
</script>
<style lang="scss" scoped>
.content {
width: 100%;
height: calc(100% - 30px);
flex-grow: 2;
min-width: 700px;
overflow: hidden;
position: relative;
}
.text {
width: 100%;
text-align: center;
margin-top: 100px;
}
.item {
width: 100%;
overflow: auto;
height: calc(100% - 100px);
padding-bottom: 10px;
}
.schedules {
height: 90px;
border-bottom: 1px solid #ccc;
padding-right: 10px;
}
</style>

View File

@@ -0,0 +1,177 @@
<!-- 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 ref="timeline" class="task-timeline"></div>
</template>
<script lang="ts" setup>
import { ref, watch, onMounted, onUnmounted } from "vue";
import dayjs from "dayjs";
import { useThrottleFn } from "@vueuse/core";
import { ElMessage } from "element-plus";
import type { EBPFTaskList } from "@/types/ebpf";
import { useTaskTimelineStore } from "@/store/modules/task-timeline";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useSelectorStore } from "@/store/modules/selectors";
import { useContinousProfilingStore } from "@/store/modules/continous-profiling";
import { DataSet, Timeline } from "vis-timeline/standalone";
import "vis-timeline/styles/vis-timeline-graph2d.css";
import { EBPFProfilingTriggerType } from "@/store/data";
const taskTimelineStore = useTaskTimelineStore();
const selectorStore = useSelectorStore();
const continousProfilingStore = useContinousProfilingStore();
const dashboardStore = useDashboardStore();
/* global defineProps, Nullable */
const props = defineProps({
data: {
type: Object,
default: () => ({}),
},
});
const timeline = ref<Nullable<HTMLDivElement>>(null);
const visGraph = ref<Nullable<any>>(null);
const oldVal = ref<{ width: number; height: number }>({ width: 0, height: 0 });
const visDate = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") => dayjs(date).format(pattern);
init();
onMounted(() => {
oldVal.value = (timeline.value && timeline.value.getBoundingClientRect()) || {
width: 0,
height: 0,
};
useThrottleFn(resize, 500)();
});
async function init() {
const serviceId = (selectorStore.currentService && selectorStore.currentService.id) || "";
const serviceInstanceId = (selectorStore.currentPod && selectorStore.currentPod.id) || "";
const type = continousProfilingStore.selectedStrategy.type;
const res = await taskTimelineStore.getContinousTaskList({
serviceId,
serviceInstanceId,
targets: type ? [type] : null,
triggerType: EBPFProfilingTriggerType.CONTINUOUS_PROFILING,
});
if (res.errors) {
ElMessage.error(res.errors);
return;
}
visTimeline();
}
function visTimeline() {
if (!timeline.value) {
return;
}
if (visGraph.value) {
visGraph.value.destroy();
}
const h = timeline.value.getBoundingClientRect().height;
const taskList = taskTimelineStore.taskList.map((d: EBPFTaskList, index: number) => {
return {
id: index,
// content: d.targetType,
start: new Date(Number(d.taskStartTime)),
end: new Date(Number(d.taskStartTime + d.fixedTriggerDuration * 1000)),
data: d,
className: d.targetType,
};
});
const items: any = new DataSet(taskList);
const options: any = {
height: h,
width: "100%",
locale: "en",
groupHeightMode: "fitItems",
autoResize: false,
tooltip: {
overflowMethod: "cap",
template(item: EBPFTaskList | any) {
const data = item.data || {};
const end = data.taskStartTime ? visDate(data.taskStartTime + data.fixedTriggerDuration * 1000) : "";
let tmp = `
<div>Task ID: ${data.taskId || ""}</div>
<div>Service Name: ${data.serviceName || ""}</div>
<div>Service Instance Name: ${data.serviceInstanceName || ""}</div>
<div>Service Process Name: ${data.processName || ""}</div>
<div>Target Type: ${data.targetType || ""}</div>
<div>Trigger Type: ${data.triggerType || ""}</div>
<div>Start Time: ${data.taskStartTime ? visDate(data.taskStartTime) : ""}</div>
<div>End Time: ${end}</div>
<div>Process Labels: ${data.processLabels.join("; ") || ""}</div>`;
let str = "";
for (const item of data.continuousProfilingCauses || []) {
str += `<div>${item.type}: ${getURI(item.uri)}${item.uri.threshold}>=${item.uri.current}</div>`;
}
return tmp + str;
},
},
};
visGraph.value = new Timeline(timeline.value, items, options);
visGraph.value.on("select", async (properties: { items: number[] }) => {
dashboardStore.selectWidget(props.data);
const index = properties.items[0];
const task = taskTimelineStore.taskList[index];
await taskTimelineStore.setSelectedTask(task);
await taskTimelineStore.getGraphData();
});
}
function getURI(uri: { uriRegex: string; uriPath: string }) {
return uri ? `(${uri.uriRegex || ""} | ${uri.uriPath || ""})` : "";
}
function resize() {
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
const cr = entry.contentRect;
if (Math.abs(cr.width - oldVal.value.width) < 3 && Math.abs(cr.height - oldVal.value.height) < 3) {
return;
}
visTimeline();
oldVal.value = { width: cr.width, height: cr.height };
});
if (timeline.value) {
observer.observe(timeline.value);
}
}
onUnmounted(() => {
if (visGraph.value) {
visGraph.value.destroy();
}
taskTimelineStore.setTaskList([]);
});
watch(
() => selectorStore.currentPod,
() => {
init();
},
);
</script>
<style lang="scss" scoped>
.task-timeline {
width: calc(100% - 5px);
margin: 0 5px 5px 0;
height: 200px;
}
.message {
max-width: 400px;
text-overflow: ellipsis;
overflow: hidden;
}
</style>