]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/prometheus: add node-proxy hardware metrics exporter
authorAfreen Misbah <afreen@ibm.com>
Thu, 11 Jun 2026 23:55:55 +0000 (05:25 +0530)
committerAfreen Misbah <afreen@ibm.com>
Thu, 2 Jul 2026 08:50:08 +0000 (14:20 +0530)
Signed-off-by: Afreen Misbah <afreen@ibm.com>
src/pybind/mgr/prometheus/module.py

index 4fb4f7d5d8844c77702612e596e930fa2e6641b9..dbdb1e7b1fa10c45c487e06d5c1d88a9ac5c5dcd 100644 (file)
@@ -140,6 +140,20 @@ NUM_OBJECTS = ['degraded', 'misplaced', 'unfound']
 SMB_METADATA = ('smb_version', 'volume',
                 'subvolume_group', 'subvolume', 'netbiosname', 'share')
 
+NODE_PROXY_STORAGE_LABELS = ('hostname', 'device', 'model', 'protocol')
+
+NODE_PROXY_CPU_LABELS = ('hostname', 'cpu', 'manufacturer', 'model')
+
+NODE_PROXY_MEMORY_LABELS = ('hostname', 'dimm', 'type', 'manufacturer')
+
+NODE_PROXY_HEALTH_LABELS = ('hostname', 'component', 'category')
+
+NODE_PROXY_TEMP_LABELS = ('hostname', 'sensor_name')
+
+NODE_PROXY_FAN_LABELS = ('hostname', 'fan_name')
+
+NODE_PROXY_FIRMWARE_LABELS = ('hostname', 'component', 'version')
+
 CEPHADM_DAEMON_STATUS = ('service_type', 'daemon_name', 'hostname', 'service_name')
 
 alert_metric = namedtuple('alert_metric', 'name description')
@@ -998,6 +1012,55 @@ class Module(MgrModule, OrchestratorClientMixin):
                 check.description,
             )
 
+        metrics['node_proxy_storage_capacity_bytes'] = Metric(
+            'gauge',
+            'node_proxy_storage_capacity_bytes',
+            'Storage device capacity in bytes',
+            NODE_PROXY_STORAGE_LABELS
+        )
+
+        metrics['node_proxy_cpu_cores'] = Metric(
+            'gauge',
+            'node_proxy_cpu_cores',
+            'Total CPU cores',
+            NODE_PROXY_CPU_LABELS
+        )
+
+        metrics['node_proxy_memory_capacity_mib'] = Metric(
+            'gauge',
+            'node_proxy_memory_capacity_mib',
+            'Memory capacity in MiB',
+            NODE_PROXY_MEMORY_LABELS
+        )
+
+        metrics['node_proxy_health'] = Metric(
+            'gauge',
+            'node_proxy_health',
+            'Hardware component health status (0=OK, 1=Warning, 2=Error)',
+            NODE_PROXY_HEALTH_LABELS
+        )
+
+        metrics['node_proxy_temperature_celsius'] = Metric(
+            'gauge',
+            'node_proxy_temperature_celsius',
+            'Hardware temperature sensor reading in Celsius',
+            NODE_PROXY_TEMP_LABELS
+        )
+
+        metrics['node_proxy_fan_rpm'] = Metric(
+            'gauge',
+            'node_proxy_fan_rpm',
+            'Hardware fan speed in RPM',
+            NODE_PROXY_FAN_LABELS
+        )
+
+        metrics['node_proxy_firmware_info'] = Metric(
+            'gauge',
+            'node_proxy_firmware_info',
+            'Firmware version information (value is always 1, version in label)',
+            NODE_PROXY_FIRMWARE_LABELS
+        )
+
         return metrics
 
     def orch_is_available(self) -> bool:
@@ -2000,6 +2063,115 @@ class Module(MgrModule, OrchestratorClientMixin):
         except Exception as e:
             self.log.error(f"Failed to get SMB metadata: {str(e)}")
 
+    @profile_method(True)
+    def get_hardware_metrics(self) -> None:
+        """Collect hardware metrics from node-proxy KV store"""
+        NODE_PROXY_CACHE_PREFIX = 'node_proxy'
+
+        try:
+            for key, value in self.get_store_prefix(f'{NODE_PROXY_CACHE_PREFIX}/data').items():
+                host = key.split('/')[-1]
+                try:
+                    data = json.loads(value)
+                except json.JSONDecodeError:
+                    self.log.error(f"Failed to decode node-proxy data for host {host}")
+                    continue
+
+                status = data.get('status', {})
+
+                # Storage metrics
+                for sys_id, devices in status.get('storage', {}).items():
+                    for device_id, device in devices.items():
+                        capacity = device.get('capacity_bytes', 0)
+                        labels = (
+                            host,
+                            device_id,
+                            device.get('model', 'unknown'),
+                            device.get('protocol', 'unknown')
+                        )
+                        self.metrics['node_proxy_storage_capacity_bytes'].set(capacity, labels)
+
+                # CPU metrics
+                for sys_id, cpus in status.get('processors', {}).items():
+                    for cpu_id, cpu in cpus.items():
+                        cores = cpu.get('total_cores', 0)
+                        labels = (
+                            host,
+                            cpu_id,
+                            cpu.get('manufacturer', 'unknown'),
+                            cpu.get('model', 'unknown')
+                        )
+                        self.metrics['node_proxy_cpu_cores'].set(cores, labels)
+
+                # Memory metrics
+                for sys_id, dimms in status.get('memory', {}).items():
+                    for dimm_id, dimm in dimms.items():
+                        capacity = dimm.get('capacity_mi_b', 0)
+                        labels = (
+                            host,
+                            dimm_id,
+                            dimm.get('memory_device_type', 'unknown'),
+                            dimm.get('manufacturer', 'unknown')
+                        )
+                        self.metrics['node_proxy_memory_capacity_mib'].set(capacity, labels)
+
+                # Health metrics for all component types
+                for category in ['storage', 'processors', 'memory', 'power', 'fans', 'network']:
+                    for sys_id, components in status.get(category, {}).items():
+                        for comp_id, comp in components.items():
+                            health_str = comp.get('status', {}).get('health', 'Unknown')
+                            if health_str == 'OK':
+                                health_value = 0
+                            elif health_str in ['Warning', 'Degraded']:
+                                health_value = 1
+                            else:
+                                health_value = 2
+
+                            labels = (host, comp_id, category)
+                            self.metrics['node_proxy_health'].set(health_value, labels)
+
+                # Temperature metrics
+                for sys_id, sensors in status.get('temperatures', {}).items():
+                    for sensor_id, sensor in sensors.items():
+                        reading = sensor.get('reading')
+                        sensor_name = sensor.get('name', sensor_id)
+                        # Skip absent/unknown sensors
+                        if reading is None or reading == 'unknown':
+                            continue
+                        try:
+                            temp_value = float(reading)
+                            labels = (host, sensor_name)
+                            self.metrics['node_proxy_temperature_celsius'].set(temp_value, labels)
+                        except (ValueError, TypeError):
+                            continue
+
+                # Fan RPM metrics
+                for sys_id, fans_data in status.get('fans', {}).items():
+                    for fan_id, fan in fans_data.items():
+                        # Redfish uses 'Reading' field per Thermal schema
+                        # node-proxy normalizes it to lowercase 'reading'
+                        rpm = fan.get('reading')
+                        fan_name = fan.get('name', fan_id)
+                        if rpm is None or rpm == 'unknown':
+                            continue
+                        try:
+                            rpm_value = float(rpm)
+                            labels = (host, fan_name)
+                            self.metrics['node_proxy_fan_rpm'].set(rpm_value, labels)
+                        except (ValueError, TypeError):
+                            continue
+
+                # Firmware version metrics
+                firmwares = data.get('firmwares', {})
+                for fw_id, fw_info in firmwares.items():
+                    component = fw_info.get('name', fw_id)
+                    version = fw_info.get('version', 'unknown')
+                    if version and version != 'unknown':
+                        labels = (host, component, str(version))
+                        self.metrics['node_proxy_firmware_info'].set(1, labels)
+        except Exception as e:
+            self.log.error(f"Failed to collect hardware metrics: {str(e)}")
+
     @profile_method(True)
     def collect(self) -> str:
         # Clear the metrics before scraping
@@ -2020,6 +2192,7 @@ class Module(MgrModule, OrchestratorClientMixin):
         self.get_num_objects()
         self.get_all_daemon_health_metrics()
         self.get_smb_metadata()
+        self.get_hardware_metrics()
         self.set_cephadm_daemon_status_metrics()
 
         if not self.get_module_option('exclude_perf_counters'):