mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2025-01-30 15:22:07 +00:00
vmui/vmanomaly: add support models that produce only anomaly_score
(#5594)
* vmui/vmanomaly: add support models that produce only `anomaly_score` * vmui/vmanomaly: fix display legend * vmui/vmanomaly: update comment on anomaly threshold
This commit is contained in:
parent
bdf4f0f1e2
commit
9588b9bd19
6 changed files with 48 additions and 30 deletions
|
@ -7,7 +7,7 @@ type Props = {
|
||||||
series: SeriesItem[];
|
series: SeriesItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const titles: Record<ForecastType, string> = {
|
const titles: Partial<Record<ForecastType, string>> = {
|
||||||
[ForecastType.yhat]: "yhat",
|
[ForecastType.yhat]: "yhat",
|
||||||
[ForecastType.yhatLower]: "yhat_lower/_upper",
|
[ForecastType.yhatLower]: "yhat_lower/_upper",
|
||||||
[ForecastType.yhatUpper]: "yhat_lower/_upper",
|
[ForecastType.yhatUpper]: "yhat_lower/_upper",
|
||||||
|
@ -39,7 +39,6 @@ const LegendAnomaly: FC<Props> = ({ series }) => {
|
||||||
return uniqSeries.map(s => ({
|
return uniqSeries.map(s => ({
|
||||||
...s,
|
...s,
|
||||||
color: typeof s.stroke === "string" ? s.stroke : anomalyColors[s.forecast || ForecastType.actual],
|
color: typeof s.stroke === "string" ? s.stroke : anomalyColors[s.forecast || ForecastType.actual],
|
||||||
forecast: titles[s.forecast || ForecastType.actual],
|
|
||||||
}));
|
}));
|
||||||
}, [series]);
|
}, [series]);
|
||||||
|
|
||||||
|
@ -49,7 +48,7 @@ const LegendAnomaly: FC<Props> = ({ series }) => {
|
||||||
return <>
|
return <>
|
||||||
<div className="vm-legend-anomaly">
|
<div className="vm-legend-anomaly">
|
||||||
{/* TODO: remove .filter() after the correct training data has been added */}
|
{/* TODO: remove .filter() after the correct training data has been added */}
|
||||||
{uniqSeriesStyles.filter(f => f.forecast !== titles[ForecastType.training]).map((s, i) => (
|
{uniqSeriesStyles.filter(f => f.forecast !== ForecastType.training).map((s, i) => (
|
||||||
<div
|
<div
|
||||||
key={`${i}_${s.forecast}`}
|
key={`${i}_${s.forecast}`}
|
||||||
className="vm-legend-anomaly-item"
|
className="vm-legend-anomaly-item"
|
||||||
|
@ -76,7 +75,7 @@ const LegendAnomaly: FC<Props> = ({ series }) => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</svg>
|
</svg>
|
||||||
<div className="vm-legend-anomaly-item__title">{s.forecast || "y"}</div>
|
<div className="vm-legend-anomaly-item__title">{titles[s.forecast || ForecastType.actual]}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,7 +5,7 @@ import useEventListener from "../../hooks/useEventListener";
|
||||||
import "../CustomPanel/style.scss";
|
import "../CustomPanel/style.scss";
|
||||||
import ExploreAnomalyHeader from "./ExploreAnomalyHeader/ExploreAnomalyHeader";
|
import ExploreAnomalyHeader from "./ExploreAnomalyHeader/ExploreAnomalyHeader";
|
||||||
import Alert from "../../components/Main/Alert/Alert";
|
import Alert from "../../components/Main/Alert/Alert";
|
||||||
import { extractFields } from "../../utils/uplot";
|
import { extractFields, isForecast } from "../../utils/uplot";
|
||||||
import { useFetchQuery } from "../../hooks/useFetchQuery";
|
import { useFetchQuery } from "../../hooks/useFetchQuery";
|
||||||
import Spinner from "../../components/Main/Spinner/Spinner";
|
import Spinner from "../../components/Main/Spinner/Spinner";
|
||||||
import GraphTab from "../CustomPanel/CustomPanelTabs/GraphTab";
|
import GraphTab from "../CustomPanel/CustomPanelTabs/GraphTab";
|
||||||
|
@ -17,6 +17,16 @@ import { useFetchAnomalySeries } from "./hooks/useFetchAnomalySeries";
|
||||||
import { useQueryDispatch } from "../../state/query/QueryStateContext";
|
import { useQueryDispatch } from "../../state/query/QueryStateContext";
|
||||||
import { useTimeDispatch } from "../../state/time/TimeStateContext";
|
import { useTimeDispatch } from "../../state/time/TimeStateContext";
|
||||||
|
|
||||||
|
const anomalySeries = [
|
||||||
|
ForecastType.yhat,
|
||||||
|
ForecastType.yhatUpper,
|
||||||
|
ForecastType.yhatLower,
|
||||||
|
ForecastType.anomalyScore
|
||||||
|
];
|
||||||
|
|
||||||
|
// Hardcoded to 1.0 for now; consider adding a UI slider for threshold adjustment in the future.
|
||||||
|
const ANOMALY_SCORE_THRESHOLD = 1;
|
||||||
|
|
||||||
const ExploreAnomaly: FC = () => {
|
const ExploreAnomaly: FC = () => {
|
||||||
const { isMobile } = useDeviceDetect();
|
const { isMobile } = useDeviceDetect();
|
||||||
|
|
||||||
|
@ -36,34 +46,31 @@ const ExploreAnomaly: FC = () => {
|
||||||
|
|
||||||
const data = useMemo(() => {
|
const data = useMemo(() => {
|
||||||
if (!graphData) return;
|
if (!graphData) return;
|
||||||
const group = queries.length + 1;
|
const detectedData = graphData.map(d => ({ ...isForecast(d.metric), ...d }));
|
||||||
const realData = graphData.filter(d => d.group === 1);
|
const realData = detectedData.filter(d => d.value === null);
|
||||||
const upperData = graphData.filter(d => d.group === 3);
|
const anomalyScoreData = detectedData.filter(d => d.isAnomalyScore);
|
||||||
const lowerData = graphData.filter(d => d.group === 4);
|
const anomalyData: MetricResult[] = realData.map((d) => {
|
||||||
const anomalyData: MetricResult[] = realData.map((d) => ({
|
const id = extractFields(d.metric);
|
||||||
group,
|
const anomalyScoreDataByLabels = anomalyScoreData.find(du => extractFields(du.metric) === id);
|
||||||
metric: { ...d.metric, __name__: ForecastType.anomaly },
|
|
||||||
values: d.values.filter(([t, v]) => {
|
return {
|
||||||
const id = extractFields(d.metric);
|
group: queries.length + 1,
|
||||||
const upperDataByLabels = upperData.find(du => extractFields(du.metric) === id);
|
metric: { ...d.metric, __name__: ForecastType.anomaly },
|
||||||
const lowerDataByLabels = lowerData.find(du => extractFields(du.metric) === id);
|
values: d.values.filter(([t]) => {
|
||||||
if (!upperDataByLabels || !lowerDataByLabels) return false;
|
if (!anomalyScoreDataByLabels) return false;
|
||||||
const max = upperDataByLabels.values.find(([tMax]) => tMax === t) as [number, string];
|
const anomalyScore = anomalyScoreDataByLabels.values.find(([tMax]) => tMax === t) as [number, string];
|
||||||
const min = lowerDataByLabels.values.find(([tMin]) => tMin === t) as [number, string];
|
return anomalyScore && promValueToNumber(anomalyScore[1]) > ANOMALY_SCORE_THRESHOLD;
|
||||||
const num = v && promValueToNumber(v);
|
})
|
||||||
const numMin = min && promValueToNumber(min[1]);
|
};
|
||||||
const numMax = max && promValueToNumber(max[1]);
|
});
|
||||||
return num < numMin || num > numMax;
|
return graphData.filter(d => d.group !== anomalyScoreData[0]?.group).concat(anomalyData);
|
||||||
})
|
|
||||||
}));
|
|
||||||
return graphData.concat(anomalyData);
|
|
||||||
}, [graphData]);
|
}, [graphData]);
|
||||||
|
|
||||||
const onChangeFilter = (expr: Record<string, string>) => {
|
const onChangeFilter = (expr: Record<string, string>) => {
|
||||||
const { __name__ = "", ...labelValue } = expr;
|
const { __name__ = "", ...labelValue } = expr;
|
||||||
let prefix = __name__.replace(/y|_y/, "");
|
let prefix = __name__.replace(/y|_y/, "");
|
||||||
if (prefix) prefix += "_";
|
if (prefix) prefix += "_";
|
||||||
const metrics = [__name__, ForecastType.yhat, ForecastType.yhatUpper, ForecastType.yhatLower];
|
const metrics = [__name__, ...anomalySeries];
|
||||||
const filters = Object.entries(labelValue).map(([key, value]) => `${key}="${value}"`).join(",");
|
const filters = Object.entries(labelValue).map(([key, value]) => `${key}="${value}"`).join(",");
|
||||||
const queries = metrics.map((m, i) => `${i ? prefix : ""}${m}{${filters}}`);
|
const queries = metrics.map((m, i) => `${i ? prefix : ""}${m}{${filters}}`);
|
||||||
queryDispatch({ type: "SET_QUERY", payload: queries });
|
queryDispatch({ type: "SET_QUERY", payload: queries });
|
||||||
|
|
|
@ -3,6 +3,8 @@ import { useAppState } from "../../../state/common/StateContext";
|
||||||
import { ErrorTypes } from "../../../types";
|
import { ErrorTypes } from "../../../types";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { MetricBase } from "../../../api/types";
|
import { MetricBase } from "../../../api/types";
|
||||||
|
import { useTimeState } from "../../../state/time/TimeStateContext";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
// TODO: Change the method of retrieving aliases from the configuration after the API has been added
|
// TODO: Change the method of retrieving aliases from the configuration after the API has been added
|
||||||
const seriesQuery = `{
|
const seriesQuery = `{
|
||||||
|
@ -12,18 +14,25 @@ const seriesQuery = `{
|
||||||
|
|
||||||
export const useFetchAnomalySeries = () => {
|
export const useFetchAnomalySeries = () => {
|
||||||
const { serverUrl } = useAppState();
|
const { serverUrl } = useAppState();
|
||||||
|
const { period: { start, end } } = useTimeState();
|
||||||
|
|
||||||
const [series, setSeries] = useState<Record<string, MetricBase["metric"][]>>();
|
const [series, setSeries] = useState<Record<string, MetricBase["metric"][]>>();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<ErrorTypes | string>();
|
const [error, setError] = useState<ErrorTypes | string>();
|
||||||
|
|
||||||
|
// TODO add cached metrics by date
|
||||||
const fetchUrl = useMemo(() => {
|
const fetchUrl = useMemo(() => {
|
||||||
|
const startDay = dayjs(start * 1000).startOf("day").valueOf() / 1000;
|
||||||
|
const endDay = dayjs(end * 1000).endOf("day").valueOf() / 1000;
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
"match[]": seriesQuery,
|
"match[]": seriesQuery,
|
||||||
|
start: `${startDay}`,
|
||||||
|
end: `${endDay}`
|
||||||
});
|
});
|
||||||
|
|
||||||
return `${serverUrl}/api/v1/series?${params}`;
|
return `${serverUrl}/api/v1/series?${params}`;
|
||||||
}, [serverUrl]);
|
}, [serverUrl, start, end]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchSeries = async () => {
|
const fetchSeries = async () => {
|
||||||
|
|
|
@ -6,7 +6,8 @@ export enum ForecastType {
|
||||||
yhatLower = "yhat_lower",
|
yhatLower = "yhat_lower",
|
||||||
anomaly = "vmui_anomalies_points",
|
anomaly = "vmui_anomalies_points",
|
||||||
training = "vmui_training_data",
|
training = "vmui_training_data",
|
||||||
actual = "actual"
|
actual = "actual",
|
||||||
|
anomalyScore = "anomaly_score",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SeriesItemStatsFormatted {
|
export interface SeriesItemStatsFormatted {
|
||||||
|
|
|
@ -26,6 +26,7 @@ export const anomalyColors: Record<ForecastType, string> = {
|
||||||
[ForecastType.yhatLower]: "#7126a1",
|
[ForecastType.yhatLower]: "#7126a1",
|
||||||
[ForecastType.yhat]: "#da42a6",
|
[ForecastType.yhat]: "#da42a6",
|
||||||
[ForecastType.anomaly]: "#da4242",
|
[ForecastType.anomaly]: "#da4242",
|
||||||
|
[ForecastType.anomalyScore]: "#7126a1",
|
||||||
[ForecastType.actual]: "#203ea9",
|
[ForecastType.actual]: "#203ea9",
|
||||||
[ForecastType.training]: `rgba(${hexToRGB("#203ea9")}, 0.2)`,
|
[ForecastType.training]: `rgba(${hexToRGB("#203ea9")}, 0.2)`,
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,7 +14,7 @@ export const extractFields = (metric: MetricBase["metric"]): string => {
|
||||||
.map(([key, value]) => `${key}: ${value}`).join(",");
|
.map(([key, value]) => `${key}: ${value}`).join(",");
|
||||||
};
|
};
|
||||||
|
|
||||||
const isForecast = (metric: MetricBase["metric"]) => {
|
export const isForecast = (metric: MetricBase["metric"]) => {
|
||||||
const metricName = metric?.__name__ || "";
|
const metricName = metric?.__name__ || "";
|
||||||
const forecastRegex = new RegExp(`(${Object.values(ForecastType).join("|")})$`);
|
const forecastRegex = new RegExp(`(${Object.values(ForecastType).join("|")})$`);
|
||||||
const match = metricName.match(forecastRegex);
|
const match = metricName.match(forecastRegex);
|
||||||
|
@ -25,6 +25,7 @@ const isForecast = (metric: MetricBase["metric"]) => {
|
||||||
isLower: value === ForecastType.yhatLower,
|
isLower: value === ForecastType.yhatLower,
|
||||||
isYhat: value === ForecastType.yhat,
|
isYhat: value === ForecastType.yhat,
|
||||||
isAnomaly: value === ForecastType.anomaly,
|
isAnomaly: value === ForecastType.anomaly,
|
||||||
|
isAnomalyScore: value === ForecastType.anomalyScore,
|
||||||
group: extractFields(metric)
|
group: extractFields(metric)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue