This is an automated email from the ASF dual-hosted git repository.
wusheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-booster-ui.git
The following commit(s) were added to refs/heads/main by this push:
new 8ea50c86 feat: visualize `Snapshot` on Alerting page (#444)
8ea50c86 is described below
commit 8ea50c8680add858f54ed16e696d87eb95166216
Author: Fine0830 <[email protected]>
AuthorDate: Mon Jan 13 17:24:32 2025 +0800
feat: visualize `Snapshot` on Alerting page (#444)
---
src/graphql/fragments/alarm.ts | 30 +++++++
src/hooks/useSnapshot.ts | 47 +++++++++++
src/locales/lang/en.ts | 2 +
src/locales/lang/es.ts | 2 +
src/locales/lang/zh.ts | 2 +
src/types/alarm.d.ts | 1 +
src/types/dashboard.d.ts | 16 ++++
src/views/alarm/Content.vue | 48 ++++++-----
src/views/alarm/components/Line.vue | 141 ++++++++++++++++++++++++++++++++
src/views/alarm/components/Snapshot.vue | 35 ++++++++
src/views/alarm/data.ts | 8 ++
src/views/dashboard/graphs/Line.vue | 15 ++--
12 files changed, 323 insertions(+), 24 deletions(-)
diff --git a/src/graphql/fragments/alarm.ts b/src/graphql/fragments/alarm.ts
index a2e9ba0c..76ed4672 100644
--- a/src/graphql/fragments/alarm.ts
+++ b/src/graphql/fragments/alarm.ts
@@ -24,6 +24,7 @@ export const Alarm = {
message
startTime
scope
+ name
tags {
key
value
@@ -43,6 +44,35 @@ export const Alarm = {
startTime
endTime
}
+ snapshot {
+ expression
+ metrics {
+ name
+ results {
+ metric {
+ labels {
+ key
+ value
+ }
+ }
+ values {
+ id
+ owner {
+ scope
+ serviceID
+ serviceName
+ normal
+ serviceInstanceID
+ serviceInstanceName
+ endpointID
+ endpointName
+ }
+ value
+ traceID
+ }
+ }
+ }
+ }
}
}`,
};
diff --git a/src/hooks/useSnapshot.ts b/src/hooks/useSnapshot.ts
new file mode 100644
index 00000000..2ddfc9b7
--- /dev/null
+++ b/src/hooks/useSnapshot.ts
@@ -0,0 +1,47 @@
+/**
+ * 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 type { MetricsResults } from "@/types/dashboard";
+
+export function useSnapshot(metrics: { name: string; results: MetricsResults[]
}[]) {
+ function processResults() {
+ const sources = metrics.map((metric: { name: string; results:
MetricsResults[] }) => {
+ const values = metric.results.map(
+ (r: { values: { value: string }[]; metric: { labels: { key: string;
value: string }[] } }) => {
+ const arr = r.values.map((v: { value: string }) => Number(v.value));
+ if (!r.metric.labels.length) {
+ return { values: arr };
+ }
+ const name = r.metric.labels
+ .map(
+ (label: { key: string; value: string }) =>
+ `${metric.name}${label ? "{" :
""}${label.key}=${label.value}${label ? "}" : ""}`,
+ )
+ .join(",");
+ return { name, values: arr };
+ },
+ );
+
+ return { name: metric.name, values };
+ });
+
+ return sources;
+ }
+
+ return {
+ processResults,
+ };
+}
diff --git a/src/locales/lang/en.ts b/src/locales/lang/en.ts
index 5640a21d..98764453 100644
--- a/src/locales/lang/en.ts
+++ b/src/locales/lang/en.ts
@@ -395,5 +395,7 @@ const msg = {
profilingEvents: "Async Profiling Events",
execArgs: "Exec Args",
instances: "Instances",
+ snapshot: "Snapshot",
+ expression: "Expression",
};
export default msg;
diff --git a/src/locales/lang/es.ts b/src/locales/lang/es.ts
index 4496b367..bc030873 100644
--- a/src/locales/lang/es.ts
+++ b/src/locales/lang/es.ts
@@ -395,5 +395,7 @@ const msg = {
profilingEvents: "Async Profiling Events",
execArgs: "Exec Args",
instances: "Instances",
+ snapshot: "Snapshot",
+ expression: "Expression",
};
export default msg;
diff --git a/src/locales/lang/zh.ts b/src/locales/lang/zh.ts
index 800d8c78..359ebb97 100644
--- a/src/locales/lang/zh.ts
+++ b/src/locales/lang/zh.ts
@@ -393,5 +393,7 @@ const msg = {
profilingEvents: "异步分析事件",
execArgs: "String任务扩展",
instances: "实例",
+ snapshot: "快照",
+ expression: "表达式",
};
export default msg;
diff --git a/src/types/alarm.d.ts b/src/types/alarm.d.ts
index 06c09958..df791ca2 100644
--- a/src/types/alarm.d.ts
+++ b/src/types/alarm.d.ts
@@ -27,6 +27,7 @@ export interface Alarm {
scope: string;
tags: Array<{ key: string; value: string }>;
events: Event[];
+ snapshot: Indexable;
}
export interface Event {
diff --git a/src/types/dashboard.d.ts b/src/types/dashboard.d.ts
index fa72ce4a..d021e48d 100644
--- a/src/types/dashboard.d.ts
+++ b/src/types/dashboard.d.ts
@@ -111,6 +111,8 @@ export interface LineConfig extends AreaConfig {
showYAxis?: boolean;
smallTips?: boolean;
showlabels?: boolean;
+ noTooltips?: boolean;
+ showLegend?: boolean;
}
export interface AreaConfig {
@@ -197,3 +199,17 @@ export type LegendOptions = {
toTheRight: boolean;
width: number;
};
+export type MetricsResults = {
+ metric: { labels: MetricLabel[] };
+ values: MetricValue[];
+};
+type MetricLabel = {
+ key: string;
+ value: string;
+};
+type MetricValue = {
+ name: string;
+ value: string;
+ owner: null | string;
+ refId: null | string;
+};
diff --git a/src/views/alarm/Content.vue b/src/views/alarm/Content.vue
index 0a8988ae..aee06071 100644
--- a/src/views/alarm/Content.vue
+++ b/src/views/alarm/Content.vue
@@ -22,15 +22,17 @@ limitations under the License. -->
<div class="message mb-5 b">
{{ i.message }}
</div>
- <div
- class="timeline-table-i-scope mr-10 l sm"
- :class="{
- blue: i.scope === 'Service',
- green: i.scope === 'Endpoint',
- yellow: i.scope === 'ServiceInstance',
- }"
- >
- {{ t(i.scope.toLowerCase()) }}
+ <div class="flex-h">
+ <div
+ class="timeline-table-i-scope"
+ :class="{
+ blue: i.scope === 'Service',
+ green: i.scope === 'Endpoint',
+ yellow: i.scope === 'ServiceInstance',
+ }"
+ >
+ {{ t(i.scope.toLowerCase()) }}
+ </div>
</div>
<div class="grey sm show-xs">
{{ dateFormat(parseInt(i.startTime)) }}
@@ -46,7 +48,7 @@ limitations under the License. -->
:destroy-on-close="true"
@closed="isShowDetails = false"
>
- <div class="mb-10 clear alarm-detail" v-for="(item, index) in
AlarmDetailCol" :key="index">
+ <div class="mb-20 clear alarm-detail" v-for="(item, index) in
AlarmDetailCol" :key="index">
<span class="g-sm-2 grey">{{ t(item.value) }}:</span>
<span v-if="item.label === 'startTime'">
{{ dateFormat(currentDetail[item.label]) }}
@@ -54,7 +56,7 @@ limitations under the License. -->
<span v-else-if="item.label === 'tags'">
<div v-for="(d, index) in alarmTags" :key="index">{{ d }}</div>
</span>
- <span v-else-if="item.label === 'events'" class="event-detail">
+ <span v-else-if="item.label === 'events'">
<div>
<ul>
<li>
@@ -75,6 +77,12 @@ limitations under the License. -->
</ul>
</div>
</span>
+ <span v-else-if="item.label === 'expression'">
+ {{ currentDetail.snapshot.expression }}
+ </span>
+ <span v-else-if="item.label === 'snapshot'">
+ <Snapshot :snapshot="currentDetail.snapshot" />
+ </span>
<span v-else>{{ currentDetail[item.label] }}</span>
</div>
</el-dialog>
@@ -85,7 +93,7 @@ limitations under the License. -->
:destroy-on-close="true"
@closed="showEventDetails = false"
>
- <div class="event-detail">
+ <div>
<div class="mb-10" v-for="(eventKey, index) in EventsDetailKeys"
:key="index">
<span class="keys">{{ t(eventKey.text) }}</span>
<span v-if="eventKey.class === 'parameters'">
@@ -117,6 +125,7 @@ limitations under the License. -->
import { useAlarmStore } from "@/store/modules/alarm";
import { EventsDetailHeaders, AlarmDetailCol, EventsDetailKeys } from
"./data";
import { dateFormat } from "@/utils/dateFormat";
+ import Snapshot from "./components/Snapshot.vue";
const { t } = useI18n();
const alarmStore = useAlarmStore();
@@ -186,11 +195,10 @@ limitations under the License. -->
}
.timeline-table-i-scope {
- display: inline-block;
- padding: 0 8px;
+ padding: 0 5px;
border: 1px solid;
- margin-top: -1px;
- border-radius: 4px;
+ border-radius: 3px;
+ display: inline-block;
}
.timeline-item {
@@ -224,9 +232,6 @@ limitations under the License. -->
}
.alarm-detail {
- max-height: 600px;
- overflow: auto;
-
ul {
min-height: 100px;
overflow: auto;
@@ -247,4 +252,9 @@ limitations under the License. -->
}
}
}
+
+ .mini-chart {
+ height: 20px;
+ width: 400px;
+ }
</style>
diff --git a/src/views/alarm/components/Line.vue
b/src/views/alarm/components/Line.vue
new file mode 100644
index 00000000..da8be69f
--- /dev/null
+++ b/src/views/alarm/components/Line.vue
@@ -0,0 +1,141 @@
+<!-- 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>
+ <Graph :option="option" @select="clickEvent" />
+</template>
+<script lang="ts" setup>
+ import { computed } from "vue";
+ import type { PropType } from "vue";
+ import type { EventParams } from "@/types/dashboard";
+ import useLegendProcess from "@/hooks/useLegendProcessor";
+ import { useAppStoreWithOut } from "@/store/modules/app";
+ import { Themes } from "@/constants/data";
+
+ /*global defineProps, defineEmits */
+ const emits = defineEmits(["click"]);
+ const props = defineProps({
+ data: {
+ type: Array as PropType<any>,
+ default: () => [],
+ },
+ intervalTime: { type: Array as PropType<string[]>, default: () => [] },
+ });
+ const appStore = useAppStoreWithOut();
+ const option = computed(() => getOption());
+ function getOption() {
+ const { chartColors } = useLegendProcess();
+ const color: string[] = chartColors();
+ const series = [];
+ const grid = [];
+ const xAxis = [];
+ const yAxis = [];
+ for (const [index, metric] of props.data.entries()) {
+ grid.push({
+ top: 300 * index + 30,
+ left: 0,
+ right: 10,
+ bottom: 5,
+ containLabel: true,
+ height: 260,
+ });
+ xAxis.push({
+ type: "category",
+ show: true,
+ axisTick: {
+ lineStyle: { color: "#c1c5ca41" },
+ alignWithLabel: true,
+ },
+ splitLine: { show: false },
+ axisLine: { lineStyle: { color: "rgba(0,0,0,0)" } },
+ axisLabel: { color: "#9da5b2", fontSize: "11" },
+ gridIndex: index,
+ });
+ yAxis.push({
+ type: "value",
+ axisLine: { show: false },
+ axisTick: { show: false },
+ splitLine: { lineStyle: { color: "#c1c5ca41", type: "dashed" } },
+ axisLabel: {
+ color: "#9da5b2",
+ fontSize: "11",
+ show: true,
+ },
+ gridIndex: index,
+ });
+ for (const item of metric.values) {
+ series.push({
+ data: item.values.map((item: number, itemIndex: number) =>
[props.intervalTime[itemIndex], item]),
+ name: item.name || metric.name,
+ type: "line",
+ symbol: "circle",
+ symbolSize: 4,
+ xAxisIndex: index,
+ yAxisIndex: index,
+ lineStyle: {
+ width: 2,
+ type: "solid",
+ },
+ });
+ }
+ }
+ const tooltip = {
+ trigger: "axis",
+ backgroundColor: appStore.theme === Themes.Dark ? "#333" : "#fff",
+ borderColor: appStore.theme === Themes.Dark ? "#333" : "#fff",
+ textStyle: {
+ fontSize: 12,
+ color: appStore.theme === Themes.Dark ? "#eee" : "#333",
+ },
+ enterable: true,
+ confine: true,
+ extraCssText: "max-height:85%; overflow: auto;",
+ axisPointer: {
+ animation: false,
+ },
+ };
+
+ return {
+ color,
+ tooltip,
+ axisPointer: {
+ link: { xAxisIndex: "all" },
+ },
+ legend: {
+ type: "scroll",
+ icon: "circle",
+ top: -5,
+ left: 0,
+ itemWidth: 12,
+ textStyle: {
+ color: appStore.theme === Themes.Dark ? "#fff" : "#333",
+ },
+ },
+ grid,
+ xAxis,
+ yAxis,
+ series,
+ };
+ }
+
+ function clickEvent(params: EventParams) {
+ emits("click", params);
+ }
+</script>
+<style lang="scss" scoped>
+ .snapshot-charts {
+ width: 100%;
+ height: 100%;
+ }
+</style>
diff --git a/src/views/alarm/components/Snapshot.vue
b/src/views/alarm/components/Snapshot.vue
new file mode 100644
index 00000000..5b7fad33
--- /dev/null
+++ b/src/views/alarm/components/Snapshot.vue
@@ -0,0 +1,35 @@
+<!-- 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>
+ <LineChart
+ :intervalTime="appStore.intervalTime"
+ :data="metrics"
+ :style="{ width: `800px`, height: `${metrics.length * 300}px` }"
+ />
+</template>
+<script lang="ts" setup>
+ import { computed } from "vue";
+ import { useSnapshot } from "@/hooks/useSnapshot";
+ import { useAppStoreWithOut } from "@/store/modules/app";
+ import LineChart from "./Line.vue";
+
+ /*global defineProps */
+ const props = defineProps({
+ snapshot: { type: Object, default: () => {} },
+ });
+ const { processResults } = useSnapshot(props.snapshot.metrics);
+ const metrics = computed(() => processResults());
+ const appStore = useAppStoreWithOut();
+</script>
diff --git a/src/views/alarm/data.ts b/src/views/alarm/data.ts
index d78f7ea8..22701308 100644
--- a/src/views/alarm/data.ts
+++ b/src/views/alarm/data.ts
@@ -52,6 +52,14 @@ export const AlarmDetailCol = [
label: "events",
value: "eventDetail",
},
+ {
+ label: "expression",
+ value: "expression",
+ },
+ {
+ label: "snapshot",
+ value: "snapshot",
+ },
];
export const EventsDetailKeys = [
diff --git a/src/views/dashboard/graphs/Line.vue
b/src/views/dashboard/graphs/Line.vue
index 11d23515..d91b31c2 100644
--- a/src/views/dashboard/graphs/Line.vue
+++ b/src/views/dashboard/graphs/Line.vue
@@ -60,6 +60,8 @@ limitations under the License. -->
showYAxis: true,
smallTips: false,
showlabels: true,
+ noTooltips: false,
+ showLegend: true,
}),
},
});
@@ -69,10 +71,12 @@ limitations under the License. -->
function getOption() {
const { showEchartsLegend, isRight, chartColors } =
useLegendProcess(props.config.legend);
setRight.value = isRight;
- const keys = Object.keys(props.data || {}).filter((i: any) =>
Array.isArray(props.data[i]) && props.data[i].length);
- const temp = keys.map((i: any) => {
+ const keys = Object.keys(props.data || {}).filter(
+ (i: string) => Array.isArray(props.data[i]) && props.data[i].length,
+ );
+ const temp = keys.map((i: string) => {
const serie: any = {
- data: props.data[i].map((item: any, itemIndex: number) =>
[props.intervalTime[itemIndex], item]),
+ data: props.data[i].map((item: number, itemIndex: number) =>
[props.intervalTime[itemIndex], item]),
name: i,
type: "line",
symbol: "circle",
@@ -95,6 +99,7 @@ limitations under the License. -->
const color: string[] = chartColors();
const tooltip = {
trigger: "axis",
+ show: !props.config.noTooltips,
backgroundColor: appStore.theme === Themes.Dark ? "#333" : "#fff",
borderColor: appStore.theme === Themes.Dark ? "#333" : "#fff",
textStyle: {
@@ -106,10 +111,10 @@ limitations under the License. -->
extraCssText: "max-height:85%; overflow: auto;",
};
const tips = {
+ show: !props.config.noTooltips,
formatter(params: any) {
return `${params[0].value[1]}`;
},
- confine: true,
extraCssText: `height: 20px; padding:0 2px;`,
trigger: "axis",
backgroundColor: appStore.theme === Themes.Dark ? "#666" : "#eee",
@@ -125,7 +130,7 @@ limitations under the License. -->
tooltip: props.config.smallTips ? tips : tooltip,
legend: {
type: "scroll",
- show: showEchartsLegend(keys),
+ show: props.config.showLegend ? showEchartsLegend(keys) : false,
icon: "circle",
top: 0,
left: 0,