This adds: * A `ScrapePoolConfig()` method to the scrape manager that allows getting the scrape config for a given pool. * An API endpoint at `/api/v1/targets/relabel_steps` that takes a pool name and a label set of a target and returns a detailed list of applied relabeling rules and their output for each step. * A "show relabeling" link/button for each target on the discovery page that shows the detailed flow of all relabeling rules (based on the API response) for that target. Note that this changes the JSON encoding of the relabeling rule config struct to output the original snake_case (instead of camelCase) field names, and before merging, we need to be sure that's ok :) See my comment about that at https://github.com/prometheus/prometheus/pull/15383#issuecomment-3405591487 Fixes https://github.com/prometheus/prometheus/issues/17283 Signed-off-by: Julius Volz <julius.volz@gmail.com>pull/17337/head
parent
fd421dc3b1
commit
8b1bd7d6c3
@ -0,0 +1,13 @@ |
||||
import { Labels } from "./targets"; |
||||
|
||||
export type RelabelStep = { |
||||
rule: { [key: string]: unknown }; |
||||
output: Labels; |
||||
keep: boolean; |
||||
}; |
||||
|
||||
// Result type for /api/v1/relabel_steps endpoint.
|
||||
// See: https://prometheus.io/docs/prometheus/latest/querying/api/#relabel_steps
|
||||
export type RelabelStepsResult = { |
||||
steps: RelabelStep[]; |
||||
}; |
||||
@ -0,0 +1,188 @@ |
||||
import { em, Group, Stack, Table, Text } from "@mantine/core"; |
||||
import { useSuspenseAPIQuery } from "../../api/api"; |
||||
import { RelabelStepsResult } from "../../api/responseTypes/relabel_steps"; |
||||
import { Labels } from "../../api/responseTypes/targets"; |
||||
import React from "react"; |
||||
import { |
||||
IconArrowDown, |
||||
IconCircleMinus, |
||||
IconCirclePlus, |
||||
IconReplace, |
||||
IconTags, |
||||
} from "@tabler/icons-react"; |
||||
|
||||
const iconStyle = { width: em(18), height: em(18), verticalAlign: "middle" }; |
||||
|
||||
const ruleTable = (rule: { [key: string]: unknown }, idx: number) => { |
||||
return ( |
||||
<Table |
||||
w="60%" |
||||
withTableBorder |
||||
withColumnBorders |
||||
bg="light-dark(var(--mantine-color-gray-0), var(--mantine-color-gray-8))" |
||||
> |
||||
<Table.Thead> |
||||
<Table.Tr |
||||
bg={ |
||||
"light-dark(var(--mantine-color-gray-1), var(--mantine-color-gray-7))" |
||||
} |
||||
> |
||||
<Table.Th colSpan={2}> |
||||
<Group gap="xs"> |
||||
<IconReplace style={iconStyle} /> Rule {idx + 1} |
||||
</Group> |
||||
</Table.Th> |
||||
</Table.Tr> |
||||
</Table.Thead> |
||||
<Table.Tbody> |
||||
{Object.entries(rule) |
||||
.sort(([a], [b]) => { |
||||
// Sort by a predefined order for known fields, otherwise alphabetically.
|
||||
const sortedRuleFieldNames: string[] = [ |
||||
"action", |
||||
"source_labels", |
||||
"regex", |
||||
"modulus", |
||||
"replacement", |
||||
"target_label", |
||||
]; |
||||
const ai = sortedRuleFieldNames.indexOf(a); |
||||
const bi = sortedRuleFieldNames.indexOf(b); |
||||
if (ai === -1 && bi === -1) { |
||||
return a.localeCompare(b); |
||||
} |
||||
if (ai === -1) { |
||||
return 1; |
||||
} |
||||
if (bi === -1) { |
||||
return -1; |
||||
} |
||||
return ai - bi; |
||||
}) |
||||
.map(([k, v]) => ( |
||||
<Table.Tr key={k}> |
||||
<Table.Th> |
||||
<code>{k}</code> |
||||
</Table.Th> |
||||
<Table.Td> |
||||
<code> |
||||
{typeof v === "object" ? JSON.stringify(v) : String(v)} |
||||
</code> |
||||
</Table.Td> |
||||
</Table.Tr> |
||||
))} |
||||
</Table.Tbody> |
||||
</Table> |
||||
); |
||||
}; |
||||
|
||||
const labelsTable = (labels: Labels, prevLabels: Labels | null) => { |
||||
if (labels === null) { |
||||
return <Text c="dimmed">dropped</Text>; |
||||
} |
||||
|
||||
return ( |
||||
<Table w="50%" withTableBorder> |
||||
<Table.Thead> |
||||
<Table.Tr |
||||
bg={ |
||||
"light-dark(var(--mantine-color-gray-1), var(--mantine-color-gray-7))" |
||||
} |
||||
> |
||||
<Table.Th colSpan={3}> |
||||
<Group gap="xs"> |
||||
<IconTags style={iconStyle} /> Labels |
||||
</Group> |
||||
</Table.Th> |
||||
</Table.Tr> |
||||
</Table.Thead> |
||||
<Table.Tbody> |
||||
{Object.entries(labels) |
||||
.concat( |
||||
prevLabels |
||||
? Object.entries(prevLabels).filter( |
||||
([k]) => labels[k] === undefined |
||||
) |
||||
: [] |
||||
) |
||||
.sort(([a], [b]) => a.localeCompare(b)) |
||||
.map(([k, v]) => { |
||||
const added = prevLabels !== null && prevLabels[k] === undefined; |
||||
const changed = |
||||
prevLabels !== null && !added && prevLabels[k] !== v; |
||||
const removed = |
||||
prevLabels !== null && |
||||
!changed && |
||||
prevLabels[k] !== undefined && |
||||
labels[k] === undefined; |
||||
return ( |
||||
<Table.Tr |
||||
key={k} |
||||
bg={ |
||||
added |
||||
? "light-dark(var(--mantine-color-green-1), var(--mantine-color-green-8))" |
||||
: changed |
||||
? "light-dark(var(--mantine-color-blue-1), var(--mantine-color-blue-8))" |
||||
: removed |
||||
? "light-dark(var(--mantine-color-red-1), var(--mantine-color-red-8))" |
||||
: undefined |
||||
} |
||||
> |
||||
<Table.Td w={40}> |
||||
{added ? ( |
||||
<IconCirclePlus style={iconStyle} /> |
||||
) : changed ? ( |
||||
<IconReplace style={iconStyle} /> |
||||
) : removed ? ( |
||||
<IconCircleMinus style={iconStyle} /> |
||||
) : null} |
||||
</Table.Td> |
||||
<Table.Th> |
||||
<code>{k}</code> |
||||
</Table.Th> |
||||
<Table.Td> |
||||
<code>{v}</code> |
||||
</Table.Td> |
||||
</Table.Tr> |
||||
); |
||||
})} |
||||
</Table.Tbody> |
||||
</Table> |
||||
); |
||||
}; |
||||
|
||||
const RelabelSteps: React.FC<{ |
||||
labels: Labels; |
||||
pool: string; |
||||
}> = ({ labels, pool }) => { |
||||
const { data } = useSuspenseAPIQuery<RelabelStepsResult>({ |
||||
path: `/targets/relabel_steps`, |
||||
params: { |
||||
labels: JSON.stringify(labels), |
||||
scrapePool: pool, |
||||
}, |
||||
}); |
||||
|
||||
return ( |
||||
<Stack align="center"> |
||||
{labelsTable(labels, null)} |
||||
{data.data.steps.map((step, idx) => ( |
||||
<React.Fragment key={idx}> |
||||
<IconArrowDown style={iconStyle} /> |
||||
{ruleTable(step.rule, idx)} |
||||
<IconArrowDown style={iconStyle} /> |
||||
{step.keep ? ( |
||||
labelsTable( |
||||
step.output, |
||||
idx === 0 ? labels : data.data.steps[idx - 1].output |
||||
) |
||||
) : ( |
||||
<Text c="dimmed">dropped</Text> |
||||
)} |
||||
</React.Fragment> |
||||
))} |
||||
</Stack> |
||||
); |
||||
}; |
||||
|
||||
export default RelabelSteps; |
||||
Loading…
Reference in new issue