Signed-off-by: Julius Volz <julius.volz@gmail.com>pull/14872/head
parent
b75a12b52f
commit
5fd860f806
@ -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; |
||||
} |
||||
@ -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; |
||||
@ -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; |
||||
Loading…
Reference in new issue