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

sarvekshayr pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/master by this push:
     new a01a27a38bd HDDS-13128. Expose per DN space utilisation as JMX metrics 
(#10913)
a01a27a38bd is described below

commit a01a27a38bdd27a28b9bfa1b5cf96e759505f0cf
Author: Anurag Parvatikar <[email protected]>
AuthorDate: Thu Aug 6 21:54:41 2026 +0530

    HDDS-13128. Expose per DN space utilisation as JMX metrics (#10913)
---
 .../common/volume/DatanodeStorageMetrics.java      | 102 ++++++++++++++++
 .../ozone/container/ozoneimpl/OzoneContainer.java  |   5 +
 .../common/volume/TestDatanodeStorageMetrics.java  | 114 ++++++++++++++++++
 .../dn/TestDatanodeStorageMetricsIntegration.java  | 129 +++++++++++++++++++++
 4 files changed, 350 insertions(+)

diff --git 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/volume/DatanodeStorageMetrics.java
 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/volume/DatanodeStorageMetrics.java
new file mode 100644
index 00000000000..fc1edbee11e
--- /dev/null
+++ 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/volume/DatanodeStorageMetrics.java
@@ -0,0 +1,102 @@
+/*
+ * 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.
+ */
+
+package org.apache.hadoop.ozone.container.common.volume;
+
+import org.apache.hadoop.metrics2.MetricsCollector;
+import org.apache.hadoop.metrics2.MetricsInfo;
+import org.apache.hadoop.metrics2.MetricsRecordBuilder;
+import org.apache.hadoop.metrics2.MetricsSource;
+import org.apache.hadoop.metrics2.annotation.Metrics;
+import org.apache.hadoop.metrics2.lib.DefaultMetricsSystem;
+import org.apache.hadoop.metrics2.lib.Interns;
+import org.apache.hadoop.metrics2.lib.MetricsRegistry;
+import org.apache.hadoop.ozone.OzoneConsts;
+import org.apache.hadoop.ozone.container.common.impl.StorageLocationReport;
+
+/**
+ * Node-level storage totals for a DataNode, aggregated over its HDDS data 
volumes only
+ * ({@code VolumeType.DATA_VOLUME}) via {@link 
MutableVolumeSet#getStorageReport()}.
+ * This is the same scope as the {@code storageReport} entries produced by
+ * {@code OzoneContainer.getNodeReport()}; meta and DB volumes are excluded.
+ * Registered as {@code 
Hadoop:service=HddsDatanode,name=DatanodeStorageMetrics}.
+ */
+@Metrics(about = "Ozone DataNode node-level storage totals",
+    context = OzoneConsts.OZONE)
+public final class DatanodeStorageMetrics implements MetricsSource {
+
+  public static final String SOURCE_NAME = 
DatanodeStorageMetrics.class.getSimpleName();
+
+  private static final MetricsInfo CAPACITY = Interns.info("OzoneCapacity",
+      "Total Ozone usable capacity across the DataNode's data volumes (bytes,"
+          + " post reserved-space adjustment)");
+  private static final MetricsInfo USED = Interns.info("OzoneUsed",
+      "Total Ozone used space across the DataNode's data volumes (bytes)");
+  private static final MetricsInfo USED_PERCENTAGE =
+      Interns.info("OzoneUsedPercentage",
+          "100 * OzoneUsed / OzoneCapacity across the DataNode's data volumes;"
+              + " 0 when OzoneCapacity is 0");
+
+  private final MetricsRegistry registry;
+  private final MutableVolumeSet volumeSet;
+
+  private DatanodeStorageMetrics(MutableVolumeSet volumeSet) {
+    this.volumeSet = volumeSet;
+    this.registry = new MetricsRegistry(SOURCE_NAME);
+  }
+
+  /**
+   * Creates a new {@code DatanodeStorageMetrics} instance and registers it
+   * with the default Metrics2 system.
+   */
+  public static DatanodeStorageMetrics create(MutableVolumeSet volumeSet) {
+    DatanodeStorageMetrics datanodeStorageMetrics = new 
DatanodeStorageMetrics(volumeSet);
+    DefaultMetricsSystem.instance().register(
+        SOURCE_NAME, "DataNode node-level storage totals", 
datanodeStorageMetrics);
+    return datanodeStorageMetrics;
+  }
+
+  /**
+   * Unregisters this source from the Metrics2 system.
+   */
+  public void unregister() {
+    DefaultMetricsSystem.instance().unregisterSource(SOURCE_NAME);
+  }
+
+  /**
+   * Metrics are computed on demand from the latest volume reports
+   * instead of maintaining cached counters.
+   */
+  @Override
+  public void getMetrics(MetricsCollector collector, boolean all) {
+    MetricsRecordBuilder builder = collector.addRecord(SOURCE_NAME);
+    registry.snapshot(builder, all);
+
+    long capacity = 0L;
+    long used = 0L;
+    for (StorageLocationReport report : volumeSet.getStorageReport()) {
+      capacity = Math.addExact(capacity, report.getCapacity());
+      used = Math.addExact(used, report.getScmUsed());
+    }
+    double usedPercentage = capacity > 0 ? (100.0 * used / capacity) : 0.0;
+
+    builder
+        .addGauge(CAPACITY, capacity)
+        .addGauge(USED, used)
+        .addGauge(USED_PERCENTAGE, usedPercentage);
+  }
+}
diff --git 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/OzoneContainer.java
 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/OzoneContainer.java
index cd717d1e2b0..08d5c18c1d1 100644
--- 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/OzoneContainer.java
+++ 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/OzoneContainer.java
@@ -92,6 +92,7 @@
 import 
org.apache.hadoop.ozone.container.common.transport.server.ratis.XceiverServerRatis;
 import org.apache.hadoop.ozone.container.common.utils.ContainerInspectorUtil;
 import org.apache.hadoop.ozone.container.common.utils.HddsVolumeUtil;
+import org.apache.hadoop.ozone.container.common.volume.DatanodeStorageMetrics;
 import org.apache.hadoop.ozone.container.common.volume.HddsVolume;
 import org.apache.hadoop.ozone.container.common.volume.MutableVolumeSet;
 import org.apache.hadoop.ozone.container.common.volume.StorageVolume;
@@ -156,6 +157,7 @@ public class OzoneContainer {
 
   private final ContainerMetrics metrics;
   private WitnessedContainerMetadataStore witnessedContainerMetadataStore;
+  private final DatanodeStorageMetrics datanodeStorageMetrics;
 
   enum InitializingStatus {
     UNINITIALIZED, INITIALIZING, INITIALIZED
@@ -330,6 +332,8 @@ public OzoneContainer(HddsDatanodeService 
hddsDatanodeService,
       tlsClientConfig = null;
     }
 
+    datanodeStorageMetrics = DatanodeStorageMetrics.create(volumeSet);
+
     initializingStatus = new 
AtomicReference<>(InitializingStatus.UNINITIALIZED);
   }
 
@@ -633,6 +637,7 @@ public void stop() {
     this.handlers.values().forEach(Handler::stop);
     hddsDispatcher.shutdown();
     volumeChecker.shutdownAndWait(0, TimeUnit.SECONDS);
+    datanodeStorageMetrics.unregister();
     volumeSet.shutdown();
     metaVolumeSet.shutdown();
     if (dbVolumeSet != null) {
diff --git 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/volume/TestDatanodeStorageMetrics.java
 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/volume/TestDatanodeStorageMetrics.java
new file mode 100644
index 00000000000..d54a8b0f9ea
--- /dev/null
+++ 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/volume/TestDatanodeStorageMetrics.java
@@ -0,0 +1,114 @@
+/*
+ * 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.
+ */
+
+package org.apache.hadoop.ozone.container.common.volume;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.apache.hadoop.metrics2.AbstractMetric;
+import org.apache.hadoop.metrics2.impl.MetricsCollectorImpl;
+import org.apache.hadoop.metrics2.impl.MetricsRecordImpl;
+import org.apache.hadoop.ozone.container.common.impl.StorageLocationReport;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link DatanodeStorageMetrics}.
+ *
+ * <p>Tests verify:
+ * <ul>
+ *   <li>Correct aggregation of Capacity and Used across multiple volumes.</li>
+ *   <li>OzoneUsedPercentage arithmetic (100 * OzoneUsed / OzoneCapacity).</li>
+ *   <li>Zero-capacity guard: OzoneUsedPercentage returns 0 instead of 
NaN/divide-by-zero.</li>
+ * </ul>
+ */
+class TestDatanodeStorageMetrics {
+
+  @Test
+  void testAggregationAcrossTwoVolumes() {
+    // vol1: capacity=100, scmUsed=40  vol2: capacity=300, scmUsed=60
+    // expected: OzoneCapacity=400, OzoneUsed=100, OzoneUsedPercentage=25.0
+    StorageLocationReport vol1 = StorageLocationReport.newBuilder()
+        .setId("vol1").setCapacity(100L).setScmUsed(40L).setRemaining(60L)
+        .build();
+    StorageLocationReport vol2 = StorageLocationReport.newBuilder()
+        .setId("vol2").setCapacity(300L).setScmUsed(60L).setRemaining(240L)
+        .build();
+
+    MutableVolumeSet volumeSet = mock(MutableVolumeSet.class);
+    when(volumeSet.getStorageReport())
+        .thenReturn(new StorageLocationReport[]{vol1, vol2});
+
+    DatanodeStorageMetrics metrics = DatanodeStorageMetrics.create(volumeSet);
+    try {
+      MetricsCollectorImpl collector = new MetricsCollectorImpl();
+      metrics.getMetrics(collector, true);
+
+      assertThat(collector.getRecords()).hasSize(1);
+      MetricsRecordImpl rec = collector.getRecords().get(0);
+
+      // Record name determines the JMX name= segment — must match verbatim.
+      assertThat(rec.name()).isEqualTo(DatanodeStorageMetrics.SOURCE_NAME);
+
+      Iterable<AbstractMetric> all = rec.metrics();
+      assertThat(findLong(all, "OzoneCapacity")).isEqualTo(400L);
+      assertThat(findLong(all, "OzoneUsed")).isEqualTo(100L);
+      assertThat(findDouble(all, "OzoneUsedPercentage")).isEqualTo(25.0);
+    } finally {
+      metrics.unregister();
+    }
+  }
+
+  @Test
+  void testZeroCapacityReturnsZeroPercentage() {
+    // No volumes → capacity=0, used=0; OzoneUsedPercentage must be 0.0, not 
NaN.
+    MutableVolumeSet volumeSet = mock(MutableVolumeSet.class);
+    when(volumeSet.getStorageReport()).thenReturn(new 
StorageLocationReport[0]);
+
+    DatanodeStorageMetrics metrics = DatanodeStorageMetrics.create(volumeSet);
+    try {
+      MetricsCollectorImpl collector = new MetricsCollectorImpl();
+      metrics.getMetrics(collector, true);
+
+      Iterable<AbstractMetric> all = collector.getRecords().get(0).metrics();
+      assertThat(findLong(all, "OzoneCapacity")).isEqualTo(0L);
+      assertThat(findLong(all, "OzoneUsed")).isEqualTo(0L);
+      assertThat(findDouble(all, "OzoneUsedPercentage")).isEqualTo(0.0);
+    } finally {
+      metrics.unregister();
+    }
+  }
+
+  private static long findLong(Iterable<AbstractMetric> metrics, String name) {
+    for (AbstractMetric m : metrics) {
+      if (name.equals(m.name())) {
+        return m.value().longValue();
+      }
+    }
+    throw new AssertionError("Missing metric: " + name);
+  }
+
+  private static double findDouble(Iterable<AbstractMetric> metrics, String 
name) {
+    for (AbstractMetric m : metrics) {
+      if (name.equals(m.name())) {
+        return m.value().doubleValue();
+      }
+    }
+    throw new AssertionError("Missing metric: " + name);
+  }
+}
diff --git 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/dn/TestDatanodeStorageMetricsIntegration.java
 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/dn/TestDatanodeStorageMetricsIntegration.java
new file mode 100644
index 00000000000..076889a7e8f
--- /dev/null
+++ 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/dn/TestDatanodeStorageMetricsIntegration.java
@@ -0,0 +1,129 @@
+/*
+ * 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.
+ */
+
+package org.apache.hadoop.ozone.dn;
+
+import static 
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_SCM_SAFEMODE_PIPELINE_CREATION;
+import static 
org.apache.hadoop.hdds.fs.SpaceUsageCheckFactory.Conf.configKeyForClassName;
+import static 
org.apache.hadoop.hdds.protocol.proto.HddsProtos.ReplicationFactor.ONE;
+import static 
org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_CONTAINER_SIZE;
+import static org.apache.ozone.test.MetricsAsserts.getDoubleGauge;
+import static org.apache.ozone.test.MetricsAsserts.getLongGauge;
+import static org.apache.ozone.test.MetricsAsserts.getMetrics;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.data.Offset.offset;
+
+import java.util.HashMap;
+import org.apache.hadoop.hdds.client.RatisReplicationConfig;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.fs.DUFactory;
+import org.apache.hadoop.hdds.fs.SpaceUsageCheckFactory;
+import org.apache.hadoop.metrics2.MetricsRecordBuilder;
+import org.apache.hadoop.ozone.MiniOzoneCluster;
+import org.apache.hadoop.ozone.client.OzoneClient;
+import org.apache.hadoop.ozone.client.io.OzoneOutputStream;
+import org.apache.hadoop.ozone.container.common.volume.DatanodeStorageMetrics;
+import org.apache.hadoop.ozone.container.common.volume.MutableVolumeSet;
+import org.apache.ozone.test.GenericTestUtils;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+/**
+ * Integration tests for {@link DatanodeStorageMetrics}.
+ *
+ * <p>Verifies that the live registered metrics source on a real DataNode
+ * reflects actual storage usage: capacity is positive, used space increases
+ * after writing data, and the percentage arithmetic holds.
+ */
+@Timeout(300)
+public class TestDatanodeStorageMetricsIntegration {
+
+  private MiniOzoneCluster cluster;
+
+  @BeforeEach
+  void startCluster() throws Exception {
+    OzoneConfiguration conf = new OzoneConfiguration();
+    conf.set(OZONE_SCM_CONTAINER_SIZE, "1GB");
+    conf.setBoolean(HDDS_SCM_SAFEMODE_PIPELINE_CREATION, false);
+    conf.setClass(configKeyForClassName(), DUFactory.class, 
SpaceUsageCheckFactory.class);
+    cluster = MiniOzoneCluster.newBuilder(conf)
+        .setNumDatanodes(1)
+        .build();
+    cluster.waitForClusterToBeReady();
+    cluster.waitForPipelineTobeReady(ONE, 30000);
+  }
+
+  @AfterEach
+  void stopCluster() {
+    if (cluster != null) {
+      cluster.shutdown();
+    }
+  }
+
+  @Test
+  void storageMetricsReflectWrittenData() throws Exception {
+    // Baseline before any write.
+    long baselineUsed = getLongGauge("OzoneUsed", storageMetrics());
+
+    // Write a key to generate real used space.
+    try (OzoneClient client = cluster.newClient()) {
+      client.getObjectStore().createVolume("vol");
+      client.getObjectStore().getVolume("vol").createBucket("bucket");
+      OzoneOutputStream key = client.getObjectStore().getVolume("vol")
+          .getBucket("bucket")
+          .createKey("key", 4096,
+              RatisReplicationConfig.getInstance(ONE), new HashMap<>());
+      key.write(new byte[4096]);
+      key.close();
+    }
+
+    // Force DU refresh so the in-memory usage cache reflects the write.
+    MutableVolumeSet volumeSet = cluster.getHddsDatanodes().get(0)
+        .getDatanodeStateMachine().getContainer().getVolumeSet();
+    volumeSet.getVolumesList().get(0).getVolumeUsage().refreshNow();
+
+    // Wait until OzoneUsed is reported as greater than the baseline.
+    GenericTestUtils.waitFor(
+        () -> getLongGauge("OzoneUsed", storageMetrics()) > baselineUsed,
+        500, 10_000);
+
+    // Read all three gauges from one storageMetrics() call so they come from
+    // the same getStorageReport() iteration and are mutually consistent.
+    MetricsRecordBuilder rb = storageMetrics();
+    long capacity = getLongGauge("OzoneCapacity", rb);
+    long used = getLongGauge("OzoneUsed", rb);
+    double usedPercentage = getDoubleGauge("OzoneUsedPercentage", rb);
+
+    assertThat(capacity).isGreaterThan(0L);
+    assertThat(used).isGreaterThan(baselineUsed);
+    assertThat(usedPercentage).isBetween(0.0, 100.0);
+
+    // Arithmetic invariant: usedPercentage == 100 * used / capacity.
+    assertThat(usedPercentage).isCloseTo(100.0 * used / capacity, 
offset(0.001));
+  }
+
+  /**
+   * Returns a fresh snapshot of the live {@link DatanodeStorageMetrics} 
source.
+   * Each call re-reads the underlying storage reports — do not mix values
+   * from different calls when checking invariants across gauges.
+   */
+  private static MetricsRecordBuilder storageMetrics() {
+    return getMetrics(DatanodeStorageMetrics.SOURCE_NAME);
+  }
+}


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

Reply via email to