Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions ozone-ui/packages/om/mock/jmxData.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,90 @@ const deletingServiceMetrics = {
NumKeysPurged: 1275,
};

// OM metrics bean (RPC operation counters + object counts). Values mirror a real
// cluster's shape: Key has activity plus a couple of failures (one of them a
// failure-only op — NumKeyLists is 0 but NumKeyListFails is not); Get/Volume/Bucket/
// List have data; the remaining metric types are absent, so their dropdown options
// are greyed out.
const omMetrics = {
name: 'Hadoop:service=OzoneManager,name=OMMetrics',
modelerType: 'OMMetrics',
'tag.Context': 'ozone',
'tag.Hostname': 'node1.test.site.com',

// Object counts (summary cards).
NumVolumes: 1,
NumBuckets: 2,
NumKeys: 485,
TotalDataCommitted: 62259,

// Key — active, with a failing op (Delete) and a failure-only op (List).
NumKeyOps: 2895,
NumKeyAllocate: 965,
NumKeyAllocateFails: 0,
NumKeyCommits: 965,
NumKeyCommitFails: 0,
NumKeyDeletes: 965,
NumKeyDeleteFails: 5,
NumKeyHSyncs: 0,
NumKeyLists: 0,
NumKeyListFails: 100,
NumKeyLookup: 0,
NumKeyLookupFails: 0,
NumKeyRenames: 0,
NumKeyRenameFails: 0,

// Get.
NumGetServiceLists: 990,

// Volume.
NumVolumeOps: 24,
NumVolumeCreates: 1,
NumVolumeCreateFails: 0,
NumVolumeInfos: 20,
NumVolumeInfoFails: 0,
NumVolumeLists: 3,
NumVolumeListFails: 0,

// Bucket.
NumBucketOps: 60,
NumBucketCreates: 2,
NumBucketCreateFails: 0,
NumBucketInfos: 48,
NumBucketInfoFails: 0,
NumBucketLists: 10,
NumBucketListFails: 0,

// List.
NumListStatus: 45,
NumListStatusFails: 0,

// Snapshot — active, with a failing Create op.
NumSnapshotOps: 11,
NumSnapshotCreates: 8,
NumSnapshotCreateFails: 1,
NumSnapshotDeletes: 3,
NumSnapshotDeleteFails: 0,

// ACL operations (Add / Set / Remove).
NumAddAcl: 40,
NumAddAclFails: 0,
NumSetAcl: 15,
NumSetAclFails: 0,
NumRemoveAcl: 5,
NumRemoveAclFails: 0,

// Multipart upload (Initiate / Commit / Complete / Abort) — Commit is failing.
NumInitiateMultipartUploads: 20,
NumInitiateMultipartUploadFails: 0,
NumCommitMultipartUploadParts: 18,
NumCommitMultipartUploadPartFails: 2,
NumCompleteMultipartUploads: 18,
NumCompleteMultipartUploadFails: 0,
NumAbortMultipartUploads: 2,
NumAbortMultipartUploadFails: 0,
};

/**
* Ordered match table. The mock server picks the first entry whose `test`
* matches the requested `qry` and returns `{ beans }`.
Expand All @@ -169,5 +253,6 @@ module.exports = [
{ test: /service=RaftServer/i, beans: [ratisRaftServer] },
{ test: /electionCount/i, beans: [leaderElectionCount] },
{ test: /lastLeaderElectionElapsedTime/i, beans: [leaderElectionElapsed] },
{ test: /name=OMMetrics/i, beans: [omMetrics] },
{ test: /DeletingServiceMetrics/i, beans: [deletingServiceMetrics] },
];
18 changes: 13 additions & 5 deletions ozone-ui/packages/om/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
import { navItems, SIDEBAR_WIDTH } from './navigation';
import { JMX_QUERY_KEY } from './api/useJmx';
import OverviewPage from './pages/Overview/OverviewPage';
import MetricsPage from './pages/Metrics/MetricsPage';
import Placeholder from './pages/Placeholder';

/** 404 page for unknown routes; the action returns to the Overview. */
Expand Down Expand Up @@ -89,12 +90,19 @@ function AppShell() {
<Routes>
<Route path="/" element={<OverviewPage />} />
<Route path="/configuration" element={<Placeholder title="Configuration" />} />
<Route path="/rpc" element={<Placeholder title="Remote Procedure Call" />} />
<Route path="/ozone-manager" element={<Placeholder title="Ozone Manager" />} />
<Route path="/jmx-info" element={<Placeholder title="JMX" />} />
{/* Metrics group */}
<Route path="/metrics/rpc" element={<Placeholder title="Remote Procedure Call" />} />
<Route
path="/metrics/ratis-event-timeline"
element={<Placeholder title="Ratis Event Timeline" />}
/>
<Route path="/metrics/ozone-manager" element={<MetricsPage />} />
<Route path="/metrics/deletion" element={<Placeholder title="Deletion" />} />
<Route path="/metrics/snapshots" element={<Placeholder title="Snapshots" />} />
{/* Common tools group */}
<Route path="/jmx" element={<Placeholder title="JMX" />} />
<Route path="/stacks" element={<Placeholder title="Stacks" />} />
<Route path="/documentation" element={<Placeholder title="Documentation" />} />
<Route path="/log-levels" element={<Placeholder title="Log levels" />} />
<Route path="/log-levels" element={<Placeholder title="Log Levels" />} />
<Route path="*" element={<NotFoundRoute />} />
</Routes>
</AppLayout>
Expand Down
138 changes: 138 additions & 0 deletions ozone-ui/packages/om/src/__tests__/metrics.parsers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { describe, expect, it } from 'vitest';
import { parseOmMetrics, type OMMetricsBean } from '../api/metrics';

const bean: OMMetricsBean = {
name: 'Hadoop:service=OzoneManager,name=OMMetrics',
'tag.Hostname': 'node1',
NumVolumes: 1,
NumBuckets: 2,
NumKeys: 485,
TotalDataCommitted: 62259,
NumKeyOps: 2895,
NumKeyAllocate: 965,
NumKeyAllocateFails: 0,
NumKeyCommits: 965,
NumKeyCommitFails: 0,
NumKeyDeletes: 965,
NumKeyDeleteFails: 5,
NumKeyHSyncs: 0,
NumKeyLists: 0,
NumKeyListFails: 100,
NumGetServiceLists: 990,
};

describe('parseOmMetrics — summary', () => {
it('extracts the object-count summary', () => {
const { summary } = parseOmMetrics(bean);
expect(summary).toEqual({
volumes: 1,
buckets: 2,
keys: 485,
totalCommittedBytes: 62259,
});
});

it('is null-safe', () => {
expect(parseOmMetrics(undefined).summary.keys).toBe(0);
});
});

describe('parseOmMetrics — Key operations', () => {
const key = parseOmMetrics(bean).byType.Key;

it('joins plural request names to their singular failure counterpart', () => {
const commit = key.operations.find((o) => o.name === 'Commit');
const del = key.operations.find((o) => o.name === 'Delete');
expect(commit).toMatchObject({ requests: 965, failures: 0, status: 'Active' });
// Deletes has 5 DeleteFails → Warning.
expect(del).toMatchObject({ requests: 965, failures: 5, status: 'Warning' });
});

it('keeps a request name that has no failure counterpart', () => {
const get = parseOmMetrics(bean).byType.Get;
expect(get.operations.map((o) => o.name)).toContain('ServiceLists');
});

it('surfaces a failure-only operation (0 requests) with Warning status', () => {
const list = key.operations.find((o) => o.name === 'List');
expect(list).toMatchObject({ requests: 0, failures: 100, status: 'Warning' });
});

it('omits operations with no activity (0 requests, 0 failures)', () => {
// NumKeyHSyncs = 0 with no failures → not shown.
expect(key.operations.find((o) => o.name === 'HSync')).toBeUndefined();
});

it('uses Num<Type>Ops for totalRequests', () => {
expect(key.totalRequests).toBe(2895);
});

it('sorts operations by requests descending', () => {
const requests = key.operations.map((o) => o.requests);
expect(requests).toEqual([...requests].sort((a, b) => b - a));
});
});

describe('parseOmMetrics — enabled flag', () => {
const { byType } = parseOmMetrics(bean);

it('enables types with activity', () => {
expect(byType.Key.enabled).toBe(true);
expect(byType.Get.enabled).toBe(true);
});

it('disables types with no metrics in the bean', () => {
expect(byType.Snapshot.enabled).toBe(false);
expect(byType.Snapshot.operations).toHaveLength(0);
});
});

describe('parseOmMetrics — parsing safety', () => {
it('does not treat NumKeys/NumVolumes/NumBuckets counts as operations', () => {
const { byType } = parseOmMetrics({ NumKeys: 485, NumVolumes: 1, NumBuckets: 2 });
expect(byType.Key.operations).toHaveLength(0);
expect(byType.Volume.operations).toHaveLength(0);
expect(byType.Bucket.operations).toHaveLength(0);
expect(byType.Key.enabled).toBe(false);
});

it('ignores non-numeric bean values (tags, modelerType, name)', () => {
const { byType } = parseOmMetrics({
name: 'Hadoop:service=OzoneManager,name=OMMetrics',
modelerType: 'OMMetrics',
'tag.Hostname': 'node1',
});
expect(Object.values(byType).every((t) => t.operations.length === 0)).toBe(true);
});

it('shows a failure-only operation type and marks it enabled', () => {
const { byType } = parseOmMetrics({ NumRecoverLeaseFails: 3 });
expect(byType.Recover.enabled).toBe(true);
expect(byType.Recover.operations).toEqual([
expect.objectContaining({ name: 'Lease', requests: 0, failures: 3, status: 'Warning' }),
]);
});

it('falls back to summing requests when Num<Type>Ops is absent', () => {
const { byType } = parseOmMetrics({ NumGetServiceLists: 990, NumGetAcl: 10 });
expect(byType.Get.totalRequests).toBe(1000);
});
});
Loading
Loading