mirror of
https://github.com/prometheus/prometheus.git
synced 2025-03-05 20:59:13 -08:00
Show label stats in tree view
Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
parent
331a8e1d6c
commit
2d9f79f59e
|
@ -7,9 +7,18 @@ import {
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import ASTNode, { nodeType } from "../../promql/ast";
|
import ASTNode, { nodeType } from "../../promql/ast";
|
||||||
import { getNodeChildren } from "../../promql/utils";
|
import { escapeString, getNodeChildren } from "../../promql/utils";
|
||||||
import { formatNode } from "../../promql/format";
|
import { formatNode } from "../../promql/format";
|
||||||
import { Box, CSSProperties, Group, Loader, Text } from "@mantine/core";
|
import {
|
||||||
|
Box,
|
||||||
|
Code,
|
||||||
|
CSSProperties,
|
||||||
|
Group,
|
||||||
|
List,
|
||||||
|
Loader,
|
||||||
|
Text,
|
||||||
|
Tooltip,
|
||||||
|
} from "@mantine/core";
|
||||||
import { useAPIQuery } from "../../api/api";
|
import { useAPIQuery } from "../../api/api";
|
||||||
import {
|
import {
|
||||||
InstantQueryResult,
|
InstantQueryResult,
|
||||||
|
@ -21,8 +30,10 @@ import { IconPointFilled } from "@tabler/icons-react";
|
||||||
import classes from "./TreeNode.module.css";
|
import classes from "./TreeNode.module.css";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useId } from "@mantine/hooks";
|
import { useId } from "@mantine/hooks";
|
||||||
|
import { functionSignatures } from "../../promql/functionSignatures";
|
||||||
|
|
||||||
const nodeIndent = 20;
|
const nodeIndent = 20;
|
||||||
|
const maxLabels = 10;
|
||||||
|
|
||||||
type NodeState = "waiting" | "running" | "error" | "success";
|
type NodeState = "waiting" | "running" | "error" | "success";
|
||||||
|
|
||||||
|
@ -68,12 +79,12 @@ const TreeNode: FC<{
|
||||||
const [responseTime, setResponseTime] = useState<number>(0);
|
const [responseTime, setResponseTime] = useState<number>(0);
|
||||||
const [resultStats, setResultStats] = useState<{
|
const [resultStats, setResultStats] = useState<{
|
||||||
numSeries: number;
|
numSeries: number;
|
||||||
labelCardinalities: Record<string, number>;
|
|
||||||
labelExamples: Record<string, { value: string; count: number }[]>;
|
labelExamples: Record<string, { value: string; count: number }[]>;
|
||||||
|
sortedLabelCards: [string, number][];
|
||||||
}>({
|
}>({
|
||||||
numSeries: 0,
|
numSeries: 0,
|
||||||
labelCardinalities: {},
|
|
||||||
labelExamples: {},
|
labelExamples: {},
|
||||||
|
sortedLabelCards: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const children = getNodeChildren(node);
|
const children = getNodeChildren(node);
|
||||||
|
@ -86,11 +97,24 @@ const TreeNode: FC<{
|
||||||
[childStates]
|
[childStates]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Optimize range vector selector fetches to give us the info we're looking for
|
||||||
|
// more cheaply. E.g. 'foo[7w]' can be expensive to fully fetch, but wrapping it
|
||||||
|
// in 'last_over_time(foo[7w])' is cheaper and also gives us all the info we
|
||||||
|
// need (number of series and labels).
|
||||||
|
let queryNode = node;
|
||||||
|
if (queryNode.type === nodeType.matrixSelector) {
|
||||||
|
queryNode = {
|
||||||
|
type: nodeType.call,
|
||||||
|
func: functionSignatures["last_over_time"],
|
||||||
|
args: [node],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const { data, error, isFetching } = useAPIQuery<InstantQueryResult>({
|
const { data, error, isFetching } = useAPIQuery<InstantQueryResult>({
|
||||||
key: [useId()],
|
key: [useId()],
|
||||||
path: "/query",
|
path: "/query",
|
||||||
params: {
|
params: {
|
||||||
query: serializeNode(node),
|
query: serializeNode(queryNode),
|
||||||
},
|
},
|
||||||
recordResponseTime: setResponseTime,
|
recordResponseTime: setResponseTime,
|
||||||
enabled: mergedChildState === "success",
|
enabled: mergedChildState === "success",
|
||||||
|
@ -145,7 +169,7 @@ const TreeNode: FC<{
|
||||||
borderTopLeftRadius: undefined,
|
borderTopLeftRadius: undefined,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}, [parentRef, reverse, nodeRef]);
|
}, [parentRef, reverse, nodeRef, setConnectorStyle]);
|
||||||
|
|
||||||
// Update the node info state based on the query result.
|
// Update the node info state based on the query result.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -166,17 +190,12 @@ const TreeNode: FC<{
|
||||||
result.forEach((s: InstantSample | RangeSamples) => {
|
result.forEach((s: InstantSample | RangeSamples) => {
|
||||||
Object.entries(s.metric).forEach(([ln, lv]) => {
|
Object.entries(s.metric).forEach(([ln, lv]) => {
|
||||||
// TODO: If we ever want to include __name__ here again, we cannot use the
|
// TODO: If we ever want to include __name__ here again, we cannot use the
|
||||||
// count_over_time(foo[7d]) optimization since that removes the metric name.
|
// last_over_time(foo[7d]) optimization since that removes the metric name.
|
||||||
if (ln !== "__name__") {
|
if (ln !== "__name__") {
|
||||||
if (!labelValuesByName.hasOwnProperty(ln)) {
|
if (!labelValuesByName[ln]) {
|
||||||
labelValuesByName[ln] = { [lv]: 1 };
|
labelValuesByName[ln] = {};
|
||||||
} else {
|
|
||||||
if (!labelValuesByName[ln].hasOwnProperty(lv)) {
|
|
||||||
labelValuesByName[ln][lv] = 1;
|
|
||||||
} else {
|
|
||||||
labelValuesByName[ln][lv]++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
labelValuesByName[ln][lv] = (labelValuesByName[ln][lv] || 0) + 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -196,7 +215,9 @@ const TreeNode: FC<{
|
||||||
|
|
||||||
setResultStats({
|
setResultStats({
|
||||||
numSeries: resultSeries,
|
numSeries: resultSeries,
|
||||||
labelCardinalities,
|
sortedLabelCards: Object.entries(labelCardinalities).sort(
|
||||||
|
(a, b) => b[1] - a[1]
|
||||||
|
),
|
||||||
labelExamples,
|
labelExamples,
|
||||||
});
|
});
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
@ -204,7 +225,7 @@ const TreeNode: FC<{
|
||||||
const innerNode = (
|
const innerNode = (
|
||||||
<Group
|
<Group
|
||||||
w="fit-content"
|
w="fit-content"
|
||||||
gap="sm"
|
gap="lg"
|
||||||
my="sm"
|
my="sm"
|
||||||
wrap="nowrap"
|
wrap="nowrap"
|
||||||
pos="relative"
|
pos="relative"
|
||||||
|
@ -260,24 +281,67 @@ const TreeNode: FC<{
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
) : (
|
) : (
|
||||||
<Text c="dimmed" fz="xs">
|
<Group gap={0} wrap="nowrap">
|
||||||
{resultStats.numSeries} result{resultStats.numSeries !== 1 && "s"} –{" "}
|
<Text c="dimmed" fz="xs" style={{ whiteSpace: "nowrap" }}>
|
||||||
{responseTime}ms
|
{resultStats.numSeries} result{resultStats.numSeries !== 1 && "s"}
|
||||||
{/* {resultStats.numSeries > 0 && (
|
–
|
||||||
<>
|
{responseTime}ms –
|
||||||
{labelNames.length > 0 ? (
|
</Text>
|
||||||
labelNames.map((v, idx) => (
|
<Group gap="xs" wrap="nowrap">
|
||||||
<React.Fragment key={idx}>
|
{resultStats.sortedLabelCards
|
||||||
{idx !== 0 && ", "}
|
.slice(0, maxLabels)
|
||||||
{v}
|
.map(([ln, cnt]) => (
|
||||||
</React.Fragment>
|
<Tooltip
|
||||||
))
|
key={ln}
|
||||||
) : (
|
position="bottom"
|
||||||
<>no labels</>
|
withArrow
|
||||||
)}
|
color="dark.6"
|
||||||
</>
|
label={
|
||||||
)} */}
|
<Box p="xs">
|
||||||
</Text>
|
<List fz="xs">
|
||||||
|
{resultStats.labelExamples[ln].map(
|
||||||
|
({ value, count }) => (
|
||||||
|
<List.Item key={value} py={1}>
|
||||||
|
<Code c="red.3" bg="gray.7">
|
||||||
|
{escapeString(value)}
|
||||||
|
</Code>{" "}
|
||||||
|
({count}
|
||||||
|
x)
|
||||||
|
</List.Item>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
{cnt > 5 && <li>...</li>}
|
||||||
|
</List>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span style={{ cursor: "pointer", whiteSpace: "nowrap" }}>
|
||||||
|
<Text
|
||||||
|
component="span"
|
||||||
|
fz="xs"
|
||||||
|
className="promql-code promql-label-name"
|
||||||
|
c="light-dark(var(--mantine-color-green-9), var(--mantine-color-green-6))"
|
||||||
|
>
|
||||||
|
{ln}
|
||||||
|
</Text>
|
||||||
|
<Text component="span" fz="xs" c="dimmed">
|
||||||
|
: {cnt}
|
||||||
|
</Text>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
{resultStats.sortedLabelCards.length > maxLabels ? (
|
||||||
|
<Text
|
||||||
|
component="span"
|
||||||
|
c="dimmed"
|
||||||
|
fz="xs"
|
||||||
|
style={{ whiteSpace: "nowrap" }}
|
||||||
|
>
|
||||||
|
...{resultStats.sortedLabelCards.length - maxLabels} more...
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue