PromQL explain view: Support set operators

Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
Julius Volz 2024-09-16 18:00:58 +02:00
parent 6febfbb3be
commit 9b0dc68d0d
3 changed files with 1125 additions and 349 deletions

View file

@ -361,309 +361,296 @@ 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)
}
/>
</Group>
<Group my="lg" justify="flex-end" gap="xl">
<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 />}>
Too many match groups to display, only showing{" "}
{Object.keys(matchGroups).length} out of {numGroups} groups.
<br />
<br />
<Anchor fz="sm" onClick={() => setMaxGroups(undefined)}>
Show all groups
</Anchor>
</Alert>
)}
{errCount > 0 && (
<Alert color="yellow" mb="md" icon={<IconAlertTriangle />}>
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 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",
borderColor:
series.length === 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 ? (
<Table.Tr>
<Table.Td
ta="center"
c="light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-5))"
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 ta="right">{s.value[1]}</Table.Td>
)}
</Table.Tr>
);
})}
</>
)}
{seriesCount > series.length && (
<Table.Tr>
<Table.Td ta="center" py="md" fw="bold" c="gray.6">
{seriesCount - series.length} more series omitted
&nbsp;&nbsp;&nbsp;&nbsp;
<Anchor
size="xs"
onClick={() => setMaxSeriesPerGroup(undefined)}
>
Show all series
</Anchor>
</Table.Td>
</Table.Tr>
)}
</Table.Tbody>
</Table>
</Box>
);
const noLHSMatches = lhs.length === 0;
const noRHSMatches = rhs.length === 0;
const groupColor = colorPool[mgIdx % colorPool.length];
const lhsTable = matchGroupTable(lhs, lhsCount, groupColor);
const rhsTable = matchGroupTable(
rhs,
rhsCount,
groupColor,
rhsColorOffset
);
const resultTable = (
<Box
style={{
borderRadius: 3,
border: `2px solid`,
borderColor:
noLHSMatches || noRHSMatches || 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 ? (
<Table.Tr>
<Table.Td
ta="center"
c="light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-5))"
py="md"
fw="bold"
>
dropped
</Table.Td>
</Table.Tr>
) : 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.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"
align="flex-start"
>
<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 ta="right">
{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 />}
>
{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>
</>
{numGroups > Object.keys(matchGroups).length && (
<Alert color="yellow" mb="md" icon={<IconAlertTriangle />}>
Too many match groups to display, only showing{" "}
{Object.keys(matchGroups).length} out of {numGroups} groups.
<br />
<br />
<Anchor fz="sm" onClick={() => setMaxGroups(undefined)}>
Show all groups
</Anchor>
</Alert>
)}
{errCount > 0 && (
<Alert color="yellow" mb="md" icon={<IconAlertTriangle />}>
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 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",
borderColor:
seriesCount === 0
? "light-dark(var(--mantine-color-gray-4), var(--mantine-color-gray-7))"
: color,
}}
>
<Table fz="xs" withRowBorders={false} verticalSpacing={5}>
<Table.Tbody>
{seriesCount === 0 ? (
<Table.Tr>
<Table.Td
ta="center"
c="light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-5))"
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 ta="right">{s.value[1]}</Table.Td>
)}
</Table.Tr>
);
})}
</>
)}
{seriesCount > series.length && (
<Table.Tr>
<Table.Td ta="center" py="md" fw="bold" c="gray.6">
{seriesCount - series.length} more series omitted
&nbsp;&nbsp;&nbsp;&nbsp;
<Anchor
size="xs"
onClick={() => setMaxSeriesPerGroup(undefined)}
>
Show all series
</Anchor>
</Table.Td>
</Table.Tr>
)}
</Table.Tbody>
</Table>
</Box>
);
const groupColor = colorPool[mgIdx % colorPool.length];
const lhsTable = matchGroupTable(lhs, lhsCount, groupColor);
const rhsTable = matchGroupTable(
rhs,
rhsCount,
groupColor,
rhsColorOffset
);
const resultTable = (
<Box
style={{
borderRadius: 3,
border: `2px solid`,
borderColor:
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>
{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"
c="light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-5))"
py="md"
fw="bold"
>
dropped
</Table.Td>
</Table.Tr>
) : 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.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"
align="flex-start"
>
<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 ta="right">
{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 />}
>
{explainError(node, mg, error)}
</Alert>
)}
</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td valign="middle" p={0}>
{lhsTable}
</Table.Td>
<Table.Td
ta="center"
fw={isSetOperator(node.op) ? "bold" : undefined}
>
{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>
</>
);
};

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,25 +340,35 @@ 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) {
if (mg.lhs.length > 1 && mg.rhs.length > 1) {
mg.error = { type: MatchErrorType.multipleMatchesOnBothSides };
} else if (mg.lhs.length > 1 || mg.rhs.length > 1) {
mg.error = {
type: MatchErrorType.multipleMatchesForOneToOneMatching,
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.
mg.error = {
type: MatchErrorType.multipleMatchesOnOneSide,
};
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) {
mg.error = {
type: MatchErrorType.multipleMatchesForOneToOneMatching,
dupeSide: mg.lhs.length > 1 ? "left" : "right",
};
}
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) {
@ -368,42 +378,79 @@ export const computeVectorVectorBinOp = (
return;
}
// Calculate the results for this match group.
mg.rhs.forEach((rs) => {
if (isSetOperator(op)) {
// Add LHS samples to the result, depending on specific operator condition and RHS length.
mg.lhs.forEach((ls, lIdx) => {
if (!ls.value || !rs.value) {
// TODO: Implement native histogram support.
throw new Error("native histogram support not implemented yet");
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,
});
}
});
const [vl, vr] =
matching.card !== vectorMatchCardinality.oneToMany
? [ls.value[1], rs.value[1]]
: [rs.value[1], ls.value[1]];
let { value, keep } = vectorElemBinop(
op,
parsePrometheusFloat(vl),
parsePrometheusFloat(vr)
);
// 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) => {
if (!ls.value || !rs.value) {
// TODO: Implement native histogram support.
throw new Error("native histogram support not implemented yet");
}
const metric = resultMetric(ls.metric, rs.metric, op, matching);
if (bool) {
value = keep ? 1.0 : 0.0;
delete metric.__name__;
}
const [vl, vr] =
matching.card !== vectorMatchCardinality.oneToMany
? [ls.value[1], rs.value[1]]
: [rs.value[1], ls.value[1]];
mg.result.push({
sample: {
metric: metric,
value: [
ls.value[0],
keep || bool ? formatPrometheusFloat(value) : filteredSampleValue,
],
},
manySideIdx: lIdx,
let { value, keep } = vectorElemBinop(
op,
parsePrometheusFloat(vl),
parsePrometheusFloat(vr)
);
const metric = resultMetric(ls.metric, rs.metric, op, matching);
if (bool) {
value = keep ? 1.0 : 0.0;
delete metric.__name__;
}
mg.result.push({
sample: {
metric: metric,
value: [
ls.value[0],
keep || bool
? formatPrometheusFloat(value)
: filteredSampleValue,
],
},
manySideIdx: lIdx,
});
});
});
});
}
});
// If we originally swapped the LHS and RHS, swap them back to the original order.