Merge pull request #14913 from prometheus/explain-view-set-ops
Some checks are pending
buf.build / lint and publish (push) Waiting to run
CI / Go tests (push) Waiting to run
CI / More Go tests (push) Waiting to run
CI / Go tests with previous Go version (push) Waiting to run
CI / UI tests (push) Waiting to run
CI / Go tests on Windows (push) Waiting to run
CI / Mixins tests (push) Waiting to run
CI / Build Prometheus for common architectures (0) (push) Waiting to run
CI / Build Prometheus for common architectures (1) (push) Waiting to run
CI / Build Prometheus for common architectures (2) (push) Waiting to run
CI / Build Prometheus for all architectures (0) (push) Waiting to run
CI / Build Prometheus for all architectures (1) (push) Waiting to run
CI / Build Prometheus for all architectures (10) (push) Waiting to run
CI / Build Prometheus for all architectures (11) (push) Waiting to run
CI / Build Prometheus for all architectures (2) (push) Waiting to run
CI / Build Prometheus for all architectures (3) (push) Waiting to run
CI / Build Prometheus for all architectures (4) (push) Waiting to run
CI / Build Prometheus for all architectures (5) (push) Waiting to run
CI / Build Prometheus for all architectures (6) (push) Waiting to run
CI / Build Prometheus for all architectures (7) (push) Waiting to run
CI / Build Prometheus for all architectures (8) (push) Waiting to run
CI / Build Prometheus for all architectures (9) (push) Waiting to run
CI / Report status of build Prometheus for all architectures (push) Blocked by required conditions
CI / Check generated parser (push) Waiting to run
CI / golangci-lint (push) Waiting to run
CI / fuzzing (push) Waiting to run
CI / codeql (push) Waiting to run
CI / Publish main branch artifacts (push) Blocked by required conditions
CI / Publish release artefacts (push) Blocked by required conditions
CI / Publish UI on npm Registry (push) Blocked by required conditions
Scorecards supply-chain security / Scorecards analysis (push) Waiting to run

PromQL explain view: Support set operators
This commit is contained in:
Julius Volz 2024-09-16 19:25:11 +02:00 committed by GitHub
commit 6acd3dee53
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 1125 additions and 349 deletions

View file

@ -361,22 +361,11 @@ const VectorVectorBinaryExprExplainView: FC<
<>
<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)
}
onChange={(event) => setShowSampleValues(event.currentTarget.checked)}
/>
</Group>
@ -402,22 +391,12 @@ const VectorVectorBinaryExprExplainView: FC<
<Table fz="xs" withRowBorders={false}>
<Table.Tbody>
{Object.values(matchGroups).map((mg, mgIdx) => {
const {
groupLabels,
lhs,
lhsCount,
rhs,
rhsCount,
result,
error,
} = mg;
const { groupLabels, lhs, lhsCount, rhs, rhsCount, result, error } =
mg;
const matchGroupTitleRow = (color: string) => (
<Table.Tr ta="center">
<Table.Td
colSpan={2}
style={{ backgroundColor: `${color}25` }}
>
<Table.Td colSpan={2} style={{ backgroundColor: `${color}25` }}>
<SeriesName labels={groupLabels} format={true} />
</Table.Td>
</Table.Tr>
@ -434,14 +413,14 @@ const VectorVectorBinaryExprExplainView: FC<
borderRadius: 3,
border: "2px solid",
borderColor:
series.length === 0
seriesCount === 0
? "light-dark(var(--mantine-color-gray-4), var(--mantine-color-gray-7))"
: color,
}}
>
<Table fz="xs" withRowBorders={false} verticalSpacing={5}>
<Table.Tbody>
{series.length === 0 ? (
{seriesCount === 0 ? (
<Table.Tr>
<Table.Td
ta="center"
@ -508,9 +487,6 @@ const VectorVectorBinaryExprExplainView: FC<
</Box>
);
const noLHSMatches = lhs.length === 0;
const noRHSMatches = rhs.length === 0;
const groupColor = colorPool[mgIdx % colorPool.length];
const lhsTable = matchGroupTable(lhs, lhsCount, groupColor);
@ -527,14 +503,25 @@ const VectorVectorBinaryExprExplainView: FC<
borderRadius: 3,
border: `2px solid`,
borderColor:
noLHSMatches || noRHSMatches || error !== null
result.length === 0 || error !== null
? "light-dark(var(--mantine-color-gray-4), var(--mantine-color-gray-7))"
: groupColor,
}}
>
<Table fz="xs" withRowBorders={false} verticalSpacing={5}>
<Table.Tbody>
{noLHSMatches || noRHSMatches ? (
{error !== null ? (
<Table.Tr>
<Table.Td
ta="center"
c="light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-5))"
py="md"
fw="bold"
>
error, result omitted
</Table.Td>
</Table.Tr>
) : result.length === 0 ? (
<Table.Tr>
<Table.Td
ta="center"
@ -569,8 +556,7 @@ const VectorVectorBinaryExprExplainView: FC<
const filtered =
sample.value[1] === filteredSampleValue;
const [lIdx, rIdx] =
matching.card ===
vectorMatchCardinality.oneToMany
matching.card === vectorMatchCardinality.oneToMany
? [0, manySideIdx]
: [manySideIdx, 0];
@ -645,7 +631,10 @@ const VectorVectorBinaryExprExplainView: FC<
<Table.Td valign="middle" p={0}>
{lhsTable}
</Table.Td>
<Table.Td ta="center">
<Table.Td
ta="center"
fw={isSetOperator(node.op) ? "bold" : undefined}
>
{node.op}
{node.bool && " bool"}
</Table.Td>
@ -663,8 +652,6 @@ const VectorVectorBinaryExprExplainView: FC<
</Table.Tbody>
</Table>
</>
)}
</>
);
};

View file

@ -73,6 +73,7 @@ const testMetricC: InstantSample[] = [
const testCases: TestCase[] = [
{
// metric_a - metric_b
desc: "one-to-one matching on all labels",
op: binaryOperatorType.sub,
matching: {
@ -238,6 +239,7 @@ const testCases: TestCase[] = [
},
},
{
// metric_a - on(label1, label2) metric_b
desc: "one-to-one matching on explicit labels",
op: binaryOperatorType.sub,
matching: {
@ -403,6 +405,7 @@ const testCases: TestCase[] = [
},
},
{
// metric_a - ignoring(same) metric_b
desc: "one-to-one matching ignoring explicit labels",
op: binaryOperatorType.sub,
matching: {
@ -568,6 +571,7 @@ const testCases: TestCase[] = [
},
},
{
// metric_b - metric_c
desc: "many-to-one matching with no matching labels specified (empty output)",
op: binaryOperatorType.sub,
matching: {
@ -689,6 +693,7 @@ const testCases: TestCase[] = [
},
},
{
// metric_b - on(label1) metric_c
desc: "many-to-one matching with matching labels specified, but no group_left (error)",
op: binaryOperatorType.sub,
matching: {
@ -778,6 +783,7 @@ const testCases: TestCase[] = [
},
},
{
// metric_b - on(label1) group_left metric_c
desc: "many-to-one matching with matching labels specified and group_left",
op: binaryOperatorType.sub,
matching: {
@ -891,6 +897,7 @@ const testCases: TestCase[] = [
},
},
{
// metric_c - on(label1) group_right metric_b
desc: "one-to-many matching with matching labels specified and group_right",
op: binaryOperatorType.sub,
matching: {
@ -1004,6 +1011,7 @@ const testCases: TestCase[] = [
},
},
{
// metric_c - on(label1) group_left metric_b
desc: "one-to-many matching with matching labels specified but incorrect group_left (error)",
op: binaryOperatorType.sub,
matching: {
@ -1091,6 +1099,7 @@ const testCases: TestCase[] = [
},
},
{
// metric_a - on(label1) metric_b
desc: "insufficient matching labels leading to many-to-many matching for intended one-to-one match (error)",
op: binaryOperatorType.sub,
matching: {
@ -1206,6 +1215,7 @@ const testCases: TestCase[] = [
},
},
{
// metric_a < metric_b
desc: "filter op keeping all series",
op: binaryOperatorType.lss,
matching: {
@ -1391,6 +1401,7 @@ const testCases: TestCase[] = [
},
},
{
// metric_a >= metric_b
desc: "filter op dropping all series",
op: binaryOperatorType.gte,
matching: {
@ -1576,6 +1587,7 @@ const testCases: TestCase[] = [
},
},
{
// metric_a >= bool metric_b
desc: "filter op dropping all series, but with bool",
op: binaryOperatorType.gte,
bool: true,
@ -1742,6 +1754,7 @@ const testCases: TestCase[] = [
},
},
{
// metric_a < bool metric_b
desc: "filter op keeping all series, but with bool",
op: binaryOperatorType.lss,
bool: true,
@ -1908,6 +1921,7 @@ const testCases: TestCase[] = [
},
},
{
// metric_a - metric_b
desc: "exceeding the match group limit",
op: binaryOperatorType.sub,
matching: {
@ -2000,6 +2014,7 @@ const testCases: TestCase[] = [
},
},
{
// metric_c - on(label1) group_left metric_b
desc: "exceeding the per-group series limit",
op: binaryOperatorType.sub,
matching: {
@ -2082,6 +2097,7 @@ const testCases: TestCase[] = [
},
},
{
// metric_c - on(label1) group_left metric_b
desc: "exceeding both group limit and per-group series limit",
op: binaryOperatorType.sub,
matching: {
@ -2131,6 +2147,732 @@ const testCases: TestCase[] = [
numGroups: 2,
},
},
{
// metric_a and metric b
desc: "and operator with no matching labels and matching groups",
op: binaryOperatorType.and,
matching: {
card: vectorMatchCardinality.manyToMany,
on: false,
include: [],
labels: [],
},
lhs: testMetricA,
rhs: testMetricB,
result: {
groups: {
[fnv1a(["a", "x", "same"])]: {
groupLabels: { label1: "a", label2: "x", same: "same" },
lhs: [
{
metric: {
__name__: "metric_a",
label1: "a",
label2: "x",
same: "same",
},
value: [0, "1"],
},
],
lhsCount: 1,
rhs: [
{
metric: {
__name__: "metric_b",
label1: "a",
label2: "x",
same: "same",
},
value: [0, "10"],
},
],
rhsCount: 1,
result: [
{
sample: {
metric: {
__name__: "metric_a",
label1: "a",
label2: "x",
same: "same",
},
value: [0, "1"],
},
manySideIdx: 0,
},
],
error: null,
},
[fnv1a(["a", "y", "same"])]: {
groupLabels: { label1: "a", label2: "y", same: "same" },
lhs: [
{
metric: {
__name__: "metric_a",
label1: "a",
label2: "y",
same: "same",
},
value: [0, "2"],
},
],
lhsCount: 1,
rhs: [
{
metric: {
__name__: "metric_b",
label1: "a",
label2: "y",
same: "same",
},
value: [0, "20"],
},
],
rhsCount: 1,
result: [
{
sample: {
metric: {
__name__: "metric_a",
label1: "a",
label2: "y",
same: "same",
},
value: [0, "2"],
},
manySideIdx: 0,
},
],
error: null,
},
[fnv1a(["b", "x", "same"])]: {
groupLabels: { label1: "b", label2: "x", same: "same" },
lhs: [
{
metric: {
__name__: "metric_a",
label1: "b",
label2: "x",
same: "same",
},
value: [0, "3"],
},
],
lhsCount: 1,
rhs: [
{
metric: {
__name__: "metric_b",
label1: "b",
label2: "x",
same: "same",
},
value: [0, "30"],
},
],
rhsCount: 1,
result: [
{
sample: {
metric: {
__name__: "metric_a",
label1: "b",
label2: "x",
same: "same",
},
value: [0, "3"],
},
manySideIdx: 0,
},
],
error: null,
},
[fnv1a(["b", "y", "same"])]: {
groupLabels: { label1: "b", label2: "y", same: "same" },
lhs: [
{
metric: {
__name__: "metric_a",
label1: "b",
label2: "y",
same: "same",
},
value: [0, "4"],
},
],
lhsCount: 1,
rhs: [
{
metric: {
__name__: "metric_b",
label1: "b",
label2: "y",
same: "same",
},
value: [0, "40"],
},
],
rhsCount: 1,
result: [
{
sample: {
metric: {
__name__: "metric_a",
label1: "b",
label2: "y",
same: "same",
},
value: [0, "4"],
},
manySideIdx: 0,
},
],
error: null,
},
},
numGroups: 4,
},
},
{
// metric_a[0...2] and on(label1) metric_b[1...3]
desc: "and operator with matching label and series on each side",
op: binaryOperatorType.and,
matching: {
card: vectorMatchCardinality.manyToMany,
on: true,
include: [],
labels: ["label1"],
},
lhs: testMetricA.slice(0, 3),
rhs: testMetricB.slice(1, 4),
result: {
groups: {
[fnv1a(["a"])]: {
groupLabels: { label1: "a" },
lhs: [
{
metric: {
__name__: "metric_a",
label1: "a",
label2: "x",
same: "same",
},
value: [0, "1"],
},
{
metric: {
__name__: "metric_a",
label1: "a",
label2: "y",
same: "same",
},
value: [0, "2"],
},
],
lhsCount: 2,
rhs: [
{
metric: {
__name__: "metric_b",
label1: "a",
label2: "y",
same: "same",
},
value: [0, "20"],
},
],
rhsCount: 1,
result: [
{
sample: {
metric: {
__name__: "metric_a",
label1: "a",
label2: "x",
same: "same",
},
value: [0, "1"],
},
manySideIdx: 0,
},
{
sample: {
metric: {
__name__: "metric_a",
label1: "a",
label2: "y",
same: "same",
},
value: [0, "2"],
},
manySideIdx: 1,
},
],
error: null,
},
[fnv1a(["b"])]: {
groupLabels: { label1: "b" },
lhs: [
{
metric: {
__name__: "metric_a",
label1: "b",
label2: "x",
same: "same",
},
value: [0, "3"],
},
],
lhsCount: 1,
rhs: [
{
metric: {
__name__: "metric_b",
label1: "b",
label2: "x",
same: "same",
},
value: [0, "30"],
},
{
metric: {
__name__: "metric_b",
label1: "b",
label2: "y",
same: "same",
},
value: [0, "40"],
},
],
rhsCount: 2,
result: [
{
sample: {
metric: {
__name__: "metric_a",
label1: "b",
label2: "x",
same: "same",
},
value: [0, "3"],
},
manySideIdx: 0,
},
],
error: null,
},
},
numGroups: 2,
},
},
{
// metric_a[0...2] unless on(label1) metric_b[1...3]
desc: "unless operator with matching label and series on each side",
op: binaryOperatorType.unless,
matching: {
card: vectorMatchCardinality.manyToMany,
on: true,
include: [],
labels: ["label1"],
},
lhs: testMetricA.slice(0, 3),
rhs: testMetricB.slice(1, 4),
result: {
groups: {
[fnv1a(["a"])]: {
groupLabels: { label1: "a" },
lhs: [
{
metric: {
__name__: "metric_a",
label1: "a",
label2: "x",
same: "same",
},
value: [0, "1"],
},
{
metric: {
__name__: "metric_a",
label1: "a",
label2: "y",
same: "same",
},
value: [0, "2"],
},
],
lhsCount: 2,
rhs: [
{
metric: {
__name__: "metric_b",
label1: "a",
label2: "y",
same: "same",
},
value: [0, "20"],
},
],
rhsCount: 1,
result: [],
error: null,
},
[fnv1a(["b"])]: {
groupLabels: { label1: "b" },
lhs: [
{
metric: {
__name__: "metric_a",
label1: "b",
label2: "x",
same: "same",
},
value: [0, "3"],
},
],
lhsCount: 1,
rhs: [
{
metric: {
__name__: "metric_b",
label1: "b",
label2: "x",
same: "same",
},
value: [0, "30"],
},
{
metric: {
__name__: "metric_b",
label1: "b",
label2: "y",
same: "same",
},
value: [0, "40"],
},
],
rhsCount: 2,
result: [],
error: null,
},
},
numGroups: 2,
},
},
{
// metric_a[0...2] or on(label1) metric_b[1...3]
desc: "or operator with matching label and series on each side",
op: binaryOperatorType.or,
matching: {
card: vectorMatchCardinality.manyToMany,
on: true,
include: [],
labels: ["label1"],
},
lhs: testMetricA.slice(0, 3),
rhs: testMetricB.slice(1, 4),
result: {
groups: {
[fnv1a(["a"])]: {
groupLabels: { label1: "a" },
lhs: [
{
metric: {
__name__: "metric_a",
label1: "a",
label2: "x",
same: "same",
},
value: [0, "1"],
},
{
metric: {
__name__: "metric_a",
label1: "a",
label2: "y",
same: "same",
},
value: [0, "2"],
},
],
lhsCount: 2,
rhs: [
{
metric: {
__name__: "metric_b",
label1: "a",
label2: "y",
same: "same",
},
value: [0, "20"],
},
],
rhsCount: 1,
result: [
{
sample: {
metric: {
__name__: "metric_a",
label1: "a",
label2: "x",
same: "same",
},
value: [0, "1"],
},
manySideIdx: 0,
},
{
sample: {
metric: {
__name__: "metric_a",
label1: "a",
label2: "y",
same: "same",
},
value: [0, "2"],
},
manySideIdx: 1,
},
],
error: null,
},
[fnv1a(["b"])]: {
groupLabels: { label1: "b" },
lhs: [
{
metric: {
__name__: "metric_a",
label1: "b",
label2: "x",
same: "same",
},
value: [0, "3"],
},
],
lhsCount: 1,
rhs: [
{
metric: {
__name__: "metric_b",
label1: "b",
label2: "x",
same: "same",
},
value: [0, "30"],
},
{
metric: {
__name__: "metric_b",
label1: "b",
label2: "y",
same: "same",
},
value: [0, "40"],
},
],
rhsCount: 2,
result: [
{
sample: {
metric: {
__name__: "metric_a",
label1: "b",
label2: "x",
same: "same",
},
value: [0, "3"],
},
manySideIdx: 0,
},
],
error: null,
},
},
numGroups: 2,
},
},
{
// metric_a[0...2] or metric_b[1...3]
desc: "or operator with only partial overlap",
op: binaryOperatorType.or,
matching: {
card: vectorMatchCardinality.manyToMany,
on: false,
include: [],
labels: [],
},
lhs: testMetricA.slice(0, 3),
rhs: testMetricB.slice(1, 4),
result: {
groups: {
[fnv1a(["a", "x", "same"])]: {
groupLabels: {
label1: "a",
label2: "x",
same: "same",
},
lhs: [
{
metric: {
__name__: "metric_a",
label1: "a",
label2: "x",
same: "same",
},
value: [0, "1"],
},
],
lhsCount: 1,
rhs: [],
rhsCount: 0,
result: [
{
sample: {
metric: {
__name__: "metric_a",
label1: "a",
label2: "x",
same: "same",
},
value: [0, "1"],
},
manySideIdx: 0,
},
],
error: null,
},
[fnv1a(["a", "y", "same"])]: {
groupLabels: {
label1: "a",
label2: "y",
same: "same",
},
lhs: [
{
metric: {
__name__: "metric_a",
label1: "a",
label2: "y",
same: "same",
},
value: [0, "2"],
},
],
lhsCount: 1,
rhs: [
{
metric: {
__name__: "metric_b",
label1: "a",
label2: "y",
same: "same",
},
value: [0, "20"],
},
],
rhsCount: 1,
result: [
{
sample: {
metric: {
__name__: "metric_a",
label1: "a",
label2: "y",
same: "same",
},
value: [0, "2"],
},
manySideIdx: 0,
},
],
error: null,
},
[fnv1a(["b", "x", "same"])]: {
groupLabels: {
label1: "b",
label2: "x",
same: "same",
},
lhs: [
{
metric: {
__name__: "metric_a",
label1: "b",
label2: "x",
same: "same",
},
value: [0, "3"],
},
],
lhsCount: 1,
rhs: [
{
metric: {
__name__: "metric_b",
label1: "b",
label2: "x",
same: "same",
},
value: [0, "30"],
},
],
rhsCount: 1,
result: [
{
sample: {
metric: {
__name__: "metric_a",
label1: "b",
label2: "x",
same: "same",
},
value: [0, "3"],
},
manySideIdx: 0,
},
],
error: null,
},
[fnv1a(["b", "y", "same"])]: {
groupLabels: {
label1: "b",
label2: "y",
same: "same",
},
lhs: [],
lhsCount: 0,
rhs: [
{
metric: {
__name__: "metric_b",
label1: "b",
label2: "y",
same: "same",
},
value: [0, "40"],
},
],
rhsCount: 1,
result: [
{
sample: {
metric: {
__name__: "metric_b",
label1: "b",
label2: "y",
same: "same",
},
value: [0, "40"],
},
manySideIdx: 0,
},
],
error: null,
},
},
numGroups: 4,
},
},
];
describe("binOp", () => {

View file

@ -340,12 +340,8 @@ export const computeVectorVectorBinOp = (
// Annotate the match groups with errors (if any) and populate the results.
Object.values(groups).forEach((mg) => {
// Do not populate results for set operators.
if (isSetOperator(op)) {
return;
}
if (matching.card === vectorMatchCardinality.oneToOne) {
switch (matching.card) {
case vectorMatchCardinality.oneToOne:
if (mg.lhs.length > 1 && mg.rhs.length > 1) {
mg.error = { type: MatchErrorType.multipleMatchesOnBothSides };
} else if (mg.lhs.length > 1 || mg.rhs.length > 1) {
@ -354,12 +350,26 @@ export const computeVectorVectorBinOp = (
dupeSide: mg.lhs.length > 1 ? "left" : "right",
};
}
} else if (mg.rhs.length > 1) {
// Check for dupes on the "one" side in one-to-many or many-to-one matching.
break;
case vectorMatchCardinality.oneToMany:
case vectorMatchCardinality.manyToOne:
if (mg.rhs.length > 1) {
mg.error = {
type: MatchErrorType.multipleMatchesOnOneSide,
};
}
break;
case vectorMatchCardinality.manyToMany:
// Should be a set operator - these don't have errors that aren't caught during parsing.
if (!isSetOperator(op)) {
throw new Error(
"unexpected many-to-many matching for non-set operator"
);
}
break;
default:
throw new Error("unknown vector matching cardinality");
}
if (mg.error) {
// We don't populate results for error cases, as especially in the case of a
@ -368,6 +378,39 @@ export const computeVectorVectorBinOp = (
return;
}
if (isSetOperator(op)) {
// Add LHS samples to the result, depending on specific operator condition and RHS length.
mg.lhs.forEach((ls, lIdx) => {
if (
(op === binaryOperatorType.and && mg.rhs.length > 0) ||
(op === binaryOperatorType.unless && mg.rhs.length === 0) ||
op === binaryOperatorType.or
) {
mg.result.push({
sample: {
metric: ls.metric,
value: ls.value,
},
manySideIdx: lIdx,
});
}
});
// For OR, also add all RHS samples to the result if the LHS for the group is empty.
if (op === binaryOperatorType.or) {
mg.rhs.forEach((rs, rIdx) => {
if (mg.lhs.length === 0) {
mg.result.push({
sample: {
metric: rs.metric,
value: rs.value,
},
manySideIdx: rIdx,
});
}
});
}
} else {
// Calculate the results for this match group.
mg.rhs.forEach((rs) => {
mg.lhs.forEach((ls, lIdx) => {
@ -380,6 +423,7 @@ export const computeVectorVectorBinOp = (
matching.card !== vectorMatchCardinality.oneToMany
? [ls.value[1], rs.value[1]]
: [rs.value[1], ls.value[1]];
let { value, keep } = vectorElemBinop(
op,
parsePrometheusFloat(vl),
@ -397,13 +441,16 @@ export const computeVectorVectorBinOp = (
metric: metric,
value: [
ls.value[0],
keep || bool ? formatPrometheusFloat(value) : filteredSampleValue,
keep || bool
? formatPrometheusFloat(value)
: filteredSampleValue,
],
},
manySideIdx: lIdx,
});
});
});
}
});
// If we originally swapped the LHS and RHS, swap them back to the original order.