This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new 40f8af23c fix(metrics): keep scalar and histogram samples on separate
series (#4358)
40f8af23c is described below
commit 40f8af23c90409209a378b9c17b707de5049b160
Author: btlqql <[email protected]>
AuthorDate: Wed Sep 16 16:01:38 2026 +0800
fix(metrics): keep scalar and histogram samples on separate series (#4358)
---
web/src/components/MetricsExplorer.tsx | 31 +++++++++++++-------
.../components/__tests__/MetricsExplorer.test.tsx | 10 ++++---
web/src/utils/metricsExplorerDiagnostics.test.ts | 34 ++++++++++++++++++++++
web/src/utils/metricsExplorerDiagnostics.ts | 26 ++++++++---------
4 files changed, 73 insertions(+), 28 deletions(-)
diff --git a/web/src/components/MetricsExplorer.tsx
b/web/src/components/MetricsExplorer.tsx
index fc205f7f9..3e066c538 100644
--- a/web/src/components/MetricsExplorer.tsx
+++ b/web/src/components/MetricsExplorer.tsx
@@ -122,14 +122,23 @@ const MetricChart = ({
hiddenSeriesText,
}: MetricChartProps) => {
const allSeries = data.series
- .map((series, index) => {
- const { samples, fromHistogram } = toMetricSeriesSamples(series);
- return {
- color: SERIES_COLORS[index % SERIES_COLORS.length],
- label: metricSeriesLabel(series, metric.name),
- samples,
- fromHistogram,
- };
+ .flatMap((series, index) => {
+ const { samples } = toMetricSeriesSamples(series);
+ const baseLabel = metricSeriesLabel(series, metric.name);
+ const isMixed =
+ samples.some((sample) => sample.kind === 'scalar') &&
+ samples.some((sample) => sample.kind === 'histogram');
+ // Keep raw floats and histogram-derived trends on separate lines.
+ return (['scalar', 'histogram'] as const).map((kind, kindIndex) => ({
+ color: SERIES_COLORS[
+ (isMixed ? index * 2 + kindIndex : index) % SERIES_COLORS.length
+ ],
+ label: isMixed
+ ? `${baseLabel} (${kind === 'histogram' ? histogramLabel :
'scalar'})`
+ : baseLabel,
+ samples: samples.filter((sample) => sample.kind === kind),
+ fromHistogram: kind === 'histogram',
+ }));
})
.filter((series) => series.samples.length > 0);
@@ -210,7 +219,7 @@ const MetricChart = ({
})}
{chartSeries.map((series) => (
<polyline
- key={series.label}
+ key={`${series.label}-${series.fromHistogram}`}
fill="none"
stroke={series.color}
strokeWidth="2.5"
@@ -246,7 +255,7 @@ const MetricChart = ({
const latest = series.samples[series.samples.length - 1];
return (
<Flex
- key={series.label}
+ key={`${series.label}-${series.fromHistogram}`}
align="center"
gap={6}
style={{ flex: '0 1 auto', minWidth: 0, maxWidth: '100%' }}
@@ -968,7 +977,7 @@ const MetricsExplorer = ({ instanceId }:
MetricsExplorerProps) => {
width: 130,
render: (value: MetricSeriesDetailRow['sampleType']) => (
<Tag color={value === 'histogram' ? 'purple' : 'blue'} style={{
marginInlineEnd: 0 }}>
- {value === 'histogram' ? copy.histogram : 'scalar'}
+ {value === 'histogram' ? copy.histogram : value === 'mixed' ?
`scalar + ${copy.histogram}` : 'scalar'}
</Tag>
),
},
diff --git a/web/src/components/__tests__/MetricsExplorer.test.tsx
b/web/src/components/__tests__/MetricsExplorer.test.tsx
index 2965b096d..8afd16a8b 100644
--- a/web/src/components/__tests__/MetricsExplorer.test.tsx
+++ b/web/src/components/__tests__/MetricsExplorer.test.tsx
@@ -551,14 +551,14 @@ describe('MetricsExplorer', () => {
expect(await screen.findByText('20 messages/s')).toBeInTheDocument();
});
- it('prefers scalar samples when a series has both values and histograms',
async () => {
+ it('shows separate scalar and histogram trends for a mixed series', async ()
=> {
vi.mocked(queryMetrics).mockResolvedValue({
...metricData,
series: [
{
...metricData.series[0],
histograms: [
- { timestamp: 1_800_000_000, histogram: { count: '99', sum: '999',
buckets: [] } },
+ { timestamp: 1_800_000_001, histogram: { count: '99', sum: '999',
buckets: [] } },
],
},
],
@@ -567,8 +567,10 @@ describe('MetricsExplorer', () => {
renderWithProviders(<MetricsExplorer />);
expect(await screen.findByText('42 messages/s')).toBeInTheDocument();
- expect(screen.queryByText('999 messages/s')).not.toBeInTheDocument();
- expect(screen.queryByText('直方图')).not.toBeInTheDocument();
+ expect(screen.getByText('999 messages/s')).toBeInTheDocument();
+ expect(screen.getByText('直方图')).toBeInTheDocument();
+ expect(screen.getByText(/broker-a.*\(scalar\)/)).toBeInTheDocument();
+ expect(screen.getByText(/broker-a.*\(直方图\)/)).toBeInTheDocument();
});
it('queries the selected data source through the datasource endpoint', async
() => {
diff --git a/web/src/utils/metricsExplorerDiagnostics.test.ts
b/web/src/utils/metricsExplorerDiagnostics.test.ts
index 717f9d8bf..60fd11e0e 100644
--- a/web/src/utils/metricsExplorerDiagnostics.test.ts
+++ b/web/src/utils/metricsExplorerDiagnostics.test.ts
@@ -88,6 +88,40 @@ const createHistoryEntry = (
});
describe('metrics explorer diagnostics', () => {
+ it('preserves both sample kinds in a mixed Prometheus matrix series', () => {
+ const data: MetricData = {
+ resultType: 'matrix',
+ warnings: [],
+ series: [{
+ labels: { instance: 'broker-a' },
+ values: [{ timestamp: 2, value: '12' }],
+ histograms: [
+ { timestamp: 3, histogram: { count: '4', sum: '20', buckets: [] } },
+ { timestamp: 1, histogram: { count: '2', sum: '10', buckets: [] } },
+ ],
+ }],
+ };
+
+ expect(toMetricSeriesSamples(data.series[0]).samples.map(({ timestamp,
kind }) => ({ timestamp, kind })))
+ .toEqual([
+ { timestamp: 1, kind: 'histogram' },
+ { timestamp: 2, kind: 'scalar' },
+ { timestamp: 3, kind: 'histogram' },
+ ]);
+ expect(summarizeMetricData(data)).toMatchObject({
+ seriesCount: 1, visibleSeriesCount: 1, sampleCount: 3,
+ scalarSampleCount: 1, histogramSampleCount: 2,
+ earliestTimestamp: 1, latestTimestamp: 3,
+ });
+ expect(buildMetricSeriesDetailRows(data, metric)[0]).toMatchObject({
+ sampleType: 'mixed', sampleCount: 3, latestTimestamp: 3,
+ latestValue: 20, histogramCount: 4, histogramSum: 20,
+ });
+ const rows = buildMetricCsvRows(data, metric, { profileName: 'test',
sourceName: 'test' });
+ expect(rows.map((row) => row.sampleType)).toEqual(['histogram', 'scalar',
'histogram']);
+ expect(rows.map((row) => row.timestamp)).toEqual([1, 2, 3]);
+ });
+
it('sorts scalar samples and ignores non-numeric values', () => {
const samples = toMetricSeriesSamples(metricData.series[0]);
diff --git a/web/src/utils/metricsExplorerDiagnostics.ts
b/web/src/utils/metricsExplorerDiagnostics.ts
index 6a37641ed..8ae1980cf 100644
--- a/web/src/utils/metricsExplorerDiagnostics.ts
+++ b/web/src/utils/metricsExplorerDiagnostics.ts
@@ -84,7 +84,7 @@ export interface MetricSeriesDetailRow {
seriesIndex: number;
seriesLabel: string;
labels: string;
- sampleType: MetricSampleKind;
+ sampleType: MetricSampleKind | 'mixed';
sampleCount: number;
latestTimestamp?: number;
latestValue?: number;
@@ -166,7 +166,7 @@ const toScalarSamples = (series: MetricSeries):
NumericMetricSample[] =>
})),
);
-// Native histograms carry no scalar samples. To keep diagnostics usable,
derive
+// To keep native histogram diagnostics usable, derive
// a trend value from the observed sum and fall back to observation count.
const toHistogramSamples = (series: MetricSeries): NumericMetricSample[] =>
sortMetricSamples(
@@ -186,10 +186,11 @@ const toHistogramSamples = (series: MetricSeries):
NumericMetricSample[] =>
export const toMetricSeriesSamples = (series: MetricSeries):
MetricSeriesSamples => {
const scalar = toScalarSamples(series);
- if (scalar.length > 0) {
- return { samples: scalar, fromHistogram: false };
- }
- return { samples: toHistogramSamples(series), fromHistogram: true };
+ const histograms = toHistogramSamples(series);
+ return {
+ samples: sortMetricSamples([...scalar, ...histograms]),
+ fromHistogram: histograms.length > 0,
+ };
};
export const summarizeMetricData = (data: MetricData): MetricResultSummary => {
@@ -200,15 +201,12 @@ export const summarizeMetricData = (data: MetricData):
MetricResultSummary => {
let latestTimestamp: number | undefined;
data.series.forEach((series) => {
- const { samples, fromHistogram } = toMetricSeriesSamples(series);
+ const { samples } = toMetricSeriesSamples(series);
if (samples.length === 0) return;
visibleSeriesCount += 1;
- if (fromHistogram) {
- histogramSampleCount += samples.length;
- } else {
- scalarSampleCount += samples.length;
- }
samples.forEach((sample) => {
+ if (sample.kind === 'histogram') histogramSampleCount += 1;
+ else scalarSampleCount += 1;
earliestTimestamp =
earliestTimestamp === undefined
? sample.timestamp
@@ -322,7 +320,9 @@ export const buildMetricSeriesDetailRows = (
seriesIndex: seriesIndex + 1,
seriesLabel: metricSeriesLabel(series, metric.name),
labels: stableLabelsText(series.labels),
- sampleType: fromHistogram ? 'histogram' : 'scalar',
+ sampleType: fromHistogram
+ ? samples.some((sample) => sample.kind === 'scalar') ? 'mixed' :
'histogram'
+ : 'scalar',
sampleCount: samples.length,
latestTimestamp: latest?.timestamp,
latestValue: latest?.value,