This is an automated email from the ASF dual-hosted git repository.

zqr10159 pushed a commit to branch 2.0.0
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git


The following commit(s) were added to refs/heads/2.0.0 by this push:
     new 713e003d69 Use source-backed OTLP metrics inventory
713e003d69 is described below

commit 713e003d6988370c0eef7931fb57bfa69e061708
Author: Logic <[email protected]>
AuthorDate: Wed Jun 10 07:53:55 2026 +0800

    Use source-backed OTLP metrics inventory
---
 .../ingestion/otlp/metrics/otlp-metrics-page.tsx   |  45 +++++++--
 web-next/app/ingestion/otlp/metrics/page.test.tsx  | 106 ++++++++++++++++++++-
 web-next/lib/otlp-metrics/view-model.test.ts       |  54 +++++++++++
 web-next/lib/otlp-metrics/view-model.ts            |  62 +++++++++++-
 4 files changed, 253 insertions(+), 14 deletions(-)

diff --git a/web-next/app/ingestion/otlp/metrics/otlp-metrics-page.tsx 
b/web-next/app/ingestion/otlp/metrics/otlp-metrics-page.tsx
index 74aca8e8a2..431c375dcd 100644
--- a/web-next/app/ingestion/otlp/metrics/otlp-metrics-page.tsx
+++ b/web-next/app/ingestion/otlp/metrics/otlp-metrics-page.tsx
@@ -12,7 +12,7 @@ import { useI18n } from 
'@/components/providers/i18n-provider';
 import { apiMessageGet } from '@/lib/api-client';
 import { copyTextToClipboard } from '@/lib/browser-clipboard';
 import { formatTime } from '@/lib/format';
-import { buildOtlpMetricsConsoleUrl, loadOtlpMetricsConsole, 
queryStateFromParams, type OtlpMetricsQueryState } from 
'@/lib/otlp-metrics/controller';
+import { buildOtlpMetricsConsoleUrl, buildOtlpMetricsInventoryUrl, 
loadOtlpMetricsConsole, loadOtlpMetricsInventory, queryStateFromParams, type 
OtlpMetricsQueryState } from '@/lib/otlp-metrics/controller';
 import { buildOtlpMetricsCsv, buildOtlpMetricsExportFilename, 
buildOtlpMetricsJsonl, type OtlpMetricsExportFormat, type 
OtlpMetricsExportScope } from '@/lib/otlp-metrics/export';
 import {
   createSignalDashboardPanelDraft,
@@ -43,6 +43,7 @@ import {
   buildMetricSeriesRows,
   buildMetricSeriesSampleRows,
   buildMetricSeriesViews,
+  buildMetricInventorySourceRows,
   buildMetricInventoryRows,
   applyMetricsFormula,
   buildMetricsChartOption,
@@ -55,7 +56,7 @@ import {
   type OtlpMetricInventorySort,
   type OtlpMetricSeriesView
 } from '@/lib/otlp-metrics/view-model';
-import type { OtlpMetricsConsole } from '@/lib/types';
+import type { OtlpMetricsConsole, OtlpMetricsInventory } from '@/lib/types';
 import { buildOtlpMetricsRoute, hasMetricsDisplayReturnLabel } from 
'./route-state';
 
 type OtlpMetricsTranslate = ReturnType<typeof useI18n>['t'];
@@ -63,6 +64,9 @@ type OtlpMetricsTranslate = ReturnType<typeof useI18n>['t'];
 type MetricsSavedQueryView = SignalSavedQueryView;
 type MetricsDashboardPanelDraftState = 'idle' | 'saving' | 'saved' | 'failed';
 type MetricAttributeOperator = 'filter' | 'contains' | 'not-contains' | 'in' | 
'not-in' | 'exclude' | 'exists' | 'not-exists' | 'replace' | 'group';
+type OtlpMetricsWorkbenchData = OtlpMetricsConsole & {
+  inventory?: OtlpMetricsInventory | null;
+};
 
 const METRICS_SAVED_QUERY_VIEW_STORAGE_KEY = 
'hertzbeat.otlp-metrics.saved-query-views';
 const METRICS_SAVED_QUERY_VIEW_LIMIT = 5;
@@ -463,8 +467,17 @@ export default function OtlpMetricsPage() {
     router.replace(appendMetricsPanelEditContext(route, panelEditContext));
   }, [panelEditContext, router]);
   const currentMetricsRoute = useMemo(() => buildOtlpMetricsRoute(query), 
[query]);
-  const workbenchCacheKey = useMemo(() => buildOtlpMetricsConsoleUrl(query), 
[query]);
-  const load = useCallback(async (): Promise<OtlpMetricsConsole> => 
loadOtlpMetricsConsole(apiMessageGet, query), [query]);
+  const workbenchCacheKey = useMemo(
+    () => 
`${buildOtlpMetricsConsoleUrl(query)}|${buildOtlpMetricsInventoryUrl(query)}`,
+    [query]
+  );
+  const load = useCallback(async (): Promise<OtlpMetricsWorkbenchData> => {
+    const [consoleData, inventory] = await Promise.all([
+      loadOtlpMetricsConsole(apiMessageGet, query),
+      loadOtlpMetricsInventory(apiMessageGet, query).catch(() => null)
+    ]);
+    return { ...consoleData, inventory };
+  }, [query]);
   const initialDraft = useMemo(() => ({
     query: query.query || '',
     filter: query.filter || '',
@@ -1260,7 +1273,23 @@ export default function OtlpMetricsPage() {
           pointCount: metricSeries[index]?.points.length ?? 0,
           series: metricSeries[index]
         }));
-        const metricInventoryRows = 
buildMetricInventoryRows(metricSeriesTableRows, metricInventorySearch, 
metricInventorySort);
+        const sourceMetricInventoryRows = 
buildMetricInventorySourceRows(mergedData.inventory, t).map((row, index) => {
+          const matchingSeriesRow = metricSeriesTableRows.find(seriesRow =>
+            seriesRow.title === row.title || seriesRow.series?.name === 
row.title
+          );
+          return {
+            ...(matchingSeriesRow || {}),
+            ...row,
+            rowKey: matchingSeriesRow?.rowKey || 
`inventory-${row.title}-${index}`,
+            series: matchingSeriesRow?.series || row.series || null,
+            meta: matchingSeriesRow?.meta || row.meta,
+            pointCount: matchingSeriesRow?.pointCount ?? row.pointCount,
+            sampleCount: matchingSeriesRow?.sampleCount ?? row.sampleCount,
+            timeSeriesCount: row.timeSeriesCount ?? 
matchingSeriesRow?.timeSeriesCount
+          };
+        });
+        const metricInventoryBaseRows = sourceMetricInventoryRows.length > 0 ? 
sourceMetricInventoryRows : metricSeriesTableRows;
+        const metricInventoryRows = 
buildMetricInventoryRows(metricInventoryBaseRows, metricInventorySearch, 
metricInventorySort);
         const metricInventoryPageSizeNumber = Number(metricInventoryPageSize);
         const metricInventoryTotalPages = Math.max(1, 
Math.ceil(metricInventoryRows.length / metricInventoryPageSizeNumber));
         const clampedMetricInventoryPageIndex = 
Math.min(Number(metricInventoryPageIndex), metricInventoryTotalPages - 1);
@@ -1276,8 +1305,8 @@ export default function OtlpMetricsPage() {
           total: metricInventoryRows.length
         });
         const metricInventorySummary = metricInventorySearch.trim()
-          ? t('otlp.metrics.inventory.filtered-count', { filtered: 
metricInventoryRows.length, total: metricSeriesTableRows.length })
-          : t('otlp.metrics.scope.series-count', { count: 
metricSeriesTableRows.length });
+          ? t('otlp.metrics.inventory.filtered-count', { filtered: 
metricInventoryRows.length, total: metricInventoryBaseRows.length })
+          : t('otlp.metrics.scope.series-count', { count: 
metricInventoryBaseRows.length });
         const firstSeries = (selectedMetricSeriesIndex >= 0 ? 
seriesRows[selectedMetricSeriesIndex] : undefined) ?? seriesRows[0] ?? {
           title: mergedData.query || t('otlp.metrics.query.unselected'),
           copy: mergedData.context?.serviceName || routeContext.serviceName || 
'-',
@@ -2262,7 +2291,7 @@ export default function OtlpMetricsPage() {
                     data-otlp-metrics-inventory="metric-inventory"
                     data-otlp-metrics-inventory-owner="hertzbeat-ui-data-table"
                     
data-otlp-metrics-inventory-filtered-count={metricInventoryRows.length}
-                    
data-otlp-metrics-inventory-total-count={metricSeriesTableRows.length}
+                    
data-otlp-metrics-inventory-total-count={metricInventoryBaseRows.length}
                     
data-otlp-metrics-inventory-page-size={metricInventoryPageSize}
                     
data-otlp-metrics-inventory-page-index={clampedMetricInventoryPageIndex}
                     variant="embedded"
diff --git a/web-next/app/ingestion/otlp/metrics/page.test.tsx 
b/web-next/app/ingestion/otlp/metrics/page.test.tsx
index a60c5a5c7b..68df062b36 100644
--- a/web-next/app/ingestion/otlp/metrics/page.test.tsx
+++ b/web-next/app/ingestion/otlp/metrics/page.test.tsx
@@ -30,6 +30,7 @@ const mockState = vi.hoisted(() => ({
 
 const apiMessageGet = vi.fn();
 const loadOtlpMetricsConsole = vi.fn();
+const loadOtlpMetricsInventory = vi.fn();
 const zhT = createTranslatorMock({ locale: 'zh-CN' });
 const originalFetch = globalThis.fetch;
 
@@ -107,6 +108,7 @@ vi.mock('@/lib/format', () => ({
 
 vi.mock('@/lib/otlp-metrics/controller', () => ({
   loadOtlpMetricsConsole,
+  loadOtlpMetricsInventory,
   buildOtlpMetricsConsoleUrl: (query: Record<string, unknown> = {}) => {
     const params = new URLSearchParams();
     Object.entries(query).forEach(([key, value]) => {
@@ -125,6 +127,17 @@ vi.mock('@/lib/otlp-metrics/controller', () => ({
     const search = params.toString();
     return `/api/otlp/v1/metrics${search ? `?${search}` : ''}`;
   },
+  buildOtlpMetricsInventoryUrl: (query: Record<string, unknown> = {}) => {
+    const params = new URLSearchParams();
+    ['entityId', 'entityType', 'serviceName', 'serviceNamespace', 
'environment', 'start', 'end', 'limit'].forEach(key => {
+      const value = query[key];
+      if (value !== undefined && value !== null && value !== '') {
+        params.set(key, String(value));
+      }
+    });
+    const search = params.toString();
+    return `/api/otlp/v1/metrics/inventory${search ? `?${search}` : ''}`;
+  },
   queryStateFromParams: (params: { get(key: string): string | null }) => ({
     query: params.get('query') || undefined,
     series: params.get('series') || undefined,
@@ -150,6 +163,7 @@ vi.mock('@/lib/otlp-metrics/controller', () => ({
     collector: params.get('collector') || undefined,
     template: params.get('template') || undefined,
     entityId: params.get('entityId') || undefined,
+    entityType: params.get('entityType') || undefined,
     entityName: params.get('entityName') || undefined,
     returnTo: params.get('returnTo') || undefined,
     traceId: params.get('traceId') || undefined,
@@ -399,10 +413,40 @@ vi.mock('@/lib/otlp-metrics/view-model', () => ({
       : tZh('otlp.metrics.series.entity-missing'),
     entityState: series.labels['hertzbeat.entity_id'] || 
series.labels.hertzbeat_entity_id ? 'present' : 'missing'
   })),
+  buildMetricInventorySourceRows: (inventory: any) => (inventory?.items || 
[]).map((item: any) => ({
+    title: item.metricName,
+    copy: item.labels?.service_name || inventory.context?.serviceName || '-',
+    meta: '-',
+    description: '-',
+    metricType: item.family || '-',
+    unit: '-',
+    pointCount: 0,
+    sampleCount: 0,
+    timeSeriesCount: item.timeSeriesCount ?? 0,
+    latestObservedAt: item.latestObservedAt ?? null,
+    entityLabel: inventory.context?.entityName || '-',
+    entityMeta: inventory.context?.entityId
+      ? tZh('otlp.metrics.series.entity-id', { entityId: 
inventory.context.entityId })
+      : tZh('otlp.metrics.series.entity-missing'),
+    entityState: inventory.context?.entityId ? 'present' : 'missing',
+    inventorySource: inventory.source,
+    inventoryLabels: item.labels || {},
+    series: null
+  })),
   buildMetricInventoryRows: (rows: any[], search: string, sort: string) => {
     const normalizedSearch = search.trim().toLowerCase();
     const filteredRows = normalizedSearch
-      ? rows.filter(row => [row.title, row.copy, row.meta, row.entityLabel, 
row.entityMeta, row.series?.name, ...Object.values(row.series?.labels || 
{})].join(' ').toLowerCase().includes(normalizedSearch))
+      ? rows.filter(row => [
+          row.title,
+          row.copy,
+          row.meta,
+          row.entityLabel,
+          row.entityMeta,
+          row.inventorySource,
+          row.series?.name,
+          ...Object.values(row.series?.labels || {}),
+          ...Object.values(row.inventoryLabels || {})
+        ].join(' ').toLowerCase().includes(normalizedSearch))
       : [...rows];
     return filteredRows.sort((left, right) => {
       if (sort === 'latest') return (right.series?.latestValue ?? 
Number.NEGATIVE_INFINITY) - (left.series?.latestValue ?? 
Number.NEGATIVE_INFINITY);
@@ -464,7 +508,9 @@ beforeEach(() => {
   mockState.metricSeries = [];
   apiMessageGet.mockReset();
   loadOtlpMetricsConsole.mockReset();
+  loadOtlpMetricsInventory.mockReset();
   loadOtlpMetricsConsole.mockResolvedValue(mockState.renderData);
+  loadOtlpMetricsInventory.mockResolvedValue(null);
 });
 
 let interactionRoot: Root | null = null;
@@ -875,7 +921,9 @@ describe('otlp metrics page', () => {
     
expect(source).toContain('data-otlp-metrics-series-table-summary-owner="hertzbeat-ui-status-badge"');
     
expect(source).toContain('data-otlp-metrics-inventory-count="filtered-series"');
     expect(source).toContain('metricInventorySummary');
-    expect(source).toContain('buildMetricInventoryRows(metricSeriesTableRows, 
metricInventorySearch, metricInventorySort)');
+    
expect(source).toContain('buildMetricInventorySourceRows(mergedData.inventory, 
t)');
+    expect(source).toContain('const metricInventoryBaseRows = 
sourceMetricInventoryRows.length > 0 ? sourceMetricInventoryRows : 
metricSeriesTableRows');
+    
expect(source).toContain('buildMetricInventoryRows(metricInventoryBaseRows, 
metricInventorySearch, metricInventorySort)');
     expect(source).toContain('metricInventoryPageRows');
     expect(source).toContain('HzPaginationBar');
     expect(source).toContain("const metricInventorySearch = 
query.inventorySearch || ''");
@@ -906,7 +954,7 @@ describe('otlp metrics page', () => {
     expect(source).toContain('data-otlp-metrics-inventory="metric-inventory"');
     
expect(source).toContain('data-otlp-metrics-inventory-owner="hertzbeat-ui-data-table"');
     
expect(source).toContain('data-otlp-metrics-inventory-filtered-count={metricInventoryRows.length}');
-    
expect(source).toContain('data-otlp-metrics-inventory-total-count={metricSeriesTableRows.length}');
+    
expect(source).toContain('data-otlp-metrics-inventory-total-count={metricInventoryBaseRows.length}');
     
expect(source).toContain('data-otlp-metrics-inventory-page-size={metricInventoryPageSize}');
     
expect(source).toContain('data-otlp-metrics-inventory-page-index={clampedMetricInventoryPageIndex}');
     expect(source).toContain('rows={metricInventoryPageRows}');
@@ -2026,6 +2074,58 @@ describe('otlp metrics page', () => {
     }));
   });
 
+  it('loads source-backed metrics inventory with the same entity and service 
context', async () => {
+    mockState.searchParams = new 
URLSearchParams('entityId=7&entityType=service&serviceName=checkout&serviceNamespace=commerce&environment=prod&start=1000&end=2000&limit=20');
+    loadOtlpMetricsInventory.mockResolvedValue({
+      source: 'promql-inventory',
+      context: { entityId: 7, entityType: 'service', serviceName: 'checkout', 
serviceNamespace: 'commerce', environment: 'prod', start: 1000, end: 2000 },
+      items: [
+        {
+          metricName: 'http_server_duration',
+          family: 'latency',
+          timeSeriesCount: 3,
+          latestObservedAt: 2000,
+          labels: { service_name: 'checkout', http_route: '/checkout' }
+        }
+      ]
+    });
+    const { default: OtlpMetricsPage } = await import('./page');
+    renderToStaticMarkup(<OtlpMetricsPage />);
+    const data = await mockState.lastLoad?.();
+
+    expect(loadOtlpMetricsInventory).toHaveBeenCalledWith(apiMessageGet, 
expect.objectContaining({
+      entityId: '7',
+      entityType: 'service',
+      serviceName: 'checkout',
+      serviceNamespace: 'commerce',
+      environment: 'prod',
+      start: '1000',
+      end: '2000',
+      limit: '20'
+    }));
+    expect(data).toMatchObject({
+      inventory: {
+        source: 'promql-inventory',
+        items: [expect.objectContaining({ metricName: 'http_server_duration' 
})]
+      }
+    });
+  });
+
+  it('falls back to console metrics when the inventory endpoint is 
unavailable', async () => {
+    mockState.searchParams = new URLSearchParams('serviceName=checkout');
+    loadOtlpMetricsInventory.mockRejectedValue(new Error('inventory 
unavailable'));
+    const { default: OtlpMetricsPage } = await import('./page');
+    renderToStaticMarkup(<OtlpMetricsPage />);
+    const data = await mockState.lastLoad?.();
+
+    expect(loadOtlpMetricsConsole).toHaveBeenCalled();
+    expect(loadOtlpMetricsInventory).toHaveBeenCalled();
+    expect(data).toMatchObject({
+      datasource: 'prometheus',
+      inventory: null
+    });
+  });
+
   it('keeps the context handoff links when opened from traced entity context', 
async () => {
     mockState.searchParams = new URLSearchParams(
       
'entityId=7&entityName=checkout&returnTo=%2Foverview&returnLabel=Overview&traceId=trace-123&spanId=span-456&serviceName=checkout&serviceNamespace=payments&environment=prod&start=1712730000000&end=1712733600000'
diff --git a/web-next/lib/otlp-metrics/view-model.test.ts 
b/web-next/lib/otlp-metrics/view-model.test.ts
index 5951c8e1ce..41b0c6435c 100644
--- a/web-next/lib/otlp-metrics/view-model.test.ts
+++ b/web-next/lib/otlp-metrics/view-model.test.ts
@@ -7,6 +7,7 @@ import {
   buildContextRows,
   buildMetricsExplorerState,
   buildMetricsHandoffLinks,
+  buildMetricInventorySourceRows,
   buildMetricInventoryRows,
   buildMetricSeriesRows,
   buildMetricSeriesSampleRows,
@@ -504,6 +505,59 @@ describe('otlp metrics view model', () => {
     ]);
   });
 
+  it('builds source-backed metric inventory rows from the backend inventory 
contract', () => {
+    const rows = buildMetricInventorySourceRows(
+      {
+        source: 'promql-inventory',
+        context: {
+          entityId: 7,
+          entityType: 'service',
+          entityName: 'Checkout API',
+          serviceName: 'checkout',
+          serviceNamespace: 'commerce',
+          environment: 'prod',
+          start: 1000,
+          end: 2000
+        },
+        total: 1,
+        items: [
+          {
+            metricName: 'http_server_duration',
+            family: 'latency',
+            timeSeriesCount: 3,
+            latestObservedAt: 2000,
+            labels: {
+              service_name: 'checkout',
+              http_route: '/checkout'
+            }
+          }
+        ]
+      },
+      t
+    );
+
+    expect(rows).toEqual([
+      expect.objectContaining({
+        title: 'http_server_duration',
+        copy: 'checkout',
+        metricType: 'latency',
+        timeSeriesCount: 3,
+        latestObservedAt: 2000,
+        entityLabel: 'Checkout API',
+        entityMeta: t('otlp.metrics.series.entity-id', { entityId: '7' }),
+        inventorySource: 'promql-inventory',
+        inventoryLabels: {
+          service_name: 'checkout',
+          http_route: '/checkout'
+        },
+        series: null
+      })
+    ]);
+    expect(buildMetricInventoryRows(rows, 'checkout', 'latest').map(row => 
row.title)).toEqual([
+      'http_server_duration'
+    ]);
+  });
+
   it('builds SigNoz-style metric inventory metadata from real frame schema 
fields', () => {
     const rows = buildMetricSeriesRows(
       buildMetricSeriesViews(
diff --git a/web-next/lib/otlp-metrics/view-model.ts 
b/web-next/lib/otlp-metrics/view-model.ts
index ed52464839..c82769cf3f 100644
--- a/web-next/lib/otlp-metrics/view-model.ts
+++ b/web-next/lib/otlp-metrics/view-model.ts
@@ -12,7 +12,7 @@ import {
 import { buildChartDataZoomTimeContext, type ChartDataZoomRange, type 
TimeContext } from '../time-context';
 import type { OtlpMetricsQueryState } from './controller';
 import type { EChartsOption } from 'echarts';
-import type { OtlpMetricsConsole } from '@/lib/types';
+import type { OtlpMetricsConsole, OtlpMetricsInventory } from '@/lib/types';
 
 type Translator = (key: string, params?: Record<string, string | number | null 
| undefined>) => string;
 
@@ -35,12 +35,16 @@ export type OtlpMetricInventoryRow = {
   meta?: string;
   entityLabel?: string;
   entityMeta?: string;
+  entityState?: 'present' | 'missing';
   pointCount?: number;
   sampleCount?: number;
   timeSeriesCount?: number;
+  latestObservedAt?: number | null;
   description?: string;
   metricType?: string;
   unit?: string;
+  inventorySource?: string;
+  inventoryLabels?: Record<string, string>;
   series?: OtlpMetricSeriesView | null;
 };
 
@@ -620,10 +624,60 @@ export function buildMetricSeriesRows(seriesList: 
OtlpMetricSeriesView[], t: Tra
   });
 }
 
+export function buildMetricInventorySourceRows(
+  inventory: OtlpMetricsInventory | null | undefined,
+  t: Translator
+): OtlpMetricInventoryRow[] {
+  return (inventory?.items || [])
+    .map(item => {
+      const metricName = item.metricName?.trim();
+      if (!metricName) return null;
+      const labels = item.labels || {};
+      const context = inventory?.context || {};
+      const entityId = readEntityIdRouteParam(firstText(
+        labels['hertzbeat.entity_id'],
+        labels.hertzbeat_entity_id,
+        labels['entity.id'],
+        labels.entity_id,
+        context.entityId == null ? undefined : String(context.entityId)
+      ));
+      return {
+        title: metricName,
+        copy: firstText(labels['service.name'], labels.service_name, 
labels.serviceName, context.serviceName)
+          || t('otlp.metrics.series.unknown-service'),
+        meta: '-',
+        description: '-',
+        metricType: item.family || '-',
+        unit: '-',
+        pointCount: 0,
+        sampleCount: 0,
+        timeSeriesCount: item.timeSeriesCount ?? 0,
+        latestObservedAt: item.latestObservedAt ?? null,
+        entityLabel: firstText(
+          labels['hertzbeat.entity_name'],
+          labels.hertzbeat_entity_name,
+          labels['entity.name'],
+          labels.entity_name,
+          context.entityName,
+          entityId
+        ) || '-',
+        entityMeta: entityId ? t('otlp.metrics.series.entity-id', { entityId 
}) : t('otlp.metrics.series.entity-missing'),
+        entityState: entityId ? 'present' : 'missing',
+        inventorySource: inventory?.source || undefined,
+        inventoryLabels: labels,
+        series: null
+      } satisfies OtlpMetricInventoryRow;
+    })
+    .filter((row): row is OtlpMetricInventoryRow => row != null);
+}
+
 function buildMetricInventorySearchText(row: OtlpMetricInventoryRow) {
   const labelText = row.series
     ? Object.entries(row.series.labels).flatMap(([key, value]) => [key, value])
     : [];
+  const inventoryLabelText = row.inventoryLabels
+    ? Object.entries(row.inventoryLabels).flatMap(([key, value]) => [key, 
value])
+    : [];
   return [
     row.title,
     row.copy,
@@ -633,8 +687,10 @@ function buildMetricInventorySearchText(row: 
OtlpMetricInventoryRow) {
     row.description,
     row.metricType,
     row.unit,
+    row.inventorySource,
     row.series?.name,
-    ...labelText
+    ...labelText,
+    ...inventoryLabelText
   ]
     .filter((value): value is string => typeof value === 'string' && 
value.trim() !== '')
     .join(' ')
@@ -646,7 +702,7 @@ function metricInventoryName(row: OtlpMetricInventoryRow) {
 }
 
 function metricInventoryLatestValue(row: OtlpMetricInventoryRow) {
-  const value = row.series?.latestValue ?? Number(row.meta);
+  const value = row.series?.latestValue ?? row.latestObservedAt ?? 
Number(row.meta);
   return Number.isFinite(value) ? value : Number.NEGATIVE_INFINITY;
 }
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to