mirror of
https://github.com/prometheus/prometheus.git
synced 2025-02-02 08:31:11 -08:00
Complete building tree view and implement "Explain" tab
Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
parent
b75a12b52f
commit
5fd860f806
109
web/ui/mantine-ui/src/pages/query/ExplainViews/Aggregation.tsx
Normal file
109
web/ui/mantine-ui/src/pages/query/ExplainViews/Aggregation.tsx
Normal file
|
@ -0,0 +1,109 @@
|
|||
import { FC } from "react";
|
||||
import ASTNode, { Aggregation, aggregationType } from "../../../promql/ast";
|
||||
import { labelNameList } from "../../../promql/format";
|
||||
import { parsePrometheusFloat } from "../../../lib/formatFloatValue";
|
||||
import { Card, Text } from "@mantine/core";
|
||||
|
||||
const describeAggregationType = (
|
||||
aggrType: aggregationType,
|
||||
param: ASTNode | null
|
||||
) => {
|
||||
switch (aggrType) {
|
||||
case "sum":
|
||||
return "sums over the sample values of the input series";
|
||||
case "min":
|
||||
return "takes the minimum of the sample values of the input series";
|
||||
case "max":
|
||||
return "takes the maximum of the sample values of the input series";
|
||||
case "avg":
|
||||
return "calculates the average of the sample values of the input series";
|
||||
case "stddev":
|
||||
return "calculates the population standard deviation of the sample values of the input series";
|
||||
case "stdvar":
|
||||
return "calculates the population standard variation of the sample values of the input series";
|
||||
case "count":
|
||||
return "counts the number of input series";
|
||||
case "group":
|
||||
return "groups the input series by the supplied grouping labels, while setting the sample value to 1";
|
||||
case "count_values":
|
||||
if (param === null) {
|
||||
throw new Error(
|
||||
"encountered count_values() node without label parameter"
|
||||
);
|
||||
}
|
||||
if (param.type !== "stringLiteral") {
|
||||
throw new Error(
|
||||
"encountered count_values() node without string literal label parameter"
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
outputs one time series for each unique sample value in the input
|
||||
series (each counting the number of occurrences of that value and
|
||||
indicating the original value in the {labelNameList([param.val])}{" "}
|
||||
label)
|
||||
</>
|
||||
);
|
||||
case "bottomk":
|
||||
return "returns the bottom K series by value";
|
||||
case "topk":
|
||||
return "returns the top K series by value";
|
||||
case "quantile":
|
||||
if (param === null) {
|
||||
throw new Error(
|
||||
"encountered quantile() node without quantile parameter"
|
||||
);
|
||||
}
|
||||
if (param.type === "numberLiteral") {
|
||||
return `calculates the ${param.val}th quantile (${
|
||||
parsePrometheusFloat(param.val) * 100
|
||||
}th percentile) over the sample values of the input series`;
|
||||
}
|
||||
return "calculates a quantile over the sample values of the input series";
|
||||
|
||||
case "limitk":
|
||||
return "limits the output to K series";
|
||||
case "limit_ratio":
|
||||
return "limits the output to a ratio of the input series";
|
||||
default:
|
||||
throw new Error(`invalid aggregation type ${aggrType}`);
|
||||
}
|
||||
};
|
||||
|
||||
const describeAggregationGrouping = (grouping: string[], without: boolean) => {
|
||||
if (without) {
|
||||
return (
|
||||
<>aggregating away the [{labelNameList(grouping)}] label dimensions</>
|
||||
);
|
||||
}
|
||||
|
||||
if (grouping.length === 1) {
|
||||
return <>grouped by their {labelNameList(grouping)} label dimension</>;
|
||||
}
|
||||
|
||||
if (grouping.length > 1) {
|
||||
return <>grouped by their [{labelNameList(grouping)}] label dimensions</>;
|
||||
}
|
||||
|
||||
return "aggregating away any label dimensions";
|
||||
};
|
||||
|
||||
interface AggregationExplainViewProps {
|
||||
node: Aggregation;
|
||||
}
|
||||
|
||||
const AggregationExplainView: FC<AggregationExplainViewProps> = ({ node }) => {
|
||||
return (
|
||||
<Card withBorder>
|
||||
<Text fz="lg" fw={600} mb="md">
|
||||
Aggregation
|
||||
</Text>
|
||||
<Text fz="sm">
|
||||
This node {describeAggregationType(node.op, node.param)},{" "}
|
||||
{describeAggregationGrouping(node.grouping, node.without)}.
|
||||
</Text>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default AggregationExplainView;
|
|
@ -0,0 +1,106 @@
|
|||
import { FC } from "react";
|
||||
import { BinaryExpr } from "../../../../promql/ast";
|
||||
import serializeNode from "../../../../promql/serialize";
|
||||
import VectorScalarBinaryExprExplainView from "./VectorScalar";
|
||||
import VectorVectorBinaryExprExplainView from "./VectorVector";
|
||||
import ScalarScalarBinaryExprExplainView from "./ScalarScalar";
|
||||
import { nodeValueType } from "../../../../promql/utils";
|
||||
import { useSuspenseAPIQuery } from "../../../../api/api";
|
||||
import { InstantQueryResult } from "../../../../api/responseTypes/query";
|
||||
import { Card, Text } from "@mantine/core";
|
||||
|
||||
interface BinaryExprExplainViewProps {
|
||||
node: BinaryExpr;
|
||||
}
|
||||
|
||||
const BinaryExprExplainView: FC<BinaryExprExplainViewProps> = ({ node }) => {
|
||||
const { data: lhs } = useSuspenseAPIQuery<InstantQueryResult>({
|
||||
path: `/query`,
|
||||
params: {
|
||||
query: serializeNode(node.lhs),
|
||||
},
|
||||
});
|
||||
const { data: rhs } = useSuspenseAPIQuery<InstantQueryResult>({
|
||||
path: `/query`,
|
||||
params: {
|
||||
query: serializeNode(node.rhs),
|
||||
},
|
||||
});
|
||||
|
||||
if (
|
||||
lhs.data.resultType !== nodeValueType(node.lhs) ||
|
||||
rhs.data.resultType !== nodeValueType(node.rhs)
|
||||
) {
|
||||
// This can happen for a brief transitionary render when "node" has changed, but "lhs" and "rhs"
|
||||
// haven't switched back to loading yet (leading to a crash in e.g. the vector-vector explain view).
|
||||
return null;
|
||||
}
|
||||
|
||||
// Scalar-scalar binops.
|
||||
if (lhs.data.resultType === "scalar" && rhs.data.resultType === "scalar") {
|
||||
return (
|
||||
<Card withBorder>
|
||||
<Text fz="lg" fw={600} mb="md">
|
||||
Scalar-to-scalar binary operation
|
||||
</Text>
|
||||
<ScalarScalarBinaryExprExplainView
|
||||
node={node}
|
||||
lhs={lhs.data.result}
|
||||
rhs={rhs.data.result}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Vector-scalar binops.
|
||||
if (lhs.data.resultType === "scalar" && rhs.data.resultType === "vector") {
|
||||
return (
|
||||
<Card withBorder>
|
||||
<Text fz="lg" fw={600} mb="md">
|
||||
Scalar-to-vector binary operation
|
||||
</Text>
|
||||
<VectorScalarBinaryExprExplainView
|
||||
node={node}
|
||||
vector={rhs.data.result}
|
||||
scalar={lhs.data.result}
|
||||
scalarLeft={true}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
if (lhs.data.resultType === "vector" && rhs.data.resultType === "scalar") {
|
||||
return (
|
||||
<Card withBorder>
|
||||
<Text fz="lg" fw={600} mb="md">
|
||||
Vector-to-scalar binary operation
|
||||
</Text>
|
||||
<VectorScalarBinaryExprExplainView
|
||||
node={node}
|
||||
scalar={rhs.data.result}
|
||||
vector={lhs.data.result}
|
||||
scalarLeft={false}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Vector-vector binops.
|
||||
if (lhs.data.resultType === "vector" && rhs.data.resultType === "vector") {
|
||||
return (
|
||||
<Card withBorder>
|
||||
<Text fz="lg" fw={600} mb="md">
|
||||
Vector-to-vector binary operation
|
||||
</Text>
|
||||
<VectorVectorBinaryExprExplainView
|
||||
node={node}
|
||||
lhs={lhs.data.result}
|
||||
rhs={rhs.data.result}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error("invalid binary operator argument types");
|
||||
};
|
||||
|
||||
export default BinaryExprExplainView;
|
|
@ -0,0 +1,54 @@
|
|||
import { FC } from "react";
|
||||
import { BinaryExpr } from "../../../../promql/ast";
|
||||
import { scalarBinOp } from "../../../../promql/binOp";
|
||||
import { Table } from "@mantine/core";
|
||||
import { SampleValue } from "../../../../api/responseTypes/query";
|
||||
import {
|
||||
formatPrometheusFloat,
|
||||
parsePrometheusFloat,
|
||||
} from "../../../../lib/formatFloatValue";
|
||||
|
||||
interface ScalarScalarBinaryExprExplainViewProps {
|
||||
node: BinaryExpr;
|
||||
lhs: SampleValue;
|
||||
rhs: SampleValue;
|
||||
}
|
||||
|
||||
const ScalarScalarBinaryExprExplainView: FC<
|
||||
ScalarScalarBinaryExprExplainViewProps
|
||||
> = ({ node, lhs, rhs }) => {
|
||||
const [lhsVal, rhsVal] = [
|
||||
parsePrometheusFloat(lhs[1]),
|
||||
parsePrometheusFloat(rhs[1]),
|
||||
];
|
||||
|
||||
return (
|
||||
<Table withColumnBorders withTableBorder>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Left value</Table.Th>
|
||||
<Table.Th>Operator</Table.Th>
|
||||
<Table.Th>Right value</Table.Th>
|
||||
<Table.Th></Table.Th>
|
||||
<Table.Th>Result</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
<Table.Tr>
|
||||
<Table.Td className="number-cell">{lhs[1]}</Table.Td>
|
||||
<Table.Td className="op-cell">
|
||||
{node.op}
|
||||
{node.bool && " bool"}
|
||||
</Table.Td>
|
||||
<Table.Td className="number-cell">{rhs[1]}</Table.Td>
|
||||
<Table.Td className="op-cell">=</Table.Td>
|
||||
<Table.Td className="number-cell">
|
||||
{formatPrometheusFloat(scalarBinOp(node.op, lhsVal, rhsVal))}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScalarScalarBinaryExprExplainView;
|
|
@ -0,0 +1,104 @@
|
|||
import { FC } from "react";
|
||||
import { BinaryExpr } from "../../../../promql/ast";
|
||||
// import SeriesName from '../../../../utils/SeriesName';
|
||||
import { isComparisonOperator } from "../../../../promql/utils";
|
||||
import { vectorElemBinop } from "../../../../promql/binOp";
|
||||
import {
|
||||
InstantSample,
|
||||
SampleValue,
|
||||
} from "../../../../api/responseTypes/query";
|
||||
import { Alert, Table, Text } from "@mantine/core";
|
||||
import {
|
||||
formatPrometheusFloat,
|
||||
parsePrometheusFloat,
|
||||
} from "../../../../lib/formatFloatValue";
|
||||
import SeriesName from "../../SeriesName";
|
||||
|
||||
interface VectorScalarBinaryExprExplainViewProps {
|
||||
node: BinaryExpr;
|
||||
scalar: SampleValue;
|
||||
vector: InstantSample[];
|
||||
scalarLeft: boolean;
|
||||
}
|
||||
|
||||
const VectorScalarBinaryExprExplainView: FC<
|
||||
VectorScalarBinaryExprExplainViewProps
|
||||
> = ({ node, scalar, vector, scalarLeft }) => {
|
||||
if (vector.length === 0) {
|
||||
return (
|
||||
<Alert>
|
||||
One side of the binary operation produces 0 results, no matching
|
||||
information shown.
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Table withTableBorder withRowBorders withColumnBorders fz="xs">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
{!scalarLeft && <Table.Th>Left labels</Table.Th>}
|
||||
<Table.Th>Left value</Table.Th>
|
||||
<Table.Th>Operator</Table.Th>
|
||||
{scalarLeft && <Table.Th>Right labels</Table.Th>}
|
||||
<Table.Th>Right value</Table.Th>
|
||||
<Table.Th></Table.Th>
|
||||
<Table.Th>Result</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{vector.map((sample: InstantSample, idx) => {
|
||||
if (!sample.value) {
|
||||
// TODO: Handle native histograms or show a better error message.
|
||||
throw new Error("Native histograms are not supported yet");
|
||||
}
|
||||
|
||||
const vecVal = parsePrometheusFloat(sample.value[1]);
|
||||
const scalVal = parsePrometheusFloat(scalar[1]);
|
||||
|
||||
let { value, keep } = scalarLeft
|
||||
? vectorElemBinop(node.op, scalVal, vecVal)
|
||||
: vectorElemBinop(node.op, vecVal, scalVal);
|
||||
if (isComparisonOperator(node.op) && scalarLeft) {
|
||||
value = vecVal;
|
||||
}
|
||||
if (node.bool) {
|
||||
value = Number(keep);
|
||||
keep = true;
|
||||
}
|
||||
|
||||
const scalarCell = <Table.Td ta="right">{scalar[1]}</Table.Td>;
|
||||
const vectorCells = (
|
||||
<>
|
||||
<Table.Td>
|
||||
<SeriesName labels={sample.metric} format={true} />
|
||||
</Table.Td>
|
||||
<Table.Td ta="right">{sample.value[1]}</Table.Td>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Table.Tr key={idx}>
|
||||
{scalarLeft ? scalarCell : vectorCells}
|
||||
<Table.Td ta="center">
|
||||
{node.op}
|
||||
{node.bool && " bool"}
|
||||
</Table.Td>
|
||||
{scalarLeft ? vectorCells : scalarCell}
|
||||
<Table.Td ta="center">=</Table.Td>
|
||||
<Table.Td ta="right">
|
||||
{keep ? (
|
||||
formatPrometheusFloat(value)
|
||||
) : (
|
||||
<Text c="dimmed">dropped</Text>
|
||||
)}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
);
|
||||
})}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
export default VectorScalarBinaryExprExplainView;
|
|
@ -0,0 +1,686 @@
|
|||
import React, { FC, useState } from "react";
|
||||
import { BinaryExpr, vectorMatchCardinality } from "../../../../promql/ast";
|
||||
import { InstantSample, Metric } from "../../../../api/responseTypes/query";
|
||||
import { isComparisonOperator, isSetOperator } from "../../../../promql/utils";
|
||||
import {
|
||||
VectorMatchError,
|
||||
BinOpMatchGroup,
|
||||
MatchErrorType,
|
||||
computeVectorVectorBinOp,
|
||||
filteredSampleValue,
|
||||
} from "../../../../promql/binOp";
|
||||
import { formatNode, labelNameList } from "../../../../promql/format";
|
||||
import {
|
||||
Alert,
|
||||
Anchor,
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
List,
|
||||
Switch,
|
||||
Table,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
import { useLocalStorage } from "@mantine/hooks";
|
||||
import { IconAlertTriangle } from "@tabler/icons-react";
|
||||
import SeriesName from "../../SeriesName";
|
||||
|
||||
// We use this color pool for two purposes:
|
||||
//
|
||||
// 1. To distinguish different match groups from each other.
|
||||
// 2. To distinguish multiple series within one match group from each other.
|
||||
const colorPool = [
|
||||
"#1f77b4",
|
||||
"#ff7f0e",
|
||||
"#2ca02c",
|
||||
"#d62728",
|
||||
"#9467bd",
|
||||
"#8c564b",
|
||||
"#e377c2",
|
||||
"#7f7f7f",
|
||||
"#bcbd22",
|
||||
"#17becf",
|
||||
"#393b79",
|
||||
"#637939",
|
||||
"#8c6d31",
|
||||
"#843c39",
|
||||
"#d6616b",
|
||||
"#7b4173",
|
||||
"#ce6dbd",
|
||||
"#9c9ede",
|
||||
"#c5b0d5",
|
||||
"#c49c94",
|
||||
"#f7b6d2",
|
||||
"#c7c7c7",
|
||||
"#dbdb8d",
|
||||
"#9edae5",
|
||||
"#393b79",
|
||||
"#637939",
|
||||
"#8c6d31",
|
||||
"#843c39",
|
||||
"#d6616b",
|
||||
"#7b4173",
|
||||
"#ce6dbd",
|
||||
"#9c9ede",
|
||||
"#c5b0d5",
|
||||
"#c49c94",
|
||||
"#f7b6d2",
|
||||
"#c7c7c7",
|
||||
"#dbdb8d",
|
||||
"#9edae5",
|
||||
"#17becf",
|
||||
"#393b79",
|
||||
"#637939",
|
||||
"#8c6d31",
|
||||
"#843c39",
|
||||
"#d6616b",
|
||||
"#7b4173",
|
||||
"#ce6dbd",
|
||||
"#9c9ede",
|
||||
"#c5b0d5",
|
||||
"#c49c94",
|
||||
"#f7b6d2",
|
||||
];
|
||||
|
||||
const rhsColorOffset = colorPool.length / 2 + 3;
|
||||
const colorForIndex = (idx: number, offset?: number) =>
|
||||
`${colorPool[(idx + (offset || 0)) % colorPool.length]}80`;
|
||||
|
||||
const seriesSwatch = (color: string) => (
|
||||
<Box
|
||||
display="inline-block"
|
||||
w={12}
|
||||
h={12}
|
||||
bg={color}
|
||||
style={{
|
||||
borderRadius: 2,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
interface VectorVectorBinaryExprExplainViewProps {
|
||||
node: BinaryExpr;
|
||||
lhs: InstantSample[];
|
||||
rhs: InstantSample[];
|
||||
}
|
||||
|
||||
const noMatchLabels = (
|
||||
metric: Metric,
|
||||
on: boolean,
|
||||
labels: string[]
|
||||
): Metric => {
|
||||
const result: Metric = {};
|
||||
for (const name in metric) {
|
||||
if (!(labels.includes(name) === on && (on || name !== "__name__"))) {
|
||||
result[name] = metric[name];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const explanationText = (node: BinaryExpr): React.ReactNode => {
|
||||
const matching = node.matching!;
|
||||
const [oneSide, manySide] =
|
||||
matching.card === vectorMatchCardinality.oneToMany
|
||||
? ["left", "right"]
|
||||
: ["right", "left"];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text size="sm">
|
||||
{isComparisonOperator(node.op) ? (
|
||||
<>
|
||||
This node filters the series from the left-hand side based on the
|
||||
result of a "
|
||||
<span className="promql-code promql-operator">{node.op}</span>"
|
||||
comparison with matching series from the right-hand side.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
This node calculates the result of applying the "
|
||||
<span className="promql-code promql-operator">{node.op}</span>"
|
||||
operator between the sample values of matching series from two sets
|
||||
of time series.
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
<List my="md" fz="sm" withPadding>
|
||||
{(matching.labels.length > 0 || matching.on) &&
|
||||
(matching.on ? (
|
||||
<List.Item>
|
||||
<span className="promql-code promql-keyword">on</span>(
|
||||
{labelNameList(matching.labels)}):{" "}
|
||||
{matching.labels.length > 0 ? (
|
||||
<>
|
||||
series on both sides are matched on the labels{" "}
|
||||
{labelNameList(matching.labels)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
all series from one side are matched to all series on the
|
||||
other side.
|
||||
</>
|
||||
)}
|
||||
</List.Item>
|
||||
) : (
|
||||
<List.Item>
|
||||
<span className="promql-code promql-keyword">ignoring</span>(
|
||||
{labelNameList(matching.labels)}): series on both sides are
|
||||
matched on all of their labels, except{" "}
|
||||
{labelNameList(matching.labels)}.
|
||||
</List.Item>
|
||||
))}
|
||||
{matching.card === vectorMatchCardinality.oneToOne ? (
|
||||
<List.Item>
|
||||
One-to-one match. Each series from the left-hand side is allowed to
|
||||
match with at most one series on the right-hand side, and vice
|
||||
versa.
|
||||
</List.Item>
|
||||
) : (
|
||||
<List.Item>
|
||||
<span className="promql-code promql-keyword">
|
||||
group_{manySide}({labelNameList(matching.include)})
|
||||
</span>
|
||||
: {matching.card} match. Each series from the {oneSide}-hand side is
|
||||
allowed to match with multiple series from the {manySide}-hand side.
|
||||
{matching.include.length !== 0 && (
|
||||
<>
|
||||
{" "}
|
||||
Any {labelNameList(matching.include)} labels found on the{" "}
|
||||
{oneSide}-hand side are propagated into the result, in addition
|
||||
to the match group's labels.
|
||||
</>
|
||||
)}
|
||||
</List.Item>
|
||||
)}
|
||||
{node.bool && (
|
||||
<List.Item>
|
||||
<span className="promql-code promql-keyword">bool</span>: Instead of
|
||||
filtering series based on the outcome of the comparison for matched
|
||||
series, keep all series, but return the comparison outcome as a
|
||||
boolean <span className="promql-code promql-number">0</span> or{" "}
|
||||
<span className="promql-code promql-number">1</span> sample value.
|
||||
</List.Item>
|
||||
)}
|
||||
</List>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const explainError = (
|
||||
binOp: BinaryExpr,
|
||||
_mg: BinOpMatchGroup,
|
||||
err: VectorMatchError
|
||||
) => {
|
||||
const fixes = (
|
||||
<>
|
||||
<Text size="sm">
|
||||
<strong>Possible fixes:</strong>
|
||||
</Text>
|
||||
<List withPadding my="md" fz="sm">
|
||||
{err.type === MatchErrorType.multipleMatchesForOneToOneMatching && (
|
||||
<List.Item>
|
||||
<Text size="sm">
|
||||
<strong>
|
||||
Allow {err.dupeSide === "left" ? "many-to-one" : "one-to-many"}{" "}
|
||||
matching
|
||||
</strong>
|
||||
: If you want to allow{" "}
|
||||
{err.dupeSide === "left" ? "many-to-one" : "one-to-many"}{" "}
|
||||
matching, you need to explicitly request it by adding a{" "}
|
||||
<span className="promql-code promql-keyword">
|
||||
group_{err.dupeSide}()
|
||||
</span>{" "}
|
||||
modifier to the operator:
|
||||
</Text>
|
||||
<Text size="sm" ta="center" my="md">
|
||||
{formatNode(
|
||||
{
|
||||
...binOp,
|
||||
matching: {
|
||||
...(binOp.matching
|
||||
? binOp.matching
|
||||
: { labels: [], on: false, include: [] }),
|
||||
card:
|
||||
err.dupeSide === "left"
|
||||
? vectorMatchCardinality.manyToOne
|
||||
: vectorMatchCardinality.oneToMany,
|
||||
},
|
||||
},
|
||||
true,
|
||||
1
|
||||
)}
|
||||
</Text>
|
||||
</List.Item>
|
||||
)}
|
||||
<List.Item>
|
||||
<strong>Update your matching parameters:</strong> Consider including
|
||||
more differentiating labels in your matching modifiers (via{" "}
|
||||
<span className="promql-code promql-keyword">on()</span> /{" "}
|
||||
<span className="promql-code promql-keyword">ignoring()</span>) to
|
||||
split multiple series into distinct match groups.
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<strong>Aggregate the input:</strong> Consider aggregating away the
|
||||
extra labels that create multiple series per group before applying the
|
||||
binary operation.
|
||||
</List.Item>
|
||||
</List>
|
||||
</>
|
||||
);
|
||||
|
||||
switch (err.type) {
|
||||
case MatchErrorType.multipleMatchesForOneToOneMatching:
|
||||
return (
|
||||
<>
|
||||
<Text size="sm">
|
||||
Binary operators only allow <strong>one-to-one</strong> matching by
|
||||
default, but we found{" "}
|
||||
<strong>multiple series on the {err.dupeSide} side</strong> for this
|
||||
match group.
|
||||
</Text>
|
||||
{fixes}
|
||||
</>
|
||||
);
|
||||
case MatchErrorType.multipleMatchesOnBothSides:
|
||||
return (
|
||||
<>
|
||||
<Text size="sm">
|
||||
We found <strong>multiple series on both sides</strong> for this
|
||||
match group. Since <strong>many-to-many matching</strong> is not
|
||||
supported, you need to ensure that at least one of the sides only
|
||||
yields a single series.
|
||||
</Text>
|
||||
{fixes}
|
||||
</>
|
||||
);
|
||||
case MatchErrorType.multipleMatchesOnOneSide: {
|
||||
const [oneSide, manySide] =
|
||||
binOp.matching!.card === vectorMatchCardinality.oneToMany
|
||||
? ["left", "right"]
|
||||
: ["right", "left"];
|
||||
return (
|
||||
<>
|
||||
<Text size="sm">
|
||||
You requested{" "}
|
||||
<strong>
|
||||
{oneSide === "right" ? "many-to-one" : "one-to-many"} matching
|
||||
</strong>{" "}
|
||||
via{" "}
|
||||
<span className="promql-code promql-keyword">
|
||||
group_{manySide}()
|
||||
</span>
|
||||
, but we also found{" "}
|
||||
<strong>multiple series on the {oneSide} side</strong> of the match
|
||||
group. Make sure that the {oneSide} side only contains a single
|
||||
series.
|
||||
</Text>
|
||||
{fixes}
|
||||
</>
|
||||
);
|
||||
}
|
||||
default:
|
||||
throw new Error("unknown match error");
|
||||
}
|
||||
};
|
||||
|
||||
const VectorVectorBinaryExprExplainView: FC<
|
||||
VectorVectorBinaryExprExplainViewProps
|
||||
> = ({ node, lhs, rhs }) => {
|
||||
// TODO: Don't use Mantine's local storage as a one-off here.
|
||||
// const [allowLineBreaks, setAllowLineBreaks] = useLocalStorage<boolean>({
|
||||
// key: "queryPage.explain.binaryOperators.breakLongLines",
|
||||
// defaultValue: true,
|
||||
// });
|
||||
|
||||
const [showSampleValues, setShowSampleValues] = useLocalStorage<boolean>({
|
||||
key: "queryPage.explain.binaryOperators.showSampleValues",
|
||||
defaultValue: false,
|
||||
});
|
||||
|
||||
const [maxGroups, setMaxGroups] = useState<number | undefined>(100);
|
||||
const [maxSeriesPerGroup, setMaxSeriesPerGroup] = useState<
|
||||
number | undefined
|
||||
>(100);
|
||||
|
||||
const { matching } = node;
|
||||
if (matching === null) {
|
||||
// The parent should make sure to only pass in vector-vector binops that have their "matching" field filled out.
|
||||
throw new Error("missing matching parameters in vector-to-vector binop");
|
||||
}
|
||||
|
||||
const { groups: matchGroups, numGroups } = computeVectorVectorBinOp(
|
||||
node.op,
|
||||
matching,
|
||||
node.bool,
|
||||
lhs,
|
||||
rhs,
|
||||
{
|
||||
maxGroups: maxGroups,
|
||||
maxSeriesPerGroup: maxSeriesPerGroup,
|
||||
}
|
||||
);
|
||||
const errCount = Object.values(matchGroups).filter((mg) => mg.error).length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text size="sm">{explanationText(node)}</Text>
|
||||
|
||||
{!isSetOperator(node.op) && (
|
||||
<>
|
||||
<Group my="lg" justify="flex-end" gap="xl">
|
||||
{/* <Switch
|
||||
label="Break long lines"
|
||||
checked={allowLineBreaks}
|
||||
onChange={(event) =>
|
||||
setAllowLineBreaks(event.currentTarget.checked)
|
||||
}
|
||||
/> */}
|
||||
<Switch
|
||||
label="Show sample values"
|
||||
checked={showSampleValues}
|
||||
onChange={(event) =>
|
||||
setShowSampleValues(event.currentTarget.checked)
|
||||
}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{numGroups > Object.keys(matchGroups).length && (
|
||||
<Alert
|
||||
color="yellow"
|
||||
mb="md"
|
||||
icon={<IconAlertTriangle size={14} />}
|
||||
>
|
||||
Too many match groups to display, only showing{" "}
|
||||
{Object.keys(matchGroups).length} out of {numGroups} groups.
|
||||
<Anchor onClick={() => setMaxGroups(undefined)}>
|
||||
Show all groups
|
||||
</Anchor>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{errCount > 0 && (
|
||||
<Alert
|
||||
color="yellow"
|
||||
mb="md"
|
||||
icon={<IconAlertTriangle size={14} />}
|
||||
>
|
||||
Found matching issues in {errCount} match group
|
||||
{errCount > 1 ? "s" : ""}. See below for per-group error details.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Table fz="xs" withRowBorders={false}>
|
||||
<Table.Tbody>
|
||||
{Object.values(matchGroups).map((mg, mgIdx) => {
|
||||
const {
|
||||
groupLabels,
|
||||
lhs,
|
||||
lhsCount,
|
||||
rhs,
|
||||
rhsCount,
|
||||
result,
|
||||
error,
|
||||
} = mg;
|
||||
|
||||
const noLHSMatches = lhs.length === 0;
|
||||
const noRHSMatches = rhs.length === 0;
|
||||
|
||||
const groupColor = colorPool[mgIdx % colorPool.length];
|
||||
const noMatchesColor = "#e0e0e0";
|
||||
const lhsGroupColor = noLHSMatches
|
||||
? noMatchesColor
|
||||
: groupColor;
|
||||
const rhsGroupColor = noRHSMatches
|
||||
? noMatchesColor
|
||||
: groupColor;
|
||||
const resultGroupColor =
|
||||
noLHSMatches || noRHSMatches ? noMatchesColor : groupColor;
|
||||
|
||||
const matchGroupTitleRow = (color: string) => (
|
||||
<Table.Tr ta="center">
|
||||
<Table.Td
|
||||
colSpan={2}
|
||||
style={{ backgroundColor: `${color}25` }}
|
||||
>
|
||||
<SeriesName labels={groupLabels} format={true} />
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
);
|
||||
|
||||
const matchGroupTable = (
|
||||
series: InstantSample[],
|
||||
seriesCount: number,
|
||||
color: string,
|
||||
colorOffset?: number
|
||||
) => (
|
||||
<Box
|
||||
style={{ borderRadius: 3, border: `2px solid ${color}` }}
|
||||
>
|
||||
<Table fz="xs" withRowBorders={false}>
|
||||
<Table.Tbody>
|
||||
{series.length === 0 ? (
|
||||
<Table.Tr>
|
||||
<Table.Td
|
||||
bg="gray.0"
|
||||
ta="center"
|
||||
c="gray.6"
|
||||
py="md"
|
||||
fw="bold"
|
||||
>
|
||||
no matching series
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
) : (
|
||||
<>
|
||||
{matchGroupTitleRow(color)}
|
||||
{series.map((s, sIdx) => {
|
||||
if (s.value === undefined) {
|
||||
// TODO: Figure out how to handle native histograms.
|
||||
throw new Error(
|
||||
"Native histograms are not supported yet"
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Table.Tr key={sIdx}>
|
||||
<Table.Td>
|
||||
<Group wrap="nowrap" gap={7} align="center">
|
||||
{seriesSwatch(
|
||||
colorForIndex(sIdx, colorOffset)
|
||||
)}
|
||||
|
||||
<SeriesName
|
||||
labels={noMatchLabels(
|
||||
s.metric,
|
||||
matching.on,
|
||||
matching.labels
|
||||
)}
|
||||
format={true}
|
||||
/>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
{showSampleValues && (
|
||||
<Table.Td>{s.value[1]}</Table.Td>
|
||||
)}
|
||||
</Table.Tr>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
{seriesCount > series.length && (
|
||||
<Table.Tr>
|
||||
<Table.Td
|
||||
bg="gray.0"
|
||||
ta="center"
|
||||
c="gray.6"
|
||||
py="md"
|
||||
fw="bold"
|
||||
>
|
||||
{seriesCount - series.length} more series omitted
|
||||
–
|
||||
<Button
|
||||
size="sm"
|
||||
variant="link"
|
||||
onClick={() => setMaxSeriesPerGroup(undefined)}
|
||||
>
|
||||
show all
|
||||
</Button>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const lhsTable = matchGroupTable(lhs, lhsCount, lhsGroupColor);
|
||||
const rhsTable = matchGroupTable(
|
||||
rhs,
|
||||
rhsCount,
|
||||
rhsGroupColor,
|
||||
rhsColorOffset
|
||||
);
|
||||
|
||||
const resultTable = (
|
||||
<Box
|
||||
style={{
|
||||
borderRadius: 3,
|
||||
border: `2px solid ${resultGroupColor}`,
|
||||
}}
|
||||
>
|
||||
<Table fz="xs" withRowBorders={false}>
|
||||
<Table.Tbody>
|
||||
{noLHSMatches || noRHSMatches ? (
|
||||
<Table.Tr>
|
||||
<Table.Td
|
||||
bg="gray.0"
|
||||
ta="center"
|
||||
c="gray.6"
|
||||
py="md"
|
||||
fw="bold"
|
||||
>
|
||||
dropped
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
) : error !== null ? (
|
||||
<Table.Tr>
|
||||
<Table.Td
|
||||
bg="gray.0"
|
||||
ta="center"
|
||||
c="gray.6"
|
||||
py="md"
|
||||
fw="bold"
|
||||
>
|
||||
error, result omitted
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
) : (
|
||||
<>
|
||||
{result.map(({ sample, manySideIdx }, resIdx) => {
|
||||
if (sample.value === undefined) {
|
||||
// TODO: Figure out how to handle native histograms.
|
||||
throw new Error(
|
||||
"Native histograms are not supported yet"
|
||||
);
|
||||
}
|
||||
|
||||
const filtered =
|
||||
sample.value[1] === filteredSampleValue;
|
||||
const [lIdx, rIdx] =
|
||||
matching.card ===
|
||||
vectorMatchCardinality.oneToMany
|
||||
? [0, manySideIdx]
|
||||
: [manySideIdx, 0];
|
||||
|
||||
return (
|
||||
<Table.Tr key={resIdx}>
|
||||
<Table.Td
|
||||
style={{ opacity: filtered ? 0.5 : 1 }}
|
||||
title={
|
||||
filtered
|
||||
? "Series has been filtered by comparison operator"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Group wrap="nowrap" gap="xs">
|
||||
<Group wrap="nowrap" gap={0}>
|
||||
{seriesSwatch(colorForIndex(lIdx))}
|
||||
<span style={{ color: "#aaa" }}>–</span>
|
||||
{seriesSwatch(
|
||||
colorForIndex(rIdx, rhsColorOffset)
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<SeriesName
|
||||
labels={sample.metric}
|
||||
format={true}
|
||||
/>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
{showSampleValues && (
|
||||
<Table.Td className="number-cell">
|
||||
{filtered ? (
|
||||
<span style={{ color: "grey" }}>
|
||||
filtered
|
||||
</span>
|
||||
) : (
|
||||
<span>{sample.value[1]}</span>
|
||||
)}
|
||||
</Table.Td>
|
||||
)}
|
||||
</Table.Tr>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Fragment key={mgIdx}>
|
||||
{mgIdx !== 0 && <tr style={{ height: 30 }}></tr>}
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={5}>
|
||||
{error && (
|
||||
<Alert
|
||||
color="red"
|
||||
mb="md"
|
||||
title="Error in match group below"
|
||||
icon={<IconAlertTriangle size={14} />}
|
||||
>
|
||||
{explainError(node, mg, error)}
|
||||
</Alert>
|
||||
)}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
<Table.Tr>
|
||||
<Table.Td valign="middle" p={0}>
|
||||
{lhsTable}
|
||||
</Table.Td>
|
||||
<Table.Td ta="center">
|
||||
{node.op}
|
||||
{node.bool && " bool"}
|
||||
</Table.Td>
|
||||
<Table.Td valign="middle" p={0}>
|
||||
{rhsTable}
|
||||
</Table.Td>
|
||||
<Table.Td ta="center">=</Table.Td>
|
||||
<Table.Td valign="middle" p={0}>
|
||||
{resultTable}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default VectorVectorBinaryExprExplainView;
|
|
@ -0,0 +1,8 @@
|
|||
.funcDoc code {
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-gray-1),
|
||||
var(--mantine-color-dark-5)
|
||||
);
|
||||
padding: 0.05em 0.2em;
|
||||
border-radius: 0.2em;
|
||||
}
|
198
web/ui/mantine-ui/src/pages/query/ExplainViews/ExplainView.tsx
Normal file
198
web/ui/mantine-ui/src/pages/query/ExplainViews/ExplainView.tsx
Normal file
|
@ -0,0 +1,198 @@
|
|||
import { FC } from "react";
|
||||
import { Alert, Text, Anchor, Card, Divider } from "@mantine/core";
|
||||
import ASTNode, { nodeType } from "../../../promql/ast";
|
||||
// import AggregationExplainView from "./Aggregation";
|
||||
// import BinaryExprExplainView from "./BinaryExpr/BinaryExpr";
|
||||
// import SelectorExplainView from "./Selector";
|
||||
import funcDocs from "../../../promql/functionDocs";
|
||||
import { escapeString } from "../../../promql/utils";
|
||||
import { formatPrometheusDuration } from "../../../lib/formatTime";
|
||||
import classes from "./ExplainView.module.css";
|
||||
import SelectorExplainView from "./Selector";
|
||||
import AggregationExplainView from "./Aggregation";
|
||||
import BinaryExprExplainView from "./BinaryExpr/BinaryExpr";
|
||||
interface ExplainViewProps {
|
||||
node: ASTNode | null;
|
||||
treeShown: boolean;
|
||||
setShowTree: () => void;
|
||||
}
|
||||
|
||||
const ExplainView: FC<ExplainViewProps> = ({
|
||||
node,
|
||||
treeShown,
|
||||
setShowTree,
|
||||
}) => {
|
||||
if (node === null) {
|
||||
return (
|
||||
<Alert>
|
||||
<>
|
||||
To use the Explain view,{" "}
|
||||
{!treeShown && (
|
||||
<>
|
||||
<Anchor fz="unset" onClick={setShowTree}>
|
||||
enable the query tree view
|
||||
</Anchor>{" "}
|
||||
(also available via the expression input dropdown) and then
|
||||
</>
|
||||
)}{" "}
|
||||
select a node in the tree above.
|
||||
</>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
switch (node.type) {
|
||||
case nodeType.aggregation:
|
||||
return <AggregationExplainView node={node} />;
|
||||
case nodeType.binaryExpr:
|
||||
return <BinaryExprExplainView node={node} />;
|
||||
case nodeType.call:
|
||||
return (
|
||||
<Card withBorder>
|
||||
<Text fz="lg" fw={600} mb="md">
|
||||
Function call
|
||||
</Text>
|
||||
<Text fz="sm">
|
||||
This node calls the{" "}
|
||||
<Anchor
|
||||
fz="inherit"
|
||||
href={`https://prometheus.io/docs/prometheus/latest/querying/functions/#${node.func.name}`}
|
||||
target="_blank"
|
||||
>
|
||||
<span className="promql-code promql-keyword">
|
||||
{node.func.name}()
|
||||
</span>
|
||||
</Anchor>{" "}
|
||||
function{node.args.length > 0 ? " on the provided inputs" : ""}.
|
||||
</Text>
|
||||
<Divider my="md" />
|
||||
{/* TODO: Some docs, like x_over_time, have relative links pointing back to the Prometheus docs,
|
||||
make sure to modify those links in the docs extraction so they work from the explain view */}
|
||||
<Text fz="sm" className={classes.funcDoc}>
|
||||
{funcDocs[node.func.name]}
|
||||
</Text>
|
||||
</Card>
|
||||
);
|
||||
case nodeType.matrixSelector:
|
||||
return <SelectorExplainView node={node} />;
|
||||
case nodeType.subquery:
|
||||
return (
|
||||
<Card withBorder>
|
||||
<Text fz="lg" fw={600} mb="md">
|
||||
Subquery
|
||||
</Text>
|
||||
<Text fz="sm">
|
||||
This node evaluates the passed expression as a subquery over the
|
||||
last{" "}
|
||||
<span className="promql-code promql-duration">
|
||||
{formatPrometheusDuration(node.range)}
|
||||
</span>{" "}
|
||||
at a query resolution{" "}
|
||||
{node.step > 0 ? (
|
||||
<>
|
||||
of{" "}
|
||||
<span className="promql-code promql-duration">
|
||||
{formatPrometheusDuration(node.step)}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
"equal to the default rule evaluation interval"
|
||||
)}
|
||||
{node.timestamp !== null ? (
|
||||
<>
|
||||
, evaluated relative to an absolute evaluation timestamp of{" "}
|
||||
<span className="promql-number">
|
||||
{(node.timestamp / 1000).toFixed(3)}
|
||||
</span>
|
||||
</>
|
||||
) : node.startOrEnd !== null ? (
|
||||
<>
|
||||
, evaluated relative to the {node.startOrEnd} of the query range
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{node.offset === 0 ? (
|
||||
<></>
|
||||
) : node.offset > 0 ? (
|
||||
<>
|
||||
, time-shifted{" "}
|
||||
<span className="promql-code promql-duration">
|
||||
{formatPrometheusDuration(node.offset)}
|
||||
</span>{" "}
|
||||
into the past
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
, time-shifted{" "}
|
||||
<span className="promql-code promql-duration">
|
||||
{formatPrometheusDuration(-node.offset)}
|
||||
</span>{" "}
|
||||
into the future
|
||||
</>
|
||||
)}
|
||||
.
|
||||
</Text>
|
||||
</Card>
|
||||
);
|
||||
case nodeType.numberLiteral:
|
||||
return (
|
||||
<Card withBorder>
|
||||
<Text fz="lg" fw={600} mb="md">
|
||||
Number literal
|
||||
</Text>
|
||||
<Text fz="sm">
|
||||
A scalar number literal with the value{" "}
|
||||
<span className="promql-code promql-number">{node.val}</span>.
|
||||
</Text>
|
||||
</Card>
|
||||
);
|
||||
case nodeType.parenExpr:
|
||||
return (
|
||||
<Card withBorder>
|
||||
<Text fz="lg" fw={600} mb="md">
|
||||
Parentheses
|
||||
</Text>
|
||||
<Text fz="sm">
|
||||
Parentheses that contain a sub-expression to be evaluated.
|
||||
</Text>
|
||||
</Card>
|
||||
);
|
||||
case nodeType.stringLiteral:
|
||||
return (
|
||||
<Card withBorder>
|
||||
<Text fz="lg" fw={600} mb="md">
|
||||
String literal
|
||||
</Text>
|
||||
<Text fz="sm">
|
||||
A string literal with the value{" "}
|
||||
<span className="promql-code promql-string">
|
||||
"{escapeString(node.val)}"
|
||||
</span>
|
||||
.
|
||||
</Text>
|
||||
</Card>
|
||||
);
|
||||
case nodeType.unaryExpr:
|
||||
return (
|
||||
<Card withBorder>
|
||||
<Text fz="lg" fw={600} mb="md">
|
||||
Unary expression
|
||||
</Text>
|
||||
<Text fz="sm">
|
||||
A unary expression that{" "}
|
||||
{node.op === "+"
|
||||
? "does not affect the expression it is applied to"
|
||||
: "changes the sign of the expression it is applied to"}
|
||||
.
|
||||
</Text>
|
||||
</Card>
|
||||
);
|
||||
case nodeType.vectorSelector:
|
||||
return <SelectorExplainView node={node} />;
|
||||
default:
|
||||
throw new Error("invalid node type");
|
||||
}
|
||||
};
|
||||
|
||||
export default ExplainView;
|
230
web/ui/mantine-ui/src/pages/query/ExplainViews/Selector.tsx
Normal file
230
web/ui/mantine-ui/src/pages/query/ExplainViews/Selector.tsx
Normal file
|
@ -0,0 +1,230 @@
|
|||
import { FC, ReactNode } from "react";
|
||||
import {
|
||||
VectorSelector,
|
||||
MatrixSelector,
|
||||
nodeType,
|
||||
LabelMatcher,
|
||||
matchType,
|
||||
} from "../../../promql/ast";
|
||||
import { escapeString } from "../../../promql/utils";
|
||||
import { useSuspenseAPIQuery } from "../../../api/api";
|
||||
import { Card, Text, Divider, List } from "@mantine/core";
|
||||
import { MetadataResult } from "../../../api/responseTypes/metadata";
|
||||
import { formatPrometheusDuration } from "../../../lib/formatTime";
|
||||
|
||||
interface SelectorExplainViewProps {
|
||||
node: VectorSelector | MatrixSelector;
|
||||
}
|
||||
|
||||
const matchingCriteriaList = (
|
||||
name: string,
|
||||
matchers: LabelMatcher[]
|
||||
): ReactNode => {
|
||||
return (
|
||||
<List fz="sm" my="md" withPadding>
|
||||
{name.length > 0 && (
|
||||
<List.Item>
|
||||
The metric name is{" "}
|
||||
<span className="promql-code promql-metric-name">{name}</span>.
|
||||
</List.Item>
|
||||
)}
|
||||
{matchers
|
||||
.filter((m) => !(m.name === "__name__"))
|
||||
.map((m) => {
|
||||
switch (m.type) {
|
||||
case matchType.equal:
|
||||
return (
|
||||
<List.Item>
|
||||
<span className="promql-code promql-label-name">
|
||||
{m.name}
|
||||
</span>
|
||||
<span className="promql-code promql-operator">{m.type}</span>
|
||||
<span className="promql-code promql-string">
|
||||
"{escapeString(m.value)}"
|
||||
</span>
|
||||
: The label{" "}
|
||||
<span className="promql-code promql-label-name">
|
||||
{m.name}
|
||||
</span>{" "}
|
||||
is exactly{" "}
|
||||
<span className="promql-code promql-string">
|
||||
"{escapeString(m.value)}"
|
||||
</span>
|
||||
.
|
||||
</List.Item>
|
||||
);
|
||||
case matchType.notEqual:
|
||||
return (
|
||||
<List.Item>
|
||||
<span className="promql-code promql-label-name">
|
||||
{m.name}
|
||||
</span>
|
||||
<span className="promql-code promql-operator">{m.type}</span>
|
||||
<span className="promql-code promql-string">
|
||||
"{escapeString(m.value)}"
|
||||
</span>
|
||||
: The label{" "}
|
||||
<span className="promql-code promql-label-name">
|
||||
{m.name}
|
||||
</span>{" "}
|
||||
is not{" "}
|
||||
<span className="promql-code promql-string">
|
||||
"{escapeString(m.value)}"
|
||||
</span>
|
||||
.
|
||||
</List.Item>
|
||||
);
|
||||
case matchType.matchRegexp:
|
||||
return (
|
||||
<List.Item>
|
||||
<span className="promql-code promql-label-name">
|
||||
{m.name}
|
||||
</span>
|
||||
<span className="promql-code promql-operator">{m.type}</span>
|
||||
<span className="promql-code promql-string">
|
||||
"{escapeString(m.value)}"
|
||||
</span>
|
||||
: The label{" "}
|
||||
<span className="promql-code promql-label-name">
|
||||
{m.name}
|
||||
</span>{" "}
|
||||
matches the regular expression{" "}
|
||||
<span className="promql-code promql-string">
|
||||
"{escapeString(m.value)}"
|
||||
</span>
|
||||
.
|
||||
</List.Item>
|
||||
);
|
||||
case matchType.matchNotRegexp:
|
||||
return (
|
||||
<List.Item>
|
||||
<span className="promql-code promql-label-name">
|
||||
{m.name}
|
||||
</span>
|
||||
<span className="promql-code promql-operator">{m.type}</span>
|
||||
<span className="promql-code promql-string">
|
||||
"{escapeString(m.value)}"
|
||||
</span>
|
||||
: The label{" "}
|
||||
<span className="promql-code promql-label-name">
|
||||
{m.name}
|
||||
</span>{" "}
|
||||
does not match the regular expression{" "}
|
||||
<span className="promql-code promql-string">
|
||||
"{escapeString(m.value)}"
|
||||
</span>
|
||||
.
|
||||
</List.Item>
|
||||
);
|
||||
default:
|
||||
throw new Error("invalid matcher type");
|
||||
}
|
||||
})}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
const SelectorExplainView: FC<SelectorExplainViewProps> = ({ node }) => {
|
||||
const baseMetricName = node.name.replace(/(_count|_sum|_bucket)$/, "");
|
||||
const { data: metricMeta } = useSuspenseAPIQuery<MetadataResult>({
|
||||
path: `/metadata`,
|
||||
params: {
|
||||
metric: baseMetricName,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Card withBorder>
|
||||
<Text fz="lg" fw={600} mb="md">
|
||||
{node.type === nodeType.vectorSelector ? "Instant" : "Range"} vector
|
||||
selector
|
||||
</Text>
|
||||
<Text fz="sm">
|
||||
{metricMeta.data === undefined ||
|
||||
metricMeta.data[baseMetricName] === undefined ||
|
||||
metricMeta.data[baseMetricName].length < 1 ? (
|
||||
<>No metric metadata found.</>
|
||||
) : (
|
||||
<>
|
||||
<strong>Metric help</strong>:{" "}
|
||||
{metricMeta.data[baseMetricName][0].help}
|
||||
<br />
|
||||
<strong>Metric type</strong>:{" "}
|
||||
{metricMeta.data[baseMetricName][0].type}
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
<Divider my="md" />
|
||||
<Text fz="sm">
|
||||
{node.type === nodeType.vectorSelector ? (
|
||||
<>
|
||||
This node selects the latest (non-stale) sample value within the
|
||||
last <span className="promql-code promql-duration">5m</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
This node selects{" "}
|
||||
<span className="promql-code promql-duration">
|
||||
{formatPrometheusDuration(node.range)}
|
||||
</span>{" "}
|
||||
of data going backward from the evaluation timestamp
|
||||
</>
|
||||
)}
|
||||
{node.timestamp !== null ? (
|
||||
<>
|
||||
, evaluated relative to an absolute evaluation timestamp of{" "}
|
||||
<span className="promql-number">
|
||||
{(node.timestamp / 1000).toFixed(3)}
|
||||
</span>
|
||||
</>
|
||||
) : node.startOrEnd !== null ? (
|
||||
<>, evaluated relative to the {node.startOrEnd} of the query range</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{node.offset === 0 ? (
|
||||
<></>
|
||||
) : node.offset > 0 ? (
|
||||
<>
|
||||
, time-shifted{" "}
|
||||
<span className="promql-code promql-duration">
|
||||
{formatPrometheusDuration(node.offset)}
|
||||
</span>{" "}
|
||||
into the past,
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
, time-shifted{" "}
|
||||
<span className="promql-code promql-duration">
|
||||
{formatPrometheusDuration(-node.offset)}
|
||||
</span>{" "}
|
||||
into the future,
|
||||
</>
|
||||
)}{" "}
|
||||
for any series that match all of the following criteria:
|
||||
</Text>
|
||||
{matchingCriteriaList(node.name, node.matchers)}
|
||||
<Text fz="sm">
|
||||
If a series has no values in the last{" "}
|
||||
<span className="promql-code promql-duration">
|
||||
{node.type === nodeType.vectorSelector
|
||||
? "5m"
|
||||
: formatPrometheusDuration(node.range)}
|
||||
</span>
|
||||
{node.offset > 0 && (
|
||||
<>
|
||||
{" "}
|
||||
(relative to the time-shifted instant{" "}
|
||||
<span className="promql-code promql-duration">
|
||||
{formatPrometheusDuration(node.offset)}
|
||||
</span>{" "}
|
||||
in the past)
|
||||
</>
|
||||
)}
|
||||
, the series will not be returned.
|
||||
</Text>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectorExplainView;
|
|
@ -14,6 +14,7 @@ import {
|
|||
IconChartLine,
|
||||
IconCheckbox,
|
||||
IconGraph,
|
||||
IconInfoCircle,
|
||||
IconSquare,
|
||||
IconTable,
|
||||
} from "@tabler/icons-react";
|
||||
|
@ -38,6 +39,7 @@ import TreeView from "./TreeView";
|
|||
import ErrorBoundary from "../../components/ErrorBoundary";
|
||||
import ASTNode from "../../promql/ast";
|
||||
import serializeNode from "../../promql/serialize";
|
||||
import ExplainView from "./ExplainViews/ExplainView";
|
||||
|
||||
export interface PanelProps {
|
||||
idx: number;
|
||||
|
@ -153,6 +155,12 @@ const QueryPanel: FC<PanelProps> = ({ idx, metricNames }) => {
|
|||
<Tabs.Tab value="graph" leftSection={<IconGraph style={iconStyle} />}>
|
||||
Graph
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value="explain"
|
||||
leftSection={<IconInfoCircle style={iconStyle} />}
|
||||
>
|
||||
Explain
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
<Tabs.Panel pt="sm" value="table">
|
||||
<TableTab expr={expr} panelIdx={idx} retriggerIdx={retriggerIdx} />
|
||||
|
@ -300,6 +308,30 @@ const QueryPanel: FC<PanelProps> = ({ idx, metricNames }) => {
|
|||
onSelectRange={onSelectRange}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel pt="sm" value="explain">
|
||||
<ErrorBoundary
|
||||
key={selectedNode?.id}
|
||||
title="Error showing explain view"
|
||||
>
|
||||
<Suspense
|
||||
fallback={
|
||||
<Box mt="lg">
|
||||
{Array.from(Array(20), (_, i) => (
|
||||
<Skeleton key={i} height={30} mb={15} width="100%" />
|
||||
))}
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<ExplainView
|
||||
node={selectedNode?.node ?? null}
|
||||
treeShown={panel.showTree}
|
||||
setShowTree={() => {
|
||||
dispatch(setShowTree({ idx, showTree: true }));
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</Stack>
|
||||
);
|
||||
|
|
|
@ -51,14 +51,14 @@ const SeriesName: FC<SeriesNameProps> = ({ labels, format }) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span>
|
||||
<span className={classes.metricName}>
|
||||
{labels ? labels.__name__ : ""}
|
||||
</span>
|
||||
{"{"}
|
||||
{labelNodes}
|
||||
{"}"}
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -88,6 +88,13 @@ const TreeNode: FC<{
|
|||
sortedLabelCards: [],
|
||||
});
|
||||
|
||||
// Deselect node when node is unmounted.
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setSelectedNode(null);
|
||||
};
|
||||
}, [setSelectedNode]);
|
||||
|
||||
const children = getNodeChildren(node);
|
||||
|
||||
const [childStates, setChildStates] = useState<NodeState[]>(
|
||||
|
@ -236,12 +243,12 @@ const TreeNode: FC<{
|
|||
// Connector line between this node and its parent.
|
||||
<Box pos="absolute" display="inline-block" style={connectorStyle} />
|
||||
)}
|
||||
{/* The node itself. */}
|
||||
{/* The node (visible box) itself. */}
|
||||
<Box
|
||||
ref={nodeRef}
|
||||
w="fit-content"
|
||||
px={10}
|
||||
py={5}
|
||||
py={4}
|
||||
style={{ borderRadius: 4, flexShrink: 0 }}
|
||||
className={clsx(classes.nodeText, {
|
||||
[classes.nodeTextError]: error,
|
||||
|
|
|
@ -3,7 +3,7 @@ import { useSuspenseAPIQuery } from "../../api/api";
|
|||
import { useAppSelector } from "../../state/hooks";
|
||||
import ASTNode from "../../promql/ast";
|
||||
import TreeNode from "./TreeNode";
|
||||
import { Box } from "@mantine/core";
|
||||
import { Card } from "@mantine/core";
|
||||
|
||||
const TreeView: FC<{
|
||||
panelIdx: number;
|
||||
|
@ -31,14 +31,14 @@ const TreeView: FC<{
|
|||
});
|
||||
|
||||
return (
|
||||
<Box fz="sm" style={{ overflowX: "auto" }} pl="sm">
|
||||
<Card withBorder fz="sm" style={{ overflowX: "auto" }} pl="sm">
|
||||
<TreeNode
|
||||
node={data.data}
|
||||
selectedNode={selectedNode}
|
||||
setSelectedNode={setSelectedNode}
|
||||
reverse={false}
|
||||
/>
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -39,7 +39,22 @@ export const decodePanelOptionsFromURLParams = (query: string): Panel[] => {
|
|||
panel.showTree = value === "1";
|
||||
});
|
||||
decodeSetting("tab", (value) => {
|
||||
panel.visualizer.activeTab = value === "0" ? "graph" : "table";
|
||||
// Numeric values are deprecated (from the old UI), but we still support decoding them.
|
||||
switch (value) {
|
||||
case "0":
|
||||
case "graph":
|
||||
panel.visualizer.activeTab = "graph";
|
||||
break;
|
||||
case "1":
|
||||
case "table":
|
||||
panel.visualizer.activeTab = "table";
|
||||
break;
|
||||
case "explain":
|
||||
panel.visualizer.activeTab = "explain";
|
||||
break;
|
||||
default:
|
||||
console.log("Unknown tab", value);
|
||||
}
|
||||
});
|
||||
decodeSetting("display_mode", (value) => {
|
||||
panel.visualizer.displayMode = value as GraphDisplayMode;
|
||||
|
@ -125,7 +140,7 @@ export const encodePanelOptionsToURLParams = (
|
|||
panels.forEach((p, idx) => {
|
||||
addParam(idx, "expr", p.expr);
|
||||
addParam(idx, "show_tree", p.showTree ? "1" : "0");
|
||||
addParam(idx, "tab", p.visualizer.activeTab === "graph" ? "0" : "1");
|
||||
addParam(idx, "tab", p.visualizer.activeTab);
|
||||
if (p.visualizer.endTime !== null) {
|
||||
addParam(idx, "end_input", formatTime(p.visualizer.endTime));
|
||||
addParam(idx, "moment_input", formatTime(p.visualizer.endTime));
|
||||
|
|
Loading…
Reference in a new issue