mirror of
https://github.com/prometheus/prometheus.git
synced 2025-03-05 20:59:13 -08:00
Refactor API fetching code, handle all possible errors correctly
Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
parent
92195e3fb6
commit
5fea050fed
|
@ -16,79 +16,79 @@ export type ErrorAPIResponse = {
|
||||||
|
|
||||||
export type APIResponse<T> = SuccessAPIResponse<T> | ErrorAPIResponse;
|
export type APIResponse<T> = SuccessAPIResponse<T> | ErrorAPIResponse;
|
||||||
|
|
||||||
export const useAPIQuery = <T>({
|
const createQueryFn =
|
||||||
key,
|
<T>({ path, params }: { path: string; params?: Record<string, string> }) =>
|
||||||
path,
|
async ({ signal }: { signal: AbortSignal }) => {
|
||||||
params,
|
const queryString = params
|
||||||
enabled,
|
? `?${new URLSearchParams(params).toString()}`
|
||||||
}: {
|
: "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/${API_PATH}/${path}${queryString}`, {
|
||||||
|
cache: "no-store",
|
||||||
|
credentials: "same-origin",
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
!res.ok &&
|
||||||
|
!res.headers.get("content-type")?.startsWith("application/json")
|
||||||
|
) {
|
||||||
|
// For example, Prometheus may send a 503 Service Unavailable response
|
||||||
|
// with a "text/plain" content type when it's starting up. But the API
|
||||||
|
// may also respond with a JSON error message and the same error code.
|
||||||
|
throw new Error(res.statusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiRes = (await res.json()) as APIResponse<T>;
|
||||||
|
|
||||||
|
if (apiRes.status === "error") {
|
||||||
|
throw new Error(
|
||||||
|
apiRes.error !== undefined
|
||||||
|
? apiRes.error
|
||||||
|
: 'missing "error" field in response JSON'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiRes as SuccessAPIResponse<T>;
|
||||||
|
} catch (error) {
|
||||||
|
if (!(error instanceof Error)) {
|
||||||
|
throw new Error("Unknown error");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (error.name) {
|
||||||
|
case "TypeError":
|
||||||
|
throw new Error("Network error or unable to reach the server");
|
||||||
|
case "SyntaxError":
|
||||||
|
throw new Error("Invalid JSON response");
|
||||||
|
default:
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type QueryOptions = {
|
||||||
key?: string;
|
key?: string;
|
||||||
path: string;
|
path: string;
|
||||||
params?: Record<string, string>;
|
params?: Record<string, string>;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
}) =>
|
};
|
||||||
useQuery<APIResponse<T>>({
|
|
||||||
queryKey: [key || path],
|
export const useAPIQuery = <T>({ key, path, params, enabled }: QueryOptions) =>
|
||||||
|
useQuery<SuccessAPIResponse<T>>({
|
||||||
|
queryKey: key ? [key] : [path, params],
|
||||||
retry: false,
|
retry: false,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
gcTime: 0,
|
gcTime: 0,
|
||||||
enabled,
|
enabled,
|
||||||
queryFn: async ({ signal }) => {
|
queryFn: createQueryFn({ path, params }),
|
||||||
const queryString = params
|
|
||||||
? `?${new URLSearchParams(params).toString()}`
|
|
||||||
: "";
|
|
||||||
return (
|
|
||||||
fetch(`/${API_PATH}/${path}${queryString}`, {
|
|
||||||
cache: "no-store",
|
|
||||||
credentials: "same-origin",
|
|
||||||
signal,
|
|
||||||
})
|
|
||||||
// TODO: think about how to check API errors here, if this code remains in use.
|
|
||||||
.then((res) => {
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(res.statusText);
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
})
|
|
||||||
.then((res) => res.json() as Promise<APIResponse<T>>)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useSuspenseAPIQuery = <T>(
|
export const useSuspenseAPIQuery = <T>({ key, path, params }: QueryOptions) =>
|
||||||
path: string,
|
|
||||||
params?: Record<string, string>
|
|
||||||
) =>
|
|
||||||
useSuspenseQuery<SuccessAPIResponse<T>>({
|
useSuspenseQuery<SuccessAPIResponse<T>>({
|
||||||
queryKey: [path],
|
queryKey: key ? [key] : [path, params],
|
||||||
retry: false,
|
retry: false,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
gcTime: 0,
|
gcTime: 0,
|
||||||
queryFn: ({ signal }) => {
|
queryFn: createQueryFn({ path, params }),
|
||||||
const queryString = params
|
|
||||||
? `?${new URLSearchParams(params).toString()}`
|
|
||||||
: "";
|
|
||||||
return (
|
|
||||||
fetch(`/${API_PATH}/${path}${queryString}`, {
|
|
||||||
cache: "no-store",
|
|
||||||
credentials: "same-origin",
|
|
||||||
signal,
|
|
||||||
})
|
|
||||||
// Introduce 3 seconds delay to simulate slow network.
|
|
||||||
// .then(
|
|
||||||
// (res) =>
|
|
||||||
// new Promise<typeof res>((resolve) =>
|
|
||||||
// setTimeout(() => resolve(res), 2000)
|
|
||||||
// )
|
|
||||||
// )
|
|
||||||
// TODO: think about how to check API errors here, if this code remains in use.
|
|
||||||
.then((res) => {
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(res.statusText);
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
})
|
|
||||||
.then((res) => res.json() as Promise<SuccessAPIResponse<T>>)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
3
web/ui/mantine-ui/src/api/responseTypes/config.ts
Normal file
3
web/ui/mantine-ui/src/api/responseTypes/config.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export default interface ConfigResult {
|
||||||
|
yaml: string;
|
||||||
|
}
|
|
@ -17,8 +17,11 @@ import { formatRelative, now } from "../lib/formatTime";
|
||||||
import { Fragment, useState } from "react";
|
import { Fragment, useState } from "react";
|
||||||
|
|
||||||
export default function AlertsPage() {
|
export default function AlertsPage() {
|
||||||
const { data } = useSuspenseAPIQuery<AlertingRulesMap>(`/rules`, {
|
const { data } = useSuspenseAPIQuery<AlertingRulesMap>({
|
||||||
|
path: `/rules`,
|
||||||
|
params: {
|
||||||
type: "alert",
|
type: "alert",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const [showAnnotations, setShowAnnotations] = useState(false);
|
const [showAnnotations, setShowAnnotations] = useState(false);
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,14 @@
|
||||||
import { CodeHighlight } from "@mantine/code-highlight";
|
import { CodeHighlight } from "@mantine/code-highlight";
|
||||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
import { useSuspenseAPIQuery } from "../api/api";
|
||||||
|
import ConfigResult from "../api/responseTypes/config";
|
||||||
|
|
||||||
export default function ConfigPage() {
|
export default function ConfigPage() {
|
||||||
const {
|
const {
|
||||||
data: {
|
data: {
|
||||||
data: { yaml },
|
data: { yaml },
|
||||||
},
|
},
|
||||||
} = useSuspenseQuery<{ data: { yaml: string } }>({
|
} = useSuspenseAPIQuery<ConfigResult>({ path: `/status/config` });
|
||||||
queryKey: ["config"],
|
|
||||||
queryFn: () => {
|
|
||||||
return fetch("/api/v1/status/config").then((res) => res.json());
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return (
|
return (
|
||||||
<CodeHighlight
|
<CodeHighlight
|
||||||
code={yaml}
|
code={yaml}
|
||||||
|
|
|
@ -83,7 +83,9 @@ function sortData(
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FlagsPage() {
|
export default function FlagsPage() {
|
||||||
const { data } = useSuspenseAPIQuery<Record<string, string>>(`/status/flags`);
|
const { data } = useSuspenseAPIQuery<Record<string, string>>({
|
||||||
|
path: `/status/flags`,
|
||||||
|
});
|
||||||
|
|
||||||
const flags = Object.entries(data.data).map(([flag, value]) => ({
|
const flags = Object.entries(data.data).map(([flag, value]) => ({
|
||||||
flag,
|
flag,
|
||||||
|
|
|
@ -28,7 +28,7 @@ const healthBadgeClass = (state: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RulesPage() {
|
export default function RulesPage() {
|
||||||
const { data } = useSuspenseAPIQuery<RulesMap>(`/rules`);
|
const { data } = useSuspenseAPIQuery<RulesMap>({ path: `/rules` });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -28,10 +28,12 @@ const statusConfig: Record<
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function StatusPage() {
|
export default function StatusPage() {
|
||||||
const { data: buildinfo } =
|
const { data: buildinfo } = useSuspenseAPIQuery<Record<string, string>>({
|
||||||
useSuspenseAPIQuery<Record<string, string>>(`/status/buildinfo`);
|
path: `/status/buildinfo`,
|
||||||
const { data: runtimeinfo } =
|
});
|
||||||
useSuspenseAPIQuery<Record<string, string>>(`/status/runtimeinfo`);
|
const { data: runtimeinfo } = useSuspenseAPIQuery<Record<string, string>>({
|
||||||
|
path: `/status/runtimeinfo`,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="lg" maw={1000} mx="auto" mt="lg">
|
<Stack gap="lg" maw={1000} mx="auto" mt="lg">
|
||||||
|
|
|
@ -13,7 +13,7 @@ export default function TSDBStatusPage() {
|
||||||
seriesCountByLabelValuePair,
|
seriesCountByLabelValuePair,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} = useSuspenseAPIQuery<TSDBMap>(`/status/tsdb`);
|
} = useSuspenseAPIQuery<TSDBMap>({ path: `/status/tsdb` });
|
||||||
|
|
||||||
const unixToTime = (unix: number): string => {
|
const unixToTime = (unix: number): string => {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -81,11 +81,6 @@ const DataTable: FC<TableProps> = ({ expr, evalTime, retriggerIdx }) => {
|
||||||
return <Alert variant="transparent">No data queried yet</Alert>;
|
return <Alert variant="transparent">No data queried yet</Alert>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.status !== "success") {
|
|
||||||
// TODO: Remove this case and handle it in useAPIQuery instead!
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { result, resultType } = data.data;
|
const { result, resultType } = data.data;
|
||||||
|
|
||||||
if (result.length === 0) {
|
if (result.length === 0) {
|
||||||
|
|
|
@ -148,10 +148,6 @@ const ExpressionInput: FC<ExpressionInputProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (formatResult) {
|
if (formatResult) {
|
||||||
if (formatResult.status !== "success") {
|
|
||||||
// TODO: Remove this case and handle it in useAPIQuery instead!
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setExpr(formatResult.data);
|
setExpr(formatResult.data);
|
||||||
notifications.show({
|
notifications.show({
|
||||||
color: "green",
|
color: "green",
|
||||||
|
@ -222,10 +218,7 @@ const ExpressionInput: FC<ExpressionInputProps> = ({
|
||||||
}
|
}
|
||||||
onClick={() => formatQuery()}
|
onClick={() => formatQuery()}
|
||||||
disabled={
|
disabled={
|
||||||
isFormatting ||
|
isFormatting || expr === "" || expr === formatResult?.data
|
||||||
expr === "" ||
|
|
||||||
(formatResult?.status === "success" &&
|
|
||||||
expr === formatResult.data)
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Format expression
|
Format expression
|
||||||
|
|
Loading…
Reference in a new issue