From 13b55ffc811c3787bf6cde9c1b61f2df179911f4 Mon Sep 17 00:00:00 2001 From: Julius Volz Date: Thu, 24 Jul 2025 13:54:58 +0200 Subject: [PATCH] UI: More inclusive metadata handling for _count/_sum/_bucket suffixes (#16910) Although these suffixes always need to be removed before querying metadata for metrics that follow the Prometheus naming best practices, there can also be metrics that don't follow these naming practices and have these suffixes without being part of either a histogram or a summary metric. Fixes https://github.com/prometheus/prometheus/issues/16907 Signed-off-by: Julius Volz --- .../src/pages/query/ExplainViews/Selector.tsx | 30 ++++++++++++------ .../query/MetricsExplorer/MetricsExplorer.tsx | 31 +++++++++++++++---- .../codemirror-promql/src/complete/hybrid.ts | 8 +++-- 3 files changed, 51 insertions(+), 18 deletions(-) diff --git a/web/ui/mantine-ui/src/pages/query/ExplainViews/Selector.tsx b/web/ui/mantine-ui/src/pages/query/ExplainViews/Selector.tsx index 1b71e3591d..ca575ca2f4 100644 --- a/web/ui/mantine-ui/src/pages/query/ExplainViews/Selector.tsx +++ b/web/ui/mantine-ui/src/pages/query/ExplainViews/Selector.tsx @@ -128,13 +128,28 @@ const matchingCriteriaList = ( const SelectorExplainView: FC = ({ node }) => { const baseMetricName = node.name.replace(/(_count|_sum|_bucket)$/, ""); const { lookbackDelta } = useSettings(); - const { data: metricMeta } = useSuspenseAPIQuery({ + + // Try to get metadata for the full unchanged metric name first. + const { data: fullMetricMeta } = useSuspenseAPIQuery({ + path: `/metadata`, + params: { + metric: node.name, + }, + }); + + // Also get prefix-stripped metric metadata in case the metadata only exists for + // the histogram / summary base metric name. + const { data: baseMetricMeta } = useSuspenseAPIQuery({ path: `/metadata`, params: { metric: baseMetricName, }, }); + // Determine which metadata to use. + const metricMeta = + fullMetricMeta.data[node.name] ?? baseMetricMeta.data[baseMetricName]; + return ( @@ -142,17 +157,13 @@ const SelectorExplainView: FC = ({ node }) => { selector - {metricMeta.data === undefined || - metricMeta.data[baseMetricName] === undefined || - metricMeta.data[baseMetricName].length < 1 ? ( + {metricMeta === undefined || metricMeta.length < 1 ? ( <>No metric metadata found. ) : ( <> - Metric help:{" "} - {metricMeta.data[baseMetricName][0].help} + Metric help: {metricMeta[0].help}
- Metric type:{" "} - {metricMeta.data[baseMetricName][0].type} + Metric type: {metricMeta[0].type} )}
@@ -161,7 +172,8 @@ const SelectorExplainView: FC = ({ node }) => { {node.type === nodeType.vectorSelector ? ( <> This node selects the latest (non-stale) sample value within the - last {lookbackDelta} + last{" "} + {lookbackDelta} ) : ( <> diff --git a/web/ui/mantine-ui/src/pages/query/MetricsExplorer/MetricsExplorer.tsx b/web/ui/mantine-ui/src/pages/query/MetricsExplorer/MetricsExplorer.tsx index 600dfbae02..9c33a3df75 100644 --- a/web/ui/mantine-ui/src/pages/query/MetricsExplorer/MetricsExplorer.tsx +++ b/web/ui/mantine-ui/src/pages/query/MetricsExplorer/MetricsExplorer.tsx @@ -1,11 +1,23 @@ import { FC, useMemo, useState } from "react"; import { useSuspenseAPIQuery } from "../../../api/api"; import { MetadataResult } from "../../../api/responseTypes/metadata"; -import { ActionIcon, CopyButton, Group, Stack, Table, TextInput } from "@mantine/core"; +import { + ActionIcon, + CopyButton, + Group, + Stack, + Table, + TextInput, +} from "@mantine/core"; import React from "react"; import { Fuzzy } from "@nexucis/fuzzy"; import sanitizeHTML from "sanitize-html"; -import { IconCheck, IconCodePlus, IconCopy, IconZoomCode } from "@tabler/icons-react"; +import { + IconCheck, + IconCodePlus, + IconCopy, + IconZoomCode, +} from "@tabler/icons-react"; import LabelsExplorer from "./LabelsExplorer"; import { useDebouncedValue } from "@mantine/hooks"; import classes from "./MetricsExplorer.module.css"; @@ -55,10 +67,17 @@ const MetricsExplorer: FC = ({ return getSearchMatches(debouncedFilterText, metricNames); }, [debouncedFilterText, metricNames]); - const getMeta = (m: string) => - data.data[m.replace(/(_count|_sum|_bucket)$/, "")] || [ - { help: "unknown", type: "unknown", unit: "unknown" }, - ]; + const getMeta = (m: string) => { + return ( + // First check if the full metric name has metadata (even if it has one of the + // histogram/summary suffixes, it may be a metric that is not following naming + // conventions, see https://github.com/prometheus/prometheus/issues/16907). + data.data[m] ?? + data.data[m.replace(/(_count|_sum|_bucket)$/, "")] ?? [ + { help: "unknown", type: "unknown", unit: "unknown" }, + ] + ); + }; if (selectedMetric !== null) { return ( diff --git a/web/ui/module/codemirror-promql/src/complete/hybrid.ts b/web/ui/module/codemirror-promql/src/complete/hybrid.ts index 05650a2952..207173fca5 100644 --- a/web/ui/module/codemirror-promql/src/complete/hybrid.ts +++ b/web/ui/module/codemirror-promql/src/complete/hybrid.ts @@ -664,9 +664,11 @@ export class HybridComplete implements CompleteStrategy { .then((metricMetadata) => { if (metricMetadata) { for (const [metricName, node] of metricCompletion) { - // For histograms and summaries, the metadata is only exposed for the base metric name, - // not separately for the _count, _sum, and _bucket time series. - const metadata = metricMetadata[metricName.replace(/(_count|_sum|_bucket)$/, '')]; + // First check if the full metric name has metadata (even if it has one of the + // histogram/summary suffixes, it may be a metric that is not following naming + // conventions, see https://github.com/prometheus/prometheus/issues/16907). + // Then fall back to the base metric name if full metadata doesn't exist. + const metadata = metricMetadata[metricName] ?? metricMetadata[metricName.replace(/(_count|_sum|_bucket)$/, '')]; if (metadata) { if (metadata.length > 1) { // it means the metricName has different possible helper and type