20 Commits

Author SHA1 Message Date
Fine0830
d65c18bd38 feat: add queries for alarm tags (#417) 2024-09-29 15:15:35 +08:00
dependabot[bot]
a5b0acda06 build(deps): bump rollup from 3.29.4 to 3.29.5 (#416) 2024-09-27 14:51:12 +08:00
Chen Ziyan
e251626374 feat: add skywalking java agent self observability menu (#415) 2024-09-15 00:25:40 +08:00
Fine0830
ed0ec0ac1f fix pagination (#414) 2024-09-05 18:51:04 +08:00
Fine0830
1945f23419 fix selectors (#413) 2024-09-05 11:46:28 +08:00
Fine0830
d10f4ca0cc add loading animations (#412) 2024-08-30 11:38:06 +08:00
Fine0830
a5073dd3d4 build(deps): bump up dependencies to fix CVEs (#408) 2024-08-26 12:13:03 +08:00
dependabot[bot]
ddcc49cb42 build(deps): bump axios and start-server-and-test (#411) 2024-08-26 10:34:10 +08:00
Fine0830
ae63538baf feat: improve dashboard queries to make page opening brisker (#410) 2024-08-22 16:47:50 +08:00
Fine0830
d9f819d143 fix: correct services and instances when changing page numbers (#409) 2024-08-16 10:52:52 +08:00
Starry
3c8b316b76 feat: introduce flame graph to the trace profiling (#407) 2024-08-05 20:48:42 +08:00
Fine0830
6b2b6a5dd2 feat: set a maximum 20 entities per query in service/instance/endpoint list widgets (#406) 2024-07-30 20:44:55 +08:00
Fine0830
4e00073ec2 fix: highlight keywords in logs (#405) 2024-07-30 10:39:18 +08:00
Fine0830
8f179f00a2 feat: polish error nodes in trace widget (#404) 2024-07-17 00:02:08 -07:00
mrproliu
fe6e853c57 Adding cilium icon for hierarchy (#403) 2024-07-08 09:29:15 +08:00
mrproliu
f664e786ac Adding cilium icon and i18n for menu (#402) 2024-06-28 17:17:23 +08:00
寻找的人
b6f57aa54e Add a SolonMVC icon (#401) 2024-06-20 19:29:58 +08:00
dependabot[bot]
afb70a371b build(deps-dev): bump ws from 8.11.0 to 8.17.1 (#400) 2024-06-18 10:17:20 +08:00
Starry
c35bdce399 fix: browser log (#399) 2024-06-04 08:51:43 +08:00
Starry
5419a69700 feat: highlight search log keywords (#398) 2024-06-02 18:33:43 +08:00
45 changed files with 1753 additions and 2761 deletions

View File

@@ -17,35 +17,10 @@
module.exports = {
ignores: [(commit) => commit.includes("init")],
extends: ["@commitlint/config-conventional"],
rules: {
"body-leading-blank": [2, "always"],
"footer-leading-blank": [1, "always"],
"header-max-length": [2, "always", 108],
"subject-empty": [2, "never"],
"type-empty": [2, "never"],
"subject-case": [0],
"type-enum": [
2,
"always",
[
"feat",
"fix",
"perf",
"style",
"docs",
"test",
"refactor",
"build",
"ci",
"chore",
"revert",
"wip",
"workflow",
"types",
"release",
"merge",
],
],
},
};

3437
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,7 @@
"check-components-types": "if (! git diff --quiet -U0 ./src/types); then echo 'type files are not updated correctly'; git diff -U0 ./src/types; exit 1; fi"
},
"dependencies": {
"axios": "^1.6.0",
"axios": "^1.7.5",
"d3": "^7.3.0",
"d3-flame-graph": "^4.1.3",
"d3-tip": "^0.9.1",
@@ -65,20 +65,20 @@
"postcss-scss": "^4.0.2",
"prettier": "^2.7.1",
"sass": "^1.56.1",
"start-server-and-test": "^1.15.2",
"start-server-and-test": "^2.0.5",
"stylelint": "15.9.0",
"stylelint-config-html": "^1.0.0",
"stylelint-config-prettier": "9.0.4",
"stylelint-config-standard": "^33.0.0",
"stylelint-order": "^6.0.3",
"typescript": "~4.7.4",
"unplugin-auto-import": "^0.7.0",
"unplugin-vue-components": "^0.19.2",
"unplugin-auto-import": "^0.18.2",
"unplugin-vue-components": "^0.27.3",
"vite": "^4.5.3",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svg-icons": "^2.0.1",
"vitest": "^0.25.6",
"vue-tsc": "^1.0.12"
"vue-tsc": "^1.8.27"
},
"browserslist": [
"> 1%",
@@ -98,10 +98,7 @@
"{!(package)*.json,*.code-snippets,.!(browserslist)*rc}": [
"prettier --write"
],
"package.json": [
"prettier --write"
],
"*.md": [
"package.json, *.md": [
"prettier --write"
]
}

View File

@@ -0,0 +1,16 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<svg width="16" height="16" viewBox="20 0 70 72" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="m49.72 45.923-5.505-9.69 5.505-9.69h10.974l5.506 9.69-5.506 9.69H49.72ZM49.72 69.367l-5.505-9.69 5.505-9.689h10.974l5.506 9.69-5.506 9.69H49.72ZM49.72 22.477l-5.505-9.689 5.505-9.69h10.974l5.506 9.69-5.506 9.69H49.72ZM70.06 57.644l-5.506-9.69 5.506-9.69h10.974l5.506 9.69-5.506 9.69H70.06ZM70.06 34.2l-5.506-9.69 5.506-9.69h10.974l5.506 9.69-5.506 9.69H70.06ZM29.357 57.644l-5.506-9.69 5.506-9.69h10.974l5.506 9.69-5.506 9.69H29.357ZM29.357 34.2l-5.506-9.69 5.506-9.69h10.974l5.506 9.69-5.506 9.69H29.357Z" stroke="#141A1F" stroke-width="2.771"/><path d="M10.784 95.947c1.026.007 " fill="#141A1F"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -28,8 +28,8 @@ limitations under the License. -->
:filterable="filterable"
>
<el-option
v-for="item in options"
:key="item.value || ''"
v-for="(item, index) in options"
:key="`${item.value}${index}`"
:label="item.label || ''"
:value="item.value || ''"
:disabled="item.disabled || false"

View File

@@ -46,3 +46,14 @@ export const Alarm = {
}
}`,
};
export const AlarmTagKeys = {
variable: "$duration: Duration!",
query: `
tagKeys: queryAlarmTagAutocompleteKeys(duration: $duration)`,
};
export const AlarmTagValues = {
variable: "$tagKey: String!, $duration: Duration!",
query: `
tagValues: queryAlarmTagAutocompleteValues(tagKey: $tagKey, duration: $duration)`,
};

View File

@@ -15,6 +15,8 @@
* limitations under the License.
*/
import { Alarm } from "../fragments/alarm";
import { Alarm, AlarmTagKeys, AlarmTagValues } from "../fragments/alarm";
export const queryAlarms = `query queryAlarms(${Alarm.variable}) {${Alarm.query}}`;
export const queryAlarmTagValues = `query queryTagValues(${AlarmTagValues.variable}) {${AlarmTagValues.query}}`;
export const queryAlarmTagKeys = `query queryTagKeys(${AlarmTagKeys.variable}) {${AlarmTagKeys.query}}`;

View File

@@ -14,6 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const MaximumEntities = 20;
export enum sizeEnum {
XS = "XS",

View File

@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { RespFields } from "./data";
import { RespFields, MaximumEntities } from "./data";
import { EntityType, ExpressionResultType } from "@/views/dashboard/data";
import { ElMessage } from "element-plus";
import { useDashboardStore } from "@/store/modules/dashboard";
@@ -24,31 +24,27 @@ import type { MetricConfigOpt } from "@/types/dashboard";
import type { Instance, Endpoint, Service } from "@/types/selector";
import type { Node, Call } from "@/types/topology";
export async function useExpressionsQueryProcessor(config: Indexable) {
function expressionsGraphqlPods() {
export async function useDashboardQueryProcessor(configList: Indexable[]) {
function expressionsGraphql(config: Indexable, idx: number) {
if (!(config.metrics && config.metrics[0])) {
return;
}
const appStore = useAppStoreWithOut();
const dashboardStore = useDashboardStore();
const selectorStore = useSelectorStore();
if (!selectorStore.currentService && dashboardStore.entity !== "All") {
return;
}
const conditions: Recordable = {
duration: appStore.durationTime,
};
const variables: string[] = [`$duration: Duration!`];
const conditions: Recordable = {};
const variables: string[] = [];
const isRelation = ["ServiceRelation", "ServiceInstanceRelation", "EndpointRelation", "ProcessRelation"].includes(
dashboardStore.entity,
);
if (isRelation && !selectorStore.currentDestService) {
return;
}
const fragment = config.metrics.map((name: string, index: number) => {
variables.push(`$expression${index}: String!`, `$entity${index}: Entity!`);
conditions[`expression${index}`] = name;
if (idx === 0) {
variables.push(`$entity: Entity!`);
const entity = {
serviceName: dashboardStore.entity === "All" ? undefined : selectorStore.currentService.value,
normal: dashboardStore.entity === "All" ? undefined : selectorStore.currentService.normal,
@@ -76,19 +72,21 @@ export async function useExpressionsQueryProcessor(config: Indexable) {
? selectorStore.currentDestProcess && selectorStore.currentDestProcess.value
: undefined,
};
conditions[`entity${index}`] = entity;
conditions[`entity`] = entity;
}
const fragment = config.metrics.map((name: string, index: number) => {
variables.push(`$expression${idx}${index}: String!`);
conditions[`expression${idx}${index}`] = name;
return `expression${index}: execExpression(expression: $expression${index}, entity: $entity${index}, duration: $duration)${RespFields.execExpression}`;
return `expression${idx}${index}: execExpression(expression: $expression${idx}${index}, entity: $entity, duration: $duration)${RespFields.execExpression}`;
});
const queryStr = `query queryData(${variables}) {${fragment}}`;
return {
queryStr,
variables,
fragment,
conditions,
};
}
function expressionsSource(resp: { errors: string; data: Indexable }) {
function expressionsSource(config: Indexable, resp: { errors: string; data: Indexable | any }) {
if (resp.errors) {
ElMessage.error(resp.errors);
return { source: {}, tips: [], typesOfMQE: [] };
@@ -97,6 +95,10 @@ export async function useExpressionsQueryProcessor(config: Indexable) {
ElMessage.error("The query is wrong");
return { source: {}, tips: [], typesOfMQE: [] };
}
if (resp.data.error) {
ElMessage.error(resp.data.error);
return { source: {}, tips: [], typesOfMQE: [] };
}
const tips: string[] = [];
const source: { [key: string]: unknown } = {};
const keys = Object.keys(resp.data);
@@ -133,30 +135,76 @@ export async function useExpressionsQueryProcessor(config: Indexable) {
return { source, tips, typesOfMQE };
}
async function fetchMetrics(configArr: any) {
const appStore = useAppStoreWithOut();
const variables: string[] = [`$duration: Duration!`];
let fragments = "";
let conditions: Recordable = {
duration: appStore.durationTime,
};
for (let i = 0; i < configArr.length; i++) {
const params = await expressionsGraphql(configArr[i], i);
if (params) {
fragments += params?.fragment;
conditions = { ...conditions, ...params.conditions };
variables.push(...params.variables);
}
}
if (!fragments) {
return { 0: { source: {}, tips: [], typesOfMQE: [] } };
}
const queryStr = `query queryData(${variables}) {${fragments}}`;
const dashboardStore = useDashboardStore();
const json = await dashboardStore.fetchMetricValue({
queryStr,
conditions,
});
if (json.errors) {
ElMessage.error(json.errors);
return { 0: { source: {}, tips: [], typesOfMQE: [] } };
}
try {
const pageData: Recordable = {};
const params = await expressionsGraphqlPods();
if (!params) {
return { source: {}, tips: [], typesOfMQE: [] };
for (let i = 0; i < configArr.length; i++) {
const resp: any = {};
for (let m = 0; m < configArr[i].metrics.length; m++) {
resp[`expression${i}${m}`] = json.data[`expression${i}${m}`];
}
const data = expressionsSource(configArr[i], { ...json, data: resp });
const id = configArr[i].id;
pageData[id] = data;
}
return pageData;
} catch (error) {
console.error(error);
return { 0: { source: {}, tips: [], typesOfMQE: [] } };
}
}
function chunkArray(array: any[], chunkSize: number) {
const result = [];
for (let i = 0; i < array.length; i += chunkSize) {
result.push(array.slice(i, i + chunkSize));
}
return result;
}
const dashboardStore = useDashboardStore();
const json = await dashboardStore.fetchMetricValue(params);
if (json.errors) {
ElMessage.error(json.errors);
return { source: {}, tips: [], typesOfMQE: [] };
const partArr = chunkArray(configList, 6);
const promiseArr = partArr.map((d: Array<Indexable>) => fetchMetrics(d));
const responseList = await Promise.all(promiseArr);
let resp = {};
for (const item of responseList) {
resp = {
...resp,
...item,
};
}
try {
const data = expressionsSource(json);
return data;
} catch (error) {
console.error(error);
return { source: {}, tips: [], typesOfMQE: [] };
}
return resp;
}
export async function useExpressionsQueryPodsMetrics(
pods: Array<(Instance | Endpoint | Service) & Indexable>,
allPods: Array<(Instance | Endpoint | Service) & Indexable>,
config: {
expressions: string[];
subExpressions: string[];
@@ -164,7 +212,7 @@ export async function useExpressionsQueryPodsMetrics(
},
scope: string,
) {
function expressionsGraphqlPods() {
function expressionsGraphqlPods(pods: Array<(Instance | Endpoint | Service) & Indexable>) {
const metrics: string[] = [];
const subMetrics: string[] = [];
config.expressions = config.expressions || [];
@@ -196,18 +244,22 @@ export async function useExpressionsQueryPodsMetrics(
variables.push(`$entity${index}: Entity!`);
conditions[`entity${index}`] = entity;
const f = metrics.map((name: string, idx: number) => {
variables.push(`$expression${index}${idx}: String!`);
conditions[`expression${index}${idx}`] = name;
if (index === 0) {
variables.push(`$expression${idx}: String!`);
conditions[`expression${idx}`] = name;
}
let str = "";
if (config.subExpressions[idx]) {
variables.push(`$subExpression${index}${idx}: String!`);
conditions[`subExpression${index}${idx}`] = config.subExpressions[idx];
str = `subexpression${index}${idx}: execExpression(expression: $subExpression${index}${idx}, entity: $entity${index}, duration: $duration)${RespFields.execExpression}`;
if (index === 0) {
variables.push(`$subExpression${idx}: String!`);
conditions[`subExpression${idx}`] = config.subExpressions[idx];
}
str = `subexpression${index}${idx}: execExpression(expression: $subExpression${idx}, entity: $entity${index}, duration: $duration)${RespFields.execExpression}`;
}
return (
str +
`expression${index}${idx}: execExpression(expression: $expression${index}${idx}, entity: $entity${index}, duration: $duration)${RespFields.execExpression}`
`expression${index}${idx}: execExpression(expression: $expression${idx}, entity: $entity${index}, duration: $duration)${RespFields.execExpression}`
);
});
return f;
@@ -218,7 +270,10 @@ export async function useExpressionsQueryPodsMetrics(
return { queryStr, conditions };
}
function expressionsPodsSource(resp: { errors: string; data: Indexable }): Indexable {
function expressionsPodsSource(
resp: { errors: string; data: Indexable },
pods: Array<(Instance | Endpoint | Service) & Indexable>,
): Indexable {
if (resp.errors) {
ElMessage.error(resp.errors);
return {};
@@ -300,17 +355,39 @@ export async function useExpressionsQueryPodsMetrics(
return { data, names, subNames, metricConfigArr, metricTypesArr, expressionsTips, subExpressionsTips };
}
const dashboardStore = useDashboardStore();
const params = await expressionsGraphqlPods();
const json = await dashboardStore.fetchMetricValue(params);
async function fetchPodsExpressionValues(pods: Array<(Instance | Endpoint | Service) & Indexable>) {
const dashboardStore = useDashboardStore();
const params = await expressionsGraphqlPods(pods);
if (json.errors) {
ElMessage.error(json.errors);
return {};
const json = await dashboardStore.fetchMetricValue(params);
if (json.errors) {
ElMessage.error(json.errors);
return {};
}
const expressionParams = expressionsPodsSource(json, pods);
return expressionParams;
}
const expressionParams = expressionsPodsSource(json);
return expressionParams;
const result = [];
for (let i = 0; i < allPods.length; i += MaximumEntities) {
result.push(allPods.slice(i, i + MaximumEntities));
}
const promiseArr = result.map((d: Array<(Instance | Endpoint | Service) & Indexable>) =>
fetchPodsExpressionValues(d),
);
const responseList = await Promise.all(promiseArr);
let resp: Indexable = { data: [], expressionsTips: [], subExpressionsTips: [] };
for (const item of responseList) {
resp = {
...item,
data: [...resp.data, ...item.data],
expressionsTips: [...resp.expressionsTips, ...item.expressionsTips],
subExpressionsTips: [...resp.subExpressionsTips, ...item.subExpressionsTips],
};
}
return resp;
}
export function useQueryTopologyExpressionsProcessor(metrics: string[], instances: (Call | Node)[]) {

View File

@@ -284,7 +284,8 @@ const msg = {
errorInfo: "Error Info",
stack: "Stack",
serviceVersion: "Service Version",
errorPage: "Error Page",
pagePath: "Page Path",
errorUrl: "Error Url",
category: "Category",
grade: "Grade",
relatedTraceLogs: "Related Logs",
@@ -300,7 +301,7 @@ const msg = {
viewLogs: "View Logs",
logsTagsTip: `Only tags defined in the core/default/searchableLogsTags are searchable.
Check more details on the Configuration Vocabulary page`,
keywordsOfContentLogTips: "Current storage of SkyWalking OAP server does not support this.",
keywordsOfContentLogTips: "Current storage of SkyWalking OAP server does not support this",
setEvent: "Set Event",
viewAttributes: "View",
attributes: "Attributes",
@@ -355,7 +356,7 @@ const msg = {
addKeywordsOfContent: "Please input a keyword of content",
addExcludingKeywordsOfContent: "Please input a keyword of excluding content",
noticeTag: "Please press Enter after inputting a tag(key=value).",
conditionNotice: "Notice: Please press Enter after inputting a key of content, exclude key of content(key=value).",
conditionNotice: "Notice: Please press Enter after inputting a key of content, exclude key of content(key=value)",
language: "Language",
save: "Save",
editStrategy: "Edit Policies",

View File

@@ -283,7 +283,8 @@ const msg = {
errorInfo: "Info Error",
stack: "Pila",
serviceVersion: "Versión Servicio",
errorPage: "Página de Error",
pagePath: "Página de Error",
errorUrl: "Ruta de Error",
category: "Categoría",
grade: "Grado",
relatedTraceLogs: "Registro de Datos Relacionados",
@@ -299,7 +300,7 @@ const msg = {
viewLogs: "Ver Registro de Datos",
logsTagsTip: `Solamente etiquetas definidas en core/default/searchableLogsTags pueden ser buscadas.
Más información en la página de Vocabulario de Configuración`,
keywordsOfContentLogTips: "El almacenamiento actual del servidor SkyWalking OAP no lo soporta.",
keywordsOfContentLogTips: "El almacenamiento actual del servidor SkyWalking OAP no lo soporta",
setEvent: "Establecer Evento",
viewAttributes: "Ver",
serviceEvents: "Eventos Servico",

View File

@@ -125,6 +125,14 @@ const titles = {
self_observability_satellite: "Satellite",
self_observability_satellite_desc:
"Satellite: an open-source agent designed for the cloud-native infrastructures, which provides a low-cost, high-efficient, and more secure way to collect telemetry data. It is the recommended load balancer for telemetry collecting.",
self_observability_java_agent: "SkyWalking Java Agent",
self_observability_java_agent_desc:
"The self observability of SkyWalking Java Agent, which provides the abilities to measure the tracing performance and error statistics of plugins.",
cilium: "Cilium",
cilium_desc:
"Cilium is a CNI plugin for Kubernetes that provides eBPF-based networking, security, and load balancing.",
cilium_service: "Cilium Service",
cilium_service_desc: "Observe Service status and resources from Cilium Hubble.",
};
export default titles;

View File

@@ -126,6 +126,14 @@ const titles = {
self_observability_satellite: "Satellite",
self_observability_satellite_desc:
"Satellite: an open-source agent designed for the cloud-native infrastructures, which provides a low-cost, high-efficient, and more secure way to collect telemetry data. It is the recommended load balancer for telemetry collecting.",
self_observability_java_agent: "SkyWalking Java Agent",
self_observability_java_agent_desc:
"The self observability of SkyWalking Java Agent, which provides the abilities to measure the tracing performance and error statistics of plugins.",
cilium: "Cilium",
cilium_desc:
"Cilium es un complemento CNI para Kubernetes que proporciona redes, seguridad y equilibrio de carga basados en eBPF.",
cilium_service: "Cilium Service",
cilium_service_desc: "Observe el estado del servicio y los recursos de Cilium Hubble.",
};
export default titles;

View File

@@ -111,6 +111,12 @@ const titles = {
self_observability_satellite: "Satellite",
self_observability_satellite_desc:
"Satellite为云原生基础设施设计的开源代理提供了一种低成本、高效、更安全的遥测数据收集方式。它是遥测采集的推荐负载均衡器。",
self_observability_java_agent: "SkyWalking Java Agent",
self_observability_java_agent_desc: "SkyWalking Java Agent 自监控提供了对 agent 插件的性能追踪和错误统计。",
cilium: "Cilium",
cilium_desc: "Cilium是Kubernetes上的CNI插件提供基于eBPF的网络、安全和负载均衡。",
cilium_service: "Cilium服务",
cilium_service_desc: "通过Cilium Hubble收集的遥测数据观察服务。",
};
export default titles;

View File

@@ -281,7 +281,8 @@ const msg = {
errorInfo: "错误信息",
stack: "堆栈",
serviceVersion: "服务版本",
errorPage: "错误页面",
pagePath: "错误页面",
errorUrl: "错误路径",
category: "类别",
grade: "等级",
relatedTraceLogs: "相关的日志",

View File

@@ -15,6 +15,7 @@
* limitations under the License.
*/
import { createApp } from "vue";
import { ElLoading } from "element-plus";
import App from "./App.vue";
import { store } from "./store";
import components from "@/components";
@@ -23,6 +24,11 @@ import { useAppStoreWithOut } from "@/store/modules/app";
import "./styles/index.ts";
import "virtual:svg-icons-register";
const loading = ElLoading.service({
lock: true,
text: "Loading...",
background: "rgba(0, 0, 0, 0.8)",
});
const app = createApp(App);
const appStore = useAppStoreWithOut();
@@ -34,7 +40,7 @@ mountApp();
async function mountApp() {
await appStore.getActivateMenus();
await appStore.queryOAPTimeInfo();
const router = await import("./router");
app.use(router.default).mount("#app");
loading.close();
}

View File

@@ -19,6 +19,7 @@ import { store } from "@/store";
import graphql from "@/graphql";
import type { AxiosResponse } from "axios";
import type { Alarm } from "@/types/alarm";
import { useAppStoreWithOut } from "@/store/modules/app";
interface AlarmState {
loading: boolean;
@@ -35,7 +36,9 @@ export const alarmStore = defineStore({
}),
actions: {
async getAlarms(params: Recordable) {
this.loading = true;
const res: AxiosResponse = await graphql.query("queryAlarms").params(params);
this.loading = false;
if (res.data.errors) {
return res.data;
}
@@ -45,6 +48,20 @@ export const alarmStore = defineStore({
}
return res.data;
},
async getAlarmTagKeys() {
const res: AxiosResponse = await graphql
.query("queryAlarmTagKeys")
.params({ duration: useAppStoreWithOut().durationTime });
return res.data;
},
async getAlarmTagValues(tagKey: string) {
const res: AxiosResponse = await graphql
.query("queryAlarmTagValues")
.params({ tagKey, duration: useAppStoreWithOut().durationTime });
return res.data;
},
},
});

View File

@@ -1,52 +1,9 @@
// Generated by 'unplugin-auto-import'
// We suggest you to commit this file into source control
declare global {
const computed: typeof import('vue')['computed']
const createApp: typeof import('vue')['createApp']
const customRef: typeof import('vue')['customRef']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const effectScope: typeof import('vue')['effectScope']
const EffectScope: typeof import('vue')['EffectScope']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const inject: typeof import('vue')['inject']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const provide: typeof import('vue')['provide']
const reactive: typeof import('vue')['reactive']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const resolveComponent: typeof import('vue')['resolveComponent']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const toRaw: typeof import('vue')['toRaw']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const triggerRef: typeof import('vue')['triggerRef']
const unref: typeof import('vue')['unref']
const useAttrs: typeof import('vue')['useAttrs']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVars: typeof import('vue')['useCssVars']
const useSlots: typeof import('vue')['useSlots']
const watch: typeof import('vue')['watch']
const watchEffect: typeof import('vue')['watchEffect']
}
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {}
declare global {
}

View File

@@ -1,9 +1,11 @@
// generated by unplugin-vue-components
// We suggest you to commit this file into source control
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
export {}
declare module '@vue/runtime-core' {
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
DateCalendar: typeof import('./../components/DateCalendar.vue')['default']
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
@@ -42,7 +44,6 @@ declare module '@vue/runtime-core' {
ElTooltip: typeof import('element-plus/es')['ElTooltip']
Graph: typeof import('./../components/Graph.vue')['default']
Icon: typeof import('./../components/Icon.vue')['default']
Loading: typeof import('element-plus/es')['ElLoadingDirective']
Radio: typeof import('./../components/Radio.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
@@ -51,6 +52,7 @@ declare module '@vue/runtime-core' {
Tags: typeof import('./../components/Tags.vue')['default']
TimePicker: typeof import('./../components/TimePicker.vue')['default']
}
export interface ComponentCustomProperties {
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
}
}
export {}

15
src/types/ebpf.d.ts vendored
View File

@@ -77,6 +77,21 @@ export type StackElement = {
rateOfRoot?: string;
rateOfParent: string;
};
export type TraceProfilingElement = {
id: string;
originId: string;
name: string;
parentId: string;
codeSignature: string;
count: number;
stackType: string;
value: number;
children?: TraceProfilingElement[];
rateOfRoot?: string;
rateOfParent: string;
duration: number;
durationChildExcluded: number;
};
export type AnalyzationTrees = {
id: string;
parentId: string;

24
src/utils/flameGraph.ts Normal file
View File

@@ -0,0 +1,24 @@
/**
* 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 function treeForeach(tree: any, func: (node: any) => void) {
for (const data of tree) {
data.children && treeForeach(data.children, func);
func(data);
}
return tree;
}

View File

@@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div class="timeline-table clear">
<div class="timeline-table clear" v-loading="alarmStore.loading">
<div v-for="(i, index) in alarmStore.alarms" :key="index" class="clear timeline-item">
<div class="g-sm-3 grey sm hide-xs time-line tr">
{{ dateFormat(parseInt(i.startTime)) }}
@@ -132,7 +132,7 @@ limitations under the License. -->
currentDetail.value = item;
currentEvents.value = item.events;
alarmTags.value = currentDetail.value.tags.map((d: { key: string; value: string }) => {
return `${d.key} = ${d.value}`;
return `${d.key}=${d.value}`;
});
}

View File

@@ -21,15 +21,7 @@ limitations under the License. -->
<span class="remove-icon" @click="removeTags(index)">×</span>
</span>
</span>
<el-input
v-if="type === 'ALARM'"
size="small"
v-model="tags"
class="trace-new-tag"
@change="addLabels"
:placeholder="t('addTags')"
/>
<el-popover v-else trigger="click" :visible="visible" width="300px">
<el-popover trigger="click" :visible="visible" width="300px">
<template #reference>
<el-input
size="small"
@@ -47,7 +39,7 @@ limitations under the License. -->
</span>
</div>
</el-popover>
<span class="tags-tip" :class="type !== 'ALARM' && tagArr.length ? 'link-tips' : ''">
<span class="tags-tip" :class="tagArr.length ? 'link-tips' : ''">
<a
target="blank"
href="https://github.com/apache/skywalking/blob/master/docs/en/setup/backend/configuration-vocabulary.md"
@@ -68,6 +60,7 @@ limitations under the License. -->
import { useI18n } from "vue-i18n";
import { useTraceStore } from "@/store/modules/trace";
import { useLogStore } from "@/store/modules/log";
import { useAlarmStore } from "@/store/modules/alarm";
import { ElMessage } from "element-plus";
import { useAppStoreWithOut } from "@/store/modules/app";
@@ -79,6 +72,7 @@ limitations under the License. -->
const traceStore = useTraceStore();
const logStore = useLogStore();
const appStore = useAppStoreWithOut();
const alarmStore = useAlarmStore();
const { t } = useI18n();
const tags = ref<string>("");
const tagsList = ref<string[]>([]);
@@ -121,10 +115,18 @@ limitations under the License. -->
let resp: Recordable = {};
if (props.type === "TRACE") {
resp = await traceStore.getTagKeys();
} else {
}
if (props.type === "LOG") {
resp = await logStore.getLogTagKeys();
}
if (props.type === "ALARM") {
resp = await alarmStore.getAlarmTagKeys();
}
if (!resp.data) {
return;
}
if (resp.errors) {
ElMessage.error(resp.errors);
return;
@@ -140,10 +142,17 @@ limitations under the License. -->
let resp: Recordable = {};
if (props.type === "TRACE") {
resp = await traceStore.getTagValues(param);
} else {
}
if (props.type === "LOG") {
resp = await logStore.getLogTagValues(param);
}
if (props.type === "ALARM") {
resp = await alarmStore.getAlarmTagValues(param);
}
if (!resp.data) {
return;
}
if (resp.errors) {
ElMessage.error(resp.errors);
return;

View File

@@ -53,7 +53,7 @@ limitations under the License. -->
import { useRoute } from "vue-router";
import { useSelectorStore } from "@/store/modules/selectors";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useExpressionsQueryProcessor } from "@/hooks/useExpressionsProcessor";
import { useDashboardQueryProcessor } from "@/hooks/useExpressionsProcessor";
import graphs from "./graphs";
import { EntityType } from "./data";
import timeFormat from "@/utils/timeFormat";
@@ -128,12 +128,16 @@ limitations under the License. -->
}
async function queryMetrics() {
loading.value = true;
const params = await useExpressionsQueryProcessor({
metrics: config.value.expressions || [],
metricConfig: config.value.metricConfig || [],
subExpressions: config.value.subExpressions || [],
});
const metrics: { [key: string]: { source: { [key: string]: unknown }; typesOfMQE: string[] } } =
await useDashboardQueryProcessor([
{
metrics: config.value.expressions || [],
metricConfig: config.value.metricConfig || [],
subExpressions: config.value.subExpressions || [],
id: config.value.i,
},
]);
const params = metrics[config.value.i];
loading.value = false;
source.value = params.source || {};
typesOfMQE.value = params.typesOfMQE;

View File

@@ -110,7 +110,7 @@ limitations under the License. -->
ExpressionResultType,
} from "@/views/dashboard/data";
import Icon from "@/components/Icon.vue";
import { useExpressionsQueryProcessor } from "@/hooks/useExpressionsProcessor";
import { useDashboardQueryProcessor } from "@/hooks/useExpressionsProcessor";
import { useI18n } from "vue-i18n";
import type { DashboardItem, MetricConfigOpt } from "@/types/dashboard";
import Standard from "./Standard.vue";
@@ -240,12 +240,13 @@ limitations under the License. -->
}
async function queryMetricsWithExpressions() {
const { expressions, metricConfig } = dashboardStore.selectedGrid;
const { expressions, metricConfig, i } = dashboardStore.selectedGrid;
if (!(expressions && expressions[0])) {
return emit("update", {});
}
const params: Indexable = (await useExpressionsQueryProcessor({ ...states, metricConfig })) || {};
const metrics: Indexable = (await useDashboardQueryProcessor([{ ...states, metricConfig, id: i }])) || {};
const params = metrics[i];
states.tips = params.tips || [];
states.metricTypes = params.typesOfMQE || [];
dashboardStore.selectWidget({

View File

@@ -113,6 +113,7 @@ limitations under the License. -->
:data="item"
:activeIndex="`${data.i}-${activeTabIndex}-${item.i}`"
:needQuery="needQuery"
:metricsValues="metricsValues"
/>
</grid-item>
</grid-layout>
@@ -129,13 +130,17 @@ limitations under the License. -->
import controls from "./tab";
import { dragIgnoreFrom, WidgetType } from "../data";
import copy from "@/utils/copy";
import { useExpressionsQueryProcessor } from "@/hooks/useExpressionsProcessor";
import { useDashboardQueryProcessor } from "@/hooks/useExpressionsProcessor";
const props = {
data: {
type: Object as PropType<LayoutConfig>,
default: () => ({ children: [] }),
},
metricsValues: {
type: Object as PropType<any>,
default: () => ({}),
},
active: { type: Boolean, default: false },
};
export default defineComponent({
@@ -259,8 +264,10 @@ limitations under the License. -->
if (!metrics.length) {
return;
}
const params: { [key: string]: any } = (await useExpressionsQueryProcessor({ metrics })) || {};
const values: { [key: string]: any } =
(await useDashboardQueryProcessor([{ metrics, id: props.data.i }])) || {};
for (const child of tabsProps.children || []) {
const params = values[props.data.i];
if (params.source[child.expression || ""]) {
child.enable =
!!Number(params.source[child.expression || ""]) &&

View File

@@ -75,11 +75,10 @@ limitations under the License. -->
import type { LayoutConfig } from "@/types/dashboard";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useAppStoreWithOut } from "@/store/modules/app";
import { useSelectorStore } from "@/store/modules/selectors";
import graphs from "../graphs";
import { useI18n } from "vue-i18n";
import { useExpressionsQueryProcessor } from "@/hooks/useExpressionsProcessor";
import { EntityType, ListChartTypes } from "../data";
import { useDashboardQueryProcessor } from "@/hooks/useExpressionsProcessor";
import { ListChartTypes } from "../data";
import type { EventParams } from "@/types/dashboard";
import getDashboard from "@/hooks/useDashboardsSession";
@@ -88,6 +87,10 @@ limitations under the License. -->
type: Object as PropType<LayoutConfig>,
default: () => ({ widget: {}, graph: {} }),
},
metricsValues: {
type: Object as PropType<{ [key: string]: { source: { [key: string]: unknown }; typesOfMQE: string[] } }>,
default: () => ({}),
},
activeIndex: { type: String, default: "" },
needQuery: { type: Boolean, default: false },
};
@@ -105,23 +108,20 @@ limitations under the License. -->
const { data } = toRefs(props);
const appStore = useAppStoreWithOut();
const dashboardStore = useDashboardStore();
const selectorStore = useSelectorStore();
const graph = computed(() => props.data.graph || {});
const widget = computed(() => props.data.widget || {});
const isList = computed(() => ListChartTypes.includes((props.data.graph && props.data.graph.type) || ""));
const typesOfMQE = ref<string[]>([]);
if ((props.needQuery || !dashboardStore.currentDashboard.id) && !isList.value) {
queryMetrics();
}
async function queryMetrics() {
loading.value = true;
const e = {
const config = {
metrics: props.data.expressions || [],
metricConfig: props.data.metricConfig || [],
id: props.data.i,
};
const params = (await useExpressionsQueryProcessor(e)) || {};
const metrics: { [key: string]: { source: { [key: string]: unknown }; typesOfMQE: string[] } } =
(await useDashboardQueryProcessor([config])) || {};
const params = metrics[data.value.i];
loading.value = false;
state.source = params.source || {};
typesOfMQE.value = params.typesOfMQE;
@@ -160,7 +160,7 @@ limitations under the License. -->
dashboardStore.selectWidget(props.data);
}
watch(
() => props.data.expressions,
() => props.data,
() => {
if (!dashboardStore.selectedGrid) {
return;
@@ -169,54 +169,19 @@ limitations under the License. -->
return;
}
const chart = dashboardStore.selectedGrid.graph || {};
if (ListChartTypes.includes(chart.type) || isList.value) {
if (ListChartTypes.includes(chart.type)) {
return;
}
queryMetrics();
},
);
watch(
() => [selectorStore.currentService, selectorStore.currentDestService],
() => props.metricsValues,
() => {
if (isList.value) {
return;
}
if ([EntityType[0].value, EntityType[4].value].includes(dashboardStore.entity)) {
queryMetrics();
}
},
);
watch(
() => [selectorStore.currentPod, selectorStore.currentDestPod],
() => {
if ([EntityType[0].value, EntityType[7].value, EntityType[8].value].includes(dashboardStore.entity)) {
return;
}
if (isList.value) {
return;
}
queryMetrics();
},
);
watch(
() => [selectorStore.currentProcess, selectorStore.currentDestProcess],
() => {
if (isList.value) {
return;
}
if ([EntityType[7].value, EntityType[8].value].includes(dashboardStore.entity)) {
queryMetrics();
}
},
);
watch(
() => appStore.durationTime,
() => {
if (isList.value) {
return;
}
if (dashboardStore.entity === EntityType[1].value) {
queryMetrics();
const params = props.metricsValues[data.value.i];
if (params) {
state.source = params.source || {};
typesOfMQE.value = params.typesOfMQE;
}
},
);

View File

@@ -67,9 +67,10 @@ limitations under the License. -->
<el-pagination
class="pagination flex-h"
layout="prev, pager, next"
:current-page="currentPage"
:page-size="pageSize"
:total="pods.length"
@current-change="changePage"
:total="searchText ? pods.filter((d: any) => d.label.includes(searchText)).length : pods.length"
@current-change="handleCurrentChange"
@prev-click="changePage"
@next-click="changePage"
/>
@@ -122,6 +123,7 @@ limitations under the License. -->
const dashboardStore = useDashboardStore();
const chartLoading = ref<boolean>(false);
const instances = ref<Instance[]>([]); // current instances
const currentPage = ref<number>(1);
const pageSize = 10;
const searchText = ref<string>("");
const colMetrics = ref<string[]>([]);
@@ -212,19 +214,27 @@ limitations under the License. -->
);
}
function changePage(pageIndex: number) {
instances.value = pods.value.filter((d: unknown, index: number) => {
if (index >= (pageIndex - 1) * pageSize && index < pageIndex * pageSize) {
return d;
}
});
function changePage() {
let podList = pods.value;
if (searchText.value) {
podList = pods.value.filter((d: { label: string }) => d.label.includes(searchText.value));
}
instances.value = podList.filter(
(_, index: number) => index >= (currentPage.value - 1) * pageSize && index < currentPage.value * pageSize,
);
queryInstanceMetrics(instances.value);
}
function handleCurrentChange(val: number) {
currentPage.value = val;
changePage();
}
function searchList() {
const searchInstances = pods.value.filter((d: { label: string }) => d.label.includes(searchText.value));
instances.value = searchInstances.filter((d: unknown, index: number) => index < pageSize);
instances.value = searchInstances.filter((_, index: number) => index < pageSize);
queryInstanceMetrics(instances.value);
currentPage.value = 1;
}
watch(

View File

@@ -60,9 +60,10 @@ limitations under the License. -->
<el-pagination
class="pagination flex-h"
layout="prev, pager, next"
:current-page="currentPage"
:page-size="pageSize"
:total="selectorStore.services.length"
@current-change="changePage"
:total="searchText ? sortServices.filter((d: any) => d.label.includes(searchText)).length : sortServices.length"
@current-change="handleCurrentChange"
@prev-click="changePage"
@next-click="changePage"
/>
@@ -112,6 +113,7 @@ limitations under the License. -->
const dashboardStore = useDashboardStore();
const appStore = useAppStoreWithOut();
const chartLoading = ref<boolean>(false);
const currentPage = ref<number>(1);
const pageSize = 10;
const services = ref<Service[]>([]);
const colMetrics = ref<string[]>([]);
@@ -247,19 +249,28 @@ limitations under the License. -->
}
return { rowspan: groups.value[param.row.group], colspan: 1 };
}
function changePage(pageIndex: number) {
const arr = sortServices.value.filter((d: Service, index: number) => {
if (index >= (pageIndex - 1) * pageSize && index < pageSize * pageIndex) {
function changePage() {
let services = sortServices.value;
if (searchText.value) {
services = sortServices.value.filter((d: { label: string }) => d.label.includes(searchText.value));
}
const arr = services.filter((d: Service, index: number) => {
if (index >= (currentPage.value - 1) * pageSize && index < pageSize * currentPage.value) {
return d;
}
});
setServices(arr);
}
function handleCurrentChange(val: number) {
currentPage.value = val;
changePage();
}
function searchList() {
const searchServices = sortServices.value.filter((d: { label: string }) => d.label.includes(searchText.value));
const services = searchServices.filter((d: unknown, index: number) => index < pageSize);
const services = searchServices.filter((_, index: number) => index < pageSize);
setServices(services);
currentPage.value = 1;
}
watch(

View File

@@ -14,12 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<grid-layout
v-if="dashboardStore.layout.length"
v-model:layout="dashboardStore.layout"
:col-num="24"
:row-height="10"
:is-draggable="dashboardStore.editMode"
:is-resizable="dashboardStore.editMode"
v-if="dashboardStore.layout.length"
v-loading.fullscreen.lock="loading"
element-loading-text="Loading..."
element-loading-background="rgba(122, 122, 122, 0.8)"
>
<grid-item
v-for="item in dashboardStore.layout"
@@ -33,33 +36,39 @@ limitations under the License. -->
:class="{ active: dashboardStore.activedGridItem === item.i }"
:drag-ignore-from="dragIgnoreFrom"
>
<component :is="item.type" :data="item" />
<component :is="item.type" :data="item" :metricsValues="metricsValues" />
</grid-item>
</grid-layout>
<div class="no-data-tips" v-else>{{ t("noWidget") }}</div>
</template>
<script lang="ts">
import { defineComponent, onBeforeUnmount } from "vue";
import { defineComponent, onBeforeUnmount, watch, ref } from "vue";
import { useI18n } from "vue-i18n";
import { useAppStoreWithOut } from "@/store/modules/app";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useSelectorStore } from "@/store/modules/selectors";
import type { LayoutConfig } from "@/types/dashboard";
import controls from "../controls/index";
import { dragIgnoreFrom } from "../data";
import { dragIgnoreFrom, ListChartTypes, WidgetType } from "../data";
import { useDashboardQueryProcessor } from "@/hooks/useExpressionsProcessor";
import { EntityType } from "../data";
export default defineComponent({
name: "Layout",
components: { ...controls },
setup() {
const { t } = useI18n();
const appStore = useAppStoreWithOut();
const dashboardStore = useDashboardStore();
const selectorStore = useSelectorStore();
const metricsValues = ref();
const loading = ref<boolean>(false);
function clickGrid(item: LayoutConfig, event: Event) {
dashboardStore.activeGridItem(item.i);
dashboardStore.selectWidget(item);
if (
item.type === "Tab" &&
item.type === WidgetType.Tab &&
!["operations", "tab-layout"].includes((event.target as HTMLDivElement)?.className) &&
(event.target as HTMLDivElement)?.classList[2] !== "icon-tool" &&
(event.target as HTMLDivElement)?.nodeName !== "use"
@@ -67,6 +76,47 @@ limitations under the License. -->
dashboardStore.setActiveTabIndex(0);
}
}
async function queryMetrics() {
const widgets = [];
for (const item of dashboardStore.layout) {
const isList = ListChartTypes.includes(item.type || "");
if (item.type === WidgetType.Widget && !isList) {
widgets.push(item);
}
if (item.type === WidgetType.Tab) {
const index = isNaN(item.activedTabIndex) ? 0 : item.activedTabIndex;
widgets.push(...item.children[index].children);
}
}
const configList = widgets.map((d: LayoutConfig) => ({
metrics: d.expressions || [],
metricConfig: d.metricConfig || [],
id: d.i,
}));
if (!widgets.length) {
return {};
}
loading.value = true;
metricsValues.value = (await useDashboardQueryProcessor(configList)) || {};
loading.value = false;
}
async function queryTabsMetrics() {
const configList = dashboardStore.currentTabItems
.filter((item: LayoutConfig) => item.type === WidgetType.Widget && !ListChartTypes.includes(item.type || ""))
.map((d: LayoutConfig) => ({
metrics: d.expressions || [],
metricConfig: d.metricConfig || [],
id: d.i,
}));
if (!configList.length) {
return {};
}
loading.value = true;
metricsValues.value = (await useDashboardQueryProcessor(configList)) || {};
loading.value = false;
}
onBeforeUnmount(() => {
dashboardStore.setLayout([]);
selectorStore.setCurrentService(null);
@@ -74,11 +124,53 @@ limitations under the License. -->
dashboardStore.setEntity("");
dashboardStore.setConfigPanel(false);
});
watch(
() => [selectorStore.currentService, selectorStore.currentDestService],
() => {
if ([EntityType[0].value, EntityType[4].value].includes(dashboardStore.entity)) {
queryMetrics();
}
},
);
watch(
() => [selectorStore.currentPod, selectorStore.currentDestPod],
() => {
if ([EntityType[0].value, EntityType[7].value, EntityType[8].value].includes(dashboardStore.entity)) {
return;
}
queryMetrics();
},
);
watch(
() => [selectorStore.currentProcess, selectorStore.currentDestProcess],
() => {
if ([EntityType[7].value, EntityType[8].value].includes(dashboardStore.entity)) {
queryMetrics();
}
},
);
watch(
() => appStore.durationTime,
() => {
if (dashboardStore.entity === EntityType[1].value) {
queryMetrics();
}
},
);
watch(
() => dashboardStore.currentTabItems,
() => {
queryTabsMetrics();
},
);
return {
dashboardStore,
clickGrid,
t,
dragIgnoreFrom,
metricsValues,
loading,
};
},
});

View File

@@ -520,7 +520,7 @@ limitations under the License. -->
instanceId: selectorStore.currentPod.id,
});
if (setPod) {
const process = params.processId || selectorStore.processes[0].id;
const process = selectorStore.currentProcess?.id || params.processId || selectorStore.processes[0].id;
const currentProcess = selectorStore.processes.find((d: { id: string }) => d.id === process);
if (currentProcess) {
selectorStore.setCurrentProcess(currentProcess);
@@ -538,7 +538,8 @@ limitations under the License. -->
isRelation: true,
});
if (setPod) {
const destProcess = params.destProcessId || selectorStore.destProcesses[0].id;
const destProcess =
selectorStore.currentDestProcess?.id || params.destProcessId || selectorStore.destProcesses[0].id;
const currentDestProcess = selectorStore.destProcesses.find((d: { id: string }) => d.id === destProcess);
if (!currentDestProcess) {
states.currentDestProcess = "";
@@ -570,7 +571,7 @@ limitations under the License. -->
states.currentDestPod = "";
return;
}
const destPod = params.destPodId || selectorStore.destPods[0].id;
const destPod = selectorStore.currentDestPod?.id || params.destPodId || selectorStore.destPods[0].id;
const currentDestPod = selectorStore.destPods.find((d: { id: string }) => d.id === destPod);
if (!currentDestPod) {
states.currentDestPod = "";
@@ -602,7 +603,7 @@ limitations under the License. -->
states.currentPod = "";
return;
}
const pod = params.podId || selectorStore.pods[0].id;
const pod = selectorStore.currentPod?.id || params.podId || selectorStore.pods[0].id;
const currentPod = selectorStore.pods.find((d: { id: string }) => d.id === pod);
if (!currentPod) {
selectorStore.setCurrentPod(null);

View File

@@ -28,6 +28,7 @@ limitations under the License. -->
import type { StackElement } from "@/types/ebpf";
import { AggregateTypes } from "./data";
import "d3-flame-graph/dist/d3-flamegraph.css";
import { treeForeach } from "@/utils/flameGraph";
/*global Nullable, defineProps*/
const props = defineProps({
@@ -180,14 +181,6 @@ limitations under the License. -->
return res;
}
function treeForeach(tree: StackElement[], func: (node: StackElement) => void) {
for (const data of tree) {
data.children && treeForeach(data.children, func);
func(data);
}
return tree;
}
watch(
() => ebpfStore.analyzeTrees,
() => {

View File

@@ -67,51 +67,55 @@ limitations under the License. -->
</div>
<ConditionTags :type="'LOG'" @update="updateTags" />
</div>
<div class="row tips" v-show="!isBrowser">
<b>{{ t("conditionNotice") }}</b>
</div>
<div class="flex-h" v-show="!isBrowser">
<div class="mr-5" v-show="logStore.supportQueryLogsByKeywords">
<span class="mr-5 grey">{{ t("keywordsOfContent") }}:</span>
<span class="log-tags">
<span class="selected" v-for="(item, index) in keywordsOfContent" :key="`keywordsOfContent${index}`">
<span>{{ item }}</span>
<span class="remove-icon" @click="removeContent(index)">×</span>
</span>
</span>
<el-input
size="small"
class="inputs-max"
:placeholder="t('addKeywordsOfContent')"
v-model="contentStr"
@change="addLabels('keywordsOfContent')"
/>
</div>
<div class="mr-5" v-show="logStore.supportQueryLogsByKeywords">
<span class="grey mr-5"> {{ t("excludingKeywordsOfContent") }}: </span>
<span class="log-tags">
<span
class="selected"
v-for="(item, index) in excludingKeywordsOfContent"
:key="`excludingKeywordsOfContent${index}`"
>
<span>{{ item }}</span>
<span class="remove-icon" @click="removeExcludeContent(index)"> × </span>
</span>
</span>
<el-input
class="inputs-max"
size="small"
:placeholder="t('addExcludingKeywordsOfContent')"
v-model="excludingContentStr"
@change="addLabels('excludingKeywordsOfContent')"
/>
<div v-show="!isBrowser">
<div class="row tips">
<b>{{ t("conditionNotice") }}</b>
<el-tooltip :content="t('keywordsOfContentLogTips')">
<span v-show="!logStore.supportQueryLogsByKeywords">
<Icon iconName="help" class="mr-5" />
<Icon iconName="help" />
</span>
</el-tooltip>
</div>
<div v-show="logStore.supportQueryLogsByKeywords">
<div class="flex-h">
<div class="mr-5">
<span class="mr-5 grey">{{ t("keywordsOfContent") }}:</span>
<span class="log-tags">
<span class="selected" v-for="(item, index) in keywordsOfContent" :key="`keywordsOfContent${index}`">
<span>{{ item }}</span>
<span class="remove-icon" @click="removeContent(index)">×</span>
</span>
</span>
<el-input
size="small"
class="inputs-max"
:placeholder="t('addKeywordsOfContent')"
v-model="contentStr"
@change="addLabels('keywordsOfContent')"
/>
</div>
<div class="mr-5">
<span class="grey mr-5"> {{ t("excludingKeywordsOfContent") }}: </span>
<span class="log-tags">
<span
class="selected"
v-for="(item, index) in excludingKeywordsOfContent"
:key="`excludingKeywordsOfContent${index}`"
>
<span>{{ item }}</span>
<span class="remove-icon" @click="removeExcludeContent(index)"> × </span>
</span>
</span>
<el-input
class="inputs-max"
size="small"
:placeholder="t('addExcludingKeywordsOfContent')"
v-model="excludingContentStr"
@change="addLabels('excludingKeywordsOfContent')"
/>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>

View File

@@ -21,7 +21,6 @@ limitations under the License. -->
:class="['log', ['message', 'stack'].includes(item.label) ? 'max-item' : '']"
>
<span v-if="item.label === 'time'">{{ dateFormat(data.time) }}</span>
<span v-else-if="item.label === 'errorUrl'">{{ data.pagePath }}</span>
<el-tooltip v-else :content="data[item.label] || '-'">
<span>
{{ data[item.label] || "-" }}
@@ -35,7 +34,7 @@ limitations under the License. -->
import { BrowserLogConstants } from "./data";
import { dateFormat } from "@/utils/dateFormat";
/*global defineProps, defineEmits, NodeListOf */
/* global defineProps, defineEmits */
const props = defineProps({
data: { type: Object as any, default: () => ({}) },
});

View File

@@ -50,7 +50,7 @@ limitations under the License. -->
return [];
}
return props.currentLog.tags.map((d: { key: string; value: string }) => {
return `${d.key} = ${d.value}`;
return `${d.key}=${d.value}`;
});
});

View File

@@ -30,7 +30,7 @@ limitations under the License. -->
<Icon iconName="merge" />
</el-tooltip>
</span>
<span v-else>{{ data[item.label] }}</span>
<span v-else v-html="highlightKeywords(data[item.label])"></span>
</div>
</div>
</template>
@@ -42,6 +42,8 @@ limitations under the License. -->
import type { LayoutConfig } from "@/types/dashboard";
import { dateFormat } from "@/utils/dateFormat";
import { WidgetType } from "@/views/dashboard/data";
import { useLogStore } from "@/store/modules/log";
const logStore = useLogStore();
/*global defineProps, defineEmits, Recordable */
const props = defineProps({
@@ -58,6 +60,11 @@ limitations under the License. -->
}
return (props.data.tags.find((d: { key: string; value: string }) => d.key === "level") || {}).value || "";
});
const highlightKeywords = (data: string) => {
const keywords = Object.values(logStore.conditions.keywordsOfContent || {});
const regex = new RegExp(keywords.join("|"), "gi");
return data.replace(regex, (match) => `<span style="color: red">${match}</span>`);
};
function selectLog(label: string, value: string) {
if (label === "traceId") {

View File

@@ -74,8 +74,8 @@ export const BrowserLogConstants = [
value: "serviceVersion",
},
{
label: "errorUrl",
value: "errorPage",
label: "pagePath",
value: "pagePath",
},
{
label: "time",
@@ -87,6 +87,10 @@ export const BrowserLogConstants = [
// drag: true,
method: 350,
},
{
label: "errorUrl",
value: "errorUrl",
},
{
label: "stack",
value: "stack",

View File

@@ -19,9 +19,11 @@ limitations under the License. -->
<SegmentList />
</div>
<div class="item">
<SpanTree @loading="loadTrees" />
<SpanTree @loading="loadTrees" @displayMode="setDisplayMode" />
<div class="thread-stack">
<div id="graph-stack" ref="graph" v-show="displayMode == 'flame'" />
<StackTable
v-show="displayMode == 'tree'"
v-if="profileStore.analyzeTrees.length"
:data="profileStore.analyzeTrees"
:highlightTop="profileStore.highlightTop"
@@ -34,19 +36,175 @@ limitations under the License. -->
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
/*global Nullable*/
import { ref, watch } from "vue";
import TaskList from "./components/TaskList.vue";
import SegmentList from "./components/SegmentList.vue";
import SpanTree from "./components/SpanTree.vue";
import StackTable from "./components/Stack/Index.vue";
import { useProfileStore } from "@/store/modules/profile";
import type { TraceProfilingElement } from "@/types/ebpf";
import { flamegraph } from "d3-flame-graph";
import * as d3 from "d3";
import d3tip from "d3-tip";
import { treeForeach } from "@/utils/flameGraph";
const stackTree = ref<Nullable<TraceProfilingElement>>(null);
const selectStack = ref<Nullable<TraceProfilingElement>>(null);
const graph = ref<Nullable<HTMLDivElement>>(null);
const flameChart = ref<any>(null);
const min = ref<number>(1);
const max = ref<number>(1);
const loading = ref<boolean>(false);
const displayMode = ref<string>("tree");
const profileStore = useProfileStore();
function loadTrees(l: boolean) {
loading.value = l;
}
function setDisplayMode(mode: string) {
displayMode.value = mode;
}
function drawGraph() {
if (flameChart.value) {
flameChart.value.destroy();
}
if (!profileStore.analyzeTrees.length) {
return (stackTree.value = null);
}
const root: TraceProfilingElement = {
parentId: "0",
originId: "1",
name: "Virtual Root",
children: [],
value: 0,
id: "1",
codeSignature: "Virtual Root",
count: 0,
stackType: "",
rateOfRoot: "",
rateOfParent: "",
duration: 0,
durationChildExcluded: 0,
};
countRange();
for (const tree of profileStore.analyzeTrees) {
const ele = processTree(tree.elements);
root.children && root.children.push(ele);
}
const param = (root.children || []).reduce(
(prev: number[], curr: TraceProfilingElement) => {
prev[0] += curr.value;
prev[1] += curr.count;
return prev;
},
[0, 0],
);
root.value = param[0];
root.count = param[1];
stackTree.value = root;
const width = (graph.value && graph.value.getBoundingClientRect().width) || 0;
const w = width < 800 ? 802 : width;
flameChart.value = flamegraph()
.width(w - 15)
.cellHeight(18)
.transitionDuration(750)
.minFrameSize(1)
.transitionEase(d3.easeCubic as any)
.sort(true)
.title("")
.selfValue(false)
.inverted(true)
.onClick((d: { data: TraceProfilingElement }) => {
selectStack.value = d.data;
})
.setColorMapper((d, originalColor) => (d.highlight ? "#6aff8f" : originalColor));
const tip = (d3tip as any)()
.attr("class", "d3-tip")
.direction("s")
.html((d: { data: TraceProfilingElement } & { parent: { data: TraceProfilingElement } }) => {
const name = d.data.name.replace("<", "&lt;").replace(">", "&gt;");
const dumpCount = `<div class="mb-5">Dump Count: ${d.data.count}</div>`;
const duration = `<div class="mb-5">Duration: ${d.data.duration} ns</div>`;
const durationChildExcluded = `<div class="mb-5">DurationChildExcluded: ${d.data.durationChildExcluded} ns</div>`;
const rateOfParent =
(d.parent &&
`<div class="mb-5">Percentage Of Selected: ${
((d.data.count / ((selectStack.value && selectStack.value.count) || root.count)) * 100).toFixed(3) + "%"
}</div>`) ||
"";
const rateOfRoot = `<div class="mb-5">Percentage Of Root: ${
((d.data.count / root.count) * 100).toFixed(3) + "%"
}</div>`;
return `<div class="mb-5 name">CodeSignature: ${name}</div>${dumpCount}${duration}${durationChildExcluded}${rateOfParent}${rateOfRoot}`;
})
.style("max-width", "400px");
flameChart.value.tooltip(tip);
d3.select("#graph-stack").datum(stackTree.value).call(flameChart.value);
}
function countRange() {
const list = [];
for (const tree of profileStore.analyzeTrees) {
for (const ele of tree.elements) {
list.push(ele.count);
}
}
max.value = Math.max(...list);
min.value = Math.min(...list);
}
function processTree(arr: TraceProfilingElement[]) {
const copyArr = JSON.parse(JSON.stringify(arr));
const obj: any = {};
let res = null;
for (const item of copyArr) {
item.parentId = String(Number(item.parentId) + 1);
item.originId = String(Number(item.id) + 1);
item.name = item.codeSignature;
delete item.id;
obj[item.originId] = item;
}
const scale = d3.scaleLinear().domain([min.value, max.value]).range([1, 200]);
for (const item of copyArr) {
if (item.parentId === "1") {
const val = Number(scale(item.count).toFixed(4));
res = item;
res.value = val;
}
for (const key in obj) {
if (item.originId === obj[key].parentId) {
const val = Number(scale(obj[key].count).toFixed(4));
obj[key].value = val;
if (item.children) {
item.children.push(obj[key]);
} else {
item.children = [obj[key]];
}
}
}
}
treeForeach([res], (node: TraceProfilingElement) => {
if (node.children) {
let val = 0;
for (const child of node.children) {
val = child.value + val;
}
node.value = node.value < val ? val : node.value;
}
});
return res;
}
watch(
() => profileStore.analyzeTrees,
() => {
drawGraph();
},
);
</script>
<style lang="scss" scoped>
.content {
@@ -78,4 +236,22 @@ limitations under the License. -->
overflow: hidden;
height: calc(50% - 20px);
}
#graph-stack {
width: 100%;
height: 100%;
cursor: pointer;
}
.tip {
display: inline-block;
width: 100%;
text-align: center;
color: red;
margin-top: 20px;
}
.name {
word-wrap: break-word;
}
</style>

View File

@@ -19,12 +19,20 @@ limitations under the License. -->
<el-input class="input mr-10 ml-5" readonly :value="profileStore.currentSegment.traceId" size="small" />
<Selector
size="small"
:value="mode"
:options="ProfileMode"
placeholder="Select a mode"
:value="dataMode"
:options="ProfileDataMode"
placeholder="Please select a profile data mode"
@change="spanModeChange"
class="mr-10"
/>
<Selector
size="small"
:value="displayMode"
:options="ProfileDisplayMode"
placeholder="Please select a profile display mode"
@change="selectDisplayMode"
class="mr-10"
/>
<el-button type="primary" size="small" :disabled="!profileStore.currentSpan.profiled" @click="analyzeProfile()">
{{ t("analyze") }}
</el-button>
@@ -49,13 +57,14 @@ limitations under the License. -->
import type { Span } from "@/types/trace";
import type { Option } from "@/types/app";
import { ElMessage } from "element-plus";
import { ProfileMode } from "./data";
import { ProfileDataMode, ProfileDisplayMode } from "./data";
/* global defineEmits*/
const emits = defineEmits(["loading"]);
const emits = defineEmits(["loading", "displayMode"]);
const { t } = useI18n();
const profileStore = useProfileStore();
const mode = ref<string>("include");
const dataMode = ref<string>("include");
const displayMode = ref<string>("tree");
const message = ref<string>("");
const timeRange = ref<Array<{ start: number; end: number }>>([]);
@@ -64,10 +73,15 @@ limitations under the License. -->
}
function spanModeChange(item: Option[]) {
mode.value = item[0].value;
dataMode.value = item[0].value;
updateTimeRange();
}
function selectDisplayMode(item: Option[]) {
displayMode.value = item[0].value;
emits("displayMode", displayMode.value);
}
async function analyzeProfile() {
if (!profileStore.currentSpan.profiled) {
ElMessage.info("It's a un-profiled span");
@@ -92,7 +106,7 @@ limitations under the License. -->
}
function updateTimeRange() {
if (mode.value === "include") {
if (dataMode.value === "include") {
timeRange.value = [
{
start: profileStore.currentSpan.startTime,
@@ -158,7 +172,7 @@ limitations under the License. -->
.profile-trace-detail-wrapper {
padding: 5px 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
border-bottom: 1px solid rgb(0 0 0 / 10%);
width: 100%;
}

View File

@@ -14,10 +14,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const ProfileMode: any[] = [
export const ProfileDataMode: any[] = [
{ label: "Include Children", value: "include" },
{ label: "Exclude Children", value: "exclude" },
];
export const ProfileDisplayMode: any[] = [
{ label: "Tree Graph", value: "tree" },
{ label: "Flame Graph", value: "flame" },
];
export const NewTaskField = {
service: { key: "", label: "None" },
monitorTime: { key: "0", label: "monitor now" },

View File

@@ -110,7 +110,7 @@ limitations under the License. -->
<div>
<span class="grey title">Tags:</span>
<div class="mb-5" v-for="(tag, index) in currentEvent.tags || []" :key="index" style="white-space: pre-wrap">
{{ tag.key + "=" + tag.value }};
{{ `${tag.key}=${tag.value}` }};
</div>
</div>
</div>

View File

@@ -218,14 +218,14 @@ export default class ListGraph {
.append("text")
.attr("x", 13)
.attr("y", 5)
.attr("fill", appStore.theme === Themes.Dark ? "#666" : "#E54C17")
.attr("fill", `#e54c17`)
.html((d: Recordable) => (d.data.isError ? "◉" : ""));
nodeEnter
.append("text")
.attr("class", "node-text")
.attr("x", 35)
.attr("y", -6)
.attr("fill", appStore.theme === Themes.Dark ? "#eee" : "#333")
.attr("fill", (d: Recordable) => (d.data.isError ? `#e54c17` : appStore.theme === Themes.Dark ? "#eee" : "#333"))
.html((d: Recordable) => {
if (d.data.label === "TRACE_ROOT") {
return "";

View File

@@ -37,7 +37,6 @@ export default ({ mode }: ConfigEnv): UserConfig => {
vueJsx(),
monacoEditorPlugin({}),
AutoImport({
imports: ["vue"],
resolvers: [ElementPlusResolver()],
dts: path.resolve(__dirname, "./src/types/auto-imports.d.ts"),
}),