From 1ceaef68029a363bb60a5326c0769582c889b9b3 Mon Sep 17 00:00:00 2001 From: Fine Date: Fri, 2 Jun 2023 12:08:51 +0800 Subject: [PATCH] feat: add metric expressions --- src/graphql/fragments/dashboard.ts | 9 + src/graphql/query/dashboard.ts | 3 + src/hooks/data.ts | 18 ++ src/hooks/useExpressionsProcessor.ts | 168 ++++++++++++++++++ src/hooks/useMetricsProcessor.ts | 2 +- src/store/modules/dashboard.ts | 5 + src/types/dashboard.d.ts | 5 +- .../configuration/widget/metric/Index.vue | 71 ++++++-- .../configuration/widget/metric/Standard.vue | 27 ++- 9 files changed, 280 insertions(+), 28 deletions(-) create mode 100644 src/hooks/useExpressionsProcessor.ts diff --git a/src/graphql/fragments/dashboard.ts b/src/graphql/fragments/dashboard.ts index f453f3d8..9b673b08 100644 --- a/src/graphql/fragments/dashboard.ts +++ b/src/graphql/fragments/dashboard.ts @@ -68,3 +68,12 @@ export const deleteTemplate = { message }`, }; +export const TypeOfMQE = { + variable: "$expression: String!", + query: ` + metricType: returnTypeOfMQE(expression: $expression) { + type + error + } + `, +}; diff --git a/src/graphql/query/dashboard.ts b/src/graphql/query/dashboard.ts index 738fafa8..a60a464e 100644 --- a/src/graphql/query/dashboard.ts +++ b/src/graphql/query/dashboard.ts @@ -21,6 +21,7 @@ import { addTemplate, changeTemplate, deleteTemplate, + TypeOfMQE, } from "../fragments/dashboard"; export const queryTypeOfMetrics = `query typeOfMetrics(${TypeOfMetrics.variable}) {${TypeOfMetrics.query}}`; @@ -34,3 +35,5 @@ export const updateTemplate = `mutation template(${changeTemplate.variable}) {${ export const removeTemplate = `mutation template(${deleteTemplate.variable}) {${deleteTemplate.query}}`; export const getTemplates = `query templates {${getAllTemplates.query}}`; + +export const getTypeOfMQE = `query returnTypeOfMQE(${TypeOfMQE.variable}) {${TypeOfMQE.query}}`; diff --git a/src/hooks/data.ts b/src/hooks/data.ts index 195483f9..d74ba1b2 100644 --- a/src/hooks/data.ts +++ b/src/hooks/data.ts @@ -112,4 +112,22 @@ export const RespFields: Indexable = { value refId }`, + execExpression: `{ + type + results { + metric { + name + labels { + key + value + } + } + values { + id + value + traceID + } + } + error + }`, }; diff --git a/src/hooks/useExpressionsProcessor.ts b/src/hooks/useExpressionsProcessor.ts new file mode 100644 index 00000000..99849108 --- /dev/null +++ b/src/hooks/useExpressionsProcessor.ts @@ -0,0 +1,168 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import dayjs from "dayjs"; +import { RespFields, MetricQueryTypes, Calculations } from "./data"; +import { ExpressionResultType } from "@/views/dashboard/data"; +import { ElMessage } from "element-plus"; +import { useDashboardStore } from "@/store/modules/dashboard"; +import { useSelectorStore } from "@/store/modules/selectors"; +import { useAppStoreWithOut } from "@/store/modules/app"; +import type { Instance, Endpoint, Service } from "@/types/selector"; +import type { MetricConfigOpt } from "@/types/dashboard"; +import { MetricCatalog } from "@/views/dashboard/data"; +import { calculateExp, aggregation } from "./useMetricsProcessor"; + +export function useExpressionsQueryProcessor(config: { + metrics: string[]; + metricTypes: string[]; + metricConfig: MetricConfigOpt[]; +}) { + if (!(config.metrics && config.metrics[0])) { + return; + } + if (!(config.metricTypes && config.metricTypes[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 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!`); + const metricType = config.metricTypes[index] || ""; + const c = (config.metricConfig && config.metricConfig[index]) || {}; + conditions[`expression${index}`] = name; + if ([ExpressionResultType.RECORD_LIST, ExpressionResultType.SORTED_LIST as string].includes(metricType)) { + conditions[`entity${index}`] = { + parentService: ["All"].includes(dashboardStore.entity) ? null : selectorStore.currentService.value, + normal: selectorStore.currentService ? selectorStore.currentService.normal : true, + topN: Number(c.topN) || 10, + order: c.sortOrder || "DES", + }; + } else { + const entity = { + serviceName: dashboardStore.entity === "All" ? undefined : selectorStore.currentService.value, + normal: dashboardStore.entity === "All" ? undefined : selectorStore.currentService.normal, + serviceInstanceName: ["ServiceInstance", "ServiceInstanceRelation", "ProcessRelation"].includes( + dashboardStore.entity, + ) + ? selectorStore.currentPod && selectorStore.currentPod.value + : undefined, + endpointName: dashboardStore.entity.includes("Endpoint") + ? selectorStore.currentPod && selectorStore.currentPod.value + : undefined, + processName: dashboardStore.entity.includes("Process") + ? selectorStore.currentProcess && selectorStore.currentProcess.value + : undefined, + destNormal: isRelation ? selectorStore.currentDestService.normal : undefined, + destServiceName: isRelation ? selectorStore.currentDestService.value : undefined, + destServiceInstanceName: ["ServiceInstanceRelation", "ProcessRelation"].includes(dashboardStore.entity) + ? selectorStore.currentDestPod && selectorStore.currentDestPod.value + : undefined, + destEndpointName: + dashboardStore.entity === "EndpointRelation" + ? selectorStore.currentDestPod && selectorStore.currentDestPod.value + : undefined, + destProcessName: dashboardStore.entity.includes("ProcessRelation") + ? selectorStore.currentDestProcess && selectorStore.currentDestProcess.value + : undefined, + }; + if ([ExpressionResultType.RECORD_LIST as string].includes(metricType)) { + conditions[`entity${index}`] = { + parentEntity: entity, + topN: Number(c.topN) || 10, + order: c.sortOrder || "DES", + }; + } else { + // if (metricType === ExpressionResultType.TIME_SERIES_VALUES) { + // const labels = (c.labelsIndex || "").split(",").map((item: string) => item.replace(/^\s*|\s*$/g, "")); + // variables.push(`$labels${index}: [String!]!`); + // conditions[`labels${index}`] = labels; + // } + conditions[`entity${index}`] = { + entity, + }; + } + } + // if (metricType === MetricQueryTypes.ReadLabeledMetricsValues) { + // return `${name}${index}: ${metricType}(condition: $condition${index}, labels: $labels${index}, duration: $duration)${RespFields[metricType]}`; + // } + + return `expression${index}: execExpression(expression: $expression${index}, entity: $entity${index}, duration: $duration)${RespFields.execExpression}`; + }); + const queryStr = `query queryData(${variables}) {${fragment}}`; + + return { + queryStr, + conditions, + }; +} + +export function useExpressionsSourceProcessor( + resp: { errors: string; data: Indexable }, + config: { + metrics: string[]; + metricTypes: string[]; + metricConfig: MetricConfigOpt[]; + }, +) { + if (resp.errors) { + ElMessage.error(resp.errors); + return {}; + } + if (!resp.data) { + ElMessage.error("The query is wrong"); + return {}; + } + const source: { [key: string]: unknown } = {}; + const keys = Object.keys(resp.data); + + config.metricTypes.forEach((type: string, index) => { + const m = config.metrics[index]; + const c = (config.metricConfig && config.metricConfig[index]) || {}; + + if (type === ExpressionResultType.TIME_SERIES_VALUES) { + source[c.label || m] = (resp.data[keys[index]] && calculateExp(resp.data.results[keys[index]].values, c)) || []; + } + if (type === ExpressionResultType.SINGLE_VALUE) { + const v = Object.values(resp.data)[0] || {}; + source[m] = v.isEmptyValue ? NaN : aggregation(Number(v.value), c); + } + if (([ExpressionResultType.RECORD_LIST, ExpressionResultType.SORTED_LIST] as string[]).includes(type)) { + source[m] = (Object.values(resp.data)[0] || []).map((d: { value: unknown; name: string }) => { + d.value = aggregation(Number(d.value), c); + + return d; + }); + } + }); + + return source; +} diff --git a/src/hooks/useMetricsProcessor.ts b/src/hooks/useMetricsProcessor.ts index 0a1265e5..3f8e7692 100644 --- a/src/hooks/useMetricsProcessor.ts +++ b/src/hooks/useMetricsProcessor.ts @@ -367,7 +367,7 @@ export function useQueryTopologyMetrics(metrics: string[], ids: string[]) { return { queryStr, conditions }; } -function calculateExp( +export function calculateExp( list: { value: number; isEmptyValue: boolean }[], config: { calculation?: string }, ): (number | string)[] { diff --git a/src/store/modules/dashboard.ts b/src/store/modules/dashboard.ts index 47051f30..2a627e0e 100644 --- a/src/store/modules/dashboard.ts +++ b/src/store/modules/dashboard.ts @@ -309,6 +309,11 @@ export const dashboardStore = defineStore({ return res.data; }, + async getTypeOfMQE(expression: string) { + const res: AxiosResponse = await graphql.query("getTypeOfMQE").params({ expression }); + + return res.data; + }, async fetchMetricList(regex: string) { const res: AxiosResponse = await graphql.query("queryMetrics").params({ regex }); diff --git a/src/types/dashboard.d.ts b/src/types/dashboard.d.ts index b4c151ef..2c7c102a 100644 --- a/src/types/dashboard.d.ts +++ b/src/types/dashboard.d.ts @@ -28,11 +28,14 @@ export interface LayoutConfig { w: number; h: number; i: string; + type: string; + metricMode?: string; widget?: WidgetConfig; graph?: GraphConfig; metrics?: string[]; - type: string; + expressions?: string[]; metricTypes?: string[]; + typesOfMQE?: string[]; children?: { name: string; children: LayoutConfig[] }[]; activedTabIndex?: number; metricConfig?: MetricConfigOpt[]; diff --git a/src/views/dashboard/configuration/widget/metric/Index.vue b/src/views/dashboard/configuration/widget/metric/Index.vue index 55d0b35c..f57c0bc4 100644 --- a/src/views/dashboard/configuration/widget/metric/Index.vue +++ b/src/views/dashboard/configuration/widget/metric/Index.vue @@ -29,7 +29,7 @@ limitations under the License. --> - + import { ElMessage } from "element-plus"; import Icon from "@/components/Icon.vue"; import { useQueryProcessor, useSourceProcessor, useGetMetricEntity } from "@/hooks/useMetricsProcessor"; + import { useExpressionsQueryProcessor, useExpressionsSourceProcessor } from "@/hooks/useExpressionsProcessor"; import { useI18n } from "vue-i18n"; import type { DashboardItem, MetricConfigOpt } from "@/types/dashboard"; import Standard from "./Standard.vue"; @@ -127,10 +123,14 @@ limitations under the License. --> const { t } = useI18n(); const emit = defineEmits(["update", "loading"]); const dashboardStore = useDashboardStore(); - const metrics = computed(() => dashboardStore.selectedGrid.metrics || []); - const graph = computed(() => dashboardStore.selectedGrid.graph || {}); - const metricTypes = computed(() => dashboardStore.selectedGrid.metricTypes || []); const isExpression = ref(dashboardStore.selectedGrid.metricMode === "Expression" ? true : false); + const metrics = computed( + () => (isExpression.value ? dashboardStore.selectedGrid.expressions : dashboardStore.selectedGrid.metrics) || [], + ); + const graph = computed(() => dashboardStore.selectedGrid.graph || {}); + const metricTypes = computed( + () => (isExpression.value ? dashboardStore.selectedGrid.typesOfMQE : dashboardStore.selectedGrid.metricTypes) || [], + ); const states = reactive<{ metrics: string[]; metricTypes: string[]; @@ -324,6 +324,10 @@ limitations under the License. --> queryMetrics(); } async function queryMetrics() { + if (isExpression.value) { + queryMetricsWithExpressions(); + return; + } if (states.isList) { return; } @@ -349,6 +353,32 @@ limitations under the License. --> emit("update", source); } + async function queryMetricsWithExpressions() { + if (states.isList) { + return; + } + const { metricConfig, typesOfMQE, expressions } = dashboardStore.selectedGrid; + if (!(expressions && expressions[0] && typesOfMQE && typesOfMQE[0])) { + return; + } + + const params = useExpressionsQueryProcessor({ ...states, metricConfig }); + if (!params) { + emit("update", {}); + return; + } + + emit("loading", true); + const json = await dashboardStore.fetchMetricValue(params); + emit("loading", false); + if (json.errors) { + ElMessage.error(json.errors); + return; + } + const source = useExpressionsSourceProcessor(json, { ...states, metricConfig }); + emit("update", source); + } + function changeDashboard(opt: any) { if (!opt[0]) { states.dashboardName = ""; @@ -368,7 +398,9 @@ limitations under the License. --> states.metrics.push(""); if (!states.isList) { states.metricTypes.push(states.metricTypes[0]); - states.metricTypeList.push(states.metricTypeList[0]); + if (!isExpression.value) { + states.metricTypeList.push(states.metricTypeList[0]); + } return; } states.metricTypes.push(""); @@ -421,16 +453,19 @@ limitations under the License. --> }; } function changeMetricMode() { - console.log(isExpression.value); - dashboardStore.selectWidget({ - ...dashboardStore.selectedGrid, - metricMode: isExpression.value ? "Expression" : "General", - }); + states.metrics = metrics.value.length ? metrics.value : [""]; + (states.metricTypes = metricTypes.value.length ? metricTypes.value : [""]), + dashboardStore.selectWidget({ + ...dashboardStore.selectedGrid, + metricMode: isExpression.value ? "Expression" : "General", + }); } - function changeExpression(event: any, index: number) { + async function changeExpression(event: any, index: number) { const params = event.target.textContent; states.metrics[index] = params; - // states.metricTypes[index] = + const resp = await dashboardStore.getTypeOfMQE(params); + console.log(resp); + // states.metricTypes[index] = resp.data.metricType }