mirror of
https://github.com/prometheus/prometheus.git
synced 2025-01-12 06:17:27 -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 const useAPIQuery = <T>({
|
||||
key,
|
||||
path,
|
||||
params,
|
||||
enabled,
|
||||
}: {
|
||||
const createQueryFn =
|
||||
<T>({ path, params }: { path: string; params?: Record<string, string> }) =>
|
||||
async ({ signal }: { signal: AbortSignal }) => {
|
||||
const queryString = params
|
||||
? `?${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;
|
||||
path: string;
|
||||
params?: Record<string, string>;
|
||||
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,
|
||||
refetchOnWindowFocus: false,
|
||||
gcTime: 0,
|
||||
enabled,
|
||||
queryFn: async ({ signal }) => {
|
||||
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>>)
|
||||
);
|
||||
},
|
||||
queryFn: createQueryFn({ path, params }),
|
||||
});
|
||||
|
||||
export const useSuspenseAPIQuery = <T>(
|
||||
path: string,
|
||||
params?: Record<string, string>
|
||||
) =>
|
||||
export const useSuspenseAPIQuery = <T>({ key, path, params }: QueryOptions) =>
|
||||
useSuspenseQuery<SuccessAPIResponse<T>>({
|
||||
queryKey: [path],
|
||||
queryKey: key ? [key] : [path, params],
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
gcTime: 0,
|
||||
queryFn: ({ signal }) => {
|
||||
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>>)
|
||||
);
|
||||
},
|
||||
queryFn: createQueryFn({ path, params }),
|
||||
});
|
||||
|
|
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";
|
||||
|
||||
export default function AlertsPage() {
|
||||
const { data } = useSuspenseAPIQuery<AlertingRulesMap>(`/rules`, {
|
||||
type: "alert",
|
||||
const { data } = useSuspenseAPIQuery<AlertingRulesMap>({
|
||||
path: `/rules`,
|
||||
params: {
|
||||
type: "alert",
|
||||
},
|
||||
});
|
||||
const [showAnnotations, setShowAnnotations] = useState(false);
|
||||
|
||||
|
|
|
@ -1,17 +1,14 @@
|
|||
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() {
|
||||
const {
|
||||
data: {
|
||||
data: { yaml },
|
||||
},
|
||||
} = useSuspenseQuery<{ data: { yaml: string } }>({
|
||||
queryKey: ["config"],
|
||||
queryFn: () => {
|
||||
return fetch("/api/v1/status/config").then((res) => res.json());
|
||||
},
|
||||
});
|
||||
} = useSuspenseAPIQuery<ConfigResult>({ path: `/status/config` });
|
||||
|
||||
return (
|
||||
<CodeHighlight
|
||||
code={yaml}
|
||||
|
|
|
@ -83,7 +83,9 @@ function sortData(
|
|||
}
|
||||
|
||||
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]) => ({
|
||||
flag,
|
||||
|
|
|
@ -28,7 +28,7 @@ const healthBadgeClass = (state: string) => {
|
|||
};
|
||||
|
||||
export default function RulesPage() {
|
||||
const { data } = useSuspenseAPIQuery<RulesMap>(`/rules`);
|
||||
const { data } = useSuspenseAPIQuery<RulesMap>({ path: `/rules` });
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -28,10 +28,12 @@ const statusConfig: Record<
|
|||
};
|
||||
|
||||
export default function StatusPage() {
|
||||
const { data: buildinfo } =
|
||||
useSuspenseAPIQuery<Record<string, string>>(`/status/buildinfo`);
|
||||
const { data: runtimeinfo } =
|
||||
useSuspenseAPIQuery<Record<string, string>>(`/status/runtimeinfo`);
|
||||
const { data: buildinfo } = useSuspenseAPIQuery<Record<string, string>>({
|
||||
path: `/status/buildinfo`,
|
||||
});
|
||||
const { data: runtimeinfo } = useSuspenseAPIQuery<Record<string, string>>({
|
||||
path: `/status/runtimeinfo`,
|
||||
});
|
||||
|
||||
return (
|
||||
<Stack gap="lg" maw={1000} mx="auto" mt="lg">
|
||||
|
|
|
@ -13,7 +13,7 @@ export default function TSDBStatusPage() {
|
|||
seriesCountByLabelValuePair,
|
||||
},
|
||||
},
|
||||
} = useSuspenseAPIQuery<TSDBMap>(`/status/tsdb`);
|
||||
} = useSuspenseAPIQuery<TSDBMap>({ path: `/status/tsdb` });
|
||||
|
||||
const unixToTime = (unix: number): string => {
|
||||
try {
|
||||
|
|
|
@ -81,11 +81,6 @@ const DataTable: FC<TableProps> = ({ expr, evalTime, retriggerIdx }) => {
|
|||
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;
|
||||
|
||||
if (result.length === 0) {
|
||||
|
|
|
@ -148,10 +148,6 @@ const ExpressionInput: FC<ExpressionInputProps> = ({
|
|||
}
|
||||
|
||||
if (formatResult) {
|
||||
if (formatResult.status !== "success") {
|
||||
// TODO: Remove this case and handle it in useAPIQuery instead!
|
||||
return;
|
||||
}
|
||||
setExpr(formatResult.data);
|
||||
notifications.show({
|
||||
color: "green",
|
||||
|
@ -222,10 +218,7 @@ const ExpressionInput: FC<ExpressionInputProps> = ({
|
|||
}
|
||||
onClick={() => formatQuery()}
|
||||
disabled={
|
||||
isFormatting ||
|
||||
expr === "" ||
|
||||
(formatResult?.status === "success" &&
|
||||
expr === formatResult.data)
|
||||
isFormatting || expr === "" || expr === formatResult?.data
|
||||
}
|
||||
>
|
||||
Format expression
|
||||
|
|
Loading…
Reference in a new issue